feat(launcher): Process.*, Http.save_to/received/expected, App.window_hide/show

Process.spawn/poll/kill/free - non-blocking child processes with no shell: posix_spawn on
macOS (process.ll), CreateProcessW with MSVC-quoted arguments and no console window on
Windows (process_win.ll), linked only when a program uses Process.*.

Http.save_to streams a response body into a file (NSURLSession with a run-time delegate
class on macOS, the WinHTTP read loop on Windows); Http.received / Http.expected report
progress while it is pending. Freeing a pending request cancels it and parks the slot
until the worker has finished.

App.window_hide / App.window_show take the game's window off the screen and back without
closing it; the run goes on while hidden. Docs, examples, tests and a changeset.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-17 13:32:23 +03:00
parent c1db0d71ed
commit 43d5eb5b88
31 changed files with 89274 additions and 86727 deletions

View file

@ -0,0 +1,9 @@
---
id: process
title: Process
order: 10
---
Child processes, started and polled — what a launcher is made of. <a href="process-spawn.html"><code>Process.spawn</code></a> starts a program directly, with no shell, and returns at once; each frame <a href="process-poll.html"><code>Process.poll</code></a> answers <code>-1</code> while the child runs and its exit code once it has ended, so a crash is a non-zero code the game can act on. <a href="process-kill.html"><code>Process.kill</code></a> ends a child and <a href="process-free.html"><code>Process.free</code></a> lets a handle go. Pair it with <a href="../app/app-window_hide.html"><code>App.window_hide</code></a> to step aside while the child runs.
The child inherits the environment and the working directory. <code>posix_spawn</code> on macOS; <code>CreateProcessW</code> on Windows, with each argument quoted by the MSVC rules and no console window. Like <code>Http.*</code> it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation.

View file

@ -0,0 +1,25 @@
---
id: process-free
name: Process.free
category: process
kind: namespace-method
tokens: Process.free
sig: Process.free(handle) -> void
tip: Let a process handle go.
order: 3
ns: Process
member: free
---
Releases the handle so it can be reused. It does <strong>not</strong> stop a child that is still running — use <a href="process-kill.html"><code>Process.kill</code></a> first for that; such a child keeps running and is reaped by the runtime when it ends, so it never lingers as a zombie. After <code>free</code>, polling the old handle answers <code>255</code>.
```ludic
program Demo {
entry {
let h = Process.spawn("/bin/sh", ["-c", "exit 0"])
var code = Process.poll(h)
while code == -1 { code = Process.poll(h) }
Process.free(h)
}
}
```

View file

@ -0,0 +1,23 @@
---
id: process-kill
name: Process.kill
category: process
kind: namespace-method
tokens: Process.kill
sig: Process.kill(handle) -> void
tip: End a running child.
order: 2
ns: Process
member: kill
---
Ends the child at once — <code>SIGKILL</code> on macOS, <code>TerminateProcess</code> on Windows — with no chance for it to save anything. A later <a href="process-poll.html"><code>Process.poll</code></a> answers <code>137</code> once it has gone. Does nothing for a child that has already ended, or a handle that is not one.
```ludic
program Demo {
entry {
let h = Process.spawn("/bin/sleep", ["30"])
Process.kill(h)
}
}
```

View file

@ -0,0 +1,32 @@
---
id: process-poll
name: Process.poll
category: process
kind: namespace-method
tokens: Process.poll
sig: Process.poll(handle) -> int
tip: -1 while running, else the exit code.
order: 1
ns: Process
member: poll
---
Returns <code>-1</code> while the child is still running and its exit code once it has ended; it never waits. The code is kept, so asking again gives the same answer until <a href="process-free.html"><code>Process.free</code></a>.
A child ended by a signal (macOS) answers <code>128</code> plus the signal number, as a shell reports it — <code>137</code> after <a href="process-kill.html"><code>Process.kill</code></a>, on both platforms. On Windows the code is the child's DWORD exit code as an <code>int</code>, so a crash such as <code>0xC0000005</code> arrives as a negative number (test for <code>!= 0</code>, not <code>&gt; 0</code>); a code of <code>0xFFFFFFFF</code> is reported as <code>255</code> so that <code>-1</code> always means "still running". A handle that is not one answers <code>255</code>.
```ludic
program Demo {
var game: int = -1
handler Watch phase Update {
if game >= 0 {
let code = Process.poll(handle: game)
if code >= 0 or code < -1 {
if code != 0 { print("the game crashed") }
Process.free(handle: game)
game = -1
}
}
}
}
```

View file

@ -0,0 +1,27 @@
---
id: process-spawn
name: Process.spawn
category: process
kind: namespace-method
tokens: Process.spawn
sig: Process.spawn(path, args) -> int
tip: Start a program; never blocks.
order: 0
ns: Process
member: spawn
---
Starts the program at <code>path</code> with the arguments in <code>args</code> (a <code>[]string</code>; the child's <code>argv[0]</code> is <code>path</code>) and returns a handle <code>&gt;= 0</code> straight away, or <code>-1</code> when the program could not be started — a missing file, a file that is not executable, or all 16 handles in use.
No shell is involved, so every argument reaches the child exactly as written: spaces, quotes and <code>$</code> are not interpreted. The child inherits this process's environment and working directory. On Windows the arguments are joined into one command line quoted so the child's C runtime splits it back into the same list, the path and arguments cross as UTF-8 and are converted to UTF-16, and the child is started without a console window (a GUI program is unaffected; a console program runs hidden). Use an absolute path: there is no <code>PATH</code> search.
```ludic
program Demo {
entry {
let h = Process.spawn("/bin/sh", ["-c", "exit 3"])
print(h >= 0)
}
}
```
See <a href="process-poll.html"><code>Process.poll</code></a> for the exit code.