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:
parent
c1db0d71ed
commit
43d5eb5b88
31 changed files with 89274 additions and 86727 deletions
|
|
@ -4,4 +4,4 @@ title: App
|
|||
order: 48
|
||||
---
|
||||
|
||||
The running application, as distinct from its window. Today that is the boot splash: the runtime raises it before <code>main</code> from the game's asset pack, and <a href="app-splash_hide"><code>App.splash_hide</code></a> takes it down when the game has something to show instead.
|
||||
The running application, as distinct from its window. Today that is the boot splash: the runtime raises it before <code>main</code> from the game's asset pack, and <a href="app-splash_hide"><code>App.splash_hide</code></a> takes it down when the game has something to show instead. <a href="app-window_hide"><code>App.window_hide</code></a> and <a href="app-window_show"><code>App.window_show</code></a> take the game's own window off the screen and bring it back without closing it — a launcher stepping aside while the game it started runs.
|
||||
|
|
|
|||
35
docs/language/app/app-window_hide.md
Normal file
35
docs/language/app/app-window_hide.md
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
id: app-window_hide
|
||||
name: App.window_hide
|
||||
category: app
|
||||
kind: namespace-method
|
||||
tokens: App.window_hide
|
||||
sig: App.window_hide() -> void
|
||||
tip: Take the game's window off the screen.
|
||||
order: 3
|
||||
ns: App
|
||||
member: window_hide
|
||||
---
|
||||
|
||||
Takes the game's window off the screen without closing it: its GL context or Vulkan surface, its size and its contents are kept, and the program goes on running — a hidden window does not end the run. <a href="app-window_show.html"><code>App.window_show</code></a> brings it back. It is how a launcher steps aside while the game it started with <a href="../process/process-spawn.html"><code>Process.spawn</code></a> runs.
|
||||
|
||||
Keep the frame loop going while hidden so the child can be polled, but there is nothing to see: skip the drawing, and throttle the loop yourself — a hidden window is not paced by the display, so an unthrottled loop spins. It does not touch the boot splash (<a href="app-splash_hide.html"><code>App.splash_hide</code></a> does). Safe to call when there is no window yet, and twice. Headless there is no window and the call lowers to nothing.
|
||||
|
||||
```ludic skip
|
||||
program Launcher {
|
||||
var game: int = -1
|
||||
function play() -> void {
|
||||
game = Process.spawn(game_path(), new []string)
|
||||
if game >= 0 { App.window_hide() }
|
||||
}
|
||||
handler Watch phase Update {
|
||||
if game >= 0 and Process.poll(game) != -1 {
|
||||
Process.free(game)
|
||||
game = -1
|
||||
App.window_show()
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<code>orderOut:</code> on macOS; <code>ShowWindow(SW_HIDE)</code> on Windows, which also forgets any key held as the window went.
|
||||
26
docs/language/app/app-window_show.md
Normal file
26
docs/language/app/app-window_show.md
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
id: app-window_show
|
||||
name: App.window_show
|
||||
category: app
|
||||
kind: namespace-method
|
||||
tokens: App.window_show
|
||||
sig: App.window_show() -> void
|
||||
tip: Bring the game's window back.
|
||||
order: 4
|
||||
ns: App
|
||||
member: window_show
|
||||
---
|
||||
|
||||
Puts a window hidden by <a href="app-window_hide.html"><code>App.window_hide</code></a> back on screen, in front and focused, as it was. Safe to call on a window that is already showing, when there is no window, and headless, where it lowers to nothing.
|
||||
|
||||
```ludic skip
|
||||
program Demo {
|
||||
entry {
|
||||
App.window_hide()
|
||||
wait_for_the_game()
|
||||
App.window_show()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
On macOS <code>makeKeyAndOrderFront:</code> and the application is activated. On Windows <code>ShowWindow(SW_SHOW)</code> and <code>SetForegroundWindow</code>; Windows may refuse the focus to a program that is not in the foreground, in which case the window comes back and its taskbar button flashes instead.
|
||||
|
|
@ -4,6 +4,6 @@ title: Http
|
|||
order: 8
|
||||
---
|
||||
|
||||
A poll-based HTTP / HTTPS client for out-of-band data — leaderboards, cloud saves, remote config, telemetry, downloads. Open a request with <a href="http-get.html"><code>Http.get</code></a> / <a href="http-post.html"><code>Http.post</code></a> (or <a href="http-open.html"><code>Http.open</code></a> to add headers first), then each frame call <a href="http-poll.html"><code>Http.poll</code></a> — it returns <code>-1</code> while pending, <code>0</code> on error, or the status code — so the frame never blocks. Read the reply with <a href="http-status.html"><code>Http.status</code></a> / <a href="http-ok.html"><code>ok</code></a> / <a href="http-text.html"><code>text</code></a> / <a href="http-header.html"><code>header</code></a>, and pair it with <code>Json.*</code> for (de)serialization. TLS is the system's, on by default.
|
||||
A poll-based HTTP / HTTPS client for out-of-band data — leaderboards, cloud saves, remote config, telemetry, downloads. Open a request with <a href="http-get.html"><code>Http.get</code></a> / <a href="http-post.html"><code>Http.post</code></a> (or <a href="http-open.html"><code>Http.open</code></a> to add headers first), then each frame call <a href="http-poll.html"><code>Http.poll</code></a> — it returns <code>-1</code> while pending, <code>0</code> on error, or the status code — so the frame never blocks. Read the reply with <a href="http-status.html"><code>Http.status</code></a> / <a href="http-ok.html"><code>ok</code></a> / <a href="http-text.html"><code>text</code></a> / <a href="http-header.html"><code>header</code></a>, and pair it with <code>Json.*</code> for (de)serialization. TLS is the system's, on by default. A large download goes to a file instead of memory with <a href="http-save_to.html"><code>Http.save_to</code></a>, and <a href="http-received.html"><code>Http.received</code></a> / <a href="http-expected.html"><code>expected</code></a> drive a progress bar while it is pending.
|
||||
|
||||
HTTP depends on the network and the wall clock, so — like <code>Net.*</code> and <code>Time.now</code> — it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation. The transport is macOS-only for now; the raw-response parser (<a href="http-parse.html"><code>Http.parse</code></a>) is pure and portable.
|
||||
HTTP depends on the network and the wall clock, so — like <code>Net.*</code> and <code>Time.now</code> — it is <strong>out-of-band</strong> and must never feed the deterministic lockstep/replay simulation. The transport is NSURLConnection / NSURLSession on macOS and WinHTTP on Windows; the raw-response parser (<a href="http-parse.html"><code>Http.parse</code></a>) is pure and portable.
|
||||
|
|
|
|||
25
docs/language/http/http-expected.md
Normal file
25
docs/language/http/http-expected.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: http-expected
|
||||
name: Http.expected
|
||||
category: http
|
||||
kind: namespace-method
|
||||
tokens: Http.expected
|
||||
sig: Http.expected(handle) -> int
|
||||
tip: The body length the server announced, or -1.
|
||||
order: 18
|
||||
ns: Http
|
||||
member: expected
|
||||
---
|
||||
|
||||
Returns the length of the response body as the server announced it (<code>Content-Length</code>), or <code>-1</code> while that is not known. For a request given <a href="http-save_to.html"><code>Http.save_to</code></a> it is known as soon as the headers arrive, and stays <code>-1</code> for a reply sent without a length (chunked) — draw an indeterminate bar then. For an ordinary in-memory request it is <code>-1</code> until the reply lands, then the body length.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
let h = Http.open("GET", "https://example.com/big.bin")
|
||||
Http.save_to(h, "big.bin.part")
|
||||
Http.send(h)
|
||||
print(Http.expected(h)) # -1: nothing has arrived yet
|
||||
}
|
||||
}
|
||||
```
|
||||
25
docs/language/http/http-received.md
Normal file
25
docs/language/http/http-received.md
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
---
|
||||
id: http-received
|
||||
name: Http.received
|
||||
category: http
|
||||
kind: namespace-method
|
||||
tokens: Http.received
|
||||
sig: Http.received(handle) -> int
|
||||
tip: Body bytes received so far.
|
||||
order: 17
|
||||
ns: Http
|
||||
member: received
|
||||
---
|
||||
|
||||
Returns how many bytes of the response body have arrived. For a request given <a href="http-save_to.html"><code>Http.save_to</code></a> it is updated live while the request is pending — the bytes already written to the file — which is what a progress bar reads. For an ordinary in-memory request it is <code>0</code> until the reply lands, then the body length. <code>0</code> before <code>Http.send</code>.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
var dl: int = 0
|
||||
handler Bar phase Render {
|
||||
let got = Http.received(handle: dl)
|
||||
let all = Http.expected(handle: dl)
|
||||
if all > 0 { Screen.bar(x: 8, y: 8, width: 200, height: 6, value: got / 1024, max: all / 1024, color: 0x55cc77, back: 0x222222) }
|
||||
}
|
||||
}
|
||||
```
|
||||
28
docs/language/http/http-save_to.md
Normal file
28
docs/language/http/http-save_to.md
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
---
|
||||
id: http-save_to
|
||||
name: Http.save_to
|
||||
category: http
|
||||
kind: namespace-method
|
||||
tokens: Http.save_to
|
||||
sig: Http.save_to(handle, path) -> void
|
||||
tip: Stream the response body to a file.
|
||||
order: 16
|
||||
ns: Http
|
||||
member: save_to
|
||||
---
|
||||
|
||||
Makes the response body of an <a href="http-open.html"><code>Http.open</code></a>ed request go straight to the file at <code>path</code> as it arrives, instead of into memory — the way to download something large (an update package of hundreds of megabytes) with a progress bar. Call it before <a href="http-send.html"><code>Http.send</code></a>; afterwards it does nothing.
|
||||
|
||||
The file is written directly at <code>path</code>, with no temporary name: <code>Http.send</code> creates it (or truncates an existing one), and a failed or cancelled download leaves whatever had arrived. So download to a name of your own such as <code>"x.nupkg.part"</code> and rename it once <a href="http-poll.html"><code>Http.poll</code></a> reports a good status — a non-2xx reply's body (an error page) is written to the file too. If the file cannot be created the request finishes with status <code>0</code>, as does one whose write fails midway (a full disk).
|
||||
|
||||
For such a request <a href="http-text.html"><code>Http.text</code></a> is empty (<code>null</code>) and <a href="http-body_len.html"><code>Http.body_len</code></a> is the number of bytes written. Follow progress with <a href="http-received.html"><code>Http.received</code></a> and <a href="http-expected.html"><code>Http.expected</code></a>. Freeing the request while it is pending cancels the download. The transfer asks for no compression (<code>Accept-Encoding: identity</code>) unless the request set that header, so the bytes counted are the bytes on disk. Files are limited to 2 GB.
|
||||
|
||||
```ludic
|
||||
program Demo {
|
||||
entry {
|
||||
let h = Http.open("GET", "https://example.com/big.bin")
|
||||
Http.save_to(h, "big.bin.part")
|
||||
Http.send(h)
|
||||
}
|
||||
}
|
||||
```
|
||||
9
docs/language/process/_section.md
Normal file
9
docs/language/process/_section.md
Normal 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.
|
||||
25
docs/language/process/process-free.md
Normal file
25
docs/language/process/process-free.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
23
docs/language/process/process-kill.md
Normal file
23
docs/language/process/process-kill.md
Normal 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)
|
||||
}
|
||||
}
|
||||
```
|
||||
32
docs/language/process/process-poll.md
Normal file
32
docs/language/process/process-poll.md
Normal 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>> 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
27
docs/language/process/process-spawn.md
Normal file
27
docs/language/process/process-spawn.md
Normal 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>>= 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.
|
||||
Loading…
Add table
Add a link
Reference in a new issue