Skip to content

v0.6.0

Choose a tag to compare

@github-actions github-actions released this 06 Oct 16:46
· 419 commits to main since this release

The host API gains an argv entry for the engine's Node, per-fd standard
streams (kind, terminal, bytes) and a shared-memory stdin, and a guest's
processes and files carry bytes and modes as Linux does. Released as v0.6.0
(the exported surface changed); the workflow dates this section when it
publishes.

New API

  • container.runNode(argv, options): the engine's Node started from an
    execve-style vector (argv[0] is process.argv0), with run's options
    and answer, through the same launch the shell's node uses; no command line
    is built and no shell runs. env is the whole environment; cwd defaults
    to /.
  • RunOptions (for run and runNode):
    • stdioKind: ['tty'|'pipe'|'file'|'char', x3] says what each of fds 0, 1,
      2 is ('char' is a character device that is not a terminal, /dev/null);
      stdioIsTTY: [b, b, b] says only which is a terminal (used where
      stdioKind is absent). Each fd is a terminal or not on its own.
    • onStdoutBytes / onStderrBytes(Uint8Array) take fd 1 / fd 2 as the
      bytes the program wrote, before any decode.
    • stdinShared: SharedArrayBuffer gives fd 0 as a ring a host writes from
      its own thread; STDIN_RING (the layout), createStdinRing,
      stdinRingProblem and the producer StdinRingWriter (write(bytes)
      waits with Atomics.waitAsync when the ring is full; close() is EOF)
      are exported. The header is eight Int32 words (32 bytes): WRITE_INDEX,
      READ_INDEX, CLOSED, CAPACITY and SIGNAL, which the producer bumps
      and notifies after every write and at close, and on which readers wait,
      so a close between a reader's look and its wait still wakes it; three
      words are reserved. A host writes the ring only through StdinRingWriter.
    • stdin accepts bytes.
  • @volter/tabnode/stdin-ring: the ring's layout, producer and helpers alone,
    a module that imports nothing, for a page that writes a run's stdin without
    loading the engine. The engine's reader is the same module.
  • Types StdioKind exported. The host-executor request a page publishes
    (Symbol.for('@volter/browser-runtime/child-process-executor')) is offered
    onStdoutBytes / onStderrBytes; NodeProcessLaunch.stdin may be bytes.
  • Declarations say what the implementation already took: the engine's process
    streams' write and a WASI host process's write accept Uint8Array as
    well as strings, and the guest stdin's read() answers
    string | Uint8Array | null, as Readable's does.
  • NodeProcessLaunch.argv0: the process's own name, argv[0] of the vector
    it was started with. A Node process host starts the process with it as
    argv[0]; argv stays the words after it. A host that rebuilt the vector
    as ["node", ...argv] gave every hosted process process.argv0 === 'node'
    (a kernel's exec of a renamed executable reported node); it must use
    argv0.

Behaviour changes a consumer must know

  • VirtualFS emits an event for every mutation: change for an entry created
    or changed (bytes, mode, times, a directory, a link; a file's carries its
    text, others none) and delete for one that went. chmodSync, utimesSync,
    symlinkSync, mkdirSync (and the directories a write or a rename makes),
    rmdirSync and renameSync emitted nothing; a rename now reports every
    path of the moved subtree gone at the old name and made at the new.
  • File modes are real. The shell's filesystem reports each file's own mode and
    its chmod sets it; the /node_modules/.bin/ rule that made only those
    files executable is gone. A host that writes an executable must write it
    0755 (or chmodSync it); a 0644 file is not found on PATH or run by path.
    The engine's own writers set modes: npm install keeps each archive entry's
    mode ((mode | 0666) & ~022, directories | 0777), makes each bin target
    and its .bin stub executable.
  • The engine writes nothing into a tree a host supplied (createContainer({ vfs })):
    the /usr/local/bin/node (process.execPath) file it used to create in any
    tree that lacked one is created, 0755, only in a tree the engine made itself.
    A host's tree carries its own program at process.execPath (any file there
    named node runs the engine's node from the shell); without one, a shell
    line naming the path gets "No such file or directory".
  • A guest's node child (spawn/execFile/fork of node or of
    process.execPath, and spawnSync/execFileSync of them) starts from its
    argv through the Node launch, not as a line for the engine's shell. It is one
    process whose pid is the one its parent's handle holds (there is no shell
    process between), and its environment is exactly the one it was spawned with
    (the shell's own HOME/USER/PATH defaults are not added). A shell line
    (exec, execSync, { shell: true }) is unchanged.
  • Child stdio carries bytes: a Node child's fd 0/1/2 reach the parent's pipe,
    inherited stdio or descriptor as bytes, and Node's child_process returns a
    Buffer or a string by its encoding option, as on Linux. Output that was
    invalid UTF-8 used to arrive with replacement characters.
  • A run given onStdoutBytes/onStderrBytes gets that fd only as bytes:
    onStdout/onStderr are not called for it and RunResult.stdout/stderr
    is empty for it. Text callbacks now decode with one streaming decoder per fd,
    so a character split across two writes arrives whole.
  • process.stdout.write(string, encoding) writes the string in that encoding
    ('ff', 'hex' is one byte); the encoding was ignored. fs.writeSync(fd, string, pos, encoding) likewise (every non-UTF-8 encoding was latin1).
  • fd 0, 1 and 2 are descriptors of every run. fs.writeSync(1|2) and writes to
    /dev/stdout, /dev/stderr, /dev/fd/1|2, /proc/self/fd/1|2 go to the
    run's stdout/stderr in order with process.stdout (they were EBADF);
    fs.readSync(0) and /dev/stdin read the run's stdin in order with
    process.stdin (EBADF before). A blocking read with the writer open and
    nothing buffered waits on stdinShared where the run has one, and is
    otherwise refused as ERR_STDIN_BLOCKING_READ; an O_NONBLOCK descriptor
    gets EAGAIN. fs.fstat(0|1|2) is a terminal (pts), a FIFO, a regular file
    or a non-terminal character device (0666, device 1,3) by the fd's kind.
    tty.isatty(fd) and guessHandleType(fd) answer from the asking run's own
    streams, as libuv does: a terminal fd is 'TTY' (so new net.Socket({ fd })
    on it fails as in Node), a file or character device 'FILE', a pipe
    'PIPE'; it was 'PIPE' for all three.
  • A run's life and its stdin, as in Node. A program that reads fd 0 with
    fs.readSync(0) or /dev/stdin and returns ends when its loop is empty,
    whether or not the stdin writer has closed: those reads take what
    process.stdin already holds and then read the ring, without starting the
    stream (the run used to live until the ring closed, and forever if it never
    did). A run whose program consumes process.stdin (a 'data' or
    'readable' listener, flowing mode, or its own read() call) is kept alive
    until the stream ends; a bare process.stdin.read() used to let it end early.
  • A WASI guest's stdout/stderr are written as its bytes, not decoded text.
  • A background job (cmd &) writes its parent's byte sinks where the parent
    has them; its output was dropped when the parent took bytes only.
  • A host's totals: an fd a host executor or a Node process host streamed
    through its byte sink has an empty total, and is never replayed through the
    text callbacks. A host that streamed no bytes is read as before. The Node
    process host is handed a view of the run's streams (its fields read through
    to the run's own object), not the object itself.
  • A Node entry whose top-level await fails prints its error on stderr (it was
    only appended to the result); a run's idle detection counts what was
    printed, by length, in either form.