Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions CHANGELIST.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions daslib/fio.das
Original file line number Diff line number Diff line change
Expand Up @@ -693,9 +693,9 @@ def rmdir_rec_result(path : string) : fs_result_bool {
typedef process = SubProcess?

def with_process(argv : array<string>; cwd : string; env : array<string>; blk : block<(var p : process) : void>) {
//! Spawn `argv` as a long-lived child (no shell; a forward-slash ``argv[0]`` spawns everywhere), run
//! `blk` with the live handle, then close it on scope exit - closing kills a child still running (the
//! Windows job tree, the POSIX group). `cwd` empty inherits the parent's; `env` is ``KEY=VALUE`` overrides.
//! Spawn `argv` as a long-lived child (no shell; a forward-slash ``argv[0]`` spawns everywhere), run `blk` with
//! the handle, then close it on scope exit - closing kills a child still running (the Windows job tree, the POSIX
//! group). `cwd` empty inherits; `env` is ``KEY=VALUE`` overrides. A binary that cannot start never throws: the handle reads as an exited child, exit 127, no output.
var p = unsafe(spawn_process(argv, cwd, env))
invoke(blk, p)
unsafe(close_process(p))
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
Spawns ``argv`` as a long-lived child and returns its handle. No shell is involved: ``argv[0]`` is the executable (a forward-slash path spawns on every platform; a relative path naming a directory resolves against the caller's directory, not ``cwd``), the rest are its arguments verbatim. ``cwd`` empty inherits the caller's directory; each ``env`` entry is a ``KEY=VALUE`` override applied over the inherited environment. The child's stdout and stderr merge into one pipe read by ``process_drain``; its stdin is empty. On Windows the child sits in a kill-on-close job object, on POSIX it leads its own process group, so ``process_terminate`` / ``process_kill`` reach the whole tree and closing the handle kills a child still running. An unspawnable executable is a thrown error on Windows and an exit code 127 on POSIX.
Spawns ``argv`` as a long-lived child and returns its handle. No shell is involved: ``argv[0]`` is the executable (a forward-slash path spawns on every platform; a relative path naming a directory resolves against the caller's directory, not ``cwd``), the rest are its arguments verbatim. ``cwd`` empty inherits the caller's directory; each ``env`` entry is a ``KEY=VALUE`` override applied over the inherited environment. The child's stdout and stderr merge into one pipe read by ``process_drain``; its stdin is empty. On Windows the child sits in a kill-on-close job object, on POSIX it leads its own process group, so ``process_terminate`` / ``process_kill`` reach the whole tree and closing the handle kills a child still running. An unspawnable executable never throws: the handle reads as an exited child on every platform - ``process_poll`` / ``process_wait`` answer 127, ``process_drain`` yields nothing - with ``process_pid`` 0 on Windows, where the failure is known before any child exists, and the real pid on POSIX, where the child itself exits 127.
15 changes: 15 additions & 0 deletions src/builtin/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,18 @@ counts once. Two histograms - string payloads by text, `TypeDecl` payloads by ma
carry the dedup estimate, and the record layer (`ast_parse.cpp`) appends one row per record with
its file, payload size and time. `ModuleFileCache::finish` prints the report through
`AstSerializer::profReport`, for the reader and the writer alike.

## 8. A child that cannot start is an exited child {#child-cannot-start}

`spawn_process` never throws for a binary it cannot start; the caller reads the failure through
the same `process_poll` / `process_wait` it reads a real run through, as exit code 127 with no
output. POSIX gives that shape for free and sets the contract: `fork` succeeds, the exec fails in
the child, and the child `_exit(127)`s, so the parent holds a live handle to a process that has
already ended. Windows learns the failure at `CreateProcess`, before any child exists, so
`spawn_process` builds the handle the POSIX path would have ended up with - reaped, exit 127,
stdout closed, pid 0, no process or job handle - and every lifecycle call reads it the way it
reads any reaped child. A throw here would split the two platforms at the one place a supervisor
could only catch it with `try` / `recover`, and an expected failure is never routed through the
panic path.
`process_pid` answers 0 on Windows for such a child and the real pid on POSIX, the one visible
difference, and the reason a supervisor that needs "did it start at all" reads the exit code.
8 changes: 6 additions & 2 deletions src/builtin/module_builtin_fio.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1900,10 +1900,14 @@ namespace das {
CloseHandle(hWrite);
if ( hNull != INVALID_HANDLE_VALUE ) CloseHandle(hNull);
if ( !ok ) {
// src/builtin/ARCHITECTURE.md#child-cannot-start
CloseHandle(hRead);
if ( hJob ) CloseHandle(hJob);
context->throw_error_at(at, "spawn_process: CreateProcess failed");
return nullptr;
DasSubProcess * dead = new DasSubProcess();
dead->stdoutOpen = false;
dead->reaped = true;
dead->exitCode = 127;
return dead;
}
if ( hJob && !AssignProcessToJobObject(hJob, pi.hProcess) ) {
CloseHandle(hJob); // a job we cannot assign (a restrictive parent job) must not shadow hProcess
Expand Down
24 changes: 24 additions & 0 deletions tests/fio/test_process.das
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ def wait_for_ready(t : T?; var p : process; ready : string) {
}
}

// src/builtin/ARCHITECTURE.md#child-cannot-start: a binary that cannot start is an exited child
// on every platform, never a throw - POSIX gives that through the child's own _exit(127), Windows
// builds the same handle at CreateProcess
[test]
def test_process_cannot_start(t : T?) {
t |> run("an unrunnable binary hands back an exited child: exit 127, no output, no throw") @(t : T?) {
let noenv : array<string>
var p = unsafe(spawn_process(["/nonexistent/daslang-that-is-not-there"], "", noenv))
t |> success(p != null, "the spawn returns a handle rather than throwing")
return if (p == null)
var lines = 0
unsafe(process_drain(p) $(_line) { lines++ })
t |> equal(lines, 0, "a child that never ran printed nothing")
t |> equal(unsafe(process_wait(p, guard(5.0))), 127, "wait reads the exec failure as exit 127")
t |> equal(unsafe(process_poll(p)), 127, "and poll re-reads it from the reaped child")
if (get_running_platform_name() == "windows") {
t |> equal(unsafe(process_pid(p)), 0, "no process ever existed on Windows, so the pid is 0")
} else {
t |> success(unsafe(process_pid(p)) > 0, "POSIX forked a real child that then failed to exec")
}
unsafe(close_process(p))
}
}

[test]
def test_process_lifecycle(t : T?) {
t |> run("spawn, drain lines, poll running, wait exit code, apply env + cwd") @(t : T?) {
Expand Down
2 changes: 1 addition & 1 deletion utils/benchctl/tests/test_bench_cli.das
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def private write_meta(dir : string; build_status : string) : string {
}

def private run_tool(args : array<string>; var output : string&) : int {
var argv <- ["bin/daslang", path_join(TOOL_DIR, "main.das"), "--"]
var argv <- [get_host_binary(), path_join(TOOL_DIR, "main.das"), "--"]
argv |> push_from(args)
return run_and_capture(argv, output, 240.0)
}
Expand Down
Loading