Process primitives

Elide ships a host-internal process layer in dev.elide.lang.javascript.process.host. It is not exposed to guest JavaScript directly — node:child_process, node:process, and node:tty layer on top.

Public types

TypePurpose
ProcessPrimitivesSync surface — spawn, wait, kill, release (drop the handle without signalling the child; later ops raise ESRCH), pid, exitCode, isAlive, stdin/stdout/stderr accessors.
ProcessPrimitivesAsyncAsync siblings returning CompletableFuture.
ProcessExceptionRuntime exception in Node's shape (code / errno / syscall / operand).
StdioDeclarative spec for one stdio stream — INHERIT, PIPE, IGNORE, file(path), appendFile(path); inspect via kind() (a Stdio.Kind discriminant) and path().
ProcessSignalsPOSIX signal-name → numeric table per OS; send(pid, name) via kill(2) (FFM).
Ttyisatty(fd), windowSize(fd), setRawMode(fd, raw) over libc / kernel32.
PtyPOSIX pseudo-terminal pair allocation — posix_openpt + grantpt + unlockpt + ptsname.
ProcessHostInitBoot-time probe; reports the active backend cluster.

Spawn

ProcessPrimitives.spawn(file, args, cwd, env, stdin, stdout, stderr) returns an opaque 64-bit handle. The spawn goes through ProcessBuilder.start(); the underlying OS launch mechanism (e.g. posix_spawn / vfork) is a JDK/HotSpot detail that Elide neither selects nor guarantees.
  • cwdnull inherits the parent's current directory; otherwise must be an existing directory (ENOTDIR raised at the boundary, before ProcessBuilder.start runs).
  • envnull inherits the parent's environment in full. A non-null map replaces the parent's env entirely; this matches Node's child_process.spawn(..., {env}) semantics. Callers wanting to extend the parent should fold System.getenv() in first.
  • Each Stdio argument can be INHERIT, PIPE (default), IGNORE, file(path) (truncate), or appendFile(path) (append). Append-mode is meaningful only on output streams; setting it on stdin raises EINVAL.

The errno classification of IOException from ProcessBuilder.start() runs through a libuv-style heuristic (the JDK does not propagate errno from execve / CreateProcess): no such fileENOENT, permission deniedEACCES, not a directoryENOTDIR, too many open filesEMFILE, falling through to EIO for any other shape.

Wait & exit

  • waitFor(handle) — block indefinitely; returns the exit code.
  • waitTimeout(handle, ms) — block up to ms milliseconds; raises ETIMEDOUT on the timeout. ms < 0 is equivalent to waitFor.
  • exitCode(handle) — sync read; raises EAGAIN if the child is still alive.
  • isAlive(handle)true/false snapshot; non-blocking.
ProcessPrimitivesAsync.waitFor(handle) hooks into Process.onExit() — no thread is parked, the JDK delivers exit notifications on its internal exit-watcher thread.

Termination

  • kill(handle, force)false → graceful (SIGTERM / Process.destroy()); true → forcible (SIGKILL / Process.destroyForcibly()). Returns true when the request was delivered, false when the child had already exited.
  • sendSignal(handle, name) — arbitrary POSIX signal by name. On POSIX hosts goes through ProcessSignals.sendkill(2) via FFM. On Windows only SIGTERM / SIGINT (graceful) and SIGKILL (forcible) are honoured; everything else raises ENOSYS.

Signals

ProcessSignals.numberFor(name) resolves a signal name to its numeric value on the host OS. The table covers the standard 32 POSIX signals; the same name maps to different numbers on Linux vs. macOS starting from signal 7. Numbers shared between both: 1 (HUP), 2 (INT), 3 (QUIT), 4 (ILL), 5 (TRAP), 6 (ABRT), 8 (FPE), 9 (KILL), 11 (SEGV), 13 (PIPE), 14 (ALRM), 15 (TERM). Notable divergences:
NumberLinuxmacOS
7SIGBUSSIGEMT
10SIGUSR1SIGBUS
12SIGUSR2SIGSYS
16SIGSTKFLTSIGURG
17SIGCHLDSIGSTOP
18SIGCONTSIGTSTP
19SIGSTOPSIGCONT
20SIGTSTPSIGCHLD
30SIGPWRSIGUSR1
31SIGSYSSIGUSR2
Sending by name (rather than by number) is the only portable path — passing a raw integer across the OS line will deliver whichever signal happens to live at that index in the local table. The existence-probe idiom kill(pid, 0) is supported: signal name "0" resolves to numeric 0 on both Linux and macOS, matching Node's process.kill(pid, 0). Signal name input may be passed with or without the SIG prefix ("SIGTERM" and "TERM" both resolve). ProcessSignals.send(pid, name) failure modes: EINVAL (unknown name), ESRCH (no such pid), EPERM (pid exists but caller lacks rights — disambiguated from ESRCH by checking ProcessHandle.of(pid)), ENOSYS (Windows non-TERM/KILL signal).

Tty

Tty.isatty(fd) returns true only for actual terminal devices — pipes, files, sockets, and closed/invalid fds all return false. Cross-platform: POSIX uses isatty(3); Windows uses GetStdHandle + GetConsoleMode (the canonical equivalent — _isatty from MSVCRT does not distinguish console handles from raw character devices reliably). Tty.windowSize(fd) returns int[]{cols, rows}. POSIX uses ioctl(fd, TIOCGWINSZ, &winsize) with the right request constant (Linux 0x5413, macOS 0x40087468); Windows reads GetConsoleScreenBufferInfo.srWindow. Non-tty fd → ENOTTY. Tty.setRawMode(fd, raw) toggles raw input mode. POSIX path uses tcgetattr / tcsetattr, clearing ISIG | ICANON | ECHO on entry and restoring the saved struct termios on exit. Windows path uses GetConsoleMode / SetConsoleMode, clearing ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT | ENABLE_PROCESSED_INPUT. The original mode is cached per fd so the toggle round-trips even across multiple raw-mode entries.

The struct termios layout differs between glibc/musl Linux (60 bytes, ints for c_*flag) and macOS (72 bytes, longs for c_*flag). The implementation allocates a 256-byte over-large buffer and computes the c_lflag offset per OS — safe because the kernel only accesses the bytes the struct definition covers.

Pty

Pty.open() allocates a fresh master/slave pair on POSIX:

1. posix_openpt(O_RDWR | O_NOCTTY) — open master end. 2. grantpt(master) — set slave permissions. 3. unlockpt(master) — release the lock so the slave can be opened. 4. ptsname_r(master, ...) (or ptsname fallback) — read the slave's pathname.

Returns Pty.Pair(masterFd, slavePath). The caller owns the master fd and is responsible for Pty.close(fd) when finished. On Windows, Pty.close — like Pty.open — raises ENOSYS. The slave path can be open(2)'d directly or threaded into a child via posix_spawn_file_actions_addopen for terminal-bound subprocesses.

Windows pty (CreatePseudoConsole, Win10 1809+) is parked behind an explicit ENOSYS. Its lifecycle (HPCON handles, ResizePseudoConsole, ClosePseudoConsole, separate read/write pipes) is substantially different from the POSIX flow and will live in a dedicated layer when it lands.

Errno surface

Every error materialises as ProcessException carrying:

  • code — POSIX-style string (ENOENT, ESRCH, EPERM, ECHILD, EINVAL, ENOSYS, …).
  • errno — numeric value on the originating platform (resolved via FsErrno.numberFor for OS-correctness; e.g. EAGAIN is 11 on Linux but 35 on macOS).
  • syscall — the operation that failed (spawn, kill, tcsetattr, posix_openpt, …).
  • operand — the executable path, signal name, or pid as a stringified integer.
FsErrno's table was extended with ESRCH (3), ECHILD (10), and ENOTTY (25) to cover the codes the process layer needs that the fs layer does not.

Threading

Every method is thread-safe. The handle table is a ConcurrentHashMap. Stdio streams returned from the surface are owned by the underlying Process — the JDK closes them when the process exits, but consumers may close them earlier (e.g. closing stdin to signal EOF after writing).

Raw-mode and saved-console-mode caches are also ConcurrentHashMaps, so a multi-threaded REPL that toggles raw mode from a different thread than the one running the read loop is safe.

Naming

The package is process.host rather than process.native: native is a Java reserved keyword. host matches the HostEnvironment and fs.host naming used elsewhere for host-side / sandbox-mediated capabilities.