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
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 5 additions & 3 deletions examples/remote-container-bridge/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ This example is the smallest real remote-container demo: open the UI, click
**Run container**, and watch stdout stream back from a container while the
process is still running.

It hosts the `@inbrowser/sandbox/remote` protocol over a Bun WebSocket server
and renders the demo through the shared React/Vite example shell.
It hosts the `@inbrowser/sandbox/remote` protocol through
`startRemoteContainerBridge`, auto-selects the Bun host while the demo runs under
Bun, runs Apple `container` when available, and renders through the shared
React/Vite example shell.

```bash
bun run --cwd examples/remote-container-bridge start
Expand All @@ -16,7 +18,7 @@ session, runs the sample command through the configured provider, appends
streamed `run.output` artifacts to the terminal, and closes the session when the
command exits. Diagnostics are available in the secondary Details tab.

The Bun bridge host listens on <http://127.0.0.1:8790>; Vite proxies
The bridge host listens on <http://127.0.0.1:8790>; Vite proxies
`/bridge-config`, `/status`, `/bridge`, and authenticated forwarded port URLs to
it.

Expand Down
22 changes: 9 additions & 13 deletions examples/remote-container-bridge/src/dev.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { startRemoteContainerBridge } from '@inbrowser/sandbox/remote/host';
import { readRemoteContainerBridgeEnv } from './env.js';
import { startBridgeHostServer } from './server.js';

const here = dirname(fileURLToPath(import.meta.url));
const exampleRoot = resolve(here, '..');
const viteEntry = fileURLToPath(import.meta.resolve('vite'));
const viteBin = resolve(dirname(viteEntry), '..', '..', 'bin', 'vite.js');
const { provider, bridgePort, uiPort, allowedOrigins } = readRemoteContainerBridgeEnv();
const { bridgeOptions, uiPort } = readRemoteContainerBridgeEnv();
const uiUrl = `http://127.0.0.1:${uiPort}/`;

const bridge = await startBridgeHostServer({
provider,
port: bridgePort,
allowedOrigins,
const bridge = await startRemoteContainerBridge({
...bridgeOptions,
uiUrl,
});

Expand All @@ -23,29 +21,27 @@ const vite = Bun.spawn(
cwd: exampleRoot,
env: {
...process.env,
REMOTE_CONTAINER_BRIDGE_TARGET: `http://127.0.0.1:${bridgePort}`,
REMOTE_CONTAINER_BRIDGE_TARGET: bridge.origin,
REMOTE_CONTAINER_UI_PORT: String(uiPort),
},
stdout: 'inherit',
stderr: 'inherit',
},
);

console.log(`remote container bridge API listening on http://127.0.0.1:${bridge.port}`);
console.log(`remote container bridge API listening on ${bridge.origin}`);
console.log(`remote container bridge UI listening on ${uiUrl}`);
console.log(`provider=${provider.kind}`);
console.log(`provider=${bridge.provider} host=${bridge.host}`);

vite.exited.then(async (code) => {
await bridge.closeSessions();
bridge.stop(true);
await bridge.stop();
if (code !== 0) process.exit(code);
});

for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, async () => {
vite.kill(signal);
await bridge.closeSessions();
bridge.stop(true);
await bridge.stop();
process.exit(0);
});
}
19 changes: 10 additions & 9 deletions examples/remote-container-bridge/src/env.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { createAppleContainerProvider } from './providers/apple-container.js';
import type { StartRemoteContainerBridgeOptions } from '@inbrowser/sandbox/remote/host';
import { createFakeContainerProvider } from './providers/fake.js';
import type { ContainerSandboxProvider } from './providers/types.js';

export interface RemoteContainerBridgeEnv {
provider: ContainerSandboxProvider;
bridgeOptions: StartRemoteContainerBridgeOptions;
providerName: string;
bridgePort: number;
uiPort: number;
allowedOrigins: string[];
image: string;
}

Expand All @@ -22,10 +20,13 @@ export function readRemoteContainerBridgeEnv(): RemoteContainerBridgeEnv {
const uiOrigins = [`http://127.0.0.1:${uiPort}`, `http://localhost:${uiPort}`];
const allowedOrigins = explicitOrigins?.length ? explicitOrigins : uiOrigins;

const provider =
providerName === 'apple'
? createAppleContainerProvider({ image })
: createFakeContainerProvider();
const bridgeOptions: StartRemoteContainerBridgeOptions = {
image,
provider: providerName === 'fake' ? createFakeContainerProvider() : 'auto',
host: 'auto',
port: bridgePort,
allowedOrigins,
};

return { provider, providerName, bridgePort, uiPort, allowedOrigins, image };
return { bridgeOptions, providerName, bridgePort, uiPort, image };
}
16 changes: 7 additions & 9 deletions examples/remote-container-bridge/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { startRemoteContainerBridge } from '@inbrowser/sandbox/remote/host';
import { readRemoteContainerBridgeEnv } from './env.js';
import { startBridgeHostServer } from './server.js';

const { provider, bridgePort, allowedOrigins } = readRemoteContainerBridgeEnv();
const { bridgeOptions } = readRemoteContainerBridgeEnv();

const server = await startBridgeHostServer({ provider, port: bridgePort, allowedOrigins });
const bridge = await startRemoteContainerBridge(bridgeOptions);

console.log(
`remote container bridge listening on ws://127.0.0.1:${server.port} (${provider.kind})`,
);
console.log(`remote container bridge API available at http://127.0.0.1:${server.port}`);
console.log(`remote container bridge listening on ${bridge.origin}${bridge.bridgeUrl}`);
console.log(`remote container bridge API available at ${bridge.origin}`);
console.log(`provider=${bridge.provider} host=${bridge.host}`);

for (const signal of ['SIGINT', 'SIGTERM'] as const) {
process.on(signal, async () => {
await server.closeSessions();
server.stop(true);
await bridge.stop();
process.exit(0);
});
}
4 changes: 2 additions & 2 deletions examples/remote-container-bridge/src/providers/fake.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import type { SandboxFileEvent, SandboxSnapshot } from '@inbrowser/sandbox';
import { createMemoryFileSystem } from '@inbrowser/workspace/fs';
import type {
ContainerSandboxProvider,
ContainerSession,
ContainerSessionOptions,
} from './types.js';
} from '@inbrowser/sandbox/remote/host';
import { createMemoryFileSystem } from '@inbrowser/workspace/fs';

export function createFakeContainerProvider(): ContainerSandboxProvider {
return {
Expand Down
61 changes: 0 additions & 61 deletions examples/remote-container-bridge/src/providers/types.ts

This file was deleted.

108 changes: 27 additions & 81 deletions examples/remote-container-bridge/test/demo.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { describe, expect, test } from 'bun:test';
import { type BridgeEnvelope, REMOTE_PROTOCOL_TYPES } from '@inbrowser/sandbox/remote';
import { createAppleContainerProvider } from '../src/providers/apple-container.js';
import {
type BridgeEnvelope,
REMOTE_PROTOCOL_TYPES,
createRemoteSandbox,
} from '@inbrowser/sandbox/remote';
import { startBridgeHostServer } from '@inbrowser/sandbox/remote/bun';
import { startRemoteContainerBridge } from '@inbrowser/sandbox/remote/host';
import { createFakeContainerProvider } from '../src/providers/fake.js';
import { startBridgeHostServer } from '../src/server.js';

describe('remote container bridge demo', () => {
test('serves bridge config and streams run output over WebSocket', async () => {
Expand Down Expand Up @@ -109,8 +113,7 @@ describe('remote container bridge demo', () => {
});
}
} finally {
await server.closeSessions();
server.stop(true);
await server.stop();
}
});

Expand All @@ -133,8 +136,7 @@ describe('remote container bridge demo', () => {
);
expect(badOrigin.status).toBe(403);
} finally {
await server.closeSessions();
server.stop(true);
await server.stop();
}
});

Expand Down Expand Up @@ -193,8 +195,7 @@ describe('remote container bridge demo', () => {
expect(await proxied.text()).toBe('proxied container response');
} finally {
socket.close();
await server.closeSessions();
server.stop(true);
await server.stop();
upstream.stop(true);
}

Expand Down Expand Up @@ -224,89 +225,34 @@ describe('remote container bridge demo', () => {
appleIntegrationTest(
'streams output from a real Apple container and cleans up the session',
async () => {
const server = await startBridgeHostServer({
provider: createAppleContainerProvider({
image: process.env.REMOTE_CONTAINER_IMAGE ?? 'ubuntu:latest',
}),
const bridge = await startRemoteContainerBridge({
image: process.env.REMOTE_CONTAINER_IMAGE ?? 'ubuntu:latest',
provider: 'auto',
host: 'bun',
port: 0,
});
const sessionId = `apple-test-${Date.now().toString(36)}`;
const socket = new WebSocket(
`ws://127.0.0.1:${server.port}/bridge?sessionId=${sessionId}&role=browser&token=${server.bridgeToken}`,
);
const pending = new Map<
string,
{
resolve(value: unknown): void;
reject(err: Error): void;
}
>();
const chunks: string[] = [];
let counter = 0;

socket.addEventListener('message', (event) => {
const envelope = JSON.parse(String(event.data)) as BridgeEnvelope;
if (
envelope.kind === 'event' &&
(envelope.payload as { type?: string }).type === 'artifact'
) {
const artifact = (envelope.payload as { artifact?: { kind?: string; chunk?: string } })
.artifact;
if (artifact?.kind === 'run.output' && artifact.chunk) chunks.push(artifact.chunk);
}
if ((envelope.kind === 'response' || envelope.kind === 'error') && envelope.replyTo) {
const waiting = pending.get(envelope.replyTo);
if (!waiting) return;
pending.delete(envelope.replyTo);
if (envelope.kind === 'error') {
waiting.reject(new Error(String((envelope.payload as { message?: string }).message)));
} else {
waiting.resolve(envelope.payload);
}
}
});

try {
await new Promise<void>((resolve, reject) => {
socket.addEventListener('open', () => resolve(), { once: true });
socket.addEventListener('error', () => reject(new Error('websocket failed')), {
once: true,
});
expect(bridge.provider).toBe('apple-container');
const sandbox = await createRemoteSandbox({
id: `apple-test-${Date.now().toString(36)}`,
transport: bridge.createWebSocketProvider(),
requestTimeoutMs: 120_000,
});
sandbox.on((event) => {
if (event.type === 'artifact' && event.artifact.kind === 'run.output') {
chunks.push(String(event.artifact.chunk));
}
});
const session = (await request(REMOTE_PROTOCOL_TYPES.sessionCreate, {
root: '/work',
})) as { root: string };
const result = (await request(REMOTE_PROTOCOL_TYPES.runStart, {
command: 'printf "real-bridge-stream\\n"',
options: { cwd: session.root },
})) as { exitCode: number; stdout: string };
const result = await sandbox.runtime.run('printf "real-bridge-stream\\n"');

expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('real-bridge-stream');
expect(chunks.join('')).toContain('real-bridge-stream');
sandbox.destroy();
} finally {
socket.close();
await server.closeSessions();
server.stop(true);
}

function request(type: string, payload: unknown): Promise<unknown> {
counter += 1;
const id = `apple-test-${counter}`;
socket.send(
JSON.stringify({
id,
sessionId,
kind: 'request',
type,
sentAt: Date.now(),
peer: 'browser',
payload,
} satisfies BridgeEnvelope),
);
return new Promise((resolve, reject) => {
pending.set(id, { resolve, reject });
});
await bridge.stop();
}
},
120_000,
Expand Down
Loading
Loading