@volter/tabnode 0.2.14-volter.88
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/LICENSE +22 -0
- package/README.md +78 -0
- package/THIRD-PARTY-NOTICES.md +12 -0
- package/dist/__sw__.js +526 -0
- package/dist/assets/runtime-worker-4MKaHFVb.js +75221 -0
- package/dist/assets/runtime-worker-4MKaHFVb.js.map +1 -0
- package/dist/code-transforms.d.ts +21 -0
- package/dist/code-transforms.d.ts.map +1 -0
- package/dist/config/cdn.d.ts +12 -0
- package/dist/config/cdn.d.ts.map +1 -0
- package/dist/create-runtime.d.ts +22 -0
- package/dist/create-runtime.d.ts.map +1 -0
- package/dist/host-globals.d.ts +62 -0
- package/dist/host-globals.d.ts.map +1 -0
- package/dist/index.cjs +36021 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +127 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.mjs +36020 -0
- package/dist/index.mjs.map +1 -0
- package/dist/node-internals.d.ts +68 -0
- package/dist/node-internals.d.ts.map +1 -0
- package/dist/node-lib/binding/blob.d.ts +12 -0
- package/dist/node-lib/binding/blob.d.ts.map +1 -0
- package/dist/node-lib/binding/buffer.d.ts +144 -0
- package/dist/node-lib/binding/buffer.d.ts.map +1 -0
- package/dist/node-lib/binding/cares_wrap.d.ts +14 -0
- package/dist/node-lib/binding/cares_wrap.d.ts.map +1 -0
- package/dist/node-lib/binding/fds.d.ts +30 -0
- package/dist/node-lib/binding/fds.d.ts.map +1 -0
- package/dist/node-lib/binding/fs.d.ts +157 -0
- package/dist/node-lib/binding/fs.d.ts.map +1 -0
- package/dist/node-lib/binding/handles.d.ts +81 -0
- package/dist/node-lib/binding/handles.d.ts.map +1 -0
- package/dist/node-lib/binding/http_parser.d.ts +122 -0
- package/dist/node-lib/binding/http_parser.d.ts.map +1 -0
- package/dist/node-lib/binding/index.d.ts +3 -0
- package/dist/node-lib/binding/index.d.ts.map +1 -0
- package/dist/node-lib/binding/llhttp-wasm.d.ts +22 -0
- package/dist/node-lib/binding/llhttp-wasm.d.ts.map +1 -0
- package/dist/node-lib/binding/misc.d.ts +123 -0
- package/dist/node-lib/binding/misc.d.ts.map +1 -0
- package/dist/node-lib/binding/pipe_wrap.d.ts +84 -0
- package/dist/node-lib/binding/pipe_wrap.d.ts.map +1 -0
- package/dist/node-lib/binding/process_wrap.d.ts +173 -0
- package/dist/node-lib/binding/process_wrap.d.ts.map +1 -0
- package/dist/node-lib/binding/spawn_sync.d.ts +30 -0
- package/dist/node-lib/binding/spawn_sync.d.ts.map +1 -0
- package/dist/node-lib/binding/stream_wrap.d.ts +191 -0
- package/dist/node-lib/binding/stream_wrap.d.ts.map +1 -0
- package/dist/node-lib/binding/stubs.d.ts +23 -0
- package/dist/node-lib/binding/stubs.d.ts.map +1 -0
- package/dist/node-lib/binding/tcp_wrap.d.ts +98 -0
- package/dist/node-lib/binding/tcp_wrap.d.ts.map +1 -0
- package/dist/node-lib/binding/tty_wrap.d.ts +36 -0
- package/dist/node-lib/binding/tty_wrap.d.ts.map +1 -0
- package/dist/node-lib/binding/util.d.ts +126 -0
- package/dist/node-lib/binding/util.d.ts.map +1 -0
- package/dist/node-lib/binding/uv.d.ts +32 -0
- package/dist/node-lib/binding/uv.d.ts.map +1 -0
- package/dist/node-lib/binding/zlib-pako.d.ts +22 -0
- package/dist/node-lib/binding/zlib-pako.d.ts.map +1 -0
- package/dist/node-lib/binding/zlib.d.ts +38 -0
- package/dist/node-lib/binding/zlib.d.ts.map +1 -0
- package/dist/node-lib/buffer-module.d.ts +51 -0
- package/dist/node-lib/buffer-module.d.ts.map +1 -0
- package/dist/node-lib/child-process-module.d.ts +26 -0
- package/dist/node-lib/child-process-module.d.ts.map +1 -0
- package/dist/node-lib/events-module.d.ts +58 -0
- package/dist/node-lib/events-module.d.ts.map +1 -0
- package/dist/node-lib/fs-module.d.ts +5 -0
- package/dist/node-lib/fs-module.d.ts.map +1 -0
- package/dist/node-lib/http-bridge.d.ts +43 -0
- package/dist/node-lib/http-bridge.d.ts.map +1 -0
- package/dist/node-lib/http-module.d.ts +35 -0
- package/dist/node-lib/http-module.d.ts.map +1 -0
- package/dist/node-lib/internals/addresses.d.ts +40 -0
- package/dist/node-lib/internals/addresses.d.ts.map +1 -0
- package/dist/node-lib/internals/buffer-and-streams.d.ts +96 -0
- package/dist/node-lib/internals/buffer-and-streams.d.ts.map +1 -0
- package/dist/node-lib/internals/child-process.d.ts +27 -0
- package/dist/node-lib/internals/child-process.d.ts.map +1 -0
- package/dist/node-lib/internals/events-util.d.ts +83 -0
- package/dist/node-lib/internals/events-util.d.ts.map +1 -0
- package/dist/node-lib/internals/index.d.ts +2 -0
- package/dist/node-lib/internals/index.d.ts.map +1 -0
- package/dist/node-lib/internals/modules.d.ts +67 -0
- package/dist/node-lib/internals/modules.d.ts.map +1 -0
- package/dist/node-lib/internals/readline-access.d.ts +2 -0
- package/dist/node-lib/internals/readline-access.d.ts.map +1 -0
- package/dist/node-lib/internals/runtime.d.ts +150 -0
- package/dist/node-lib/internals/runtime.d.ts.map +1 -0
- package/dist/node-lib/internals/util.d.ts +33 -0
- package/dist/node-lib/internals/util.d.ts.map +1 -0
- package/dist/node-lib/lazy.d.ts +31 -0
- package/dist/node-lib/lazy.d.ts.map +1 -0
- package/dist/node-lib/load.d.ts +27 -0
- package/dist/node-lib/load.d.ts.map +1 -0
- package/dist/node-lib/module-hooks.d.ts +89 -0
- package/dist/node-lib/module-hooks.d.ts.map +1 -0
- package/dist/node-lib/net-module.d.ts +181 -0
- package/dist/node-lib/net-module.d.ts.map +1 -0
- package/dist/node-lib/node-versions.d.ts +27 -0
- package/dist/node-lib/node-versions.d.ts.map +1 -0
- package/dist/node-lib/public-modules.d.ts +2 -0
- package/dist/node-lib/public-modules.d.ts.map +1 -0
- package/dist/node-lib/require-hook.d.ts +15 -0
- package/dist/node-lib/require-hook.d.ts.map +1 -0
- package/dist/node-lib/small-modules.d.ts +16 -0
- package/dist/node-lib/small-modules.d.ts.map +1 -0
- package/dist/node-lib/sources.d.ts +4 -0
- package/dist/node-lib/sources.d.ts.map +1 -0
- package/dist/node-lib/stream-module.d.ts +77 -0
- package/dist/node-lib/stream-module.d.ts.map +1 -0
- package/dist/node-lib/timers.d.ts +19 -0
- package/dist/node-lib/timers.d.ts.map +1 -0
- package/dist/node-lib/util-module.d.ts +76 -0
- package/dist/node-lib/util-module.d.ts.map +1 -0
- package/dist/node-resolution.d.ts +50 -0
- package/dist/node-resolution.d.ts.map +1 -0
- package/dist/node-resolver.d.ts +7 -0
- package/dist/node-resolver.d.ts.map +1 -0
- package/dist/npm/index.d.ts +73 -0
- package/dist/npm/index.d.ts.map +1 -0
- package/dist/npm/registry.d.ts +111 -0
- package/dist/npm/registry.d.ts.map +1 -0
- package/dist/npm/resolver.d.ts +79 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/tarball.d.ts +28 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/process-tokens.d.ts +77 -0
- package/dist/process-tokens.d.ts.map +1 -0
- package/dist/punycode-source.d.ts +3 -0
- package/dist/punycode-source.d.ts.map +1 -0
- package/dist/runtime-interface.d.ts +74 -0
- package/dist/runtime-interface.d.ts.map +1 -0
- package/dist/runtime.d.ts +144 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/server-bridge.d.ts +147 -0
- package/dist/server-bridge.d.ts.map +1 -0
- package/dist/shims/async_hooks.d.ts +50 -0
- package/dist/shims/async_hooks.d.ts.map +1 -0
- package/dist/shims/child_process.d.ts +104 -0
- package/dist/shims/child_process.d.ts.map +1 -0
- package/dist/shims/cluster.d.ts +52 -0
- package/dist/shims/cluster.d.ts.map +1 -0
- package/dist/shims/command-line.d.ts +43 -0
- package/dist/shims/command-line.d.ts.map +1 -0
- package/dist/shims/constants.d.ts +222 -0
- package/dist/shims/constants.d.ts.map +1 -0
- package/dist/shims/crypto-constants.d.ts +12 -0
- package/dist/shims/crypto-constants.d.ts.map +1 -0
- package/dist/shims/crypto.d.ts +144 -0
- package/dist/shims/crypto.d.ts.map +1 -0
- package/dist/shims/dgram.d.ts +34 -0
- package/dist/shims/dgram.d.ts.map +1 -0
- package/dist/shims/dns.d.ts +91 -0
- package/dist/shims/dns.d.ts.map +1 -0
- package/dist/shims/domain.d.ts +25 -0
- package/dist/shims/domain.d.ts.map +1 -0
- package/dist/shims/esbuild.d.ts +205 -0
- package/dist/shims/esbuild.d.ts.map +1 -0
- package/dist/shims/fs.d.ts +25 -0
- package/dist/shims/fs.d.ts.map +1 -0
- package/dist/shims/http2.d.ts +81 -0
- package/dist/shims/http2.d.ts.map +1 -0
- package/dist/shims/inspector.d.ts +25 -0
- package/dist/shims/inspector.d.ts.map +1 -0
- package/dist/shims/module.d.ts +22 -0
- package/dist/shims/module.d.ts.map +1 -0
- package/dist/shims/os.d.ts +159 -0
- package/dist/shims/os.d.ts.map +1 -0
- package/dist/shims/path.d.ts +52 -0
- package/dist/shims/path.d.ts.map +1 -0
- package/dist/shims/perf_hooks.d.ts +50 -0
- package/dist/shims/perf_hooks.d.ts.map +1 -0
- package/dist/shims/process.d.ts +226 -0
- package/dist/shims/process.d.ts.map +1 -0
- package/dist/shims/rollup.d.ts +71 -0
- package/dist/shims/rollup.d.ts.map +1 -0
- package/dist/shims/string_decoder.d.ts +41 -0
- package/dist/shims/string_decoder.d.ts.map +1 -0
- package/dist/shims/sync-child.d.ts +64 -0
- package/dist/shims/sync-child.d.ts.map +1 -0
- package/dist/shims/tls.d.ts +53 -0
- package/dist/shims/tls.d.ts.map +1 -0
- package/dist/shims/url.d.ts +84 -0
- package/dist/shims/url.d.ts.map +1 -0
- package/dist/shims/v8.d.ts +73 -0
- package/dist/shims/v8.d.ts.map +1 -0
- package/dist/shims/vfs-adapter.d.ts +126 -0
- package/dist/shims/vfs-adapter.d.ts.map +1 -0
- package/dist/shims/vm.d.ts +52 -0
- package/dist/shims/vm.d.ts.map +1 -0
- package/dist/shims/wasi.d.ts +78 -0
- package/dist/shims/wasi.d.ts.map +1 -0
- package/dist/shims/worker_threads.d.ts +66 -0
- package/dist/shims/worker_threads.d.ts.map +1 -0
- package/dist/shims/zlib-constants.d.ts +66 -0
- package/dist/shims/zlib-constants.d.ts.map +1 -0
- package/dist/shims/zlib.d.ts +102 -0
- package/dist/shims/zlib.d.ts.map +1 -0
- package/dist/transform.d.ts +24 -0
- package/dist/transform.d.ts.map +1 -0
- package/dist/types/package-json.d.ts +18 -0
- package/dist/types/package-json.d.ts.map +1 -0
- package/dist/utils/binary-encoding.d.ts +13 -0
- package/dist/utils/binary-encoding.d.ts.map +1 -0
- package/dist/utils/glob.d.ts +19 -0
- package/dist/utils/glob.d.ts.map +1 -0
- package/dist/utils/hash.d.ts +6 -0
- package/dist/utils/hash.d.ts.map +1 -0
- package/dist/virtual-fs.d.ts +259 -0
- package/dist/virtual-fs.d.ts.map +1 -0
- package/dist/vite-plugin.cjs +50 -0
- package/dist/vite-plugin.cjs.map +1 -0
- package/dist/vite-plugin.d.ts +36 -0
- package/dist/vite-plugin.d.ts.map +1 -0
- package/dist/vite-plugin.mjs +31 -0
- package/dist/vite-plugin.mjs.map +1 -0
- package/dist/worker/runtime-worker.d.ts +8 -0
- package/dist/worker/runtime-worker.d.ts.map +1 -0
- package/dist/worker-runtime.d.ts +50 -0
- package/dist/worker-runtime.d.ts.map +1 -0
- package/package.json +88 -0
- package/src/code-transforms.ts +560 -0
- package/src/config/cdn.ts +13 -0
- package/src/create-runtime.ts +89 -0
- package/src/host-globals.ts +121 -0
- package/src/index.ts +235 -0
- package/src/node-internals.ts +283 -0
- package/src/node-lib/LICENSE +47 -0
- package/src/node-lib/_http_agent.js +547 -0
- package/src/node-lib/_http_client.js +996 -0
- package/src/node-lib/_http_common.js +269 -0
- package/src/node-lib/_http_incoming.js +453 -0
- package/src/node-lib/_http_outgoing.js +1262 -0
- package/src/node-lib/_http_server.js +1230 -0
- package/src/node-lib/assert/strict.js +3 -0
- package/src/node-lib/assert.js +844 -0
- package/src/node-lib/binding/blob.ts +35 -0
- package/src/node-lib/binding/buffer.ts +465 -0
- package/src/node-lib/binding/cares_wrap.ts +57 -0
- package/src/node-lib/binding/fds.ts +80 -0
- package/src/node-lib/binding/fs.ts +857 -0
- package/src/node-lib/binding/handles.ts +187 -0
- package/src/node-lib/binding/http_parser.ts +536 -0
- package/src/node-lib/binding/index.ts +77 -0
- package/src/node-lib/binding/llhttp-wasm.ts +670 -0
- package/src/node-lib/binding/misc.ts +252 -0
- package/src/node-lib/binding/pipe_wrap.ts +166 -0
- package/src/node-lib/binding/process_wrap.ts +371 -0
- package/src/node-lib/binding/spawn_sync.ts +141 -0
- package/src/node-lib/binding/stream_wrap.ts +413 -0
- package/src/node-lib/binding/stubs.ts +19 -0
- package/src/node-lib/binding/tcp_wrap.ts +254 -0
- package/src/node-lib/binding/tty_wrap.ts +56 -0
- package/src/node-lib/binding/util.ts +274 -0
- package/src/node-lib/binding/uv.ts +102 -0
- package/src/node-lib/binding/zlib-pako.ts +50 -0
- package/src/node-lib/binding/zlib.ts +259 -0
- package/src/node-lib/buffer-module.ts +70 -0
- package/src/node-lib/buffer.js +1373 -0
- package/src/node-lib/child-process-module.ts +58 -0
- package/src/node-lib/child_process.js +1046 -0
- package/src/node-lib/constants.js +41 -0
- package/src/node-lib/diagnostics_channel.js +438 -0
- package/src/node-lib/events-module.ts +80 -0
- package/src/node-lib/events.js +1223 -0
- package/src/node-lib/fs-module.ts +43 -0
- package/src/node-lib/fs.js +3377 -0
- package/src/node-lib/http-bridge.ts +299 -0
- package/src/node-lib/http-module.ts +53 -0
- package/src/node-lib/http.js +198 -0
- package/src/node-lib/https.js +429 -0
- package/src/node-lib/internal/assert/assertion_error.js +417 -0
- package/src/node-lib/internal/assert/calltracker.js +153 -0
- package/src/node-lib/internal/assert/myers_diff.js +177 -0
- package/src/node-lib/internal/assert/utils.js +278 -0
- package/src/node-lib/internal/buffer.js +1120 -0
- package/src/node-lib/internal/child_process/serialization.js +169 -0
- package/src/node-lib/internal/child_process.js +1135 -0
- package/src/node-lib/internal/constants.js +56 -0
- package/src/node-lib/internal/deps/minimatch/index.js +1628 -0
- package/src/node-lib/internal/errors.js +1912 -0
- package/src/node-lib/internal/events/abort_listener.js +54 -0
- package/src/node-lib/internal/events/symbols.js +11 -0
- package/src/node-lib/internal/fixed_queue.js +119 -0
- package/src/node-lib/internal/freelist.js +30 -0
- package/src/node-lib/internal/fs/cp/cp-sync.js +242 -0
- package/src/node-lib/internal/fs/cp/cp.js +382 -0
- package/src/node-lib/internal/fs/dir.js +369 -0
- package/src/node-lib/internal/fs/glob.js +763 -0
- package/src/node-lib/internal/fs/promises.js +1312 -0
- package/src/node-lib/internal/fs/read/context.js +129 -0
- package/src/node-lib/internal/fs/recursive_watch.js +275 -0
- package/src/node-lib/internal/fs/rimraf.js +311 -0
- package/src/node-lib/internal/fs/streams.js +571 -0
- package/src/node-lib/internal/fs/sync_write_stream.js +49 -0
- package/src/node-lib/internal/fs/utils.js +987 -0
- package/src/node-lib/internal/fs/watchers.js +418 -0
- package/src/node-lib/internal/http.js +62 -0
- package/src/node-lib/internal/mime.js +389 -0
- package/src/node-lib/internal/modules/customization_hooks.js +406 -0
- package/src/node-lib/internal/net.js +95 -0
- package/src/node-lib/internal/querystring.js +119 -0
- package/src/node-lib/internal/readline/callbacks.js +132 -0
- package/src/node-lib/internal/readline/emitKeypressEvents.js +99 -0
- package/src/node-lib/internal/readline/interface.js +1418 -0
- package/src/node-lib/internal/readline/promises.js +144 -0
- package/src/node-lib/internal/readline/utils.js +405 -0
- package/src/node-lib/internal/socket_list.js +108 -0
- package/src/node-lib/internal/stream_base_commons.js +280 -0
- package/src/node-lib/internal/streams/add-abort-signal.js +60 -0
- package/src/node-lib/internal/streams/compose.js +257 -0
- package/src/node-lib/internal/streams/destroy.js +361 -0
- package/src/node-lib/internal/streams/duplex.js +203 -0
- package/src/node-lib/internal/streams/duplexify.js +379 -0
- package/src/node-lib/internal/streams/duplexpair.js +62 -0
- package/src/node-lib/internal/streams/end-of-stream.js +331 -0
- package/src/node-lib/internal/streams/from.js +208 -0
- package/src/node-lib/internal/streams/lazy_transform.js +57 -0
- package/src/node-lib/internal/streams/legacy.js +125 -0
- package/src/node-lib/internal/streams/operators.js +436 -0
- package/src/node-lib/internal/streams/passthrough.js +47 -0
- package/src/node-lib/internal/streams/pipeline.js +465 -0
- package/src/node-lib/internal/streams/readable.js +1762 -0
- package/src/node-lib/internal/streams/state.js +51 -0
- package/src/node-lib/internal/streams/transform.js +204 -0
- package/src/node-lib/internal/streams/utils.js +363 -0
- package/src/node-lib/internal/streams/writable.js +1163 -0
- package/src/node-lib/internal/tty.js +260 -0
- package/src/node-lib/internal/util/colors.js +40 -0
- package/src/node-lib/internal/util/comparisons.js +1028 -0
- package/src/node-lib/internal/util/debuglog.js +524 -0
- package/src/node-lib/internal/util/inspect.js +2535 -0
- package/src/node-lib/internal/util/parse_args/parse_args.js +412 -0
- package/src/node-lib/internal/util/parse_args/utils.js +197 -0
- package/src/node-lib/internal/util.js +1003 -0
- package/src/node-lib/internal/validators.js +654 -0
- package/src/node-lib/internals/addresses.ts +159 -0
- package/src/node-lib/internals/buffer-and-streams.ts +224 -0
- package/src/node-lib/internals/child-process.ts +64 -0
- package/src/node-lib/internals/events-util.ts +105 -0
- package/src/node-lib/internals/index.ts +66 -0
- package/src/node-lib/internals/modules.ts +117 -0
- package/src/node-lib/internals/readline-access.ts +16 -0
- package/src/node-lib/internals/runtime.ts +277 -0
- package/src/node-lib/internals/util.ts +98 -0
- package/src/node-lib/lazy.ts +158 -0
- package/src/node-lib/load.ts +390 -0
- package/src/node-lib/module-hooks.ts +281 -0
- package/src/node-lib/net-module.ts +248 -0
- package/src/node-lib/net.js +2540 -0
- package/src/node-lib/node-versions.ts +34 -0
- package/src/node-lib/os.js +356 -0
- package/src/node-lib/path.js +1694 -0
- package/src/node-lib/primordials.js +743 -0
- package/src/node-lib/public-modules.ts +109 -0
- package/src/node-lib/punycode.js +455 -0
- package/src/node-lib/querystring.js +484 -0
- package/src/node-lib/readline/promises.js +73 -0
- package/src/node-lib/readline.js +523 -0
- package/src/node-lib/require-hook.ts +27 -0
- package/src/node-lib/small-modules.ts +35 -0
- package/src/node-lib/sources.ts +200 -0
- package/src/node-lib/stream/promises.js +44 -0
- package/src/node-lib/stream-module.ts +101 -0
- package/src/node-lib/stream.js +152 -0
- package/src/node-lib/timers.ts +33 -0
- package/src/node-lib/tty.js +168 -0
- package/src/node-lib/util-module.ts +104 -0
- package/src/node-lib/util.js +689 -0
- package/src/node-lib/wasi.js +179 -0
- package/src/node-lib/zlib.js +1036 -0
- package/src/node-resolution.ts +241 -0
- package/src/node-resolver.ts +33 -0
- package/src/npm/index.ts +425 -0
- package/src/npm/registry.ts +235 -0
- package/src/npm/resolver.ts +589 -0
- package/src/npm/tarball.ts +246 -0
- package/src/process-tokens.ts +156 -0
- package/src/punycode-source.ts +6 -0
- package/src/runtime-interface.ts +85 -0
- package/src/runtime.ts +3073 -0
- package/src/server-bridge.ts +971 -0
- package/src/shims/async_hooks.ts +181 -0
- package/src/shims/child_process.ts +1591 -0
- package/src/shims/cluster.ts +79 -0
- package/src/shims/command-line.ts +79 -0
- package/src/shims/constants.ts +68 -0
- package/src/shims/crypto-constants.ts +11 -0
- package/src/shims/crypto.ts +1061 -0
- package/src/shims/dgram.ts +47 -0
- package/src/shims/dns.ts +185 -0
- package/src/shims/domain.ts +58 -0
- package/src/shims/esbuild.ts +1638 -0
- package/src/shims/fs.ts +82 -0
- package/src/shims/http2.ts +96 -0
- package/src/shims/inspector.ts +30 -0
- package/src/shims/module.ts +86 -0
- package/src/shims/os.ts +195 -0
- package/src/shims/path.ts +246 -0
- package/src/shims/perf_hooks.ts +92 -0
- package/src/shims/process.ts +804 -0
- package/src/shims/rollup.ts +149 -0
- package/src/shims/string_decoder.ts +396 -0
- package/src/shims/sync-child.ts +469 -0
- package/src/shims/tls.ts +94 -0
- package/src/shims/url.ts +217 -0
- package/src/shims/v8.ts +113 -0
- package/src/shims/vfs-adapter.ts +408 -0
- package/src/shims/vm.ts +132 -0
- package/src/shims/wasi.ts +1440 -0
- package/src/shims/worker_threads.ts +114 -0
- package/src/shims/zlib-constants.ts +65 -0
- package/src/shims/zlib.ts +228 -0
- package/src/transform.ts +324 -0
- package/src/types/external.d.ts +55 -0
- package/src/types/package-json.ts +17 -0
- package/src/types/pako-zlib.d.ts +30 -0
- package/src/types/sha.js.d.ts +14 -0
- package/src/utils/binary-encoding.ts +43 -0
- package/src/utils/glob.ts +54 -0
- package/src/utils/hash.ts +12 -0
- package/src/virtual-fs.ts +1048 -0
- package/src/vite-plugin.ts +76 -0
- package/src/worker/runtime-worker.ts +128 -0
- package/src/worker-runtime.ts +145 -0
|
@@ -0,0 +1,1591 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's process model: what a run of a program IS here.
|
|
3
|
+
*
|
|
4
|
+
* `child_process` itself is Node's own file now, vendored in
|
|
5
|
+
* `src/node-lib/child_process.js` and loaded over the binding in
|
|
6
|
+
* `src/node-lib/binding/`. What is left here is everything that file does not
|
|
7
|
+
* own and cannot know: the `node` command -- a guest program running as a run
|
|
8
|
+
* of the engine, with its own process, its own timers, its own uncaught
|
|
9
|
+
* exceptions and its own idea of when it is done -- the routing of a command
|
|
10
|
+
* line to the host's process host or to the engine's shell, the streams and
|
|
11
|
+
* the name a host gives one run, and the rule that decides a quiet program is
|
|
12
|
+
* finished.
|
|
13
|
+
*
|
|
14
|
+
* `Process.spawn` in the binding reaches all of it through the one door
|
|
15
|
+
* installed at the bottom of `initChildProcess`.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
// Polyfill process for just-bash (it expects Node.js environment)
|
|
19
|
+
if (typeof globalThis.process === 'undefined') {
|
|
20
|
+
(globalThis as any).process = {
|
|
21
|
+
env: {
|
|
22
|
+
HOME: '/home/user',
|
|
23
|
+
USER: 'user',
|
|
24
|
+
PATH: '/usr/local/bin:/usr/bin:/bin',
|
|
25
|
+
},
|
|
26
|
+
cwd: () => '/',
|
|
27
|
+
platform: 'linux',
|
|
28
|
+
version: 'v18.0.0',
|
|
29
|
+
versions: { node: '18.0.0' },
|
|
30
|
+
stdout: { write: () => {} },
|
|
31
|
+
stderr: { write: () => {} },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
import { forGuestRealm, heldWork } from '../host-globals';
|
|
36
|
+
import { Bash, defineCommand } from 'just-bash';
|
|
37
|
+
import type { CommandContext, ExecResult as JustBashExecResult } from 'just-bash';
|
|
38
|
+
import { EventEmitter } from '../node-lib/events-module';
|
|
39
|
+
import { __substrateExitCode, __substrateUncaughtCapture } from './process';
|
|
40
|
+
import { Buffer } from '../node-lib/buffer-module';
|
|
41
|
+
import type { VirtualFS } from '../virtual-fs';
|
|
42
|
+
import { VirtualFSAdapter } from './vfs-adapter';
|
|
43
|
+
import { __releaseOwnedServers, __ownedServerPorts } from '../node-lib/net-module';
|
|
44
|
+
import { __ownedHandleCount, __releaseOwnedHandles } from '../node-lib/net-module';
|
|
45
|
+
import { setProcessRunner, type RunRequest, type StartedRun } from '../node-lib/binding/process_wrap';
|
|
46
|
+
import { Pipe, constants as pipeConstants } from '../node-lib/binding/pipe_wrap';
|
|
47
|
+
import { registerRunFd, releaseRunFds } from '../node-lib/binding/fds';
|
|
48
|
+
import { UV_ESRCH } from '../node-lib/binding/uv';
|
|
49
|
+
import { setupChannel } from '../node-lib/child-process-module';
|
|
50
|
+
import { getCommandNames } from 'just-bash';
|
|
51
|
+
import { diagnosticsChannelModule } from '../node-lib/small-modules';
|
|
52
|
+
import { __substrateExecPath, __substrateProgramName, __substrateLineFor, __substrateShellLine, setProgramResolver } from './command-line';
|
|
53
|
+
|
|
54
|
+
import { __substrateChildren, __onUncaughtException, __reportUncaughtException } from './process';
|
|
55
|
+
|
|
56
|
+
import { PROCESS_TOKEN_ENV, __recordRun, __runFor, __currentProcessToken, __lastLaunchedToken, __setLastLaunchedToken, __stopOwnedProcess, enterRun, mintPid, setRunPid, runPid, forgetRunPid, type ProcessToken } from '../process-tokens';
|
|
57
|
+
/** The host's own `process`, where it has one that emits, taken as the shim loads and before any guest's takes the global name. */
|
|
58
|
+
const __hostProcess: { on(event: string, listener: (reason: unknown) => void): unknown; off(event: string, listener: (reason: unknown) => void): unknown } | null =
|
|
59
|
+
typeof process !== 'undefined' && process !== null && typeof (process as { on?: unknown }).on === 'function' && typeof (process as { off?: unknown }).off === 'function'
|
|
60
|
+
? (process as { on(event: string, listener: (reason: unknown) => void): unknown; off(event: string, listener: (reason: unknown) => void): unknown })
|
|
61
|
+
: null;
|
|
62
|
+
import { Runtime, pendingGuestTimers, stopGuestTimers, __substratePendingOf } from '../runtime';
|
|
63
|
+
import { __nodeResolverFor } from '../node-resolver';
|
|
64
|
+
import { resolve as __resolvePath } from './path';
|
|
65
|
+
import { setSyncChildVfs, warmSyncChild } from './sync-child';
|
|
66
|
+
import type { PackageJson } from '../types/package-json';
|
|
67
|
+
|
|
68
|
+
// Singleton bash instance - uses VFS adapter for two-way file sync
|
|
69
|
+
let bashInstance: Bash | null = null;
|
|
70
|
+
let vfsAdapter: VirtualFSAdapter | null = null;
|
|
71
|
+
let currentVfs: VirtualFS | null = null;
|
|
72
|
+
/**
|
|
73
|
+
* One shell per tree. Two `createContainer` calls in one realm used to share
|
|
74
|
+
* this module's `currentVfs` and `bashInstance`, so the first container's
|
|
75
|
+
* `node /main.cjs` ran on the second's tree and printed SECOND. The first
|
|
76
|
+
* tree still occupies `currentVfs` / `bashInstance` until a second arrives.
|
|
77
|
+
*/
|
|
78
|
+
const shells = new WeakMap<VirtualFS, { bash: Bash; adapter: VirtualFSAdapter }>();
|
|
79
|
+
let processRunnerInstalled = false;
|
|
80
|
+
|
|
81
|
+
/** The tree of the guest whose code is executing, else the last tree given to the engine. */
|
|
82
|
+
function treeForRun(): VirtualFS | null {
|
|
83
|
+
// Same symbol Runtime hangs the tree on; named here so this file does not
|
|
84
|
+
// import the fs binding (that import put constantsBinding in the
|
|
85
|
+
// consumer bundle's cycle with crypto).
|
|
86
|
+
const realm = globalThis as unknown as { process?: Record<symbol, unknown> };
|
|
87
|
+
const found = realm.process?.[Symbol.for('tabnode.run.vfs')] as VirtualFS | undefined;
|
|
88
|
+
return found ?? currentVfs;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function shellOf(tree: VirtualFS | null | undefined): { bash: Bash; adapter: VirtualFSAdapter } | null {
|
|
92
|
+
if (tree) {
|
|
93
|
+
const owned = shells.get(tree);
|
|
94
|
+
if (owned) return owned;
|
|
95
|
+
}
|
|
96
|
+
return bashInstance && vfsAdapter ? { bash: bashInstance, adapter: vfsAdapter } : null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Track active forked child processes so the node command can detect when children exit.
|
|
100
|
+
// When the last child exits, the node command uses a shorter idle timeout.
|
|
101
|
+
let _activeForkedChildren = 0;
|
|
102
|
+
let _onForkedChildExit: (() => void) | null = null;
|
|
103
|
+
|
|
104
|
+
// Patch Object.defineProperty globally to force configurable: true on globalThis properties.
|
|
105
|
+
// In real Node.js, each process has its own globalThis. In our browser environment,
|
|
106
|
+
// all forks share globalThis, so libraries like vitest that define non-configurable
|
|
107
|
+
// properties (e.g. __vitest_index__) need them to be configurable for re-runs.
|
|
108
|
+
const _realDefineProperty = Object.defineProperty;
|
|
109
|
+
Object.defineProperty = function(target: object, key: PropertyKey, descriptor: PropertyDescriptor): object {
|
|
110
|
+
if (target === globalThis && descriptor && !descriptor.configurable) {
|
|
111
|
+
descriptor = { ...descriptor, configurable: true };
|
|
112
|
+
}
|
|
113
|
+
return _realDefineProperty.call(Object, target, key, descriptor) as object;
|
|
114
|
+
} as typeof Object.defineProperty;
|
|
115
|
+
|
|
116
|
+
/** The stdin of a held run's guest, as the run's own. */
|
|
117
|
+
type RunStdin = { emit: (event: string, ...args: unknown[]) => void; push: (chunk: string | Uint8Array | null) => boolean };
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* What a host gave one run: where its output goes, the handle that aborts it,
|
|
121
|
+
* whether the host holds it open, and — while it lasts — its guest's stdin.
|
|
122
|
+
*
|
|
123
|
+
* This is per run and never a module global. A child a guest spawns is a
|
|
124
|
+
* second run on the same engine, so a global here meant the later run took the
|
|
125
|
+
* earlier one's stream, its signal and its stdin: a trivial child was read as
|
|
126
|
+
* a watch (it arrived with an abort handle) and never ended, and its parent's
|
|
127
|
+
* output went down the child's channel.
|
|
128
|
+
*/
|
|
129
|
+
export interface RunStreams {
|
|
130
|
+
onStdout?: (data: string) => void;
|
|
131
|
+
onStderr?: (data: string) => void;
|
|
132
|
+
signal?: AbortSignal;
|
|
133
|
+
/** The host keeps this run open; a run that is not held ends when its loop has nothing left. */
|
|
134
|
+
held: boolean;
|
|
135
|
+
/**
|
|
136
|
+
* The run's fd 0 has a writer that has not closed it. A held run's prompt is
|
|
137
|
+
* one; a spawned child whose `stdio[0]` is a pipe is the other, and its
|
|
138
|
+
* writer is the parent, which may write long after the child has started.
|
|
139
|
+
* Without it the child's standard input ended before the first write.
|
|
140
|
+
*/
|
|
141
|
+
stdinOpen?: boolean;
|
|
142
|
+
stdin?: RunStdin | null;
|
|
143
|
+
/**
|
|
144
|
+
* The run's fd 2 is a pipe the parent holds, not the host's own stdio and
|
|
145
|
+
* not inherit. A run that ends uncaught or by `process.exit` with a
|
|
146
|
+
* nonzero code on that pipe writes one line to the host realm's console.
|
|
147
|
+
*/
|
|
148
|
+
stderrIsPipe?: boolean;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* The runs in flight, in the order they started. `container.run` registers one
|
|
153
|
+
* before exec and releases it when the run ends.
|
|
154
|
+
*/
|
|
155
|
+
const _runStreams = new Map<ProcessToken, RunStreams>();
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* One line on the host realm's console when a run the tab owns dies and
|
|
159
|
+
* nobody is reading its stderr.
|
|
160
|
+
*
|
|
161
|
+
* Node prints an uncaught exception to that process's stderr. A parent that
|
|
162
|
+
* pipes the fd and drops it — openvscode-server pipes the extension host
|
|
163
|
+
* into `@vscode/spdlog`, a native module that does not load — loses the
|
|
164
|
+
* reason, in Node too. The child's stderr still goes to its parent; this
|
|
165
|
+
* is the one extra line the host realm can say, because the tab owns the
|
|
166
|
+
* run. Inherit and the host's own stdio already show the reason and get
|
|
167
|
+
* no duplicate. Article 6.
|
|
168
|
+
*/
|
|
169
|
+
function writePipedEndReceipt(
|
|
170
|
+
pid: number,
|
|
171
|
+
argv0: string,
|
|
172
|
+
firstArg: string,
|
|
173
|
+
kind: 'uncaught' | number,
|
|
174
|
+
error: unknown,
|
|
175
|
+
): void {
|
|
176
|
+
const who = firstArg.length > 0 ? `${argv0} ${firstArg}` : argv0;
|
|
177
|
+
const ended = kind === 'uncaught' ? 'uncaught' : String(kind);
|
|
178
|
+
let detail = '';
|
|
179
|
+
if (error instanceof Error) {
|
|
180
|
+
const stack = error.stack ?? `${error.name}: ${error.message}`;
|
|
181
|
+
const lines = stack.split('\n');
|
|
182
|
+
const head = (lines[0] ?? '').trimEnd();
|
|
183
|
+
let frame = '';
|
|
184
|
+
for (let i = 1; i < lines.length; i += 1) {
|
|
185
|
+
const line = lines[i]!.trim();
|
|
186
|
+
if (line.startsWith('at ')) { frame = line; break; }
|
|
187
|
+
}
|
|
188
|
+
detail = frame.length > 0 ? `${head} ${frame}` : head;
|
|
189
|
+
} else if (error !== undefined && error !== null) {
|
|
190
|
+
detail = String(error).split('\n')[0] ?? '';
|
|
191
|
+
}
|
|
192
|
+
console.error(`[run ${pid}] ${who} ended ${ended}${detail.length > 0 ? `: ${detail}` : ''}`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** Register what the host gave this run, under the run's own name. */
|
|
196
|
+
export function registerRunStreams(token: ProcessToken, streams: RunStreams): void {
|
|
197
|
+
_runStreams.delete(token);
|
|
198
|
+
_runStreams.set(token, streams);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Forget a run that has ended. */
|
|
202
|
+
export function releaseRunStreams(token: ProcessToken): void {
|
|
203
|
+
_runStreams.delete(token);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** What the host gave the named run, while it lasts. */
|
|
207
|
+
function runStreamsFor(token: ProcessToken): RunStreams | undefined {
|
|
208
|
+
return _runStreams.get(token);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Send data to the stdin of one run's guest process.
|
|
213
|
+
* Emits both 'data' and 'keypress' events (vitest uses readline keypress events).
|
|
214
|
+
*
|
|
215
|
+
* With a token, that run's stdin. Without one — the door a host that has no id
|
|
216
|
+
* to give still has — the most recently started run the host holds, which is
|
|
217
|
+
* the prompt a person is typing to; a run nobody holds has no prompt.
|
|
218
|
+
*/
|
|
219
|
+
export function sendStdin(data: string, token?: ProcessToken): void {
|
|
220
|
+
let target: RunStdin | null | undefined;
|
|
221
|
+
if (typeof token === 'string') {
|
|
222
|
+
target = _runStreams.get(token)?.stdin;
|
|
223
|
+
} else {
|
|
224
|
+
// Insertion order is start order, so the last held run is the last match.
|
|
225
|
+
for (const streams of _runStreams.values()) {
|
|
226
|
+
if (streams.held && streams.stdin) target = streams.stdin;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
if (target) {
|
|
230
|
+
// Bytes on the guest's fd 0 go through `push`, the way a pipe's arriving
|
|
231
|
+
// bytes reach a Node stream: a reader in paused mode, an iterator, or a
|
|
232
|
+
// `pipe` sees them, where a bare `emit('data')` reached only a listener
|
|
233
|
+
// that happened to be in flowing mode.
|
|
234
|
+
target.push(data);
|
|
235
|
+
for (const ch of data) {
|
|
236
|
+
target.emit('keypress', ch, {
|
|
237
|
+
sequence: ch,
|
|
238
|
+
name: ch,
|
|
239
|
+
ctrl: false,
|
|
240
|
+
meta: false,
|
|
241
|
+
shift: false,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Initialize the child_process shim with a VirtualFS instance
|
|
249
|
+
* Creates a single Bash instance with VirtualFSAdapter for efficient file access
|
|
250
|
+
*/
|
|
251
|
+
/**
|
|
252
|
+
* The last resort, the way Node's `process._fatalException` is one.
|
|
253
|
+
*
|
|
254
|
+
* A guest's callbacks are entered from the realm's own loop through more doors
|
|
255
|
+
* than the engine wraps — a `setImmediate`, a microtask, a host callback — and
|
|
256
|
+
* an exception out of one of those reaches the realm's global `error` event.
|
|
257
|
+
* In the tab that realm is a worker, and the substrate's container reads a
|
|
258
|
+
* worker error as a dead host and disposes it: one program's throw ended every
|
|
259
|
+
* program in the tab. Such an error still belongs to the program that was
|
|
260
|
+
* running, so it is reported as that program's, and the event is cancelled so
|
|
261
|
+
* the page's `Worker.onerror` never fires. An error with no program to own it
|
|
262
|
+
* is left alone, so the host's own failure reporting still sees it.
|
|
263
|
+
*
|
|
264
|
+
* Only a realm that exists to run guests gets this: registered for the guest
|
|
265
|
+
* realm, and installed in a worker. In a Node host the engine is a library and
|
|
266
|
+
* the host reports its own failures.
|
|
267
|
+
*/
|
|
268
|
+
let __backstopInstalled = false;
|
|
269
|
+
forGuestRealm(() => {
|
|
270
|
+
if (__backstopInstalled) return;
|
|
271
|
+
if (typeof (globalThis as Record<string, unknown>).WorkerGlobalScope === 'undefined') return;
|
|
272
|
+
if (typeof globalThis.addEventListener !== 'function') return;
|
|
273
|
+
__backstopInstalled = true;
|
|
274
|
+
globalThis.addEventListener('error', (event: Event) => {
|
|
275
|
+
const token = __currentProcessToken() ?? __lastLaunchedToken;
|
|
276
|
+
if (token === null) return;
|
|
277
|
+
const raised = event as ErrorEvent;
|
|
278
|
+
const error = raised.error ?? new Error(raised.message || 'uncaught exception');
|
|
279
|
+
if (__runFor(token)?.reportUncaught(error)) event.preventDefault();
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
export function initChildProcess(vfs: VirtualFS): void {
|
|
284
|
+
const existing = shells.get(vfs);
|
|
285
|
+
if (existing) {
|
|
286
|
+
currentVfs = vfs;
|
|
287
|
+
bashInstance = existing.bash;
|
|
288
|
+
vfsAdapter = existing.adapter;
|
|
289
|
+
setSyncChildVfs(vfs);
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
// This tree's own shell. The `node` and `npm` commands close over it, so a
|
|
293
|
+
// later `initChildProcess` of another tree cannot redirect them.
|
|
294
|
+
const tree = vfs;
|
|
295
|
+
currentVfs = tree;
|
|
296
|
+
// A synchronous child runs on a thread over this same tree. In a browser
|
|
297
|
+
// realm the thread cannot start inside the synchronous call that needs it —
|
|
298
|
+
// a worker is started by the realm's own loop — so it is warmed here, while
|
|
299
|
+
// the loop still runs. A Node host starts its threads itself, and is not
|
|
300
|
+
// given one it may never use.
|
|
301
|
+
setSyncChildVfs(tree);
|
|
302
|
+
// Node's `process.execPath` names a program that exists. Every tool that
|
|
303
|
+
// re-runs itself, and every one of Node's own tests that spawns a child,
|
|
304
|
+
// writes the path rather than the name — `execSync('"' + process.execPath +
|
|
305
|
+
// '" file')` — and the engine's shell answered "No such file or directory",
|
|
306
|
+
// because its node was a command with no file anywhere. The engine's node is
|
|
307
|
+
// placed where its execPath says it is: a line of shell that runs the
|
|
308
|
+
// command under its plain name.
|
|
309
|
+
try {
|
|
310
|
+
if (!tree.existsSync(__substrateExecPath)) {
|
|
311
|
+
tree.mkdirSync(__substrateExecPath.slice(0, __substrateExecPath.lastIndexOf('/')), { recursive: true });
|
|
312
|
+
tree.writeFileSync(__substrateExecPath, 'node "$@"\n');
|
|
313
|
+
}
|
|
314
|
+
} catch { /* a tree that refuses the write keeps the command under its plain name */ }
|
|
315
|
+
if (typeof (globalThis as Record<string, unknown>).WorkerGlobalScope !== 'undefined' || typeof (globalThis as Record<string, unknown>).document !== 'undefined') warmSyncChild();
|
|
316
|
+
vfsAdapter = new VirtualFSAdapter(tree);
|
|
317
|
+
|
|
318
|
+
// Create custom 'node' command that runs JS files using the Runtime
|
|
319
|
+
/**
|
|
320
|
+
* Hears a realm's unhandled promise rejections for the length of a run and
|
|
321
|
+
* answers a detach. A browser realm raises `unhandledrejection` on its
|
|
322
|
+
* global (the default, a console line, is prevented: the run reports it);
|
|
323
|
+
* a Node host has no such event and reports through its own `process`,
|
|
324
|
+
* the one the host had before any guest's took the global name.
|
|
325
|
+
*
|
|
326
|
+
* Both surfaces carry the promise that rejected, and the report is given it:
|
|
327
|
+
* Node's `unhandledRejection` listener is called `(reason, promise)`, and a
|
|
328
|
+
* listener that uses its second argument is ordinary. openvscode-server's
|
|
329
|
+
* own (`out/server-main.js`) keeps the promise and calls `promise.catch` a
|
|
330
|
+
* second later; under an engine that handed it `undefined` every rejection
|
|
331
|
+
* of the server's became `TypeError: Cannot read properties of undefined
|
|
332
|
+
* (reading 'catch')` inside a timer, 59 of them in one boot.
|
|
333
|
+
*/
|
|
334
|
+
const __listenForUnhandledRejections = (report: (reason: unknown, promise?: Promise<unknown>) => void): (() => void) => {
|
|
335
|
+
if (typeof globalThis.addEventListener === 'function') {
|
|
336
|
+
const target = globalThis as unknown as EventTarget;
|
|
337
|
+
const listener = (event: Event) => { event.preventDefault(); report((event as PromiseRejectionEvent).reason, (event as PromiseRejectionEvent).promise); };
|
|
338
|
+
target.addEventListener('unhandledrejection', listener);
|
|
339
|
+
return () => target.removeEventListener('unhandledrejection', listener);
|
|
340
|
+
}
|
|
341
|
+
const host = __hostProcess;
|
|
342
|
+
if (host) {
|
|
343
|
+
const listener = (reason: unknown, promise?: Promise<unknown>) => report(reason, promise);
|
|
344
|
+
host.on('unhandledRejection', listener);
|
|
345
|
+
return () => host.off('unhandledRejection', listener);
|
|
346
|
+
}
|
|
347
|
+
return () => {};
|
|
348
|
+
};
|
|
349
|
+
const nodeCommand = defineCommand('node', async (args, ctx) => {
|
|
350
|
+
if (!tree) {
|
|
351
|
+
return { stdout: '', stderr: 'VFS not initialized\n', exitCode: 1 };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// Node reads its own options before the script: `node --turbo-fast-api-calls
|
|
355
|
+
// file.js a b` runs file.js with `a b`, and the options it were given are
|
|
356
|
+
// its `execArgv`. Node's own suite spawns children that way, and the
|
|
357
|
+
// engine took the first option for the script and died on
|
|
358
|
+
// "Cannot find module '--turbo-fast-api-calls'".
|
|
359
|
+
//
|
|
360
|
+
// An option may also take its value as the next word, and the engine
|
|
361
|
+
// cannot know which do: `node --conditions node child.js` gave `node` as
|
|
362
|
+
// the script. So the script is the first argument that NAMES A FILE, and
|
|
363
|
+
// everything before it is `execArgv` -- which is what Node's own option
|
|
364
|
+
// parser arrives at, by knowing its options rather than by looking. A
|
|
365
|
+
// command line that names no file at all keeps the first word after the
|
|
366
|
+
// options, so `node missing.js` still says which module it cannot find.
|
|
367
|
+
//
|
|
368
|
+
// The path on a `node` command line is a path, and it is resolved as
|
|
369
|
+
// `require` resolves one: the file itself, then `.js`, `.mjs`, `.cjs`,
|
|
370
|
+
// `.json`, then a directory's `package.json` `main` or its `index.js`.
|
|
371
|
+
// `fork` used to do this for itself; `fork` is Node's own file now and
|
|
372
|
+
// hands the module path straight to `node`, so the resolution belongs
|
|
373
|
+
// where a path on a command line is read. Opened literally, the path
|
|
374
|
+
// openvscode-server forks its pty host with, `<server>/out/bootstrap-fork`
|
|
375
|
+
// for the file `bootstrap-fork.js`, was ENOENT and the pty host died at
|
|
376
|
+
// its first fork.
|
|
377
|
+
const execArgv: string[] = [];
|
|
378
|
+
let first = 0;
|
|
379
|
+
while (first < args.length && args[first]!.startsWith('-') && args[first] !== '-' && args[first] !== '--') {
|
|
380
|
+
execArgv.push(args[first]!);
|
|
381
|
+
first += 1;
|
|
382
|
+
}
|
|
383
|
+
if (args[first] === '--') first += 1;
|
|
384
|
+
const resolver = __nodeResolverFor(tree, 'runtime');
|
|
385
|
+
const fileNamed = (word: string): string | null => {
|
|
386
|
+
const requested = __resolvePath(ctx.cwd, word);
|
|
387
|
+
const found = resolver.resolve(requested, ctx.cwd);
|
|
388
|
+
if (found !== null && tree.existsSync(found)) return found;
|
|
389
|
+
return tree.existsSync(requested) ? requested : null;
|
|
390
|
+
};
|
|
391
|
+
let script = first;
|
|
392
|
+
let resolvedPath: string | null = null;
|
|
393
|
+
for (let index = first; index < args.length; index += 1) {
|
|
394
|
+
const found = fileNamed(args[index]!);
|
|
395
|
+
if (found !== null) { script = index; resolvedPath = found; break; }
|
|
396
|
+
}
|
|
397
|
+
for (let index = first; index < script; index += 1) execArgv.push(args[index]!);
|
|
398
|
+
if (!args[script]) {
|
|
399
|
+
return { stdout: '', stderr: 'Usage: node <script.js> [args...]\n', exitCode: 1 };
|
|
400
|
+
}
|
|
401
|
+
if (resolvedPath === null) {
|
|
402
|
+
return { stdout: '', stderr: `Error: Cannot find module '${__resolvePath(ctx.cwd, args[script]!)}'\n`, exitCode: 1 };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
let stdout = '';
|
|
406
|
+
let stderr = '';
|
|
407
|
+
|
|
408
|
+
// A run the host named is this guest process, for as long as the run
|
|
409
|
+
// lasts: `pendingTimers`, `processPorts` and `stopProcess` are answered
|
|
410
|
+
// from here, and a server the guest opens registers under this name.
|
|
411
|
+
const runToken = runTokenOf(ctx);
|
|
412
|
+
// What the host gave THIS run — its streams, its abort handle, whether it
|
|
413
|
+
// holds the run open. Read once here and nowhere from a module global, so
|
|
414
|
+
// a child spawned by this guest (a second run) cannot take them.
|
|
415
|
+
const streams = runToken === null ? undefined : runStreamsFor(runToken);
|
|
416
|
+
|
|
417
|
+
// Track whether process.exit() was called
|
|
418
|
+
let exitCalled = false;
|
|
419
|
+
let exitCode = 0;
|
|
420
|
+
let syncExecution = true;
|
|
421
|
+
let exitResolve: ((code: number) => void) | null = null;
|
|
422
|
+
const exitPromise = new Promise<number>((resolve) => { exitResolve = resolve; });
|
|
423
|
+
|
|
424
|
+
// Helper to append to stdout, also streaming if configured
|
|
425
|
+
// A process that has exited writes nothing more, as Node's cannot: a
|
|
426
|
+
// continuation of the guest that runs on after its exit keeps its output
|
|
427
|
+
// to itself.
|
|
428
|
+
const appendStdout = (data: string) => {
|
|
429
|
+
if (exitCalled) return;
|
|
430
|
+
stdout += data;
|
|
431
|
+
streams?.onStdout?.(data);
|
|
432
|
+
};
|
|
433
|
+
const appendStderr = (data: string) => {
|
|
434
|
+
if (exitCalled) return;
|
|
435
|
+
stderr += data;
|
|
436
|
+
streams?.onStderr?.(data);
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
// A child started with an IPC channel is told its descriptor in its
|
|
440
|
+
// environment, exactly as Node tells one; the variables are taken out
|
|
441
|
+
// before the guest sees them, as Node's own bootstrap deletes them, so a
|
|
442
|
+
// grandchild does not inherit a channel that is not its own.
|
|
443
|
+
const guestEnv = guestEnvironmentOf(ctx);
|
|
444
|
+
const channelFd = Number.parseInt(guestEnv.NODE_CHANNEL_FD ?? '', 10);
|
|
445
|
+
const channelSerialization = guestEnv.NODE_CHANNEL_SERIALIZATION_MODE || 'json';
|
|
446
|
+
delete guestEnv.NODE_CHANNEL_FD;
|
|
447
|
+
delete guestEnv.NODE_CHANNEL_SERIALIZATION_MODE;
|
|
448
|
+
|
|
449
|
+
// Create a runtime with output capture for both console.log AND process.stdout.write
|
|
450
|
+
const runtime = new Runtime(tree, {
|
|
451
|
+
cwd: ctx.cwd,
|
|
452
|
+
env: guestEnv,
|
|
453
|
+
onConsole: (method, consoleArgs) => {
|
|
454
|
+
const msg = consoleArgs.map(a => String(a)).join(' ') + '\n';
|
|
455
|
+
if (method === 'error') {
|
|
456
|
+
appendStderr(msg);
|
|
457
|
+
} else {
|
|
458
|
+
appendStdout(msg);
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
onStdout: (data: string) => {
|
|
462
|
+
appendStdout(data);
|
|
463
|
+
},
|
|
464
|
+
onStderr: (data: string) => {
|
|
465
|
+
appendStderr(data);
|
|
466
|
+
},
|
|
467
|
+
// The guest's standard input is what the shell put on its fd 0: the text
|
|
468
|
+
// left of a pipe, or the `stdin` a host gave `container.run`. A held run
|
|
469
|
+
// (one the host streams and can still feed with `sendStdin`) leaves it
|
|
470
|
+
// open, as a pipe whose writer has not closed.
|
|
471
|
+
stdin: typeof ctx.stdin === 'string' ? ctx.stdin : '',
|
|
472
|
+
...(streams?.held || streams?.stdinOpen ? { stdinHeld: true } : {}),
|
|
473
|
+
...(streams?.held ? { tty: true } : {}),
|
|
474
|
+
// The numbers this run was started with, so the guest's `process.pid`
|
|
475
|
+
// is the one its parent's handle carries.
|
|
476
|
+
...(runPid(runToken) ? { pid: runPid(runToken)!.pid, ppid: runPid(runToken)!.ppid } : {}),
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Override process.exit to resolve the completion promise
|
|
480
|
+
const proc = runtime.getProcess();
|
|
481
|
+
// This run's numbers, recorded under its name: a child it spawns reads
|
|
482
|
+
// them for its own `ppid`, and `process.kill(pid, 0)` asks this registry
|
|
483
|
+
// whether a pid is a live process.
|
|
484
|
+
if (runToken !== null) setRunPid(runToken, proc.pid, proc.ppid);
|
|
485
|
+
const releaseRun = runToken === null ? null : __recordRun(runToken, {
|
|
486
|
+
pendingTimers: () => pendingGuestTimers(proc),
|
|
487
|
+
stopTimers: () => stopGuestTimers(proc),
|
|
488
|
+
reportUncaught: (error: unknown) => __reportUncaughtException(proc, error),
|
|
489
|
+
});
|
|
490
|
+
const launchedBefore = __lastLaunchedToken;
|
|
491
|
+
if (runToken !== null) __setLastLaunchedToken(runToken);
|
|
492
|
+
let hostReceiptWritten = false;
|
|
493
|
+
let lastUncaught: unknown;
|
|
494
|
+
const writeHostReceipt = (kind: 'uncaught' | number, error?: unknown): void => {
|
|
495
|
+
if (hostReceiptWritten || streams?.stderrIsPipe !== true) return;
|
|
496
|
+
hostReceiptWritten = true;
|
|
497
|
+
writePipedEndReceipt(proc.pid, proc.argv0 || 'node', proc.argv[1] ?? '', kind, error);
|
|
498
|
+
};
|
|
499
|
+
proc.exit = ((code = 0) => {
|
|
500
|
+
if (!exitCalled) {
|
|
501
|
+
exitCalled = true;
|
|
502
|
+
// As Node takes one: a string from a command line becomes its number.
|
|
503
|
+
exitCode = __substrateExitCode(code);
|
|
504
|
+
code = exitCode;
|
|
505
|
+
// A handled uncaught that then `process.exit(1)` is a normal exit
|
|
506
|
+
// with code 1. The receipt still names the error the handler saw:
|
|
507
|
+
// VS Code's host installs `uncaughtException` and exits 1, and the
|
|
508
|
+
// previous line was `ended 1` with no reason.
|
|
509
|
+
if (exitCode !== 0) writeHostReceipt(exitCode, lastUncaught);
|
|
510
|
+
// Node runs a program's `exit` listeners while everything it holds is
|
|
511
|
+
// still open, and closes the loop's handles after them. Releasing
|
|
512
|
+
// first closed a forked child's IPC channel before its own exit
|
|
513
|
+
// listeners ran, and VS Code's file-watcher child -- which pipes its
|
|
514
|
+
// console over `process.send` and dies of a failed require -- wrote on
|
|
515
|
+
// the closed channel and took `write EBADF` as its last act.
|
|
516
|
+
proc.emit('exit', code);
|
|
517
|
+
exitResolve!(code);
|
|
518
|
+
}
|
|
519
|
+
// `process.exit()` ends a Node process and everything it holds; a named
|
|
520
|
+
// run's servers and timers go with it.
|
|
521
|
+
if (runToken !== null) { __releaseOwnedServers(runToken); __releaseOwnedHandles(runToken); }
|
|
522
|
+
// In sync context, throw to stop execution (like real process.exit)
|
|
523
|
+
// In async context, return silently to avoid unhandled rejections
|
|
524
|
+
if (syncExecution) {
|
|
525
|
+
throw new Error(`Process exited with code ${code}`);
|
|
526
|
+
}
|
|
527
|
+
}) as (code?: number) => never;
|
|
528
|
+
|
|
529
|
+
// Set up process.argv for the script. Node fills argv[0] with the
|
|
530
|
+
// executable's path, the same value `process.execPath` reports; `argv0`
|
|
531
|
+
// keeps the original argv[0], the plain word.
|
|
532
|
+
proc.argv = [__substrateExecPath, resolvedPath, ...args.slice(script + 1)];
|
|
533
|
+
proc.argv0 = 'node';
|
|
534
|
+
proc.execArgv = execArgv;
|
|
535
|
+
|
|
536
|
+
// Whoever writes to this run's fd 0 writes here: a person typing at a held
|
|
537
|
+
// run's prompt, through `sendStdin`, or a parent writing to the stdin pipe
|
|
538
|
+
// of a child it spawned.
|
|
539
|
+
if (streams) streams.stdin = proc.stdin;
|
|
540
|
+
|
|
541
|
+
// For long-running commands (watch mode), report as TTY so tools like
|
|
542
|
+
// vitest set up interactive features (file watching, stdin commands). A
|
|
543
|
+
// spawned child's stdio is a pipe, as Node's is, and gets none of this.
|
|
544
|
+
if (streams?.held) {
|
|
545
|
+
proc.stdout.isTTY = true;
|
|
546
|
+
proc.stderr.isTTY = true;
|
|
547
|
+
proc.stdin.isTTY = true;
|
|
548
|
+
proc.stdin.setRawMode = () => proc.stdin;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// A child started with a channel wires its own end of it before its
|
|
552
|
+
// module runs, which is what `lib/internal/process/pre_execution.js` does
|
|
553
|
+
// in Node and `_forkChild` is the body of. Inside the run, so the pipe is
|
|
554
|
+
// this run's handle and the descriptor is read against this run's table.
|
|
555
|
+
if (Number.isInteger(channelFd) && channelFd >= 0 && runToken !== null) {
|
|
556
|
+
enterRun(runToken, () => { attachChannel(proc, channelFd, channelSerialization); });
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// A promise rejection nobody handles ends the program, as it ends one in
|
|
560
|
+
// Node (since 15 an unhandled rejection is an uncaught exception): a guest
|
|
561
|
+
// listener for `unhandledRejection` on its process takes it, else one for
|
|
562
|
+
// `uncaughtException` with origin 'unhandledRejection', else the error is
|
|
563
|
+
// printed to stderr and the program exits 1. It was reported only while
|
|
564
|
+
// the runner waited on a quiet program, only through a realm's
|
|
565
|
+
// `unhandledrejection` event (a Node host has none; it reports through
|
|
566
|
+
// `process`), and never ended the program: vue-pure-admin's mock loader
|
|
567
|
+
// died in a rejection and the program ended silently with exit 0, where
|
|
568
|
+
// Node prints the error and exits 1. The report is attached for the whole
|
|
569
|
+
// run, on whichever surface the realm has.
|
|
570
|
+
const onUnhandledRejection = (reason: unknown, promise?: Promise<unknown>): void => {
|
|
571
|
+
if (reason instanceof Error && reason.message.startsWith('Process exited with code')) return;
|
|
572
|
+
lastUncaught = reason;
|
|
573
|
+
if (proc.listenerCount('unhandledRejection') > 0) { proc.emit('unhandledRejection', reason, promise); return; }
|
|
574
|
+
if (proc.listenerCount('uncaughtException') > 0) { proc.emit('uncaughtException', reason, 'unhandledRejection'); return; }
|
|
575
|
+
const errorMsg = reason instanceof Error ? `${reason.message}\n${reason.stack || ''}` : String(reason);
|
|
576
|
+
appendStderr(`Error: ${errorMsg}\n`);
|
|
577
|
+
writeHostReceipt('uncaught', reason);
|
|
578
|
+
if (exitCalled) return;
|
|
579
|
+
const wasSync = syncExecution;
|
|
580
|
+
syncExecution = false;
|
|
581
|
+
try { proc.exit(1); } finally { syncExecution = wasSync; }
|
|
582
|
+
};
|
|
583
|
+
const detachRejections = __listenForUnhandledRejections(onUnhandledRejection);
|
|
584
|
+
|
|
585
|
+
// An exception nobody caught ends the program it was thrown in, as it ends
|
|
586
|
+
// one in Node: a guest listener for `uncaughtException` on its process
|
|
587
|
+
// takes it, else the stack is printed to this program's stderr and the
|
|
588
|
+
// program exits 1. Nothing of it reaches the realm. Unreported, a throw
|
|
589
|
+
// from a timer callback of openvscode-server's reached the worker's global
|
|
590
|
+
// `error` event, the substrate's container read that as a dead host and
|
|
591
|
+
// disposed the worker, and every run's writes failed from then on.
|
|
592
|
+
const onUncaughtException = (error: unknown): void => {
|
|
593
|
+
if (error instanceof Error && error.message.startsWith('Process exited with code')) return;
|
|
594
|
+
lastUncaught = error;
|
|
595
|
+
// A program that set a capture callback takes every uncaught exception
|
|
596
|
+
// itself, before any listener, which is Node's own order.
|
|
597
|
+
const capture = __substrateUncaughtCapture();
|
|
598
|
+
if (capture) { capture(error); return; }
|
|
599
|
+
if (proc.listenerCount('uncaughtException') > 0) { proc.emit('uncaughtException', error, 'uncaughtException'); return; }
|
|
600
|
+
appendStderr(`${error instanceof Error ? (error.stack ?? `${error.name}: ${error.message}`) : String(error)}\n`);
|
|
601
|
+
writeHostReceipt('uncaught', error);
|
|
602
|
+
if (exitCalled) return;
|
|
603
|
+
const wasSync = syncExecution;
|
|
604
|
+
syncExecution = false;
|
|
605
|
+
try { proc.exit(1); } finally { syncExecution = wasSync; }
|
|
606
|
+
};
|
|
607
|
+
const detachUncaught = __onUncaughtException(proc, onUncaughtException);
|
|
608
|
+
try {
|
|
609
|
+
|
|
610
|
+
let entrySettling: Promise<unknown> | undefined;
|
|
611
|
+
try {
|
|
612
|
+
// Run the script (synchronous part)
|
|
613
|
+
// The entry runs AS this run: the token a `spawn` inside it reads is
|
|
614
|
+
// this program's, not whichever run started last. The engine's storage
|
|
615
|
+
// carries it into the timers, microtasks and `then` callbacks the entry
|
|
616
|
+
// schedules from here, so a child spawned later still names its parent.
|
|
617
|
+
const runEntry = () => runtime.runFile(resolvedPath);
|
|
618
|
+
entrySettling = __substratePendingOf(
|
|
619
|
+
(runToken === null ? runEntry() : enterRun(runToken, runEntry)).exports,
|
|
620
|
+
);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
// process.exit() throws to stop sync execution — this is expected
|
|
623
|
+
if (error instanceof Error && error.message.startsWith('Process exited with code')) {
|
|
624
|
+
return { stdout, stderr, exitCode };
|
|
625
|
+
}
|
|
626
|
+
// A throw out of the entry is this program's uncaught exception and
|
|
627
|
+
// goes through the same door as any other: the guest's own
|
|
628
|
+
// `uncaughtException` listeners, else its stderr and exit 1 -- which
|
|
629
|
+
// runs its `exit` listeners. Returning a result straight from here
|
|
630
|
+
// skipped them, and VS Code's file-watcher child, which pipes its
|
|
631
|
+
// console over IPC and dies of a failed require, never sent the lines
|
|
632
|
+
// that say why.
|
|
633
|
+
syncExecution = false;
|
|
634
|
+
onUncaughtException(error);
|
|
635
|
+
return { stdout, stderr, exitCode: exitCalled ? exitCode : 1 };
|
|
636
|
+
} finally {
|
|
637
|
+
// After runFile returns, switch to async mode (no more throwing from process.exit)
|
|
638
|
+
syncExecution = false;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// If process.exit was called synchronously (but didn't throw for some reason), return
|
|
642
|
+
if (exitCalled) {
|
|
643
|
+
return { stdout, stderr, exitCode };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// An entry still settling, a top-level `await` in it or in what it
|
|
647
|
+
// imports, has run when it has settled; one that fails there fails the
|
|
648
|
+
// program, as Node prints the error and exits 1.
|
|
649
|
+
// The program may exit before the entry settles, by `process.exit` or a
|
|
650
|
+
// fatal rejection in the meantime; the run ends at the exit, as Node's
|
|
651
|
+
// does, and what the entry does after is its own.
|
|
652
|
+
if (entrySettling) {
|
|
653
|
+
const settling = entrySettling;
|
|
654
|
+
try { await Promise.race([settling, exitPromise]); }
|
|
655
|
+
catch (error) {
|
|
656
|
+
if (error instanceof Error && error.message.startsWith('Process exited with code')) return { stdout, stderr, exitCode };
|
|
657
|
+
const errorMsg = error instanceof Error ? `${error.message}\n${error.stack || ''}` : String(error);
|
|
658
|
+
return { stdout, stderr: stderr + `Error: ${errorMsg}\n`, exitCode: 1 };
|
|
659
|
+
}
|
|
660
|
+
if (exitCalled) { settling.catch(() => {}); return { stdout, stderr, exitCode }; }
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Script returned without calling process.exit().
|
|
664
|
+
// Heuristic: if we already captured output, the script likely finished synchronously
|
|
665
|
+
// (e.g. a simple "console.log('hello')" script). Return immediately.
|
|
666
|
+
// A program that printed and still holds a timer, or a build, is working:
|
|
667
|
+
// the timers are the engine's own count for this guest, where this asked
|
|
668
|
+
// an adapter that is gone through an ambient name nothing ever installed.
|
|
669
|
+
// An active handle keeps Node's loop alive, so a run that owns one is not
|
|
670
|
+
// idle however long it has been quiet. The engine registers a guest's
|
|
671
|
+
// servers under this run's name as it opens them, and releases them when
|
|
672
|
+
// the run ends; counting them here is what a `vite` a guest spawned needs,
|
|
673
|
+
// which listened, printed, and was cut half a second later with its server
|
|
674
|
+
// still up. A connected socket is such a handle too, and counting only the
|
|
675
|
+
// listening ones settled VS Code's extension host — a program whose only
|
|
676
|
+
// handle is one socket back to the server that forked it and which sets no
|
|
677
|
+
// timer — as idle, with exit 0, three times over.
|
|
678
|
+
const __ownsHandles = () => runToken !== null && (__ownedServerPorts(runToken).length > 0 || __ownedHandleCount(runToken) > 0);
|
|
679
|
+
const __printedThenWorking = () => pendingGuestTimers(proc) > 0 || heldWork().count > 0 || __ownsHandles();
|
|
680
|
+
if ((stdout.length > 0 || stderr.length > 0) && !__printedThenWorking()) {
|
|
681
|
+
// Settling the command is host work. Killing guest timers must not
|
|
682
|
+
// cancel this continuation and leave the command's promise unresolved.
|
|
683
|
+
await new Promise(r => (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, r, 0));
|
|
684
|
+
// Work that began during that tick is the program's still: a build
|
|
685
|
+
// reached through a dynamic import's chain, a timer the entry set.
|
|
686
|
+
// vue-pure-admin's mock loader, `import('bundle-import').then(...)`
|
|
687
|
+
// after one printed line, was cut here with exit 0 before its build
|
|
688
|
+
// began, where Node's loop runs until nothing is left. Such a program
|
|
689
|
+
// waits below with the others.
|
|
690
|
+
if (exitCalled) return { stdout, stderr, exitCode };
|
|
691
|
+
// A run a host holds under a signal is the host's to end: it reads the
|
|
692
|
+
// program's servers and work through doors of its own and keeps the
|
|
693
|
+
// program open by them. Such a run reports settled here, as it did,
|
|
694
|
+
// once nothing is held; holding it for a pending timer instead made
|
|
695
|
+
// the substrate's loop take a `vite` quiet during its start for a
|
|
696
|
+
// finished run and abort it before it listened. A run nobody holds
|
|
697
|
+
// waits for its timers below, as Node's loop does.
|
|
698
|
+
if (streams?.held || (!__printedThenWorking() && pendingGuestTimers(proc) === 0)) {
|
|
699
|
+
return { stdout, stderr, exitCode: typeof proc !== 'undefined' && typeof proc.exitCode === 'number' ? proc.exitCode : 0 };
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// No output yet — script likely has async work (e.g. vitest test runner).
|
|
704
|
+
// Wait for process.exit() or until output stabilizes.
|
|
705
|
+
|
|
706
|
+
// Listen for forked child exits to shorten the idle timeout.
|
|
707
|
+
// Many CLI tools (vitest, jest, etc.) fork workers and exit shortly after
|
|
708
|
+
// all children complete. We use a shorter timeout once children are done.
|
|
709
|
+
let childrenExited = false;
|
|
710
|
+
const prevChildExitHandler = _onForkedChildExit;
|
|
711
|
+
_onForkedChildExit = () => {
|
|
712
|
+
if (_activeForkedChildren <= 0) childrenExited = true;
|
|
713
|
+
prevChildExitHandler?.();
|
|
714
|
+
};
|
|
715
|
+
|
|
716
|
+
try {
|
|
717
|
+
// Poll until process.exit is called, output stabilizes, or we time out
|
|
718
|
+
const MAX_TOTAL_MS = 60000;
|
|
719
|
+
const IDLE_TIMEOUT_MS = 500;
|
|
720
|
+
const SILENT_IDLE_TIMEOUT_MS = 2000;
|
|
721
|
+
const POST_CHILD_EXIT_IDLE_MS = 100; // short timeout after children finish
|
|
722
|
+
const CHECK_MS = 50;
|
|
723
|
+
const startTime = Date.now();
|
|
724
|
+
let lastOutputLen = stdout.length + stderr.length;
|
|
725
|
+
let idleMs = 0;
|
|
726
|
+
// A timer the guest still holds is work in Node's loop, whether or not
|
|
727
|
+
// the program has printed; so is a handle it has open.
|
|
728
|
+
const stillWorking = (): boolean => pendingGuestTimers(proc) > 0 || __ownsHandles();
|
|
729
|
+
|
|
730
|
+
// When an abort signal is present (e.g. watch mode), don't apply idle timeout —
|
|
731
|
+
// only exit when aborted or process.exit is called.
|
|
732
|
+
const isLongRunning = !!streams?.held;
|
|
733
|
+
|
|
734
|
+
while (!exitCalled) {
|
|
735
|
+
// Check abort signal for long-running commands (watch mode)
|
|
736
|
+
if (streams?.signal?.aborted) break;
|
|
737
|
+
|
|
738
|
+
// Check if exitPromise resolved (non-blocking)
|
|
739
|
+
const raceResult = await Promise.race([
|
|
740
|
+
exitPromise.then(() => 'exit' as const),
|
|
741
|
+
new Promise<'tick'>(r => (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, () => r('tick'), CHECK_MS)),
|
|
742
|
+
]);
|
|
743
|
+
|
|
744
|
+
if (raceResult === 'exit' || exitCalled) break;
|
|
745
|
+
if (streams?.signal?.aborted) break;
|
|
746
|
+
|
|
747
|
+
const currentLen = stdout.length + stderr.length;
|
|
748
|
+
if (currentLen > lastOutputLen) {
|
|
749
|
+
// New output — reset idle timer
|
|
750
|
+
lastOutputLen = currentLen;
|
|
751
|
+
idleMs = 0;
|
|
752
|
+
} else {
|
|
753
|
+
idleMs += CHECK_MS;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
// Use shorter idle timeout once all forked children have exited
|
|
757
|
+
// Skip idle timeout for long-running commands (watch mode)
|
|
758
|
+
if (!isLongRunning) {
|
|
759
|
+
const effectiveIdle = childrenExited ? POST_CHILD_EXIT_IDLE_MS : IDLE_TIMEOUT_MS;
|
|
760
|
+
if (heldWork().count > 0) idleMs = 0;
|
|
761
|
+
// A program ends when its loop has nothing left, in Node whether or
|
|
762
|
+
// not it printed. The break waited for output, so a program that
|
|
763
|
+
// wrote nothing, a passing test of Node's own suite, waited the
|
|
764
|
+
// whole minute. A silent program that has been idle for a while,
|
|
765
|
+
// with no build held and no timer of its own pending, is done; the
|
|
766
|
+
// longer wait is for a start that is quiet while it fetches, which
|
|
767
|
+
// the engine cannot yet see as work.
|
|
768
|
+
const silentIdle = SILENT_IDLE_TIMEOUT_MS;
|
|
769
|
+
if (lastOutputLen > 0 && !stillWorking() && idleMs >= effectiveIdle) break;
|
|
770
|
+
if (lastOutputLen === 0 && !stillWorking() && idleMs >= silentIdle) break;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// The hard timeout is for a program whose work the engine cannot see:
|
|
774
|
+
// a start that is quiet while it fetches, a loop with nothing in the
|
|
775
|
+
// engine's own registries. A run that still holds a handle or a timer
|
|
776
|
+
// is not that -- Node's loop runs while one is open, for as long as it
|
|
777
|
+
// is open -- and cutting it at a minute ended a forked child that was
|
|
778
|
+
// a server, which is every extension host and every pty host.
|
|
779
|
+
if (!isLongRunning && !stillWorking() && Date.now() - startTime >= MAX_TOTAL_MS) break;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return { stdout, stderr, exitCode: exitCalled ? exitCode : (typeof proc !== 'undefined' && typeof proc.exitCode === 'number' ? proc.exitCode : 0) };
|
|
783
|
+
} finally {
|
|
784
|
+
if (streams) streams.stdin = null;
|
|
785
|
+
_onForkedChildExit = prevChildExitHandler;
|
|
786
|
+
}
|
|
787
|
+
} finally {
|
|
788
|
+
detachRejections();
|
|
789
|
+
detachUncaught();
|
|
790
|
+
// A process that ends releases its listening sockets, whatever ended it:
|
|
791
|
+
// a return from the entry, a throw, an exit. Only `process.exit` released
|
|
792
|
+
// them here, so `vite --host` — which opened 5173 and then died at startup
|
|
793
|
+
// with an ESLint error, exit 1 — left 5173 in the registry with nothing
|
|
794
|
+
// behind it, and the page went on previewing a port no process held.
|
|
795
|
+
// Safe twice: a run that exited already emptied its own entries.
|
|
796
|
+
if (runToken !== null) { __releaseOwnedServers(runToken, false); __releaseOwnedHandles(runToken); }
|
|
797
|
+
// A run that has ended is no longer a process: the number it wrote into
|
|
798
|
+
// a lock file answers ESRCH from here on, which is how a stale lock is
|
|
799
|
+
// stolen rather than waited on.
|
|
800
|
+
if (runToken !== null) forgetRunPid(runToken);
|
|
801
|
+
// A program that has ended holds no timers, as an ended Node process
|
|
802
|
+
// holds none. `process.exit` asked for them to be stopped on a tick of
|
|
803
|
+
// its own, and the run was taken out of the registry here before that
|
|
804
|
+
// tick came, so the stop found nothing: an `setInterval` whose callback
|
|
805
|
+
// threw ended its program and then went on firing forever, into a run
|
|
806
|
+
// that no longer had anywhere to report an exception, and out to the
|
|
807
|
+
// realm. The run's own timers stop here, while the process is still in
|
|
808
|
+
// hand; the scheduled stop then finds nothing left to do.
|
|
809
|
+
stopGuestTimers(proc);
|
|
810
|
+
releaseRun?.();
|
|
811
|
+
// A run that has ended owns nothing more: a server a later, unnamed run
|
|
812
|
+
// opens is nobody's, not the last named run's.
|
|
813
|
+
if (runToken !== null && __lastLaunchedToken === runToken) __setLastLaunchedToken(launchedBefore);
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
|
|
817
|
+
// Create custom 'npm' command that runs scripts from package.json
|
|
818
|
+
const npmCommand = defineCommand('npm', async (args, ctx) => {
|
|
819
|
+
if (!tree) {
|
|
820
|
+
return { stdout: '', stderr: 'VFS not initialized\n', exitCode: 1 };
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
const subcommand = args[0];
|
|
824
|
+
|
|
825
|
+
if (!subcommand || subcommand === 'help' || subcommand === '--help') {
|
|
826
|
+
return {
|
|
827
|
+
stdout: 'Usage: npm <command>\n\nCommands:\n run <script> Run a script from package.json\n start Run the start script\n test Run the test script\n install [pkg] Install packages\n ls List installed packages\n',
|
|
828
|
+
stderr: '',
|
|
829
|
+
exitCode: 0,
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
switch (subcommand) {
|
|
834
|
+
case 'run':
|
|
835
|
+
case 'run-script':
|
|
836
|
+
return handleNpmRun(args.slice(1), ctx);
|
|
837
|
+
case 'start':
|
|
838
|
+
return handleNpmRun(['start'], ctx);
|
|
839
|
+
case 'test':
|
|
840
|
+
case 't':
|
|
841
|
+
case 'tst':
|
|
842
|
+
return handleNpmRun(['test'], ctx);
|
|
843
|
+
case 'install':
|
|
844
|
+
case 'i':
|
|
845
|
+
case 'add':
|
|
846
|
+
return handleNpmInstall(args.slice(1), ctx);
|
|
847
|
+
case 'ls':
|
|
848
|
+
case 'list':
|
|
849
|
+
return handleNpmList(ctx);
|
|
850
|
+
default:
|
|
851
|
+
return {
|
|
852
|
+
stdout: '',
|
|
853
|
+
stderr: `npm ERR! Unknown command: "${subcommand}"\n`,
|
|
854
|
+
exitCode: 1,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
bashInstance = new Bash({
|
|
860
|
+
fs: vfsAdapter,
|
|
861
|
+
cwd: '/',
|
|
862
|
+
env: {
|
|
863
|
+
HOME: '/home/user',
|
|
864
|
+
USER: 'user',
|
|
865
|
+
PATH: '/usr/local/bin:/usr/bin:/bin:/node_modules/.bin',
|
|
866
|
+
},
|
|
867
|
+
customCommands: [nodeCommand, npmCommand],
|
|
868
|
+
});
|
|
869
|
+
shells.set(tree, { bash: bashInstance, adapter: vfsAdapter });
|
|
870
|
+
|
|
871
|
+
if (processRunnerInstalled) return;
|
|
872
|
+
processRunnerInstalled = true;
|
|
873
|
+
|
|
874
|
+
// What a `Process` handle runs. Node's own `child_process.js` starts, ends
|
|
875
|
+
// and pipes a child through the binding; the binding asks this.
|
|
876
|
+
setProgramResolver(engineProgramFor);
|
|
877
|
+
setProcessRunner({
|
|
878
|
+
resolves: (request: RunRequest): boolean => {
|
|
879
|
+
if (!bashInstance) return false;
|
|
880
|
+
// A directory that is not there is `ENOENT` before the program is even
|
|
881
|
+
// looked for, as it is for `execve`: `spawn(cmd, { cwd: 'nope' })`
|
|
882
|
+
// reports a child that never started, and `child.pid` is undefined.
|
|
883
|
+
if (typeof request.cwd === 'string' && request.cwd.length > 0 && !existsInTree(request.cwd)) return false;
|
|
884
|
+
// A `{ shell: true }` spawn is `/bin/sh -c <line>`, and the shell is
|
|
885
|
+
// always there; what the line names is the line's own business, as it
|
|
886
|
+
// is on a machine.
|
|
887
|
+
if (__substrateShellLine(request.file, request.args) !== null) return true;
|
|
888
|
+
// A page that published a process host carries programs the engine
|
|
889
|
+
// cannot enumerate -- a registered command, a WALI pack -- so the host
|
|
890
|
+
// is asked by running it, and reports its own absence.
|
|
891
|
+
if (hostExecutor() !== null) return true;
|
|
892
|
+
return programExists(request.file, request.cwd, request.env);
|
|
893
|
+
},
|
|
894
|
+
start: startChildRun,
|
|
895
|
+
});
|
|
896
|
+
watchChildren();
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* The child's end of an IPC channel, wired onto its own process before its
|
|
901
|
+
* module runs. This is `lib/internal/process/pre_execution.js`'s
|
|
902
|
+
* `setupChildProcessIpcChannel` and `child_process._forkChild`'s body, over
|
|
903
|
+
* the engine's own process object rather than the realm's `process`, because
|
|
904
|
+
* a realm here holds many programs and the channel belongs to one of them.
|
|
905
|
+
*/
|
|
906
|
+
function attachChannel(proc: ReturnType<Runtime['getProcess']>, fd: number, serialization: string): void {
|
|
907
|
+
const channel = new Pipe(pipeConstants.IPC);
|
|
908
|
+
if (channel.open(fd) !== 0) { channel.close(); return; }
|
|
909
|
+
// Node unrefs a child's channel: the channel alone does not hold a program
|
|
910
|
+
// open, and `Control.refCounted` puts it back the moment the program
|
|
911
|
+
// listens for a message.
|
|
912
|
+
channel.unref();
|
|
913
|
+
const control = setupChannel(proc, channel, serialization);
|
|
914
|
+
proc.on('newListener', (name: unknown) => {
|
|
915
|
+
if (name === 'message' || name === 'disconnect') control.refCounted();
|
|
916
|
+
});
|
|
917
|
+
proc.on('removeListener', (name: unknown) => {
|
|
918
|
+
if (name === 'message' || name === 'disconnect') control.unrefCounted();
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
/** The shell's environment as a record: just-bash hands a command a Map. */
|
|
923
|
+
function environmentOf(ctx: CommandContext): Record<string, string> {
|
|
924
|
+
const env: unknown = ctx.env;
|
|
925
|
+
if (env instanceof Map) return Object.fromEntries(env as Map<string, string>);
|
|
926
|
+
return { ...((env ?? {}) as Record<string, string>) };
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
/**
|
|
930
|
+
* The environment a guest process sees. The name its run travels under is the
|
|
931
|
+
* shell's business, not the program's: Node's `process.env` carries no name
|
|
932
|
+
* for the process, so the variable is taken out here while the shell keeps it
|
|
933
|
+
* for a `node` a script of its own goes on to run.
|
|
934
|
+
*/
|
|
935
|
+
function guestEnvironmentOf(ctx: CommandContext): Record<string, string> {
|
|
936
|
+
const env = environmentOf(ctx);
|
|
937
|
+
delete env[PROCESS_TOKEN_ENV];
|
|
938
|
+
return env;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/** The name the host gave the run this command belongs to, where it named one. */
|
|
942
|
+
function runTokenOf(ctx: CommandContext): ProcessToken | null {
|
|
943
|
+
const value = ctx.env instanceof Map ? ctx.env.get(PROCESS_TOKEN_ENV) : (ctx.env as Record<string, string> | undefined)?.[PROCESS_TOKEN_ENV];
|
|
944
|
+
return typeof value === 'string' && value.length > 0 ? value : null;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Read and parse package.json from the VFS
|
|
949
|
+
*/
|
|
950
|
+
function readPackageJson(cwd: string): { pkgJson: PackageJson; error?: undefined } | { pkgJson?: undefined; error: JustBashExecResult } {
|
|
951
|
+
const pkgJsonPath = `${cwd}/package.json`.replace(/\/+/g, '/');
|
|
952
|
+
|
|
953
|
+
if (!currentVfs!.existsSync(pkgJsonPath)) {
|
|
954
|
+
return {
|
|
955
|
+
error: {
|
|
956
|
+
stdout: '',
|
|
957
|
+
stderr: 'npm ERR! no package.json found\n',
|
|
958
|
+
exitCode: 1,
|
|
959
|
+
},
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
try {
|
|
964
|
+
const pkgJson = JSON.parse(currentVfs!.readFileSync(pkgJsonPath, 'utf8')) as PackageJson;
|
|
965
|
+
return { pkgJson };
|
|
966
|
+
} catch {
|
|
967
|
+
return {
|
|
968
|
+
error: {
|
|
969
|
+
stdout: '',
|
|
970
|
+
stderr: 'npm ERR! Failed to parse package.json\n',
|
|
971
|
+
exitCode: 1,
|
|
972
|
+
},
|
|
973
|
+
};
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Handle `npm run [script]` — execute a script from package.json
|
|
979
|
+
*/
|
|
980
|
+
async function handleNpmRun(args: string[], ctx: CommandContext): Promise<JustBashExecResult> {
|
|
981
|
+
const scriptName = args[0];
|
|
982
|
+
|
|
983
|
+
// "npm run" with no script name: list available scripts
|
|
984
|
+
if (!scriptName) {
|
|
985
|
+
return listScripts(ctx);
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const result = readPackageJson(ctx.cwd);
|
|
989
|
+
if (result.error) return result.error;
|
|
990
|
+
const pkgJson = result.pkgJson;
|
|
991
|
+
|
|
992
|
+
const scripts = pkgJson.scripts || {};
|
|
993
|
+
const scriptCommand = scripts[scriptName];
|
|
994
|
+
|
|
995
|
+
if (!scriptCommand) {
|
|
996
|
+
const available = Object.keys(scripts);
|
|
997
|
+
let msg = `npm ERR! Missing script: "${scriptName}"\n`;
|
|
998
|
+
if (available.length > 0) {
|
|
999
|
+
msg += '\nnpm ERR! Available scripts:\n';
|
|
1000
|
+
for (const name of available) {
|
|
1001
|
+
msg += `npm ERR! ${name}\n`;
|
|
1002
|
+
msg += `npm ERR! ${scripts[name]}\n`;
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return { stdout: '', stderr: msg, exitCode: 1 };
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
if (!ctx.exec) {
|
|
1009
|
+
return {
|
|
1010
|
+
stdout: '',
|
|
1011
|
+
stderr: 'npm ERR! Script execution not available in this context\n',
|
|
1012
|
+
exitCode: 1,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
// Set up npm-specific environment variables
|
|
1017
|
+
const npmEnv: Record<string, string> = {
|
|
1018
|
+
...environmentOf(ctx),
|
|
1019
|
+
npm_lifecycle_event: scriptName,
|
|
1020
|
+
};
|
|
1021
|
+
if (pkgJson.name) npmEnv.npm_package_name = pkgJson.name;
|
|
1022
|
+
if (pkgJson.version) npmEnv.npm_package_version = pkgJson.version;
|
|
1023
|
+
|
|
1024
|
+
let allStdout = '';
|
|
1025
|
+
let allStderr = '';
|
|
1026
|
+
const label = `${pkgJson.name || ''}@${pkgJson.version || ''}`;
|
|
1027
|
+
|
|
1028
|
+
// Run pre<script> if it exists
|
|
1029
|
+
const preScript = scripts[`pre${scriptName}`];
|
|
1030
|
+
if (preScript) {
|
|
1031
|
+
allStderr += `\n> ${label} pre${scriptName}\n> ${preScript}\n\n`;
|
|
1032
|
+
const preResult = await ctx.exec(preScript, { cwd: ctx.cwd, env: npmEnv });
|
|
1033
|
+
allStdout += preResult.stdout;
|
|
1034
|
+
allStderr += preResult.stderr;
|
|
1035
|
+
if (preResult.exitCode !== 0) {
|
|
1036
|
+
return { stdout: allStdout, stderr: allStderr, exitCode: preResult.exitCode };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Run the main script
|
|
1041
|
+
allStderr += `\n> ${label} ${scriptName}\n> ${scriptCommand}\n\n`;
|
|
1042
|
+
const mainResult = await ctx.exec(scriptCommand, { cwd: ctx.cwd, env: npmEnv });
|
|
1043
|
+
allStdout += mainResult.stdout;
|
|
1044
|
+
allStderr += mainResult.stderr;
|
|
1045
|
+
|
|
1046
|
+
if (mainResult.exitCode !== 0) {
|
|
1047
|
+
return { stdout: allStdout, stderr: allStderr, exitCode: mainResult.exitCode };
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Run post<script> if it exists
|
|
1051
|
+
const postScript = scripts[`post${scriptName}`];
|
|
1052
|
+
if (postScript) {
|
|
1053
|
+
allStderr += `\n> ${label} post${scriptName}\n> ${postScript}\n\n`;
|
|
1054
|
+
const postResult = await ctx.exec(postScript, { cwd: ctx.cwd, env: npmEnv });
|
|
1055
|
+
allStdout += postResult.stdout;
|
|
1056
|
+
allStderr += postResult.stderr;
|
|
1057
|
+
if (postResult.exitCode !== 0) {
|
|
1058
|
+
return { stdout: allStdout, stderr: allStderr, exitCode: postResult.exitCode };
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
return { stdout: allStdout, stderr: allStderr, exitCode: 0 };
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* List available scripts from package.json (when `npm run` is called with no args)
|
|
1067
|
+
*/
|
|
1068
|
+
function listScripts(ctx: CommandContext): JustBashExecResult {
|
|
1069
|
+
const result = readPackageJson(ctx.cwd);
|
|
1070
|
+
if (result.error) return result.error;
|
|
1071
|
+
const pkgJson = result.pkgJson;
|
|
1072
|
+
|
|
1073
|
+
const scripts = pkgJson.scripts || {};
|
|
1074
|
+
const names = Object.keys(scripts);
|
|
1075
|
+
|
|
1076
|
+
if (names.length === 0) {
|
|
1077
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const lifecycle = ['prestart', 'start', 'poststart', 'pretest', 'test', 'posttest', 'prestop', 'stop', 'poststop'];
|
|
1081
|
+
const lifecyclePresent = names.filter(n => lifecycle.includes(n));
|
|
1082
|
+
const customPresent = names.filter(n => !lifecycle.includes(n));
|
|
1083
|
+
|
|
1084
|
+
let output = `Lifecycle scripts included in ${pkgJson.name || ''}:\n`;
|
|
1085
|
+
for (const name of lifecyclePresent) {
|
|
1086
|
+
output += ` ${name}\n ${scripts[name]}\n`;
|
|
1087
|
+
}
|
|
1088
|
+
if (customPresent.length > 0) {
|
|
1089
|
+
output += '\navailable via `npm run-script`:\n';
|
|
1090
|
+
for (const name of customPresent) {
|
|
1091
|
+
output += ` ${name}\n ${scripts[name]}\n`;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
return { stdout: output, stderr: '', exitCode: 0 };
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Handle `npm install [pkg]` — bridge to PackageManager
|
|
1100
|
+
*/
|
|
1101
|
+
async function handleNpmInstall(args: string[], ctx: CommandContext): Promise<JustBashExecResult> {
|
|
1102
|
+
const { PackageManager } = await import('../npm/index');
|
|
1103
|
+
const pm = new PackageManager(currentVfs!, { cwd: ctx.cwd });
|
|
1104
|
+
|
|
1105
|
+
let stdout = '';
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
const pkgArgs = args.filter(a => !a.startsWith('-'));
|
|
1109
|
+
if (pkgArgs.length === 0) {
|
|
1110
|
+
// npm install (no package name) -> install from package.json.
|
|
1111
|
+
// npm installs devDependencies unless told not to: `--omit=dev`,
|
|
1112
|
+
// `--production`, `--prod`, or `NODE_ENV=production`. A bare install that
|
|
1113
|
+
// left them out made a project whose build script runs `tsc` or `tsup`
|
|
1114
|
+
// install cleanly and then find no such command.
|
|
1115
|
+
const __omitDev = args.includes('--omit=dev') || args.includes('--production') || args.includes('--prod') || (environmentOf(ctx).NODE_ENV === 'production');
|
|
1116
|
+
const installResult = await pm.installFromPackageJson({
|
|
1117
|
+
includeDev: !__omitDev,
|
|
1118
|
+
onProgress: (msg: string) => { stdout += msg + '\n'; },
|
|
1119
|
+
});
|
|
1120
|
+
stdout += `added ${installResult.added.length} packages\n`;
|
|
1121
|
+
} else {
|
|
1122
|
+
// npm install <pkg> [<pkg> ...]
|
|
1123
|
+
for (const arg of pkgArgs) {
|
|
1124
|
+
const installResult = await pm.install(arg, {
|
|
1125
|
+
save: true,
|
|
1126
|
+
onProgress: (msg: string) => { stdout += msg + '\n'; },
|
|
1127
|
+
});
|
|
1128
|
+
stdout += `added ${installResult.added.length} packages\n`;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
return { stdout, stderr: '', exitCode: 0 };
|
|
1132
|
+
} catch (error) {
|
|
1133
|
+
const msg = error instanceof Error ? error.stack || error.message : String(error);
|
|
1134
|
+
return { stdout, stderr: `npm ERR! ${msg}\n`, exitCode: 1 };
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Handle `npm ls` — list installed packages
|
|
1140
|
+
*/
|
|
1141
|
+
async function handleNpmList(ctx: CommandContext): Promise<JustBashExecResult> {
|
|
1142
|
+
const { PackageManager } = await import('../npm/index');
|
|
1143
|
+
const pm = new PackageManager(currentVfs!, { cwd: ctx.cwd });
|
|
1144
|
+
const packages = pm.list();
|
|
1145
|
+
const entries = Object.entries(packages);
|
|
1146
|
+
|
|
1147
|
+
if (entries.length === 0) {
|
|
1148
|
+
return { stdout: '(empty)\n', stderr: '', exitCode: 0 };
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
let output = `${ctx.cwd}\n`;
|
|
1152
|
+
for (const [name, version] of entries) {
|
|
1153
|
+
output += `+-- ${name}@${version}\n`;
|
|
1154
|
+
}
|
|
1155
|
+
return { stdout: output, stderr: '', exitCode: 0 };
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/** What a host asks of `runCommand`, the engine's own door onto its shell. */
|
|
1159
|
+
export interface ExecOptions {
|
|
1160
|
+
cwd?: string;
|
|
1161
|
+
env?: Record<string, string>;
|
|
1162
|
+
/** What the shell reads on stdin, so a builtin reads what was piped to it. */
|
|
1163
|
+
stdin?: string;
|
|
1164
|
+
/**
|
|
1165
|
+
* A name for this run, which the `node` command records its guest process
|
|
1166
|
+
* under and the container answers `pendingTimers`, `processPorts` and
|
|
1167
|
+
* `stopProcess` about. It travels in the shell's environment so that two
|
|
1168
|
+
* runs in flight at once each reach their own command.
|
|
1169
|
+
*/
|
|
1170
|
+
processToken?: string;
|
|
1171
|
+
/** The tree this command runs on, so two containers in one realm keep theirs. */
|
|
1172
|
+
vfs?: VirtualFS;
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
/** What `runCommand` answers with, in the shape a shell answers. */
|
|
1176
|
+
export type ExecCallback = (
|
|
1177
|
+
error: (Error & { code?: number }) | null,
|
|
1178
|
+
stdout: string,
|
|
1179
|
+
stderr: string
|
|
1180
|
+
) => void;
|
|
1181
|
+
|
|
1182
|
+
/**
|
|
1183
|
+
* The one host hook Tabnode lacks: a child process can execute through the
|
|
1184
|
+
* embedding runtime's shared process host instead of this module's private
|
|
1185
|
+
* just-bash instance. The host publishes an executor on a well-known global
|
|
1186
|
+
* symbol; when none is published every call falls through to just-bash. This
|
|
1187
|
+
* is the door a page-registered program and a WALI pack are reached by, and
|
|
1188
|
+
* it is what a `Process` handle runs when the host has taken it.
|
|
1189
|
+
*/
|
|
1190
|
+
/** What the embedding runtime's process host answers when a command finishes. */
|
|
1191
|
+
interface ChildProcessHostResult {
|
|
1192
|
+
stdout?: string;
|
|
1193
|
+
stderr?: string;
|
|
1194
|
+
exitCode: number;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
/** How a command is handed to that host. */
|
|
1198
|
+
interface ChildProcessHostRequest {
|
|
1199
|
+
cwd?: string;
|
|
1200
|
+
env?: Record<string, string>;
|
|
1201
|
+
/** Bytes already on fd 0 when the command begins, as Node delivers them. */
|
|
1202
|
+
stdin?: string;
|
|
1203
|
+
stdinStream: AsyncIterable<Uint8Array>;
|
|
1204
|
+
signal: AbortSignal;
|
|
1205
|
+
hold: { value: boolean };
|
|
1206
|
+
onStdout: (data: string) => void;
|
|
1207
|
+
onStderr: (data: string) => void;
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/** The executor a host publishes on the well-known global symbol. */
|
|
1211
|
+
interface ChildProcessHost {
|
|
1212
|
+
run(command: string, request: ChildProcessHostRequest): Promise<ChildProcessHostResult>;
|
|
1213
|
+
/** Runs the host leaves to just-bash instead, one per count. */
|
|
1214
|
+
bypass: number;
|
|
1215
|
+
parentSignal?: AbortSignal;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const __browserRuntimeChildProcessExecutorSymbol = Symbol.for("@volter/browser-runtime/child-process-executor");
|
|
1219
|
+
|
|
1220
|
+
/** The host's executor, where one is published and has not bypassed this run. */
|
|
1221
|
+
function hostExecutor(): ChildProcessHost | null {
|
|
1222
|
+
const bridge = (globalThis as unknown as Record<symbol, ChildProcessHost | undefined>)[__browserRuntimeChildProcessExecutorSymbol];
|
|
1223
|
+
if (!bridge || typeof bridge.run !== 'function') return null;
|
|
1224
|
+
return bridge;
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
/** What a run of a command answers, however it was routed. */
|
|
1228
|
+
interface CommandOutcome {
|
|
1229
|
+
stdout: string;
|
|
1230
|
+
stderr: string;
|
|
1231
|
+
exitCode: number;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
/** What a caller gives one run of a command line. */
|
|
1235
|
+
interface CommandRun {
|
|
1236
|
+
command: string;
|
|
1237
|
+
/**
|
|
1238
|
+
* Whether the engine's own shell answers for this command before the host's
|
|
1239
|
+
* process host is asked. A run the HOST asked for is the host's first: that
|
|
1240
|
+
* is how a page-registered program and a WALI pack are reached. A child a
|
|
1241
|
+
* GUEST spawned is the engine's first, because the engine has a command for
|
|
1242
|
+
* it and because a host that takes the child over reports it as a run of its
|
|
1243
|
+
* own -- which is why every line a forked child printed reached the tab's
|
|
1244
|
+
* terminal twice, once as the child's run and once down its parent's
|
|
1245
|
+
* inherited stdout. The host is still asked for what the engine does not
|
|
1246
|
+
* carry.
|
|
1247
|
+
*/
|
|
1248
|
+
engineFirst?: boolean;
|
|
1249
|
+
cwd?: string;
|
|
1250
|
+
env?: Record<string, string>;
|
|
1251
|
+
stdin?: string;
|
|
1252
|
+
/** Bytes a writer still holds the run's fd 0 open with, as they arrive. */
|
|
1253
|
+
stdinStream?: AsyncIterable<Uint8Array>;
|
|
1254
|
+
signal?: AbortSignal;
|
|
1255
|
+
onStdout?: (data: string) => void;
|
|
1256
|
+
onStderr?: (data: string) => void;
|
|
1257
|
+
vfs?: VirtualFS;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/**
|
|
1261
|
+
* One command line, run by whichever door answers for it: the host's own
|
|
1262
|
+
* process host where the page published one (a registered program, a WALI
|
|
1263
|
+
* pack), else the engine's shell, whose `node` and `npm` are its own commands.
|
|
1264
|
+
* This is the whole of the engine's routing, and it is what a `Process`
|
|
1265
|
+
* handle, a `runCommand` and a synchronous child all reach the world through.
|
|
1266
|
+
*/
|
|
1267
|
+
async function routeCommand(run: CommandRun): Promise<CommandOutcome> {
|
|
1268
|
+
const tree = run.vfs ?? treeForRun();
|
|
1269
|
+
const shell = shellOf(tree);
|
|
1270
|
+
if (tree && shell) {
|
|
1271
|
+
currentVfs = tree;
|
|
1272
|
+
bashInstance = shell.bash;
|
|
1273
|
+
vfsAdapter = shell.adapter;
|
|
1274
|
+
}
|
|
1275
|
+
const bash = shell?.bash ?? bashInstance;
|
|
1276
|
+
const bridge = run.engineFirst === true && bash !== null ? null : hostExecutor();
|
|
1277
|
+
if (bridge !== null && bridge.bypass > 0) bridge.bypass -= 1;
|
|
1278
|
+
else if (bridge !== null) {
|
|
1279
|
+
const controller = new AbortController();
|
|
1280
|
+
const abortWithParent = (): void => {
|
|
1281
|
+
if (!controller.signal.aborted) controller.abort(bridge.parentSignal?.reason);
|
|
1282
|
+
};
|
|
1283
|
+
if (run.signal) {
|
|
1284
|
+
if (run.signal.aborted) controller.abort(run.signal.reason);
|
|
1285
|
+
else run.signal.addEventListener('abort', () => controller.abort(run.signal?.reason), { once: true });
|
|
1286
|
+
}
|
|
1287
|
+
if (bridge.parentSignal) {
|
|
1288
|
+
if (bridge.parentSignal.aborted) abortWithParent();
|
|
1289
|
+
else bridge.parentSignal.addEventListener('abort', abortWithParent, { once: true });
|
|
1290
|
+
}
|
|
1291
|
+
let streamedOut = '';
|
|
1292
|
+
let streamedErr = '';
|
|
1293
|
+
const onStdout = (data: string): void => { streamedOut += String(data); run.onStdout?.(String(data)); };
|
|
1294
|
+
const onStderr = (data: string): void => { streamedErr += String(data); run.onStderr?.(String(data)); };
|
|
1295
|
+
try {
|
|
1296
|
+
const result = await bridge.run(run.command, {
|
|
1297
|
+
cwd: run.cwd,
|
|
1298
|
+
env: run.env,
|
|
1299
|
+
...(typeof run.stdin === 'string' ? { stdin: run.stdin } : {}),
|
|
1300
|
+
stdinStream: run.stdinStream ?? emptyStdinStream(),
|
|
1301
|
+
signal: controller.signal,
|
|
1302
|
+
hold: { value: true },
|
|
1303
|
+
onStdout,
|
|
1304
|
+
onStderr,
|
|
1305
|
+
});
|
|
1306
|
+
const stdout = result.stdout || streamedOut;
|
|
1307
|
+
const stderr = result.stderr || streamedErr;
|
|
1308
|
+
if (!streamedOut && result.stdout) run.onStdout?.(result.stdout);
|
|
1309
|
+
if (!streamedErr && result.stderr) run.onStderr?.(result.stderr);
|
|
1310
|
+
return { stdout, stderr, exitCode: result.exitCode };
|
|
1311
|
+
} finally {
|
|
1312
|
+
if (bridge.parentSignal) bridge.parentSignal.removeEventListener('abort', abortWithParent);
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
if (!bash) throw new Error('child_process not initialized');
|
|
1317
|
+
const result = await bash.exec(run.command, {
|
|
1318
|
+
cwd: run.cwd,
|
|
1319
|
+
env: run.env,
|
|
1320
|
+
...(typeof run.stdin === 'string' ? { stdin: run.stdin } : {}),
|
|
1321
|
+
});
|
|
1322
|
+
return { stdout: result.stdout || '', stderr: result.stderr || '', exitCode: result.exitCode };
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
/** A child whose stdin nobody writes to still needs a stream the host can read. */
|
|
1326
|
+
function emptyStdinStream(): AsyncIterable<Uint8Array> {
|
|
1327
|
+
return { async *[Symbol.asyncIterator]() { /* nothing is ever written */ } };
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
/**
|
|
1331
|
+
* The host's door onto the engine's shell: one named run of a command line,
|
|
1332
|
+
* with the host's streams and its abort handle. This is `container.run`'s
|
|
1333
|
+
* half, not Node's `child_process.exec` -- Node's is its own file's, over the
|
|
1334
|
+
* binding, and it knows nothing of a run's name.
|
|
1335
|
+
*/
|
|
1336
|
+
export function runCommand(command: string, options: ExecOptions, callback?: ExecCallback): void {
|
|
1337
|
+
void (async () => {
|
|
1338
|
+
try {
|
|
1339
|
+
const outcome = await routeCommand({
|
|
1340
|
+
command,
|
|
1341
|
+
cwd: options.cwd,
|
|
1342
|
+
env: typeof options.processToken === 'string'
|
|
1343
|
+
? { ...(options.env ?? {}), [PROCESS_TOKEN_ENV]: options.processToken }
|
|
1344
|
+
: options.env,
|
|
1345
|
+
stdin: options.stdin,
|
|
1346
|
+
signal: typeof options.processToken === 'string' ? _runStreams.get(options.processToken)?.signal : undefined,
|
|
1347
|
+
vfs: options.vfs,
|
|
1348
|
+
});
|
|
1349
|
+
if (!callback) return;
|
|
1350
|
+
if (outcome.exitCode !== 0) {
|
|
1351
|
+
const error = Object.assign(new Error(`Command failed: ${command}`), { code: outcome.exitCode });
|
|
1352
|
+
callback(error, outcome.stdout, outcome.stderr);
|
|
1353
|
+
} else {
|
|
1354
|
+
callback(null, outcome.stdout, outcome.stderr);
|
|
1355
|
+
}
|
|
1356
|
+
} catch (error) {
|
|
1357
|
+
callback?.(error as Error & { code?: number }, '', '');
|
|
1358
|
+
}
|
|
1359
|
+
})();
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* The engine's own names for the programs it carries, for the one question
|
|
1364
|
+
* libuv answers before a child exists: is there such a program at all?
|
|
1365
|
+
* `spawn('nope')` is an `ENOENT` on the child, not a shell's 127 reported as
|
|
1366
|
+
* an exit status, and Node's `ChildProcess` can only tell the two apart if
|
|
1367
|
+
* `Process.spawn` says so when it is called.
|
|
1368
|
+
*/
|
|
1369
|
+
let __shellCommandNames: Set<string> | null = null;
|
|
1370
|
+
function shellCommandNames(): Set<string> {
|
|
1371
|
+
if (__shellCommandNames === null) __shellCommandNames = new Set([...getCommandNames(), 'node', 'npm']);
|
|
1372
|
+
return __shellCommandNames;
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/** Whether a path names something the engine has. */
|
|
1376
|
+
function existsInTree(path: string): boolean {
|
|
1377
|
+
const tree = treeForRun();
|
|
1378
|
+
if (!tree) return false;
|
|
1379
|
+
try { return tree.existsSync(path); } catch { return false; }
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* The program the engine will actually run for the path a caller named.
|
|
1384
|
+
*
|
|
1385
|
+
* The engine's shell carries its commands under their plain names, and a
|
|
1386
|
+
* machine carries them at `/bin` and `/usr/bin`: `spawn('/usr/bin/env', …)`
|
|
1387
|
+
* means the shell's `env`, and that is what Node's own `test-child-process-env`
|
|
1388
|
+
* spawns. A file that really is in the tree at the path wins, because that is
|
|
1389
|
+
* a program the engine can run; the engine's own `node`, written to
|
|
1390
|
+
* `/usr/local/bin/node` when the shim is initialized, is one of those.
|
|
1391
|
+
*/
|
|
1392
|
+
function engineProgramFor(file: string, cwd?: string): string {
|
|
1393
|
+
const name = __substrateProgramName(file);
|
|
1394
|
+
if (!name.includes('/')) return name;
|
|
1395
|
+
if (existsInTree(__resolvePath(cwd ?? '/', name))) return name;
|
|
1396
|
+
const bare = name.slice(name.lastIndexOf('/') + 1);
|
|
1397
|
+
return shellCommandNames().has(bare) ? bare : name;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/** Whether the engine's shell can find a program under this name. */
|
|
1401
|
+
function programExists(file: string, cwd: string | undefined, env: Record<string, string>): boolean {
|
|
1402
|
+
const name = __substrateProgramName(file);
|
|
1403
|
+
const bare = name.includes('/') ? name.slice(name.lastIndexOf('/') + 1) : name;
|
|
1404
|
+
// The engine's shell carries its commands under their plain names and at
|
|
1405
|
+
// the paths a machine carries them at: `/usr/bin/env` is `env`, which is
|
|
1406
|
+
// what `spawn('/usr/bin/env', …)` means and what a Dockerfile's shebang
|
|
1407
|
+
// reaches. A file in the tree is a program too, and so is one on PATH.
|
|
1408
|
+
if (shellCommandNames().has(bare)) return true;
|
|
1409
|
+
if (name.includes('/')) return existsInTree(__resolvePath(cwd ?? '/', name));
|
|
1410
|
+
for (const dir of (env.PATH ?? '/usr/local/bin:/usr/bin:/bin:/node_modules/.bin').split(':')) {
|
|
1411
|
+
if (dir.length > 0 && existsInTree(`${dir}/${name}`.replace(/\/+/gu, '/'))) return true;
|
|
1412
|
+
}
|
|
1413
|
+
return false;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** The runs a `Process` handle started, so a later name never lands on a live one. */
|
|
1417
|
+
let __nextChildRun = 1;
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* What a `Process` handle runs: a named run of the engine, with its streams
|
|
1421
|
+
* wired to the handle's pipes and its end reported once.
|
|
1422
|
+
*
|
|
1423
|
+
* A killed run reports its end at once. The engine has no signal to deliver:
|
|
1424
|
+
* a command already inside the shell cannot always be interrupted, and a
|
|
1425
|
+
* parent that called `kill` has no process left to wait for -- so the abort
|
|
1426
|
+
* goes out, the run's timers and handles are released as ending a process
|
|
1427
|
+
* releases them, and `onexit` carries the signal. Whatever the command does
|
|
1428
|
+
* afterwards is nobody's.
|
|
1429
|
+
*/
|
|
1430
|
+
function startChildRun(request: RunRequest): StartedRun {
|
|
1431
|
+
const token: ProcessToken = `child-${__nextChildRun++}`;
|
|
1432
|
+
// A child is a process and has its own number, which its parent reads off
|
|
1433
|
+
// the handle and the child itself reports as `process.pid`; `ppid` is the
|
|
1434
|
+
// number of the run that spawned it. `src/process-tokens.ts` says what
|
|
1435
|
+
// reads them.
|
|
1436
|
+
const pid = mintPid();
|
|
1437
|
+
setRunPid(token, pid, runPid(__currentProcessToken() ?? __lastLaunchedToken)?.pid ?? 0);
|
|
1438
|
+
const controller = new AbortController();
|
|
1439
|
+
const pendingStdin: Array<Uint8Array | null> = [];
|
|
1440
|
+
let stdinSink: RunStdin | null = null;
|
|
1441
|
+
let finished = false;
|
|
1442
|
+
let started = false;
|
|
1443
|
+
let killedBy: string | null = null;
|
|
1444
|
+
/** What the parent wrote to the child's fd 0 before the command began. */
|
|
1445
|
+
const initialStdin: Uint8Array[] = [];
|
|
1446
|
+
|
|
1447
|
+
const flushStdin = (): void => {
|
|
1448
|
+
if (!stdinSink) return;
|
|
1449
|
+
while (pendingStdin.length > 0) stdinSink.push(pendingStdin.shift() as Uint8Array | null);
|
|
1450
|
+
};
|
|
1451
|
+
|
|
1452
|
+
let streamedOut = 0;
|
|
1453
|
+
let streamedErr = 0;
|
|
1454
|
+
const streams: RunStreams = {
|
|
1455
|
+
onStdout: (data: string) => { streamedOut += data.length; request.stdout?.(data); },
|
|
1456
|
+
onStderr: (data: string) => { streamedErr += data.length; request.stderr?.(data); },
|
|
1457
|
+
signal: controller.signal,
|
|
1458
|
+
held: false,
|
|
1459
|
+
stdinOpen: request.stdinIsPipe,
|
|
1460
|
+
stderrIsPipe: request.stderrIsPipe,
|
|
1461
|
+
get stdin(): RunStdin | null | undefined { return stdinSink; },
|
|
1462
|
+
set stdin(sink: RunStdin | null | undefined) { stdinSink = sink ?? null; if (stdinSink) flushStdin(); },
|
|
1463
|
+
};
|
|
1464
|
+
registerRunStreams(token, streams);
|
|
1465
|
+
// The child is started with the descriptors its parent wired: an IPC channel
|
|
1466
|
+
// is at the number its `NODE_CHANNEL_FD` will name, under this run's name,
|
|
1467
|
+
// because every child is told the same number.
|
|
1468
|
+
if (request.channel) registerRunFd(token, request.channel.fd, 'PIPE', request.channel.pipe);
|
|
1469
|
+
|
|
1470
|
+
const end = (code: number, signal: string | null): void => {
|
|
1471
|
+
if (finished) return;
|
|
1472
|
+
finished = true;
|
|
1473
|
+
_activeForkedChildren -= 1;
|
|
1474
|
+
releaseRunStreams(token);
|
|
1475
|
+
releaseRunFds(token);
|
|
1476
|
+
// A run that has ended is no longer a process: its number answers ESRCH.
|
|
1477
|
+
forgetRunPid(token);
|
|
1478
|
+
_onForkedChildExit?.();
|
|
1479
|
+
request.exit(code, signal);
|
|
1480
|
+
};
|
|
1481
|
+
|
|
1482
|
+
/**
|
|
1483
|
+
* The command begins here, once, with whatever its standard input already
|
|
1484
|
+
* holds. The engine's shell takes a run's standard input as one string
|
|
1485
|
+
* given before the command runs -- `cat` and every other builtin read
|
|
1486
|
+
* `ctx.stdin` and nothing after it -- so a child whose fd 0 is a pipe waits
|
|
1487
|
+
* for the turn in which it was spawned to finish: `spawn('cat')`,
|
|
1488
|
+
* `stdin.write(...)`, `stdin.end()` is one synchronous block in every
|
|
1489
|
+
* program that does it, and the writes have landed by the time the realm's
|
|
1490
|
+
* own loop comes back. A write after that reaches a `node` run, whose fd 0
|
|
1491
|
+
* is a real stream, and is lost on a builtin, which has already read.
|
|
1492
|
+
*/
|
|
1493
|
+
const begin = (): void => {
|
|
1494
|
+
if (started || finished) return;
|
|
1495
|
+
started = true;
|
|
1496
|
+
const stdin = initialStdin.length > 0 ? Buffer.concat(initialStdin).toString('utf8') : undefined;
|
|
1497
|
+
void (async () => {
|
|
1498
|
+
let outcome: CommandOutcome;
|
|
1499
|
+
try {
|
|
1500
|
+
outcome = await routeCommand({
|
|
1501
|
+
command: __substrateLineFor(request.file, request.args, request.cwd),
|
|
1502
|
+
engineFirst: programExists(request.file, request.cwd, request.env),
|
|
1503
|
+
cwd: request.cwd,
|
|
1504
|
+
env: { ...request.env, [PROCESS_TOKEN_ENV]: token },
|
|
1505
|
+
stdin,
|
|
1506
|
+
signal: controller.signal,
|
|
1507
|
+
onStdout: streams.onStdout,
|
|
1508
|
+
onStderr: streams.onStderr,
|
|
1509
|
+
});
|
|
1510
|
+
} catch (error) {
|
|
1511
|
+
outcome = { stdout: '', stderr: `${error instanceof Error ? error.message : String(error)}\n`, exitCode: 1 };
|
|
1512
|
+
}
|
|
1513
|
+
// A command the engine's shell ran to the end and did not stream --
|
|
1514
|
+
// every builtin -- delivers its output here; the `node` command streamed
|
|
1515
|
+
// its own as it went, and only what it has not already sent is left.
|
|
1516
|
+
if (outcome.stdout.length > streamedOut) request.stdout?.(outcome.stdout.slice(streamedOut));
|
|
1517
|
+
if (outcome.stderr.length > streamedErr) request.stderr?.(outcome.stderr.slice(streamedErr));
|
|
1518
|
+
end(outcome.exitCode, null);
|
|
1519
|
+
})();
|
|
1520
|
+
};
|
|
1521
|
+
|
|
1522
|
+
_activeForkedChildren += 1;
|
|
1523
|
+
if (request.stdinIsPipe) (globalThis.__browserRuntimeNativeSetTimeout ?? setTimeout).call(globalThis, begin, 0);
|
|
1524
|
+
else begin();
|
|
1525
|
+
|
|
1526
|
+
return {
|
|
1527
|
+
token,
|
|
1528
|
+
pid,
|
|
1529
|
+
kill(signal: string): number {
|
|
1530
|
+
if (finished) return UV_ESRCH;
|
|
1531
|
+
killedBy = signal;
|
|
1532
|
+
controller.abort(signal);
|
|
1533
|
+
__releaseOwnedServers(token, false);
|
|
1534
|
+
__releaseOwnedHandles(token);
|
|
1535
|
+
__stopOwnedProcess(token);
|
|
1536
|
+
end(0, killedBy);
|
|
1537
|
+
return 0;
|
|
1538
|
+
},
|
|
1539
|
+
writeStdin(bytes: Uint8Array): void {
|
|
1540
|
+
if (!started) { initialStdin.push(bytes); return; }
|
|
1541
|
+
pendingStdin.push(bytes);
|
|
1542
|
+
flushStdin();
|
|
1543
|
+
},
|
|
1544
|
+
endStdin(): void {
|
|
1545
|
+
if (!started) {
|
|
1546
|
+
// The writer closed before the command began: the run's fd 0 is the
|
|
1547
|
+
// string already collected and nothing more, so it is not held open.
|
|
1548
|
+
streams.stdinOpen = false;
|
|
1549
|
+
begin();
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
pendingStdin.push(null);
|
|
1553
|
+
flushStdin();
|
|
1554
|
+
},
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
/**
|
|
1559
|
+
* The guest's children are processes it can see, so `process.kill(pid, 0)`
|
|
1560
|
+
* answers about a live child rather than ESRCH for every pid but its own.
|
|
1561
|
+
* Node publishes each `ChildProcess` on the `child_process` diagnostics
|
|
1562
|
+
* channel as it is constructed, which is the door this reads: the pid is
|
|
1563
|
+
* filled by `spawn` a moment later, so the child is recorded on the next tick.
|
|
1564
|
+
*/
|
|
1565
|
+
function watchChildren(): void {
|
|
1566
|
+
// Node's own `diagnostics_channel`, the same module the vendored
|
|
1567
|
+
// `child_process.js` publishes on. A channel is a name in one registry, and
|
|
1568
|
+
// the engine's own imitation of the module was a second registry: a
|
|
1569
|
+
// subscriber there heard nothing a vendored file published.
|
|
1570
|
+
const diagnosticsChannel = diagnosticsChannelModule as unknown as {
|
|
1571
|
+
channel(name: string): { subscribe(listener: (message: unknown) => void): void };
|
|
1572
|
+
};
|
|
1573
|
+
diagnosticsChannel.channel('child_process').subscribe((message: unknown) => {
|
|
1574
|
+
const child = (message as { process?: { pid?: number; exitCode: number | null; signalCode: string | null; kill(signal?: string): boolean; once(event: string, listener: () => void): unknown } }).process;
|
|
1575
|
+
if (!child) return;
|
|
1576
|
+
queueMicrotask(() => {
|
|
1577
|
+
const pid = child.pid;
|
|
1578
|
+
if (typeof pid !== 'number' || pid === 0) return;
|
|
1579
|
+
__substrateChildren.set(pid, child);
|
|
1580
|
+
child.once('exit', () => { __substrateChildren.delete(pid); });
|
|
1581
|
+
});
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
|
|
1585
|
+
export default {
|
|
1586
|
+
initChildProcess,
|
|
1587
|
+
registerRunStreams,
|
|
1588
|
+
releaseRunStreams,
|
|
1589
|
+
runCommand,
|
|
1590
|
+
sendStdin,
|
|
1591
|
+
};
|