Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion sdk/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ child.on('close', (code) => console.log('exit:', code));

### 2. `spawnSandbox(script, policy, ...)` — convenience

Quick path for **process-isolation only** (`processcontainer` on Windows, `lxc` on Linux, `seatbelt` on macOS). Returns a `node-pty` `IPty` with merged stdout/stderr.
Quick path for **process isolation only**. The abstract `process` intent lands on ProcessContainer on Windows, Bubblewrap on Linux, and Seatbelt on macOS. Returns a `node-pty` `IPty` with merged stdout/stderr.

```typescript
import {
Expand Down
101 changes: 63 additions & 38 deletions sdk/node/src/bindings/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
type Pointer = unknown;
type NativeLibraryHandle = MxcNativeLibrary['handle'];
type NativeFreeCompletion = (error: Error | null) => void;
type NativeSpawnCompletion = (error: Error | null, status: number) => void;
type NativeWaitCompletion = (error: Error | null, status: number) => void;

const AbiSandbox = koffi.opaque('MxcSandbox');
Expand All @@ -49,7 +50,8 @@ export interface StreamingNativeFacade {
request: string,
outHandle: Pointer[],
error: AbiErrorDetail,
): number;
completion: NativeSpawnCompletion,
): void;
stateAwareExec(
request: string,
experimental: number,
Expand Down Expand Up @@ -86,7 +88,8 @@ function bindStreamingNativeFacade(
const sandboxPointer = koffi.pointer(AbiSandbox);

// Koffi exposes asynchronous invocation on the bound function object, so
// wait and free keep their raw bindings behind callback-shaped facade methods.
// spawn, wait and free keep their raw bindings behind callback-shaped facade
// methods.
const freeAsyncBinding = bindNativeFunction<
KoffiFunc<(sandbox: Pointer) => void>
>(
Expand All @@ -98,6 +101,22 @@ function bindStreamingNativeFacade(
},
);

// LXC preparation downloads an image and waits for a DHCP lease, so a
// synchronous call here would stall the event loop for tens of seconds.
const spawnAsyncBinding = bindNativeFunction<KoffiFunc<(
request: string,
outHandle: Pointer[],
error: AbiErrorDetail,
) => number>>(handle, {
symbol: 'mxc_spawn_request',
result: 'int32_t',
parameters: [
'const char *',
koffi.out(koffi.pointer(AbiSandbox, 2)),
koffi.out(koffi.pointer(AbiErrorDetailType)),
],
});

const waitAsyncBinding = bindNativeFunction<KoffiFunc<(
sandbox: Pointer,
outExit: number[],
Expand All @@ -113,15 +132,9 @@ function bindStreamingNativeFacade(
});

const native: StreamingNativeFacade = {
spawn: bindNativeFunction(handle, {
symbol: 'mxc_spawn_request',
result: 'int32_t',
parameters: [
'const char *',
koffi.out(koffi.pointer(AbiSandbox, 2)),
koffi.out(koffi.pointer(AbiErrorDetailType)),
],
}),
spawn(request, outHandle, error, completion) {
spawnAsyncBinding.async(request, outHandle, error, completion);
},

stateAwareExec: bindNativeFunction(handle, {
symbol: 'mxc_state_aware_exec',
Expand Down Expand Up @@ -364,15 +377,12 @@ function beginFailedSpawnCleanup(
void freeSandboxAsync(native, handle).catch(() => {});
}

/** Internal constructor with injectable native and stream dependencies. */
function createStreamingDriverFromSpawn(
function takeSpawnedHandle(
native: StreamingNativeFacade,
factory: NativeStreamFactory,
spawn: (outHandle: Pointer[], error: AbiErrorDetail) => number,
): NativeLifecycleDriver {
const outHandle: Pointer[] = [null];
const error = {} as AbiErrorDetail;
const status = spawn(outHandle, error);
outHandle: Pointer[],
error: AbiErrorDetail,
status: number,
): Pointer {
if (status !== 0) {
try {
throw nativeStatusError(status, error);
Expand All @@ -388,7 +398,14 @@ function createStreamingDriverFromSpawn(
'native runtime returned a null lifecycle handle',
);
}
return handle;
}

function adoptSpawnedHandle(
native: StreamingNativeFacade,
factory: NativeStreamFactory,
handle: Pointer,
): NativeLifecycleDriver {
let streams: NativeStdioStreams | undefined;
try {
const stdio = {} as NativeStdioHandles;
Expand Down Expand Up @@ -419,19 +436,24 @@ function createStreamingDriverFromSpawn(
}

/** Internal constructor with injectable native and stream dependencies. */
export function createStreamingDriver(
export async function createStreamingDriver(
request: RequestSpec,
native: StreamingNativeFacade,
factory: NativeStreamFactory,
): NativeLifecycleDriver {
return createStreamingDriverFromSpawn(
): Promise<NativeLifecycleDriver> {
const outHandle: Pointer[] = [null];
const error = {} as AbiErrorDetail;
const requestJson = JSON.stringify(request);
const status = await new Promise<number>((resolve, reject) => {
native.spawn(requestJson, outHandle, error, (failure, nativeStatus) => {
if (failure !== null) reject(failure);
else resolve(nativeStatus);
});
});
return adoptSpawnedHandle(
native,
factory,
(outHandle, error) => native.spawn(
JSON.stringify(request),
outHandle,
error,
),
takeSpawnedHandle(native, outHandle, error, status),
);
}

Expand All @@ -453,15 +475,15 @@ function ensureSupportedNodeVersion(): void {
});
}

function spawnDriver(request: RequestSpec): NativeLifecycleDriver {
function spawnDriver(request: RequestSpec): Promise<NativeLifecycleDriver> {
ensureSupportedNodeVersion();
return createStreamingDriver(request, getNative(), nodeStreamFactory);
}

export function spawnBindingSandboxProcess(
export async function spawnBindingSandboxProcess(
request: RequestSpec,
): MxcSandboxProcess {
const driver = spawnDriver(request);
): Promise<MxcSandboxProcess> {
const driver = await spawnDriver(request);
return createSandboxProcess(driver, request.policy.timeoutMs);
}

Expand All @@ -472,15 +494,18 @@ export function createStateAwareStreamingDriver(
native: StreamingNativeFacade,
factory: NativeStreamFactory,
): NativeLifecycleDriver {
return createStreamingDriverFromSpawn(
const outHandle: Pointer[] = [null];
const error = {} as AbiErrorDetail;
const status = native.stateAwareExec(
requestJson,
experimental ? 1 : 0,
outHandle,
error,
);
return adoptSpawnedHandle(
native,
factory,
(outHandle, error) => native.stateAwareExec(
requestJson,
experimental ? 1 : 0,
outHandle,
error,
),
takeSpawnedHandle(native, outHandle, error, status),
);
}

Expand Down
6 changes: 3 additions & 3 deletions sdk/node/tests/integration/native-streaming.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ interface RequestModule {
}

interface StreamingModule {
spawnBindingSandboxProcess(request: unknown): NativeSandbox;
spawnBindingSandboxProcess(request: unknown): Promise<NativeSandbox>;
}

const platformSupport = sdk.getPlatformSupport();
Expand Down Expand Up @@ -119,7 +119,7 @@ describe(`Internal native streaming (schema ${schemaVersion})`, { skip: skipReas
const request = requestModule.prepareRequestSpec(config, {
experimental: debugSpawnOptions.experimental,
});
const sandbox = streamingModule.spawnBindingSandboxProcess(request);
const sandbox = await streamingModule.spawnBindingSandboxProcess(request);
const standardInput = sandbox.standardInput;
const standardOutput = sandbox.standardOutput;
const standardError = sandbox.standardError;
Expand Down Expand Up @@ -184,7 +184,7 @@ describe(`Internal native streaming (schema ${schemaVersion})`, { skip: skipReas
const request = requestModule.prepareRequestSpec(config, {
experimental: debugSpawnOptions.experimental,
});
const sandbox = streamingModule.spawnBindingSandboxProcess(request);
const sandbox = await streamingModule.spawnBindingSandboxProcess(request);

const result = await sandbox.waitAsync();
assert.strictEqual(result.exitCode, 0);
Expand Down
Loading
Loading