ludic/runtime/native/process.ludic
Orkuncakilkaya 43d5eb5b88 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>
2026-09-17 13:32:23 +03:00

134 lines
4.7 KiB
Text

# ============================================================================
# process.ludic — the Process.* standard library.
#
# Child processes, started and polled, never waited on. Process.spawn(path, args) starts a
# program directly - no shell, so nothing in an argument is interpreted - with this
# process's environment and working directory, and returns at once with a handle. Each
# frame the game asks Process.poll(h): -1 while the child runs, then its exit code. A child
# killed by a signal (POSIX) answers 128 + the signal, as a shell would report it, and
# Process.kill answers 137 on both platforms. Process.free lets the handle go; it does not
# stop a child that is still running.
#
# This is what a launcher is made of: start the game, hide the launcher's window
# (App.window_hide), notice the game has gone - and whether it crashed - and come back.
#
# The argument list is turned into what each platform wants here, in Ludic: an argv array
# for posix_spawn (process.ll), and one command line quoted by the MSVC rules for
# CreateProcessW (process_win.ll), which starts the child with no console window. Linked
# only into a program that uses Process.*.
#
# Like Http.* and Time.now it is OUT-OF-BAND and must never feed a deterministic
# lockstep/replay simulation.
# ============================================================================
extern function lu_proc_spawn(path: pointer, argv: pointer, cmdline: pointer) -> int = "lu_proc_spawn"
extern function lu_proc_poll(h: int) -> int = "lu_proc_poll"
extern function lu_proc_kill(h: int) -> void = "lu_proc_kill"
extern function lu_proc_free(h: int) -> void = "lu_proc_free"
function proc_cstr_len(p: pointer) -> int {
if p == null { return 0 }
var n = 0
while p[n] != 0 { n += 1 }
return n
}
# does an argument need quoting on a Windows command line?
function proc_needs_quote(a: pointer) -> bool {
if (a == null) or (a[0] == 0) { return true }
var i = 0
while a[i] != 0 {
let c = a[i]
if (c == ' ') or (c == 9) or (c == 10) or (c == 11) or (c == '"') { return true }
i += 1
}
return false
}
# append `a` to `out` at `o`, quoted so CommandLineToArgvW / the MSVC CRT read it back as
# exactly `a`: backslashes are literal unless they run into a quote, where they double
function proc_put_arg(out: pointer, o: int, a: pointer) -> int {
var k = o
if not proc_needs_quote(a) {
var j = 0
while a[j] != 0 { out[k] = a[j]; k += 1; j += 1 }
return k
}
out[k] = '"'; k += 1
var i = 0
var slashes = 0
while (a != null) and (a[i] != 0) {
let c = a[i]
if c == 92 {
slashes += 1
} else if c == '"' {
var s = 0
while s < (slashes * 2) + 1 { out[k] = 92; k += 1; s += 1 }
out[k] = '"'; k += 1
slashes = 0
} else {
var s2 = 0
while s2 < slashes { out[k] = 92; k += 1; s2 += 1 }
out[k] = c; k += 1
slashes = 0
}
i += 1
}
# before the closing quote the backslashes double
var s3 = 0
while s3 < slashes * 2 { out[k] = 92; k += 1; s3 += 1 }
out[k] = '"'; k += 1
return k
}
# The Windows command line for path + args. argv[0] follows its own rule (quotes toggle,
# backslashes are always literal), and a path cannot hold a quote, so it is simply quoted.
function proc_cmdline(path: pointer, args: []pointer) -> pointer {
let n = len(args)
var cap = proc_cstr_len(path) + 4
var i = 0
while i < n { cap += (proc_cstr_len(args[i]) * 2) + 4; i += 1 }
let out = bytes(cap)
var k = 0
out[k] = '"'; k += 1
var j = 0
while path[j] != 0 { out[k] = path[j]; k += 1; j += 1 }
out[k] = '"'; k += 1
i = 0
while i < n {
out[k] = ' '; k += 1
k = proc_put_arg(out, k, args[i])
i += 1
}
out[k] = 0
return out
}
# Start `path` with `args` (argv[0] is `path`), inheriting the environment and the working
# directory. Never blocks. A handle >= 0, or -1 when the program could not be started.
function process_spawn(path: pointer, args: []pointer) -> int {
if (path == null) or (path[0] == 0) { return -1 }
let n = len(args)
var argv: pointers = bytes((n + 2) * 8)
argv[0] = path
var i = 0
while i < n {
var a: pointer = args[i]
if a == null { a = "" }
argv[i + 1] = a
i += 1
}
argv[n + 1] = null
let cmd = proc_cmdline(path, args)
let h = lu_proc_spawn(path, argv, cmd)
free(cmd)
free(argv)
return h
}
# -1 while the child runs, then its exit code (128 + signal for a child a signal ended)
function process_poll(h: int) -> int { return lu_proc_poll(h) }
# end the child now; a later poll answers 137
function process_kill(h: int) -> void { lu_proc_kill(h) }
# let the handle go (a running child keeps running, and is reaped when it ends)
function process_free(h: int) -> void { lu_proc_free(h) }