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
| Type | Purpose |
|---|---|
ProcessPrimitives | Sync surface — spawn, wait, kill, release (drop the handle without signalling the child; later ops raise ESRCH), pid, exitCode, isAlive, stdin/stdout/stderr accessors. |
ProcessPrimitivesAsync | Async siblings returning CompletableFuture. |
ProcessException | Runtime exception in Node's shape (code / errno / syscall / operand). |
Stdio | Declarative spec for one stdio stream — INHERIT, PIPE, IGNORE, file(path), appendFile(path); inspect via kind() (a Stdio.Kind discriminant) and path(). |
ProcessSignals | POSIX signal-name → numeric table per OS; send(pid, name) via kill(2) (FFM). |
Tty | isatty(fd), windowSize(fd), setRawMode(fd, raw) over libc / kernel32. |
Pty | POSIX pseudo-terminal pair allocation — posix_openpt + grantpt + unlockpt + ptsname. |
ProcessHostInit | Boot-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.
cwd—nullinherits the parent's current directory; otherwise must be an existing directory (ENOTDIRraised at the boundary, beforeProcessBuilder.startruns).env—nullinherits the parent's environment in full. A non-null map replaces the parent's env entirely; this matches Node'schild_process.spawn(..., {env})semantics. Callers wanting to extend the parent should foldSystem.getenv()in first.- Each
Stdioargument can beINHERIT,PIPE(default),IGNORE,file(path)(truncate), orappendFile(path)(append). Append-mode is meaningful only on output streams; setting it on stdin raisesEINVAL.
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 file →
ENOENT, permission denied → EACCES, not a directory → ENOTDIR, too many open files →
EMFILE, falling through to EIO for any other shape.
Wait & exit
waitFor(handle)— block indefinitely; returns the exit code.waitTimeout(handle, ms)— block up tomsmilliseconds; raisesETIMEDOUTon the timeout.ms < 0is equivalent towaitFor.exitCode(handle)— sync read; raisesEAGAINif the child is still alive.isAlive(handle)—true/falsesnapshot; 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()). Returnstruewhen the request was delivered,falsewhen the child had already exited.sendSignal(handle, name)— arbitrary POSIX signal by name. On POSIX hosts goes throughProcessSignals.send→kill(2)via FFM. On Windows onlySIGTERM/SIGINT(graceful) andSIGKILL(forcible) are honoured; everything else raisesENOSYS.
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:
| Number | Linux | macOS |
|---|---|---|
| 7 | SIGBUS | SIGEMT |
| 10 | SIGUSR1 | SIGBUS |
| 12 | SIGUSR2 | SIGSYS |
| 16 | SIGSTKFLT | SIGURG |
| 17 | SIGCHLD | SIGSTOP |
| 18 | SIGCONT | SIGTSTP |
| 19 | SIGSTOP | SIGCONT |
| 20 | SIGTSTP | SIGCHLD |
| 30 | SIGPWR | SIGUSR1 |
| 31 | SIGSYS | SIGUSR2 |
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 viaFsErrno.numberForfor OS-correctness; e.g.EAGAINis 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.