You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by wy...@apache.org on 2026/09/16 05:13:14 UTC
(maka) branch feat/runtime-host-rust updated: feat(desktop): detect remote native targets without Node
This is an automated email from the ASF dual-hosted git repository.
M4n5ter pushed a commit to branch feat/runtime-host-rust
in repository https://gitbox.apache.org/repos/asf/maka.git
The following commit(s) were added to refs/heads/feat/runtime-host-rust by this push:
new 79b760cf85 feat(desktop): detect remote native targets without Node
79b760cf85 is described below
commit 79b760cf85047d1f51f9dcb5ed344486f1912412
Author: Wang <m4...@gmail.com>
AuthorDate: Wed Sep 16 13:13:08 2026 +0800
feat(desktop): detect remote native targets without Node
Generated-by: OpenAI Codex
---
.../main/__tests__/runtime-host-management.test.ts | 4 +-
.../main/__tests__/runtime-host-onboarding.test.ts | 9 +--
.../__tests__/runtime-host-ssh-terminal.test.ts | 61 +++++++++++++------
.../__tests__/runtime-host-wsl-controller.test.ts | 40 +++++++++++++
apps/desktop/src/main/runtime-host-boot.ts | 6 +-
.../desktop/src/main/runtime-host-framed-output.ts | 10 +++-
apps/desktop/src/main/runtime-host-management.ts | 12 ++--
apps/desktop/src/main/runtime-host-onboarding.ts | 31 ++++++----
apps/desktop/src/main/runtime-host-ssh-terminal.ts | 51 ++++++++--------
apps/desktop/src/main/runtime-host-target.ts | 69 ++++++++++++++++++++++
.../src/main/runtime-host-wsl-controller.ts | 26 ++++++++
11 files changed, 246 insertions(+), 73 deletions(-)
diff --git a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
index a72a25fd0c..bc8bf1eac7 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-management.test.ts
@@ -732,7 +732,7 @@ test('publishes update progress and waits for the managed profile to reconnect',
runUpdateReconciliation: async () =>
assert.fail('update reconciliation is not expected'),
setupPackageMode: 'published',
- resolveSshNodeIdentity: async () =>
+ resolveSshTargetIdentity: async () =>
assert.fail('published update must not inspect the development target'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.3.0' }),
currentHostEpoch: () => 'host-before-update',
@@ -1499,7 +1499,7 @@ function unusedUpdateDependencies() {
assert.fail('direct peer management is not expected'),
directPeerClientAvailable: false,
setupPackageMode: 'published' as const,
- resolveSshNodeIdentity: async (): Promise<never> =>
+ resolveSshTargetIdentity: async (): Promise<never> =>
assert.fail('published update must not inspect the development target'),
resolveUpdatePackage: () => ({ kind: 'npm', specifier: 'maka-agent@1.2.3' } as const),
currentHostEpoch: () => undefined,
diff --git a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
index 7f21b797eb..848c46f1b3 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-onboarding.test.ts
@@ -164,8 +164,8 @@ test('onboards WSL as a credential-free environment profile', async () => {
test('projects WSL setup failures as recoverable onboarding state', async () => {
const harness = createHarness({
- runWslSetup: async () => {
- throw new Error('WSL requires Linux Node.js');
+ resolveWslTargetIdentity: async () => {
+ throw new Error('Native Linux Runtime Host requires GNU libc');
},
});
@@ -176,7 +176,7 @@ test('projects WSL setup failures as recoverable onboarding state', async () =>
}),
{
kind: 'failed',
- message: 'WSL requires Linux Node.js',
+ message: 'Native Linux Runtime Host requires GNU libc',
revision: 3,
},
);
@@ -329,7 +329,8 @@ function createHarness(overrides: HarnessOverrides = {}) {
...profiles,
},
setupPackageMode: 'published',
- resolveSshNodeIdentity: async () => ({ platform: 'darwin', architecture: 'x64' }),
+ resolveSshTargetIdentity: async () => ({ platform: 'darwin', architecture: 'x64' }),
+ resolveWslTargetIdentity: async () => ({ platform: 'linux', architecture: 'x64', glibcVersion: '2.39' }),
resolveSetupPackage: () => ({ kind: 'npm', specifier: 'maka-agent@0.2.0' }),
runSetup: async () => assert.fail('SSH must not start'),
runWslSetup: async () => assert.fail('WSL must not start'),
diff --git a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
index aa1a9a6d97..92a95b25c4 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-ssh-terminal.test.ts
@@ -38,7 +38,7 @@ import {
} from '@maka/runtime-host/operator';
import {
createDesktopRuntimeHostSshTerminal,
- runtimeHostPeerTargetFromNode,
+ runtimeHostPeerTargetFromPlatform,
} from '../runtime-host-ssh-terminal.js';
import { waitFor as pollFor } from '@maka/core/test-only/async-primitives';
@@ -56,35 +56,58 @@ const WINDOWS_OPERATOR = {
modulePath: 'C:\\Users\\operator\\AppData\\Local\\Maka\\operator.mjs',
};
-test('maps supported SSH Node identities to peer targets', () => {
- assert.equal(runtimeHostPeerTargetFromNode('linux', 'x64'), 'linux-x64');
- assert.equal(runtimeHostPeerTargetFromNode('linux', 'arm64'), 'linux-arm64');
- assert.equal(runtimeHostPeerTargetFromNode('darwin', 'arm64'), 'darwin-arm64');
- assert.equal(runtimeHostPeerTargetFromNode('win32', 'x64'), 'win32-x64');
+test('maps supported SSH platform identities to peer targets', () => {
+ assert.equal(runtimeHostPeerTargetFromPlatform('linux', 'x64'), 'linux-x64');
+ assert.equal(runtimeHostPeerTargetFromPlatform('linux', 'arm64'), 'linux-arm64');
+ assert.equal(runtimeHostPeerTargetFromPlatform('darwin', 'arm64'), 'darwin-arm64');
+ assert.equal(runtimeHostPeerTargetFromPlatform('win32', 'x64'), 'win32-x64');
assert.throws(
- () => runtimeHostPeerTargetFromNode('linux', 'riscv64'),
+ () => runtimeHostPeerTargetFromPlatform('linux', 'riscv64'),
/not available/u,
);
});
test('detects the peer target through the bounded SSH preflight', async () => {
const harness = createHarness('pending');
- const detection = harness.terminal.resolveNodeIdentity({
+ const detection = harness.terminal.resolveTargetIdentity({
destination: 'operator@example.com',
});
await waitFor(() => harness.pty.hasDataListener());
const command = harness.launchArgs[0]?.at(-1) ?? '';
- const marker = command.match(/__MAKA_RUNTIME_HOST_TARGET_[0-9a-f]+__/u)?.[0];
- assert.ok(marker);
- harness.pty.emitData(`${marker}linux:x64\r\n`);
+ const suffix = command.match(/[0-9a-f]{32}__/u)?.[0];
+ assert.ok(suffix);
+ const marker = `__MAKA_RUNTIME_HOST_TARGET_${suffix}`;
+ assert.doesNotMatch(command, /\bnode\b/u);
+ harness.pty.emitData(`${marker}Linux:x86_64:glibc 2.39\r\n`);
harness.pty.exit(0);
- assert.deepEqual(await detection, { platform: 'linux', architecture: 'x64' });
+ assert.deepEqual(await detection, { platform: 'linux', architecture: 'x64', glibcVersion: '2.39' });
assert.doesNotMatch(JSON.stringify(harness.events), /MAKA_RUNTIME_HOST_TARGET/u);
await harness.terminal.close();
});
-test('retries target detection through the POSIX login shell when Node is not on the default PATH', async (t) => {
+test('rejects unsupported SSH targets without crashing the output listener or retrying another OS', async () => {
+ for (const [value, expected] of [
+ ['Linux:x86_64:unknown', /requires GNU libc/u],
+ ['Linux:riscv64:glibc 2.39', /Unsupported.*architecture/u],
+ ['Linux:x86_64:glibc 2.39:extra', /Invalid.*result/u],
+ ] as const) {
+ const harness = createHarness('pending');
+ const detection = harness.terminal.resolveTargetIdentity({ destination: 'operator@example.com' });
+ const rejected = assert.rejects(detection, expected);
+ await waitFor(() => harness.pty.hasDataListener());
+ const suffix = harness.launchArgs[0]?.at(-1)?.match(/[0-9a-f]{32}__/u)?.[0];
+ assert.ok(suffix);
+ const marker = `__MAKA_RUNTIME_HOST_TARGET_${suffix}`;
+ harness.pty.emitData(`${marker}${value}\r\n`);
+ harness.pty.exit(0);
+ await rejected;
+ assert.equal(harness.launchArgs.length, 1);
+ await harness.terminal.close();
+ }
+});
+
+test('detects a Windows SSH target through PowerShell when no POSIX shell exists', async (t) => {
const handlers = new Map<string, (...args: unknown[]) => unknown>();
const launches: Array<{ args: string[]; pty: FakePty }> = [];
const terminal = createDesktopRuntimeHostSshTerminal({
@@ -101,18 +124,20 @@ test('retries target detection through the POSIX login shell when Node is not on
});
t.after(() => terminal.close());
- const detection = terminal.resolveNodeIdentity({ destination: 'operator@example.com' });
+ const detection = terminal.resolveTargetIdentity({ destination: 'operator@example.com' });
await waitFor(() => launches.length === 1);
launches[0]?.pty.exit(127);
await waitFor(() => launches.length === 2);
const command = launches[1]?.args.at(-1) ?? '';
- const marker = command.match(/__MAKA_RUNTIME_HOST_TARGET_[0-9a-f]+__/u)?.[0];
+ const script = Buffer.from(command.split(' ').at(-1) ?? '', 'base64').toString('utf16le');
+ const marker = script.match(/__MAKA_RUNTIME_HOST_TARGET_[0-9a-f]+__/u)?.[0];
assert.ok(marker);
- assert.match(command, /\$\{SHELL:-\/bin\/sh\}.*-lic/u);
- launches[1]?.pty.emitData(`${marker}linux:x64\r\n`);
+ assert.match(command, /powershell.exe .* -EncodedCommand/u);
+ assert.match(script, /OSArchitecture/u);
+ launches[1]?.pty.emitData(`${marker}Windows:X64:\r\n`);
launches[1]?.pty.exit(0);
- assert.deepEqual(await detection, { platform: 'linux', architecture: 'x64' });
+ assert.deepEqual(await detection, { platform: 'win32', architecture: 'x64' });
});
test('keeps a connecting SSH prompt observable across renderer presentation changes', async () => {
diff --git a/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts b/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts
index 78688e36cf..c73bf7344c 100644
--- a/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts
+++ b/apps/desktop/src/main/__tests__/runtime-host-wsl-controller.test.ts
@@ -31,6 +31,7 @@ import {
RUNTIME_HOST_SETUP_SOURCE_PACKAGE_INTEGRITY_ENV,
} from '@maka/runtime-host/operator';
import {
+ resolveDesktopRuntimeHostWslTarget,
runDesktopRuntimeHostWslManagement,
runDesktopRuntimeHostWslSetup,
runDesktopRuntimeHostWslUpdate,
@@ -43,6 +44,45 @@ const OPERATOR = {
modulePath: '/home/operator/.local/share/maka/operator.mjs',
};
+test('WSL target detection uses the selected Linux environment without Node and can be cancelled', async () => {
+ for (const cancel of [false, true]) {
+ const abort = new AbortController();
+ let killed = false;
+ const outcome = resolveDesktopRuntimeHostWslTarget({ distribution: 'Ubuntu Arm', signal: abort.signal }, {
+ wslExecutable: 'wsl.exe',
+ processFactory: (executable, args) => {
+ assert.equal(executable, 'wsl.exe');
+ assert.deepEqual(args.slice(0, 5), ['--distribution', 'Ubuntu Arm', '--exec', '/bin/sh', '-c']);
+ const script = args.at(-1) ?? '';
+ assert.doesNotMatch(script, /\bnode\b/u);
+ const suffix = script.match(/[0-9a-f]{32}__/u)?.[0];
+ assert.ok(suffix);
+ const marker = `__MAKA_RUNTIME_HOST_TARGET_${suffix}`;
+ const child = new EventEmitter() as ChildProcessWithoutNullStreams;
+ const stdout = new PassThrough();
+ const stderr = new PassThrough();
+ const finish = () => { stdout.end(); stderr.end(); child.emit('close', cancel ? 1 : 0, null); };
+ Object.assign(child, { stdin: new PassThrough(), stdout, stderr, kill: () => {
+ killed = true; process.nextTick(finish); return true;
+ } });
+ process.nextTick(() => {
+ if (cancel) { abort.abort(); return; }
+ stdout.write(`${marker}Linux:aarch64:glibc 2.39\n`);
+ finish();
+ });
+ return child;
+ },
+ });
+ if (cancel) {
+ await assert.rejects(outcome, /abort/iu);
+ assert.equal(killed, true);
+ } else {
+ assert.deepEqual(await outcome, { platform: 'linux', architecture: 'arm64', glibcVersion: '2.39' });
+ assert.equal(killed, false);
+ }
+ }
+});
+
test('WSL management invokes the stable operator directly with the exact deployment target', async () => {
let launch:
| { readonly executable: string; readonly args: readonly string[]; readonly environment: NodeJS.ProcessEnv }
diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts
index 68dd53e301..a3b5cc8b61 100644
--- a/apps/desktop/src/main/runtime-host-boot.ts
+++ b/apps/desktop/src/main/runtime-host-boot.ts
@@ -228,6 +228,7 @@ import {
import {
runDesktopRuntimeHostWslManagement,
runDesktopRuntimeHostWslSetup,
+ resolveDesktopRuntimeHostWslTarget,
} from './runtime-host-wsl-controller.js';
import {
createRuntimeHostSetupPackageResolver,
@@ -755,9 +756,10 @@ const runtimeHostOnboarding = createDesktopRuntimeHostOnboarding({
profiles: runtimeHostProfileService,
runSetup: runtimeHostSshTerminal.runSetup,
runWslSetup: runDesktopRuntimeHostWslSetup,
+ resolveWslTargetIdentity: resolveDesktopRuntimeHostWslTarget,
listWslDistributions: listRuntimeHostWslDistributions,
setupPackageMode: runtimeHostSetupPackage.mode,
- resolveSshNodeIdentity: runtimeHostSshTerminal.resolveNodeIdentity,
+ resolveSshTargetIdentity: runtimeHostSshTerminal.resolveTargetIdentity,
resolveSetupPackage: runtimeHostSetupPackage.resolve,
send: (snapshot) =>
mainWindowController.send("runtime-host-onboarding:changed", snapshot),
@@ -828,7 +830,7 @@ const runtimeHostManagement = createDesktopRuntimeHostManagement({
runUpdatePolicy: runtimeHostSshTerminal.runUpdatePolicy,
runUpdateReconciliation: runtimeHostSshTerminal.runUpdateReconciliation,
setupPackageMode: runtimeHostSetupPackage.mode,
- resolveSshNodeIdentity: runtimeHostSshTerminal.resolveNodeIdentity,
+ resolveSshTargetIdentity: runtimeHostSshTerminal.resolveTargetIdentity,
resolveUpdatePackage: runtimeHostSetupPackage.resolve,
currentHostEpoch: (profileId) =>
runtimeHostManager?.current(profileId)?.candidate?.client.hostEpoch,
diff --git a/apps/desktop/src/main/runtime-host-framed-output.ts b/apps/desktop/src/main/runtime-host-framed-output.ts
index 5a24062815..f843b4bb2d 100644
--- a/apps/desktop/src/main/runtime-host-framed-output.ts
+++ b/apps/desktop/src/main/runtime-host-framed-output.ts
@@ -58,9 +58,13 @@ export function createRuntimeHostFramedOutputFilter<Frame>(input: {
}
const line = pending.slice(0, newline + 1);
pending = pending.slice(newline + 1);
- const frame = input.decode(line);
- if (frame) input.onFrame(frame);
- else input.onError(new Error(`${input.label} returned an invalid result`));
+ try {
+ const frame = input.decode(line);
+ if (frame) input.onFrame(frame);
+ else input.onError(new Error(`${input.label} returned an invalid result`));
+ } catch (error) {
+ input.onError(error instanceof Error ? error : new Error(`${input.label} returned an invalid result`));
+ }
continue;
}
if (finished) {
diff --git a/apps/desktop/src/main/runtime-host-management.ts b/apps/desktop/src/main/runtime-host-management.ts
index a7fa881c88..7579613490 100644
--- a/apps/desktop/src/main/runtime-host-management.ts
+++ b/apps/desktop/src/main/runtime-host-management.ts
@@ -53,11 +53,11 @@ import {
} from './runtime-host-managed-services.js';
import { requireProjectDirectoryRoots } from '../shared/runtime-host-project-directory-policy.js';
import {
- runtimeHostPeerTargetFromNode,
+ runtimeHostPeerTargetFromPlatform,
type DesktopRuntimeHostSshCleanupInput,
type DesktopRuntimeHostSshAccessInput,
type DesktopRuntimeHostSshManagementInput,
- type DesktopRuntimeHostSshNodeIdentity,
+ type RuntimeHostTargetIdentity,
type DesktopRuntimeHostSshPeerManagementInput,
type DesktopRuntimeHostSshUpdateInput,
type DesktopRuntimeHostSshUpdatePolicyInput,
@@ -130,11 +130,11 @@ export function createDesktopRuntimeHostManagement(input: {
onProgress: (phase: DesktopRuntimeHostManagementProgress['phase']) => void,
) => Promise<RuntimeHostServiceUpdateReconciliationTerminalFrame>;
readonly setupPackageMode: 'published' | 'development';
- readonly resolveSshNodeIdentity: (input: {
+ readonly resolveSshTargetIdentity: (input: {
readonly destination: string;
readonly sshPort?: number;
readonly signal?: AbortSignal;
- }) => Promise<DesktopRuntimeHostSshNodeIdentity>;
+ }) => Promise<RuntimeHostTargetIdentity>;
readonly resolveUpdatePackage: (
peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
) =>
@@ -657,11 +657,11 @@ export function createDesktopRuntimeHostManagement(input: {
input.sendProgress({ profileId, phase: 'preparing_cli' });
let peerTarget: DesktopRuntimeHostDevelopmentPeerTarget = 'none';
if (input.setupPackageMode === 'development') {
- const identity = await input.resolveSshNodeIdentity({
+ const identity = await input.resolveSshTargetIdentity({
destination: transport.destination,
...(transport.sshPort === undefined ? {} : { sshPort: transport.sshPort }),
});
- peerTarget = runtimeHostPeerTargetFromNode(identity.platform, identity.architecture);
+ peerTarget = runtimeHostPeerTargetFromPlatform(identity.platform, identity.architecture);
}
const setupPackage = await input.resolveUpdatePackage(peerTarget);
execute = () => input.runUpdate(
diff --git a/apps/desktop/src/main/runtime-host-onboarding.ts b/apps/desktop/src/main/runtime-host-onboarding.ts
index 54e55b0b92..8d792fe1f1 100644
--- a/apps/desktop/src/main/runtime-host-onboarding.ts
+++ b/apps/desktop/src/main/runtime-host-onboarding.ts
@@ -32,8 +32,8 @@ import type {
} from '../preload/bridge-contract.js';
import type { DesktopRuntimeHostProfileService } from './runtime-host-profile-service.js';
import {
- runtimeHostPeerTargetFromNode,
- type DesktopRuntimeHostSshNodeIdentity,
+ runtimeHostPeerTargetFromPlatform,
+ type RuntimeHostTargetIdentity,
type DesktopRuntimeHostSshSetupInput,
} from './runtime-host-ssh-terminal.js';
import type { DesktopRuntimeHostWslSetupInput } from './runtime-host-wsl-controller.js';
@@ -81,13 +81,17 @@ export function createDesktopRuntimeHostOnboarding(input: {
readonly operator: RuntimeHostNodeOperatorCommand<'posix'> | RuntimeHostNativeOperatorCommand<'posix'>;
}>;
readonly listWslDistributions: () => Promise<readonly string[]>;
+ readonly resolveWslTargetIdentity: (input: {
+ readonly distribution: string;
+ readonly signal?: AbortSignal;
+ }) => Promise<Extract<RuntimeHostTargetIdentity, { platform: 'linux' }>>;
readonly send: (snapshot: DesktopRuntimeHostOnboardingSnapshot) => void;
readonly setupPackageMode: 'published' | 'development';
- readonly resolveSshNodeIdentity: (input: {
+ readonly resolveSshTargetIdentity: (input: {
readonly destination: string;
readonly sshPort?: number;
readonly signal?: AbortSignal;
- }) => Promise<DesktopRuntimeHostSshNodeIdentity>;
+ }) => Promise<RuntimeHostTargetIdentity>;
readonly resolveSetupPackage: (
peerTarget: DesktopRuntimeHostDevelopmentPeerTarget,
signal?: AbortSignal,
@@ -138,12 +142,17 @@ export function createDesktopRuntimeHostOnboarding(input: {
): Promise<DesktopRuntimeHostOnboardingSnapshot> => {
try {
if (request.kind === 'wsl') {
- const setupPackage = await input.resolveSetupPackage('none', signal);
+ publish({ kind: 'running', phase: 'connecting_wsl' });
+ const target = await input.resolveWslTargetIdentity({ distribution: request.distribution, signal });
+ publish({ kind: 'running', phase: 'preparing_cli' });
+ const peerTarget = input.setupPackageMode === 'development'
+ ? runtimeHostPeerTargetFromPlatform(target.platform, target.architecture) : 'none';
+ const setupPackage = await input.resolveSetupPackage(peerTarget, signal);
return await runWsl(request, setupPackage, signal);
}
- const nodeIdentity = await resolveSshNodeIdentity(request, signal);
+ const targetIdentity = await resolveSshTargetIdentity(request, signal);
const peerTarget = input.setupPackageMode === 'development'
- ? runtimeHostPeerTargetFromNode(nodeIdentity.platform, nodeIdentity.architecture)
+ ? runtimeHostPeerTargetFromPlatform(targetIdentity.platform, targetIdentity.architecture)
: 'none';
const setupPackage = await input.resolveSetupPackage(
peerTarget,
@@ -167,7 +176,7 @@ export function createDesktopRuntimeHostOnboarding(input: {
destination: request.destination,
...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
setupPackage,
- remotePlatform: nodeIdentity.platform === 'win32' ? 'win32' : 'posix',
+ remotePlatform: targetIdentity.platform === 'win32' ? 'win32' : 'posix',
lifecycle,
principalId: `desktop:${input.clientInstanceId}`,
...(request.projectDirectoryRoots
@@ -238,12 +247,12 @@ export function createDesktopRuntimeHostOnboarding(input: {
}
};
- const resolveSshNodeIdentity = async (
+ const resolveSshTargetIdentity = async (
request: Extract<DesktopRuntimeHostOnboardingInput, { readonly kind: 'ssh' }>,
signal: AbortSignal,
- ): Promise<DesktopRuntimeHostSshNodeIdentity> => {
+ ): Promise<RuntimeHostTargetIdentity> => {
publish({ kind: 'running', phase: 'connecting_ssh' });
- const identity = await input.resolveSshNodeIdentity({
+ const identity = await input.resolveSshTargetIdentity({
destination: request.destination,
...(request.sshPort === undefined ? {} : { sshPort: request.sshPort }),
signal,
diff --git a/apps/desktop/src/main/runtime-host-ssh-terminal.ts b/apps/desktop/src/main/runtime-host-ssh-terminal.ts
index 9354cc1606..a6c1ed2b06 100644
--- a/apps/desktop/src/main/runtime-host-ssh-terminal.ts
+++ b/apps/desktop/src/main/runtime-host-ssh-terminal.ts
@@ -75,6 +75,13 @@ import type {
DesktopRuntimeHostSshTerminalSnapshot,
} from '../preload/bridge-contract.js';
import { createRuntimeHostFramedOutputFilter } from './runtime-host-framed-output.js';
+import {
+ decodeRuntimeHostTarget,
+ posixRuntimeHostTargetProbe,
+ windowsRuntimeHostTargetProbe,
+ type RuntimeHostTargetIdentity,
+} from './runtime-host-target.js';
+export type { RuntimeHostTargetIdentity } from './runtime-host-target.js';
import {
runtimeHostSetupPackageVersion,
type DesktopRuntimeHostDevelopmentPeerTarget,
@@ -125,11 +132,6 @@ export interface DesktopRuntimeHostSshTargetInput {
readonly signal?: AbortSignal;
}
-export interface DesktopRuntimeHostSshNodeIdentity {
- readonly platform: string;
- readonly architecture: string;
-}
-
export interface DesktopRuntimeHostSshManagementInput {
readonly destination: string;
readonly sshPort?: number;
@@ -271,9 +273,9 @@ export function createDesktopRuntimeHostSshTerminal(input: {
input: RuntimeHostSshOperatorActivationInput,
): Promise<RuntimeHostActivationResult>;
openSshTunnel(input: RuntimeHostSshTunnelInput): Promise<RuntimeHostSshTunnel>;
- resolveNodeIdentity(
+ resolveTargetIdentity(
input: DesktopRuntimeHostSshTargetInput,
- ): Promise<DesktopRuntimeHostSshNodeIdentity>;
+ ): Promise<RuntimeHostTargetIdentity>;
runSetup(
input: DesktopRuntimeHostSshSetupInput,
onProgress: (frame: Extract<RuntimeHostSetupFrame, { kind: 'progress' }>) => void,
@@ -653,7 +655,7 @@ export function createDesktopRuntimeHostSshTerminal(input: {
}
return tunnel;
},
- resolveNodeIdentity: async (targetInput) => {
+ resolveTargetIdentity: async (targetInput) => {
if (closed) throw new Error('Runtime Host SSH terminal is closed');
targetInput.signal?.throwIfAborted();
const destination = normalizeRuntimeHostSshDestination(targetInput.destination);
@@ -661,26 +663,21 @@ export function createDesktopRuntimeHostSshTerminal(input: {
? undefined
: requireSetupPort(targetInput.sshPort);
const marker = `__MAKA_RUNTIME_HOST_TARGET_${randomUUID().replaceAll('-', '')}__`;
- const nodeProbe = `node -e "process.stdout.write('${marker}'+process.platform+':'+process.arch+'\\n')"`;
+ const deadline = Date.now() + 60_000;
const detect = async (remoteCommand: string) => {
- let identity: DesktopRuntimeHostSshNodeIdentity | undefined;
+ let identity: RuntimeHostTargetIdentity | undefined;
let failure: Error | undefined;
const filter = createRuntimeHostFramedOutputFilter({
prefix: marker,
pendingMaxBytes: 256,
- decode: (line) => line.slice(marker.length).replaceAll('\r', '').trimEnd(),
+ decode: (line) => decodeRuntimeHostTarget(line.slice(marker.length).replaceAll('\r', '').trimEnd()),
label: 'Remote Runtime Host target detection',
onFrame: (value) => {
if (identity) {
failure = new Error('Remote Runtime Host target detection returned multiple results');
return;
}
- const [platform, architecture, ...extra] = value.split(':');
- if (!platform || !architecture || extra.length > 0) {
- failure = new Error('Remote Runtime Host target detection returned an invalid result');
- return;
- }
- identity = { platform, architecture };
+ identity = value;
},
onError: (error) => {
failure = error;
@@ -694,7 +691,7 @@ export function createDesktopRuntimeHostSshTerminal(input: {
);
const wait = await waitForTerminalProcess(process, {
signal: targetInput.signal,
- timeoutMs: input.managementTimeoutMs ?? MANAGEMENT_TIMEOUT_MS,
+ timeoutMs: Math.max(1, Math.min(input.managementTimeoutMs ?? 60_000, deadline - Date.now())),
stopGraceMs: input.processStopGraceMs,
onAbort: () => dismissPresentation(terminal),
}, input.terminateProcessTree);
@@ -704,27 +701,27 @@ export function createDesktopRuntimeHostSshTerminal(input: {
completePresentation(terminal);
return { identity, exitCode: wait.exit.code };
};
- const direct = await detect(nodeProbe);
+ const direct = await detect(`sh -c ${quotePosix(posixRuntimeHostTargetProbe(marker))}`);
if (direct.exitCode === 0) {
if (!direct.identity)
throw new Error('Remote Runtime Host target detection returned no result');
return direct.identity;
}
- if (direct.exitCode !== 127) {
+ if (direct.identity || ![1, 127, 9009].includes(direct.exitCode ?? -1)) {
throw new Error(
`Remote Runtime Host target detection exited with code ${String(direct.exitCode)}`,
);
}
- const loginProbe = `exec "\${SHELL:-/bin/sh}" -lic ${quotePosix(`exec ${nodeProbe}`)}`;
- const login = await detect(loginProbe);
- if (login.exitCode !== 0) {
+ targetInput.signal?.throwIfAborted();
+ const windows = await detect(windowsRuntimeHostTargetProbe(marker));
+ if (windows.exitCode !== 0) {
throw new Error(
- `Remote Runtime Host target detection exited with code ${String(login.exitCode)}`,
+ `Remote Runtime Host target detection exited with code ${String(windows.exitCode)}`,
);
}
- if (!login.identity)
+ if (!windows.identity)
throw new Error('Remote Runtime Host target detection returned no result');
- return login.identity;
+ return windows.identity;
},
runSetup: async (setupInput, onProgress, onComplete) => {
if (closed) throw new Error('Runtime Host SSH terminal is closed');
@@ -983,7 +980,7 @@ export function createDesktopRuntimeHostSshTerminal(input: {
};
}
-export function runtimeHostPeerTargetFromNode(
+export function runtimeHostPeerTargetFromPlatform(
platform: string,
arch: string,
): Exclude<DesktopRuntimeHostDevelopmentPeerTarget, 'none'> {
diff --git a/apps/desktop/src/main/runtime-host-target.ts b/apps/desktop/src/main/runtime-host-target.ts
new file mode 100644
index 0000000000..02cf51a1f9
--- /dev/null
+++ b/apps/desktop/src/main/runtime-host-target.ts
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+export type RuntimeHostTargetIdentity =
+ | { readonly platform: 'linux'; readonly architecture: 'x64' | 'arm64'; readonly glibcVersion: string }
+ | { readonly platform: 'darwin' | 'win32'; readonly architecture: 'x64' | 'arm64' };
+
+/** OS tools only; shared by SSH preflight and WSL's actual Linux environment. */
+export function posixRuntimeHostTargetProbe(marker: string): string {
+ requireMarker(marker);
+ return [
+ 'maka_os=$(uname -s) || exit 1',
+ 'maka_arch=$(uname -m) || exit 1',
+ 'maka_libc=',
+ 'case "$maka_os" in Linux) maka_libc=$(getconf GNU_LIBC_VERSION 2>/dev/null) || maka_libc=unknown ;; Darwin) if [ "$(sysctl -in sysctl.proc_translated 2>/dev/null)" = 1 ]; then maka_arch=arm64; fi ;; *) exit 127 ;; esac',
+ // Split the marker so shell diagnostics echoing this command cannot be
+ // mistaken for a result. Keep the command single-line for csh/tcsh SSH shells.
+ `printf '%s%s%s:%s:%s\\n' '__MAKA_RUNTIME_HOST_TARGET_' '${marker.slice('__MAKA_RUNTIME_HOST_TARGET_'.length)}' "$maka_os" "$maka_arch" "$maka_libc"`,
+ ].join('; ');
+}
+
+export function windowsRuntimeHostTargetProbe(marker: string): string {
+ requireMarker(marker);
+ const script = [
+ "$ErrorActionPreference = 'Stop'",
+ "$architecture = [Runtime.InteropServices.RuntimeInformation]::OSArchitecture.ToString()",
+ `[Console]::WriteLine('${marker}Windows:' + $architecture + ':')`,
+ ].join('; ');
+ return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${Buffer.from(script, 'utf16le').toString('base64')}`;
+}
+
+export function decodeRuntimeHostTarget(value: string): RuntimeHostTargetIdentity {
+ const [os, cpu, libc, ...extra] = value.split(':');
+ if (extra.length || libc === undefined) throw new Error('Invalid Runtime Host target result');
+ const architecture = cpu === 'x86_64' || cpu === 'X64' ? 'x64'
+ : cpu === 'aarch64' || cpu === 'arm64' || cpu === 'Arm64' ? 'arm64' : undefined;
+ if (!architecture) throw new Error(`Unsupported Runtime Host architecture: ${cpu}`);
+ if (os === 'Linux') {
+ const version = /^glibc ([0-9]+\.[0-9]+)$/u.exec(libc)?.[1];
+ if (!version) throw new Error('Native Linux Runtime Host requires GNU libc; target libc could not be verified');
+ return { platform: 'linux', architecture, glibcVersion: version };
+ }
+ if ((os === 'Darwin' || os === 'Windows') && libc === '') {
+ return { platform: os === 'Darwin' ? 'darwin' : 'win32', architecture };
+ }
+ throw new Error(`Unsupported Runtime Host operating system: ${os}`);
+}
+
+function requireMarker(marker: string): void {
+ if (!/^__MAKA_RUNTIME_HOST_TARGET_[0-9a-f]+__$/u.test(marker)) {
+ throw new Error('Invalid Runtime Host target marker');
+ }
+}
diff --git a/apps/desktop/src/main/runtime-host-wsl-controller.ts b/apps/desktop/src/main/runtime-host-wsl-controller.ts
index 59ef1a7af1..2ae7b7b0d7 100644
--- a/apps/desktop/src/main/runtime-host-wsl-controller.ts
+++ b/apps/desktop/src/main/runtime-host-wsl-controller.ts
@@ -18,6 +18,7 @@
*/
import { spawn, type ChildProcessWithoutNullStreams } from 'node:child_process';
+import { randomUUID } from 'node:crypto';
import type { Readable } from 'node:stream';
import {
normalizeRuntimeHostWslDistribution,
@@ -43,11 +44,36 @@ import {
} from '@maka/runtime-host/operator';
import { createRuntimeHostFramedOutputFilter } from './runtime-host-framed-output.js';
import type { DesktopRuntimeHostSetupPackage } from './runtime-host-setup-package.js';
+import { decodeRuntimeHostTarget, posixRuntimeHostTargetProbe, type RuntimeHostTargetIdentity } from './runtime-host-target.js';
const WSL_SETUP_TIMEOUT_MS = 10 * 60_000;
const WSL_SETUP_OUTPUT_MAX_BYTES = 64 * 1024;
const WSL_SETUP_STDERR_MAX_BYTES = 8 * 1024;
+export async function resolveDesktopRuntimeHostWslTarget(
+ input: { readonly distribution: string; readonly signal?: AbortSignal },
+ overrides: { readonly processFactory?: RuntimeHostWslProcessFactory; readonly wslExecutable?: string } = {},
+): Promise<Extract<RuntimeHostTargetIdentity, { platform: 'linux' }>> {
+ input.signal?.throwIfAborted();
+ const marker = `__MAKA_RUNTIME_HOST_TARGET_${randomUUID().replaceAll('-', '')}__`;
+ const child = (overrides.processFactory ?? spawnWsl)(
+ overrides.wslExecutable ?? resolveSystemRuntimeHostWslExecutable(),
+ ['--distribution', normalizeRuntimeHostWslDistribution(input.distribution), '--exec',
+ '/bin/sh', '-c', posixRuntimeHostTargetProbe(marker)],
+ );
+ const deadline = AbortSignal.timeout(30_000);
+ return runWslFramedProcess({
+ child, signal: input.signal ? AbortSignal.any([input.signal, deadline]) : deadline,
+ prefix: marker,
+ decode: (line) => decodeRuntimeHostTarget(line.slice(marker.length).replaceAll('\r', '').trimEnd()),
+ label: 'WSL Runtime Host target detection',
+ onFrame: (identity) => {
+ if (identity.platform !== 'linux') throw new Error('WSL did not report a Linux target');
+ return identity;
+ },
+ });
+}
+
type RuntimeHostSetupCompleteFrame = Extract<RuntimeHostSetupFrame, { kind: 'complete' | 'existing_environment' }>;
type RuntimeHostWslSetupCompleteFrame = Omit<RuntimeHostSetupCompleteFrame, 'operator'> & {
readonly operator: RuntimeHostNodeOperatorCommand<'posix'> | RuntimeHostNativeOperatorCommand<'posix'>;
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org