diff --git a/src/main/managed-server/install/orcad-remote-context.ts b/src/main/managed-server/install/orcad-remote-context.ts index 2e9f945a5a7..ad2663aff35 100644 --- a/src/main/managed-server/install/orcad-remote-context.ts +++ b/src/main/managed-server/install/orcad-remote-context.ts @@ -3,6 +3,8 @@ import type { SshTarget } from '../../../shared/ssh-types' import type { OrcadActivationRecord } from '../activation/orcad-activation-record' import { readOrcadActivationRecord } from '../activation/orcad-activation-record-store' import { resolveOrcadRuntimeTarget } from '../lifecycle/orcad-runtime-target' +import { LINUX_LIBC_FIRST_PROBE } from '../../node-runtime/linux-libc-probe' +import { execCommand } from '../../ssh/remote-exec/remote-exec-command' import { prepareWindowsOrcadHost } from '../host-state/orcad-windows-host-preparation' import { isWslOrcadHostTarget, @@ -53,8 +55,11 @@ export async function resolveOrcadRemoteContext> { const host = detectedHost ?? (await detectRemoteHostPlatform(connection, { signal })) if (!host) { @@ -68,12 +73,26 @@ export async function resolveOrcadRemoteContext { + if (command === LINUX_LIBC_FIRST_PROBE && firstLibcProbeOutput !== undefined) { + const output = firstLibcProbeOutput + firstLibcProbeOutput = undefined + return output + } + // The fallbacks run exactly as the default exec would. + return execCommand(connection, command, { signal }) + } + }) }) if (!isWslOrcadHostTarget(target)) { rememberSshHostPlatform(target.id, host, serverTarget) diff --git a/src/main/managed-server/install/orcad-remote-primitives.test.ts b/src/main/managed-server/install/orcad-remote-primitives.test.ts index 70ebf43a094..bed3ea5b032 100644 --- a/src/main/managed-server/install/orcad-remote-primitives.test.ts +++ b/src/main/managed-server/install/orcad-remote-primitives.test.ts @@ -292,10 +292,40 @@ describe('orcad remote context', () => { }) const wslTarget = { kind: 'wsl', id: 'wsl:u', distro: 'U', label: 'U' } as const - const context = await resolveOrcadRemoteContext(wslTarget, conn, undefined, linux, '/home/u\n') + const context = await resolveOrcadRemoteContext(wslTarget, conn, undefined, linux, { + loginHome: '/home/u\n', + firstLibcProbeOutput: 'ldd (Ubuntu GLIBC 2.39-0ubuntu8) 2.39\n' + }) - expect(context).toMatchObject({ remoteHome: '/home/u', userDataDir: '/home/u/.orca' }) + expect(context).toMatchObject({ + remoteHome: '/home/u', + userDataDir: '/home/u/.orca', + serverTarget: 'linux-x64-glibc' + }) expect(detectRemoteHostPlatform).not.toHaveBeenCalled() expect(commands.some((command) => command.includes('$HOME'))).toBe(false) + expect(commands.some((command) => command.includes('ldd'))).toBe(false) + }) + + it('runs the libc fallbacks when the folded first probe proves nothing', async () => { + vi.mocked(detectRemoteHostPlatform).mockClear() + const commands: string[] = [] + mockExec.mockImplementation(async (_conn, command: string) => { + commands.push(command) + return command.includes('getconf') ? 'glibc 2.39\n' : '__ORCAD_RECORD_ABSENT__\n' + }) + const wslTarget = { kind: 'wsl', id: 'wsl:u', distro: 'U', label: 'U' } as const + + const context = await resolveOrcadRemoteContext(wslTarget, conn, undefined, linux, { + loginHome: '/home/u', + firstLibcProbeOutput: 'sh: ldd: not found\n' + }) + + expect(context.serverTarget).toBe('linux-x64-glibc') + // The same plain exec the default probe uses, not the fenced orcad runner. + expect(mockExec).toHaveBeenCalledWith(conn, expect.stringContaining('getconf'), { + signal: undefined + }) + expect(commands.some((command) => command.includes('ldd --version'))).toBe(false) }) }) diff --git a/src/main/node-runtime/linux-libc-probe.ts b/src/main/node-runtime/linux-libc-probe.ts index 1713368bf43..53d0b60f8b2 100644 --- a/src/main/node-runtime/linux-libc-probe.ts +++ b/src/main/node-runtime/linux-libc-probe.ts @@ -35,8 +35,11 @@ export function parseGlibcVersion(output: string): GlibcVersion | null { return match ? { major: Number(match[1]), minor: Number(match[2]) } : null } +/** The first probe, which answers on nearly every glibc or musl host; callers may run it early. */ +export const LINUX_LIBC_FIRST_PROBE = 'LC_ALL=C ldd --version 2>&1 || true' + const PROBES = [ - 'LC_ALL=C ldd --version 2>&1 || true', + LINUX_LIBC_FIRST_PROBE, 'LC_ALL=C getconf GNU_LIBC_VERSION 2>/dev/null || true', // Appliances may omit both tools; newer GNU loaders can report their own version. 'for loader in /lib/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 /lib/ld-linux-aarch64.so.1; do ' + diff --git a/src/main/wsl/wsl-orcad-deploy.test.ts b/src/main/wsl/wsl-orcad-deploy.test.ts index 165b8953b25..41195fdaf44 100644 --- a/src/main/wsl/wsl-orcad-deploy.test.ts +++ b/src/main/wsl/wsl-orcad-deploy.test.ts @@ -110,7 +110,7 @@ function launch(kernel = '5.15.167.4-microsoft-standard-WSL2', options = {}) { mocks.exec.mockImplementation(async (_conn: unknown, command: string) => { if (command.includes('uname -r')) { mocks.steps.push('uname') - return `__ORCA_WSL_KERNEL__ ${kernel}\n__ORCA_WSL_HOME__ /home/u\n\n__ORCA_REMOTE_PLATFORM__ Linux x86_64\n` + return `__ORCA_WSL_KERNEL__ ${kernel}\n__ORCA_WSL_HOME__ /home/u\n\n__ORCA_REMOTE_PLATFORM__ Linux x86_64\n\n__ORCA_WSL_LIBC__\nldd (GNU libc) 2.39\n` } return '4242\n' }) diff --git a/src/main/wsl/wsl-orcad-held-context.test.ts b/src/main/wsl/wsl-orcad-held-context.test.ts index 552c1e146ae..dc5f42985dd 100644 --- a/src/main/wsl/wsl-orcad-held-context.test.ts +++ b/src/main/wsl/wsl-orcad-held-context.test.ts @@ -86,8 +86,8 @@ function holderRejectingOn(gate: Promise): WslDistroHolder { } /** What WSL_HOST_FACTS_COMMAND prints. */ -function facts(kernel: string, home: string, uname: string): string { - return `__ORCA_WSL_KERNEL__ ${kernel}\n__ORCA_WSL_HOME__ ${home}\n\n__ORCA_REMOTE_PLATFORM__ ${uname}\n` +function facts(kernel: string, home: string, uname: string, libc = 'ldd (GNU libc) 2.39'): string { + return `__ORCA_WSL_KERNEL__ ${kernel}\n__ORCA_WSL_HOME__ ${home}\n\n__ORCA_REMOTE_PLATFORM__ ${uname}\n\n__ORCA_WSL_LIBC__\n${libc}\n` } beforeEach(() => { @@ -192,14 +192,14 @@ describe('the folded host facts probe', () => { expect(mocks.spawned.filter((label) => label === 'host-facts')).toHaveLength(1) expect(mocks.contextArgs).toEqual([ expect.objectContaining({ os: 'linux', arch: 'x64' }), - '/home/u' + { loginHome: '/home/u', firstLibcProbeOutput: 'ldd (GNU libc) 2.39\n' } ]) }) it('leaves the context to probe what the facts did not prove', async () => { await resolveWith(facts('5.15.167.4-microsoft-standard-WSL2', '', 'Linux sparc')) - expect(mocks.contextArgs).toEqual([undefined, undefined]) + expect(mocks.contextArgs).toEqual([undefined, { loginHome: undefined }]) expect(mocks.spawned[0]).toBe('host-facts') expect(mocks.spawned).not.toContain('uname -r') }) @@ -207,7 +207,7 @@ describe('the folded host facts probe', () => { it('falls back to the separate probes for a HOME with a newline, never a truncated path', async () => { await resolveWith(facts('5.15.167.4-microsoft-standard-WSL2', '/home/a\nb', 'Linux x86_64')) - expect(mocks.contextArgs).toEqual([undefined, undefined]) + expect(mocks.contextArgs).toEqual([undefined, { loginHome: undefined }]) expect(mocks.spawned.slice(0, 2)).toEqual(['host-facts', 'uname -r']) }) @@ -216,10 +216,22 @@ describe('the folded host facts probe', () => { `Welcome to Ubuntu\n${facts('5.15.167.4-microsoft-standard-WSL2', '/home/u', 'Linux x86_64')}` ) - expect(mocks.contextArgs).toEqual([undefined, undefined]) + expect(mocks.contextArgs).toEqual([undefined, { loginHome: undefined }]) expect(mocks.spawned.slice(0, 2)).toEqual(['host-facts', 'uname -r']) }) + it('keeps a multi-line libc answer whole, and rejects output missing the libc section', () => { + const musl = 'musl libc (x86_64)\nVersion 1.2.4\nDynamic Program Loader' + expect( + readWslHostFacts(facts('5.15.167.4-microsoft-standard-WSL2', '/home/u', 'Linux x86_64', musl)) + ).toMatchObject({ firstLibcProbeOutput: `${musl}\n` }) + expect( + readWslHostFacts( + '__ORCA_WSL_KERNEL__ 5.15\n__ORCA_WSL_HOME__ /home/u\n\n__ORCA_REMOTE_PLATFORM__ Linux x86_64\n' + ) + ).toBeNull() + }) + it('refuses WSL 1 from the kernel line before holding or probing', async () => { mocks.facts = facts('4.4.0-19041-Microsoft', '/home/u', 'Linux x86_64') const holder = heldNow() diff --git a/src/main/wsl/wsl-orcad-held-context.ts b/src/main/wsl/wsl-orcad-held-context.ts index ea52c0a7f1d..df8314192d8 100644 --- a/src/main/wsl/wsl-orcad-held-context.ts +++ b/src/main/wsl/wsl-orcad-held-context.ts @@ -7,15 +7,23 @@ import { UNAME_PLATFORM_PROBE_MARKER } from '../ssh/ssh-remote-platform-detection' import type { RemoteHostPlatform } from '../ssh/ssh-remote-platform' +import { LINUX_LIBC_FIRST_PROBE } from '../node-runtime/linux-libc-probe' import type { WslDistroHolder } from './wsl-distro-holder' import { refuseMissingWslDistro, WslDistroVersion1Error } from './wsl-distro-unsupported' import type { WslOrcadHostTarget } from './wsl-orcad-host-target' const KERNEL_MARKER = '__ORCA_WSL_KERNEL__ ' const HOME_MARKER = '__ORCA_WSL_HOME__ ' -export const WSL_HOST_FACTS_COMMAND = `printf '${KERNEL_MARKER}%s\\n${HOME_MARKER}%s\\n' "$(uname -r)" "$HOME"; ${UNAME_PLATFORM_PROBE_COMMAND}` +const LIBC_MARKER = '__ORCA_WSL_LIBC__' +// The libc probe goes last: its output may span lines, so it takes everything after its marker. +export const WSL_HOST_FACTS_COMMAND = `printf '${KERNEL_MARKER}%s\\n${HOME_MARKER}%s\\n' "$(uname -r)" "$HOME"; ${UNAME_PLATFORM_PROBE_COMMAND}; printf '\\n%s\\n' '${LIBC_MARKER}'; ${LINUX_LIBC_FIRST_PROBE}` -type WslHostFacts = { kernel: string; loginHome?: string; platform?: RemoteHostPlatform } +type WslHostFacts = { + kernel: string + loginHome?: string + platform?: RemoteHostPlatform + firstLibcProbeOutput?: string +} /** * Accepts only the exact shape the facts command prints; anything else (startup noise, a HOME @@ -23,13 +31,15 @@ type WslHostFacts = { kernel: string; loginHome?: string; platform?: RemoteHostP */ export function readWslHostFacts(output: string): WslHostFacts | null { const lines = output.split('\n') - const [kernelLine = '', homeLine = '', gap, platformLine = '', ...rest] = lines + const [kernelLine = '', homeLine = '', gap, platformLine = '', libcGap, libcMarker, ...libc] = + lines if ( !kernelLine.startsWith(KERNEL_MARKER) || !homeLine.startsWith(HOME_MARKER) || gap !== '' || !platformLine.startsWith(UNAME_PLATFORM_PROBE_MARKER) || - rest.some((line) => line.trim() !== '') + libcGap !== '' || + libcMarker !== LIBC_MARKER ) { return null } @@ -37,7 +47,8 @@ export function readWslHostFacts(output: string): WslHostFacts | null { return { kernel: kernelLine.slice(KERNEL_MARKER.length).trim(), ...(loginHome.trim() ? { loginHome } : {}), - ...withPlatform(remoteHostPlatformFromUnameProbe(`\n${platformLine}`)) + ...withPlatform(remoteHostPlatformFromUnameProbe(`\n${platformLine}`)), + firstLibcProbeOutput: libc.join('\n') } } @@ -85,7 +96,11 @@ export async function holdWslDistroWithContext( probes.abort(error) throw error }), - resolveOrcadRemoteContext(target, conn, probes.signal, facts.platform, facts.loginHome) + resolveOrcadRemoteContext(target, conn, probes.signal, facts.platform, { + loginHome: facts.loginHome, + // Only with a proven platform: the libc answer belongs to the same host shape. + ...(facts.platform ? { firstLibcProbeOutput: facts.firstLibcProbeOutput } : {}) + }) ]) return { held, context } } catch (error) {