wasm-idle 1.0.12 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +150 -37
- package/dist/playground/applicationAssets.js +2 -0
- package/dist/playground/assetBridge.js +106 -31
- package/dist/playground/assets.d.ts +2 -2
- package/dist/playground/c3.js +22 -0
- package/dist/playground/c3Memory.d.ts +19 -0
- package/dist/playground/c3Memory.js +117 -0
- package/dist/playground/cachedClang.js +143 -34
- package/dist/playground/clangAssetIntegrity.js +6 -6
- package/dist/playground/clangWorkerProtocol.d.ts +12 -1
- package/dist/playground/cobol.d.ts +3 -0
- package/dist/playground/cobol.js +10 -1
- package/dist/playground/commonlisp.d.ts +8 -0
- package/dist/playground/commonlisp.js +33 -0
- package/dist/playground/commonlispAssets.d.ts +30 -0
- package/dist/playground/commonlispAssets.js +184 -0
- package/dist/playground/compiledArtifactCache.d.ts +3 -1
- package/dist/playground/compiledArtifactCache.js +31 -5
- package/dist/playground/container/ContainerExplorer.svelte +307 -0
- package/dist/playground/container/ContainerExplorer.svelte.d.ts +14 -0
- package/dist/playground/container/binding.d.ts +26 -0
- package/dist/playground/container/binding.js +247 -0
- package/dist/playground/container/clang-runtime.d.ts +38 -0
- package/dist/playground/container/clang-runtime.js +343 -0
- package/dist/playground/container/client.d.ts +15 -0
- package/dist/playground/container/client.js +189 -0
- package/dist/playground/container/explorer.d.ts +31 -0
- package/dist/playground/container/explorer.js +112 -0
- package/dist/playground/container/owner.worker.d.ts +1 -0
- package/dist/playground/container/owner.worker.js +390 -0
- package/dist/playground/container/protocol.d.ts +73 -0
- package/dist/playground/container/protocol.js +28 -0
- package/dist/playground/container/release.d.ts +31 -0
- package/dist/playground/container/release.js +612 -0
- package/dist/playground/container/runtime-adapter.d.ts +73 -0
- package/dist/playground/container/runtime-adapter.js +474 -0
- package/dist/playground/container/shell-control.d.ts +49 -0
- package/dist/playground/container/shell-control.js +303 -0
- package/dist/playground/container/workspace.d.ts +8 -0
- package/dist/playground/container/workspace.js +29 -0
- package/dist/playground/d.d.ts +6 -1
- package/dist/playground/d.js +83 -5
- package/dist/playground/dotnet.js +6 -8
- package/dist/playground/fortran.d.ts +3 -0
- package/dist/playground/fortran.js +10 -1
- package/dist/playground/go.js +14 -3
- package/dist/playground/goDebugInstrumentation.js +184 -54
- package/dist/playground/index.d.ts +1 -1
- package/dist/playground/index.js +14 -0
- package/dist/playground/java.js +3 -2
- package/dist/playground/javaRuntimeErrors.d.ts +9 -0
- package/dist/playground/javaRuntimeErrors.js +60 -0
- package/dist/playground/javaRuntimeStdin.d.ts +9 -0
- package/dist/playground/javaRuntimeStdin.js +145 -0
- package/dist/playground/lean4.d.ts +13 -0
- package/dist/playground/lean4.js +50 -0
- package/dist/playground/lean4Assets.d.ts +25 -0
- package/dist/playground/lean4Assets.js +123 -0
- package/dist/playground/lfortran.js +10 -0
- package/dist/playground/lfortranAssets.d.ts +2 -2
- package/dist/playground/loadingProgress.js +4 -1
- package/dist/playground/nim.js +22 -0
- package/dist/playground/objectivec.d.ts +3 -0
- package/dist/playground/objectivec.js +11 -2
- package/dist/playground/options.d.ts +2 -0
- package/dist/playground/python.js +8 -0
- package/dist/playground/runtimeAssetCache.d.ts +1 -32
- package/dist/playground/runtimeAssetCache.js +1 -193
- package/dist/playground/runtimeSession.d.ts +2 -2
- package/dist/playground/runtimeSession.js +11 -11
- package/dist/playground/rust.d.ts +6 -0
- package/dist/playground/rust.js +103 -6
- package/dist/playground/staticStdinRing.d.ts +2 -1
- package/dist/playground/staticStdinRing.js +21 -10
- package/dist/playground/staticWorkerRuntime.d.ts +15 -1
- package/dist/playground/staticWorkerRuntime.js +119 -17
- package/dist/playground/tinygo.d.ts +3 -0
- package/dist/playground/tinygo.js +81 -6
- package/dist/playground/v.d.ts +3 -0
- package/dist/playground/v.js +10 -1
- package/dist/playground/wasmAwkVersion.d.ts +13 -13
- package/dist/playground/wasmAwkVersion.js +4 -4
- package/dist/playground/wasmBqnVersion.d.ts +2 -2
- package/dist/playground/wasmBqnVersion.js +2 -2
- package/dist/playground/wasmC3Version.d.ts +2 -2
- package/dist/playground/wasmC3Version.js +2 -2
- package/dist/playground/wasmClojureScriptVersion.d.ts +2 -2
- package/dist/playground/wasmClojureScriptVersion.js +2 -2
- package/dist/playground/wasmCommonLispVersion.d.ts +32 -0
- package/dist/playground/wasmCommonLispVersion.js +33 -0
- package/dist/playground/wasmDIntegrity.d.ts +5 -5
- package/dist/playground/wasmDIntegrity.js +5 -5
- package/dist/playground/wasmDVersion.d.ts +16 -1
- package/dist/playground/wasmDVersion.js +16 -1
- package/dist/playground/wasmDebugVersion.d.ts +1 -1
- package/dist/playground/wasmDebugVersion.js +1 -1
- package/dist/playground/wasmForthVersion.d.ts +2 -2
- package/dist/playground/wasmForthVersion.js +2 -2
- package/dist/playground/wasmGleamVersion.d.ts +2 -2
- package/dist/playground/wasmGleamVersion.js +2 -2
- package/dist/playground/wasmGrainVersion.d.ts +2 -2
- package/dist/playground/wasmGrainVersion.js +2 -2
- package/dist/playground/wasmJVersion.d.ts +2 -2
- package/dist/playground/wasmJVersion.js +2 -2
- package/dist/playground/wasmJanetVersion.js +2 -2
- package/dist/playground/wasmJuliaVersion.js +2 -2
- package/dist/playground/wasmLean4Version.d.ts +110 -0
- package/dist/playground/wasmLean4Version.js +111 -0
- package/dist/playground/wasmLfortranVersion.d.ts +2 -2
- package/dist/playground/wasmLfortranVersion.js +2 -2
- package/dist/playground/wasmNimVersion.d.ts +9 -9
- package/dist/playground/wasmNimVersion.js +6 -6
- package/dist/playground/wasmOcamlVersion.d.ts +2 -2
- package/dist/playground/wasmOcamlVersion.js +1 -1
- package/dist/playground/wasmPascalVersion.js +2 -2
- package/dist/playground/wasmPerlVersion.js +2 -2
- package/dist/playground/wasmPrologVersion.d.ts +2 -2
- package/dist/playground/wasmPrologVersion.js +2 -2
- package/dist/playground/wasmReScriptVersion.d.ts +2 -2
- package/dist/playground/wasmReScriptVersion.js +2 -2
- package/dist/playground/wasmRustVersion.d.ts +8 -8
- package/dist/playground/wasmRustVersion.js +7 -7
- package/dist/playground/wasmTclVersion.d.ts +4 -4
- package/dist/playground/wasmTclVersion.js +2 -2
- package/dist/playground/wasmTinyGoVersion.d.ts +14 -14
- package/dist/playground/wasmTinyGoVersion.js +14 -14
- package/dist/playground/wasmTypeScriptVersion.d.ts +5 -5
- package/dist/playground/wasmTypeScriptVersion.js +5 -5
- package/dist/playground/worker/cachedClangCompile.js +52 -4
- package/dist/playground/worker/cachedClangExecute.js +6 -5
- package/dist/playground/worker/clang.js +32 -10
- package/dist/playground/worker/cobol.js +2 -2
- package/dist/playground/worker/d.js +12 -5
- package/dist/playground/worker/fortran.js +2 -2
- package/dist/playground/worker/go.js +16 -5
- package/dist/playground/worker/haskell.js +10 -7
- package/dist/playground/worker/java.js +92 -39
- package/dist/playground/worker/lua.js +2 -3
- package/dist/playground/worker/php.js +15 -9
- package/dist/playground/worker/python.js +131 -131
- package/dist/playground/worker/pythonDebugPreview.d.ts +2 -0
- package/dist/playground/worker/pythonDebugPreview.js +55 -0
- package/dist/playground/worker/pythonExecution.d.ts +8 -0
- package/dist/playground/worker/pythonExecution.js +183 -0
- package/dist/playground/worker/pythonStdio.d.ts +32 -0
- package/dist/playground/worker/pythonStdio.js +253 -0
- package/dist/playground/worker/pythonWorkspaceFiles.d.ts +6 -0
- package/dist/playground/worker/pythonWorkspaceFiles.js +26 -0
- package/dist/playground/worker/pythonWorkspaceImports.d.ts +4 -0
- package/dist/playground/worker/pythonWorkspaceImports.js +129 -0
- package/dist/playground/worker/r.js +13 -6
- package/dist/playground/worker/rust.js +34 -2
- package/dist/playground/worker/sqlite.js +4 -3
- package/dist/playground/worker/tinygo.js +11 -3
- package/dist/playground/worker/typescript.js +2 -3
- package/dist/playground/worker/v.js +2 -2
- package/dist/playground/worker/wat.js +2 -3
- package/dist/playground/worker/zig.js +20 -6
- package/dist/playground/workerSession.d.ts +1 -28
- package/dist/playground/workerSession.js +1 -164
- package/package.json +30 -15
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import { createContainerWorkerClient } from './client';
|
|
2
|
+
const copyFiles = (files) => files.map((file) => ({
|
|
3
|
+
path: file.path,
|
|
4
|
+
content: typeof file.content === 'string' ? file.content : Uint8Array.from(file.content)
|
|
5
|
+
}));
|
|
6
|
+
const bytes = (content) => typeof content === 'string' ? new TextEncoder().encode(content) : content;
|
|
7
|
+
const equal = (left, right) => {
|
|
8
|
+
const a = bytes(left), b = bytes(right);
|
|
9
|
+
return a.length === b.length && a.every((byte, index) => byte === b[index]);
|
|
10
|
+
};
|
|
11
|
+
function sameFiles(left, right) {
|
|
12
|
+
return (left.length === right.length &&
|
|
13
|
+
left.every((file) => {
|
|
14
|
+
const other = right.find((candidate) => candidate.path === file.path);
|
|
15
|
+
return other && equal(file.content, other.content);
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
/** Existing Terminal/Debug controls use this facade; every execution belongs to the owner Container. */
|
|
19
|
+
export function createContainerPlaygroundBinding(runtimeAssets, options) {
|
|
20
|
+
let client, disposed = false;
|
|
21
|
+
const sandboxes = new Map();
|
|
22
|
+
const published = new Map();
|
|
23
|
+
const changes = new Set();
|
|
24
|
+
const queues = new Map();
|
|
25
|
+
let shellOutput;
|
|
26
|
+
let shellError;
|
|
27
|
+
let shellConnection;
|
|
28
|
+
let requestedShellLanguage;
|
|
29
|
+
const decoders = new Map();
|
|
30
|
+
const decoder = (key) => {
|
|
31
|
+
let value = decoders.get(key);
|
|
32
|
+
if (!value)
|
|
33
|
+
decoders.set(key, (value = new TextDecoder()));
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
const connection = () => {
|
|
37
|
+
if (disposed)
|
|
38
|
+
throw new Error('Container playground has been disposed');
|
|
39
|
+
if (client)
|
|
40
|
+
return client;
|
|
41
|
+
client =
|
|
42
|
+
options.clientFactory?.() ??
|
|
43
|
+
createContainerWorkerClient({
|
|
44
|
+
runtimeAssets,
|
|
45
|
+
applicationRootURL: options.applicationRootURL,
|
|
46
|
+
workspaceId: options.workspaceId
|
|
47
|
+
});
|
|
48
|
+
client.onEvent((event) => {
|
|
49
|
+
if (event.name === 'workspace') {
|
|
50
|
+
for (const listener of changes)
|
|
51
|
+
listener(event);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (event.name === 'shell-output') {
|
|
55
|
+
const data = event.data;
|
|
56
|
+
if (data.error)
|
|
57
|
+
shellError?.(new Error(data.error));
|
|
58
|
+
if (data.bytes)
|
|
59
|
+
shellOutput?.(decoder(`shell:${data.stream}`).decode(data.bytes, { stream: true }));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const sandbox = event.language && sandboxes.get(event.language);
|
|
63
|
+
if (!sandbox)
|
|
64
|
+
return;
|
|
65
|
+
if (event.name === 'output')
|
|
66
|
+
sandbox.output?.(typeof event.data === 'string'
|
|
67
|
+
? event.data
|
|
68
|
+
: decoder(event.language).decode(event.data, {
|
|
69
|
+
stream: true
|
|
70
|
+
}));
|
|
71
|
+
if (event.name === 'debug')
|
|
72
|
+
sandbox.ondebug?.(event.data);
|
|
73
|
+
if (event.name === 'diagnostic')
|
|
74
|
+
sandbox.oncompilerdiagnostic?.(event.data);
|
|
75
|
+
if (event.name === 'image')
|
|
76
|
+
sandbox.image?.(event.data);
|
|
77
|
+
});
|
|
78
|
+
return client;
|
|
79
|
+
};
|
|
80
|
+
const syncWorkspace = (language, files) => {
|
|
81
|
+
const snapshot = copyFiles(files);
|
|
82
|
+
const previous = queues.get(language) ?? Promise.resolve();
|
|
83
|
+
const next = previous
|
|
84
|
+
.catch(() => { })
|
|
85
|
+
.then(async () => {
|
|
86
|
+
const baseline = published.get(language);
|
|
87
|
+
if (baseline && sameFiles(baseline, snapshot))
|
|
88
|
+
return;
|
|
89
|
+
await connection().call('syncWorkspace', [
|
|
90
|
+
language,
|
|
91
|
+
snapshot,
|
|
92
|
+
baseline && copyFiles(baseline)
|
|
93
|
+
]);
|
|
94
|
+
published.set(language, copyFiles(snapshot));
|
|
95
|
+
});
|
|
96
|
+
queues.set(language, next);
|
|
97
|
+
return next;
|
|
98
|
+
};
|
|
99
|
+
const shell = {
|
|
100
|
+
connect(output, error) {
|
|
101
|
+
shellOutput = output;
|
|
102
|
+
shellError = error;
|
|
103
|
+
return (shellConnection ??= (async () => {
|
|
104
|
+
const language = requestedShellLanguage ?? options.getLanguage?.() ?? 'CPP';
|
|
105
|
+
await queues.get(language)?.catch(() => { });
|
|
106
|
+
await connection().call('shellStart', [
|
|
107
|
+
{ cwd: `/workspace/${language}`, env: { PS1: '\\w $ ' } }
|
|
108
|
+
]);
|
|
109
|
+
})());
|
|
110
|
+
},
|
|
111
|
+
async write(text) {
|
|
112
|
+
await shellConnection;
|
|
113
|
+
await connection().call('shellWrite', [text]);
|
|
114
|
+
},
|
|
115
|
+
async eof() {
|
|
116
|
+
await shellConnection;
|
|
117
|
+
await connection().call('shellEof');
|
|
118
|
+
},
|
|
119
|
+
async interrupt() {
|
|
120
|
+
await shellConnection;
|
|
121
|
+
await connection().call('shellInterrupt');
|
|
122
|
+
},
|
|
123
|
+
async dispose() {
|
|
124
|
+
if (client && !disposed)
|
|
125
|
+
await client.call('shellDispose');
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
const binding = {
|
|
129
|
+
async setShellDirectory(language) {
|
|
130
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(language))
|
|
131
|
+
throw new TypeError('Invalid shell language');
|
|
132
|
+
requestedShellLanguage = language;
|
|
133
|
+
// Selecting a language before Terminal mounts must not start a temporary shell.
|
|
134
|
+
if (!shellConnection)
|
|
135
|
+
return;
|
|
136
|
+
await shellConnection;
|
|
137
|
+
await connection().call('shellSetDirectory', [language]);
|
|
138
|
+
},
|
|
139
|
+
runtimeAssets,
|
|
140
|
+
terminalProps: undefined,
|
|
141
|
+
shell,
|
|
142
|
+
async load(language) {
|
|
143
|
+
let sandbox = sandboxes.get(language);
|
|
144
|
+
if (sandbox)
|
|
145
|
+
return sandbox;
|
|
146
|
+
const invoke = (method, args = [], execution, progress) => connection().call(method, [language, ...args], {
|
|
147
|
+
signal: execution?.signal,
|
|
148
|
+
progress
|
|
149
|
+
});
|
|
150
|
+
const record = {
|
|
151
|
+
runtimeAssets,
|
|
152
|
+
elapse: 0,
|
|
153
|
+
async load(code, log = true, args = [], execution = {}, progress) {
|
|
154
|
+
await queues.get(language);
|
|
155
|
+
await invoke('load', [code, log, args, execution], execution, progress);
|
|
156
|
+
},
|
|
157
|
+
async run(code, prepare, log = true, progress, args = [], execution = {}) {
|
|
158
|
+
await queues.get(language);
|
|
159
|
+
const started = performance.now();
|
|
160
|
+
try {
|
|
161
|
+
return await invoke(prepare ? 'prepare' : 'run', [code, log, args, execution], execution, progress);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
record.elapse = Math.round(performance.now() - started);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
async clear() {
|
|
168
|
+
await invoke('clear');
|
|
169
|
+
},
|
|
170
|
+
async restart() {
|
|
171
|
+
await invoke('restart');
|
|
172
|
+
},
|
|
173
|
+
async terminate() {
|
|
174
|
+
await invoke('stop');
|
|
175
|
+
},
|
|
176
|
+
async kill() {
|
|
177
|
+
await invoke('stop');
|
|
178
|
+
},
|
|
179
|
+
async dispose() {
|
|
180
|
+
if (client && !disposed)
|
|
181
|
+
await invoke('clear');
|
|
182
|
+
},
|
|
183
|
+
write(text) {
|
|
184
|
+
void invoke('write', [text]).catch((error) => shellError?.(error));
|
|
185
|
+
},
|
|
186
|
+
eof() {
|
|
187
|
+
void invoke('eof').catch((error) => shellError?.(error));
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
for (const method of [
|
|
191
|
+
'debugCommand',
|
|
192
|
+
'debugPause',
|
|
193
|
+
'setBreakpoints',
|
|
194
|
+
'debugEvaluate',
|
|
195
|
+
'debugVariables',
|
|
196
|
+
'debugScopes',
|
|
197
|
+
'debugFrameScopes',
|
|
198
|
+
'debugFrameName',
|
|
199
|
+
'debugReadMemory',
|
|
200
|
+
'debugWriteMemory',
|
|
201
|
+
'debugDataBreakpointInfo',
|
|
202
|
+
'debugSetDataBreakpoints'
|
|
203
|
+
]) {
|
|
204
|
+
record[method] = (...args) => invoke(method, args);
|
|
205
|
+
}
|
|
206
|
+
sandboxes.set(language, record);
|
|
207
|
+
return record;
|
|
208
|
+
},
|
|
209
|
+
// Owner and command index can be ready without fetching compiler bodies at page boot.
|
|
210
|
+
async prewarm() {
|
|
211
|
+
connection();
|
|
212
|
+
return true;
|
|
213
|
+
},
|
|
214
|
+
async setPrewarmEnabled() { },
|
|
215
|
+
async cancelPrewarm() { },
|
|
216
|
+
syncWorkspace,
|
|
217
|
+
async readWorkspace(language) {
|
|
218
|
+
await queues.get(language)?.catch(() => { });
|
|
219
|
+
return await connection().call('readWorkspace', [language]);
|
|
220
|
+
},
|
|
221
|
+
async listWorkspaceFiles() {
|
|
222
|
+
return await connection().call('listWorkspaceFiles');
|
|
223
|
+
},
|
|
224
|
+
publishedWorkspace(language) {
|
|
225
|
+
return copyFiles(published.get(language) ?? []);
|
|
226
|
+
},
|
|
227
|
+
acceptWorkspace(language, files) {
|
|
228
|
+
published.set(language, copyFiles(files));
|
|
229
|
+
},
|
|
230
|
+
onWorkspaceChange(listener) {
|
|
231
|
+
changes.add(listener);
|
|
232
|
+
return () => changes.delete(listener);
|
|
233
|
+
},
|
|
234
|
+
async inspect() {
|
|
235
|
+
return await connection().call('inspect');
|
|
236
|
+
},
|
|
237
|
+
async dispose() {
|
|
238
|
+
if (disposed)
|
|
239
|
+
return;
|
|
240
|
+
disposed = true;
|
|
241
|
+
changes.clear();
|
|
242
|
+
await client?.dispose();
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
binding.terminalProps = { playground: binding, runtimeAssets };
|
|
246
|
+
return binding;
|
|
247
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Container } from '@wasm-idle/container';
|
|
2
|
+
import { type SandboxExecutionOptions } from '../options';
|
|
3
|
+
import type { ProgressLike } from '@wasm-idle/core';
|
|
4
|
+
import type { ContainerWorkerEvent } from './protocol';
|
|
5
|
+
import type { WebsiteRuntimeAdapter } from './runtime-adapter';
|
|
6
|
+
type Profile = {
|
|
7
|
+
container: Container;
|
|
8
|
+
prepareCommand(invocation: unknown, signal: AbortSignal): Promise<void>;
|
|
9
|
+
};
|
|
10
|
+
/** The Run button uses the same real Clang command and artifacts as Bash. Debug uses its qualified backend. */
|
|
11
|
+
export declare class WebsiteClangRuntime {
|
|
12
|
+
#private;
|
|
13
|
+
private readonly profile;
|
|
14
|
+
private readonly files;
|
|
15
|
+
private readonly emit;
|
|
16
|
+
constructor(profile: Profile, files: WebsiteRuntimeAdapter, emit: (event: ContainerWorkerEvent) => void);
|
|
17
|
+
private queue;
|
|
18
|
+
private key;
|
|
19
|
+
private drain;
|
|
20
|
+
private success;
|
|
21
|
+
private headerKey;
|
|
22
|
+
private discardHeader;
|
|
23
|
+
prepare(language: string, code: string, args: string[], options: SandboxExecutionOptions, signal: AbortSignal, progress: ProgressLike | undefined, requestId: number): Promise<boolean>;
|
|
24
|
+
run(language: string, code: string, args: string[], options: SandboxExecutionOptions, signal: AbortSignal, progress: ProgressLike | undefined, requestId: number): Promise<boolean>;
|
|
25
|
+
private flush;
|
|
26
|
+
write(language: string, text: string): Promise<void>;
|
|
27
|
+
eof(language: string): Promise<void>;
|
|
28
|
+
stop(language: string): Promise<void>;
|
|
29
|
+
clear(language: string): void;
|
|
30
|
+
inspect(): {
|
|
31
|
+
language: string;
|
|
32
|
+
phase: "run" | "prepare";
|
|
33
|
+
pid: number | undefined;
|
|
34
|
+
filesystem: string;
|
|
35
|
+
}[];
|
|
36
|
+
dispose(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { resolveSandboxExecutionArgs } from '../options';
|
|
2
|
+
/** The Run button uses the same real Clang command and artifacts as Bash. Debug uses its qualified backend. */
|
|
3
|
+
export class WebsiteClangRuntime {
|
|
4
|
+
profile;
|
|
5
|
+
files;
|
|
6
|
+
emit;
|
|
7
|
+
#active = new Map();
|
|
8
|
+
#prepared = new Map();
|
|
9
|
+
#input = new Map();
|
|
10
|
+
#header;
|
|
11
|
+
constructor(profile, files, emit) {
|
|
12
|
+
this.profile = profile;
|
|
13
|
+
this.files = files;
|
|
14
|
+
this.emit = emit;
|
|
15
|
+
}
|
|
16
|
+
queue(language) {
|
|
17
|
+
let value = this.#input.get(language);
|
|
18
|
+
if (!value)
|
|
19
|
+
this.#input.set(language, (value = { chunks: [], size: 0, eof: false }));
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
key(language, code, args, options) {
|
|
23
|
+
return JSON.stringify([
|
|
24
|
+
language,
|
|
25
|
+
code,
|
|
26
|
+
args,
|
|
27
|
+
options.cppVersion,
|
|
28
|
+
options.cVersion,
|
|
29
|
+
options.compileArgs,
|
|
30
|
+
options.activePath,
|
|
31
|
+
options.env,
|
|
32
|
+
options.workspaceFiles
|
|
33
|
+
]);
|
|
34
|
+
}
|
|
35
|
+
async drain(process, language, requestId, diagnostics = false, quiet = false) {
|
|
36
|
+
const output = async (stream, stderr) => {
|
|
37
|
+
const reader = stream.getReader(), decoder = new TextDecoder();
|
|
38
|
+
let pending = '';
|
|
39
|
+
try {
|
|
40
|
+
for (;;) {
|
|
41
|
+
const { value, done } = await reader.read();
|
|
42
|
+
const text = done ? decoder.decode() : decoder.decode(value, { stream: true });
|
|
43
|
+
if (text && !quiet)
|
|
44
|
+
this.emit({ language, name: 'output', data: text, requestId });
|
|
45
|
+
if (stderr && diagnostics) {
|
|
46
|
+
pending += text;
|
|
47
|
+
const lines = pending.split('\n');
|
|
48
|
+
pending = lines.pop() ?? '';
|
|
49
|
+
for (const line of lines) {
|
|
50
|
+
const match = /^(.*?):(\d+):(\d+): (fatal error|error|warning|note): (.*)$/.exec(line);
|
|
51
|
+
if (match)
|
|
52
|
+
this.emit({
|
|
53
|
+
language,
|
|
54
|
+
name: 'diagnostic',
|
|
55
|
+
requestId,
|
|
56
|
+
data: {
|
|
57
|
+
fileName: match[1].replace(`/workspace/${language}/`, '/workspace/'),
|
|
58
|
+
lineNumber: Number(match[2]),
|
|
59
|
+
columnNumber: Number(match[3]),
|
|
60
|
+
severity: match[4].includes('error')
|
|
61
|
+
? 'error'
|
|
62
|
+
: match[4] === 'warning'
|
|
63
|
+
? 'warning'
|
|
64
|
+
: 'other',
|
|
65
|
+
message: match[5]
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (pending.length > 65536)
|
|
70
|
+
pending = '';
|
|
71
|
+
}
|
|
72
|
+
if (done)
|
|
73
|
+
break;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
finally {
|
|
77
|
+
reader.releaseLock();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const [result] = await Promise.all([
|
|
81
|
+
process.exited,
|
|
82
|
+
output(process.stdout, false),
|
|
83
|
+
output(process.stderr, true)
|
|
84
|
+
]);
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
success(result, signal) {
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
throw signal.reason ?? new DOMException('Command cancelled', 'AbortError');
|
|
90
|
+
if (result.reason !== 'exited')
|
|
91
|
+
throw new Error(`Container command ended with ${result.reason}`);
|
|
92
|
+
return result.code === 0;
|
|
93
|
+
}
|
|
94
|
+
headerKey(language, code, compileArgs, options, cwd, standard) {
|
|
95
|
+
// A textual prefix and the fixed system headers are the only admitted inputs.
|
|
96
|
+
// Custom arguments, environment paths, or workspace headers can alter declarations.
|
|
97
|
+
if (language !== 'CPP' ||
|
|
98
|
+
compileArgs.length ||
|
|
99
|
+
Object.keys(options.env ?? {}).length ||
|
|
100
|
+
options.workspaceFiles?.some((file) => file.path !== options.activePath && !/\.(?:c|cc|cpp|cxx|wasm)$/i.test(file.path)))
|
|
101
|
+
return;
|
|
102
|
+
let rest = code.replace(/^\uFEFF/, '');
|
|
103
|
+
for (;;) {
|
|
104
|
+
rest = rest.replace(/^\s+/, '');
|
|
105
|
+
if (rest.startsWith('//')) {
|
|
106
|
+
const end = rest.indexOf('\n');
|
|
107
|
+
if (end < 0 || rest.slice(0, end).trimEnd().endsWith('\\'))
|
|
108
|
+
return;
|
|
109
|
+
rest = rest.slice(end + 1);
|
|
110
|
+
}
|
|
111
|
+
else if (rest.startsWith('/*')) {
|
|
112
|
+
const end = rest.indexOf('*/', 2);
|
|
113
|
+
if (end < 0)
|
|
114
|
+
return;
|
|
115
|
+
rest = rest.slice(end + 2);
|
|
116
|
+
}
|
|
117
|
+
else
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (!/^#[ \t]*include[ \t]*<bits\/stdc\+\+\.h>/.test(rest))
|
|
121
|
+
return;
|
|
122
|
+
return JSON.stringify([cwd, standard, '-fno-exceptions']);
|
|
123
|
+
}
|
|
124
|
+
discardHeader() {
|
|
125
|
+
if (!this.#header)
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
this.profile.container.fs.unlink(this.#header.path);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* Already removed. */
|
|
132
|
+
}
|
|
133
|
+
this.#header = undefined;
|
|
134
|
+
}
|
|
135
|
+
async prepare(language, code, args, options, signal, progress, requestId) {
|
|
136
|
+
if (this.#active.has(language))
|
|
137
|
+
throw new Error(`${language} already has an active command`);
|
|
138
|
+
const input = this.files.inputs(language, code, options);
|
|
139
|
+
const cwd = this.files.directory(language), artifact = `${cwd}/program.wasm`;
|
|
140
|
+
const job = { language, phase: 'prepare', signal };
|
|
141
|
+
this.#active.set(language, job);
|
|
142
|
+
this.#prepared.delete(language);
|
|
143
|
+
try {
|
|
144
|
+
progress?.report?.({
|
|
145
|
+
kind: 'activity',
|
|
146
|
+
phase: 'initializing',
|
|
147
|
+
label: 'Preparing the Clang toolchain'
|
|
148
|
+
});
|
|
149
|
+
await this.profile.prepareCommand({ path: '/usr/local/bin/clang' }, signal);
|
|
150
|
+
const { compileArgs } = resolveSandboxExecutionArgs(language, args, options);
|
|
151
|
+
const standard = language === 'CPP'
|
|
152
|
+
? (options.cppVersion ?? 'CPP20').replace(/^CPP/, 'c++').toLowerCase()
|
|
153
|
+
: options.cVersion?.toLowerCase();
|
|
154
|
+
// An opaque response file or -x override can select C++ for a C editor.
|
|
155
|
+
// Keep its default source mode C while enabling the C++ driver's link libraries.
|
|
156
|
+
const override = compileArgs.some((arg) => arg.startsWith('@') || arg.startsWith('-x'));
|
|
157
|
+
const common = [
|
|
158
|
+
...(language === 'CPP' || override ? ['--driver-mode=g++'] : []),
|
|
159
|
+
...(language === 'C' && override ? ['-x', 'c'] : []),
|
|
160
|
+
...(standard ? [`-std=${standard}`] : []),
|
|
161
|
+
'-fno-color-diagnostics',
|
|
162
|
+
'-fno-exceptions',
|
|
163
|
+
...compileArgs
|
|
164
|
+
];
|
|
165
|
+
const compile = async (argv, quiet = false) => {
|
|
166
|
+
job.process = await this.profile.container.spawn('clang', argv, {
|
|
167
|
+
cwd,
|
|
168
|
+
signal,
|
|
169
|
+
env: options.env
|
|
170
|
+
});
|
|
171
|
+
const drained = this.drain(job.process, language, requestId, !quiet, quiet);
|
|
172
|
+
await job.process.stdin.getWriter().close();
|
|
173
|
+
const result = await drained;
|
|
174
|
+
if (job.stopped)
|
|
175
|
+
throw new DOMException('Command cancelled', 'AbortError');
|
|
176
|
+
return this.success(result, signal);
|
|
177
|
+
};
|
|
178
|
+
const headerKey = this.headerKey(language, input.code, compileArgs, input.options, cwd, standard);
|
|
179
|
+
if (headerKey && this.#header?.key !== headerKey)
|
|
180
|
+
this.discardHeader();
|
|
181
|
+
let header = headerKey === this.#header?.key ? this.#header : undefined;
|
|
182
|
+
if (header) {
|
|
183
|
+
// Restore owned bytes instead of trusting a writable temporary file.
|
|
184
|
+
try {
|
|
185
|
+
try {
|
|
186
|
+
this.profile.container.fs.unlink(header.path);
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* Already removed. */
|
|
190
|
+
}
|
|
191
|
+
this.profile.container.fs.writeFile(header.path, header.bytes);
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
this.discardHeader();
|
|
195
|
+
header = undefined;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
let okay = await compile([
|
|
199
|
+
...common,
|
|
200
|
+
...(header ? ['-include-pch', header.path] : []),
|
|
201
|
+
input.options.activePath,
|
|
202
|
+
'-lc-printscan-long-double',
|
|
203
|
+
'-o',
|
|
204
|
+
artifact
|
|
205
|
+
]);
|
|
206
|
+
if (header && okay)
|
|
207
|
+
progress?.set?.(95, 'Compiled with precompiled <bits/stdc++.h>');
|
|
208
|
+
else if (header) {
|
|
209
|
+
this.discardHeader();
|
|
210
|
+
okay = await compile([
|
|
211
|
+
...common,
|
|
212
|
+
input.options.activePath,
|
|
213
|
+
'-lc-printscan-long-double',
|
|
214
|
+
'-o',
|
|
215
|
+
artifact
|
|
216
|
+
]);
|
|
217
|
+
}
|
|
218
|
+
if (okay && headerKey && !this.#header) {
|
|
219
|
+
const path = `/tmp/wasm-idle-stdcpp-${crypto.randomUUID()}.pch`;
|
|
220
|
+
try {
|
|
221
|
+
if (await compile([
|
|
222
|
+
...common,
|
|
223
|
+
'-c',
|
|
224
|
+
'-x',
|
|
225
|
+
'c++-header',
|
|
226
|
+
'/usr/share/wasm-sysroot/include/bits/stdc++.h',
|
|
227
|
+
'-o',
|
|
228
|
+
path
|
|
229
|
+
], true)) {
|
|
230
|
+
const bytes = this.profile.container.fs.readFile(path);
|
|
231
|
+
if (bytes.byteLength > 0 && bytes.byteLength <= 32 * 1024 * 1024)
|
|
232
|
+
this.#header = { key: headerKey, path, bytes };
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (signal.aborted || job.stopped)
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
finally {
|
|
240
|
+
if (this.#header?.path !== path) {
|
|
241
|
+
try {
|
|
242
|
+
this.profile.container.fs.unlink(path);
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
/* No usable header. */
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
if (okay)
|
|
251
|
+
this.#prepared.set(language, this.key(language, input.code, args, options));
|
|
252
|
+
return okay;
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
if (this.#active.get(language) === job)
|
|
256
|
+
this.#active.delete(language);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
async run(language, code, args, options, signal, progress, requestId) {
|
|
260
|
+
const input = this.files.inputs(language, code, options);
|
|
261
|
+
if (this.#prepared.get(language) !== this.key(language, input.code, args, options) &&
|
|
262
|
+
!(await this.prepare(language, code, args, options, signal, progress, requestId)))
|
|
263
|
+
return false;
|
|
264
|
+
const cwd = this.files.directory(language), job = { language, phase: 'run', signal };
|
|
265
|
+
if (this.#active.has(language))
|
|
266
|
+
throw new Error(`${language} already has an active command`);
|
|
267
|
+
this.#active.set(language, job);
|
|
268
|
+
try {
|
|
269
|
+
const { programArgs } = resolveSandboxExecutionArgs(language, args, options);
|
|
270
|
+
job.process = await this.profile.container.spawn('wasm-run', [`${cwd}/program.wasm`, ...programArgs], { cwd, signal, env: options.env });
|
|
271
|
+
job.writer = job.process.stdin.getWriter();
|
|
272
|
+
progress?.report?.({
|
|
273
|
+
kind: 'ready',
|
|
274
|
+
state: 'running',
|
|
275
|
+
reason: 'started',
|
|
276
|
+
label: 'Container program started'
|
|
277
|
+
});
|
|
278
|
+
const drained = this.drain(job.process, language, requestId);
|
|
279
|
+
if (options.stdin !== undefined) {
|
|
280
|
+
await job.writer.write(new TextEncoder().encode(options.stdin));
|
|
281
|
+
await job.writer.close();
|
|
282
|
+
}
|
|
283
|
+
else
|
|
284
|
+
await this.flush(language);
|
|
285
|
+
return this.success(await drained, signal);
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
job.writer?.releaseLock();
|
|
289
|
+
this.#input.delete(language);
|
|
290
|
+
if (this.#active.get(language) === job)
|
|
291
|
+
this.#active.delete(language);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async flush(language) {
|
|
295
|
+
const writer = this.#active.get(language)?.writer;
|
|
296
|
+
if (!writer)
|
|
297
|
+
return;
|
|
298
|
+
const queue = this.queue(language);
|
|
299
|
+
for (const text of queue.chunks.splice(0)) {
|
|
300
|
+
const bytes = new TextEncoder().encode(text);
|
|
301
|
+
await writer.write(bytes);
|
|
302
|
+
queue.size -= bytes.byteLength;
|
|
303
|
+
}
|
|
304
|
+
if (queue.eof) {
|
|
305
|
+
queue.eof = false;
|
|
306
|
+
await writer.close();
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async write(language, text) {
|
|
310
|
+
const q = this.queue(language);
|
|
311
|
+
q.size += new TextEncoder().encode(text).length;
|
|
312
|
+
if (q.size > 8 * 1024 * 1024)
|
|
313
|
+
throw new Error('Pending stdin exceeds its limit');
|
|
314
|
+
q.chunks.push(text);
|
|
315
|
+
await this.flush(language);
|
|
316
|
+
}
|
|
317
|
+
async eof(language) {
|
|
318
|
+
this.queue(language).eof = true;
|
|
319
|
+
await this.flush(language);
|
|
320
|
+
}
|
|
321
|
+
async stop(language) {
|
|
322
|
+
this.#input.delete(language);
|
|
323
|
+
const job = this.#active.get(language);
|
|
324
|
+
if (job)
|
|
325
|
+
job.stopped = true;
|
|
326
|
+
await job?.process?.terminate();
|
|
327
|
+
}
|
|
328
|
+
clear(language) {
|
|
329
|
+
this.#input.delete(language);
|
|
330
|
+
}
|
|
331
|
+
inspect() {
|
|
332
|
+
return [...this.#active.values()].map((job) => ({
|
|
333
|
+
language: job.language,
|
|
334
|
+
phase: job.phase,
|
|
335
|
+
pid: job.process?.pid,
|
|
336
|
+
filesystem: 'live'
|
|
337
|
+
}));
|
|
338
|
+
}
|
|
339
|
+
async dispose() {
|
|
340
|
+
await Promise.allSettled([...this.#active.keys()].map((language) => this.stop(language)));
|
|
341
|
+
this.discardHeader();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type ContainerWorkerCallOptions, type ContainerWorkerEvent, type ContainerWorkerInitialization } from './protocol';
|
|
2
|
+
export interface ContainerWorkerClient {
|
|
3
|
+
call<T = unknown>(method: string, args?: unknown[], options?: ContainerWorkerCallOptions): Promise<T>;
|
|
4
|
+
onEvent(listener: (event: ContainerWorkerEvent) => void): () => void;
|
|
5
|
+
dispose(): Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
type OwnerWorker = Pick<Worker, 'postMessage' | 'addEventListener' | 'removeEventListener' | 'terminate'>;
|
|
8
|
+
interface ClientOptions extends ContainerWorkerInitialization {
|
|
9
|
+
/** Injection is used only by deterministic transport tests. */
|
|
10
|
+
workerFactory?: () => OwnerWorker;
|
|
11
|
+
initializationTimeoutMs?: number;
|
|
12
|
+
cleanupDeadlineMs?: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function createContainerWorkerClient(options: ClientOptions): ContainerWorkerClient;
|
|
15
|
+
export {};
|