@executablemd/runtime 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/launcher.js +36 -18
- package/package.json +3 -3
- package/types/launcher.d.ts +19 -0
package/esm/launcher.js
CHANGED
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
* document help and inspection free of any of this.
|
|
28
28
|
*/
|
|
29
29
|
import { createApi } from "@effectionx/context-api";
|
|
30
|
-
import { ensure, race, resource, scoped, until
|
|
30
|
+
import { ensure, race, resource, scoped, until } from "effection";
|
|
31
|
+
import { once } from "@effectionx/node/events";
|
|
31
32
|
import { spawn as spawnChild } from "node:child_process";
|
|
32
33
|
import process from "node:process";
|
|
33
34
|
export const NATIVE_LAUNCHER_UNAVAILABLE = "no native launcher is installed — this host does not hand a native agent UI " +
|
|
@@ -141,8 +142,6 @@ function runForeground(request) {
|
|
|
141
142
|
if (command === undefined) {
|
|
142
143
|
throw new Error("native launch: command must not be empty");
|
|
143
144
|
}
|
|
144
|
-
const settled = withResolvers();
|
|
145
|
-
const failed = withResolvers();
|
|
146
145
|
let child;
|
|
147
146
|
// Interrupt, then insist. A cancelled document may not continue — or
|
|
148
147
|
// finish tearing down — while a child still holds the terminal, so this
|
|
@@ -153,28 +152,42 @@ function runForeground(request) {
|
|
|
153
152
|
// `inherit` is the whole point: the child reads this terminal and draws on
|
|
154
153
|
// it directly, so nothing between it and the person using it can buffer,
|
|
155
154
|
// reorder, capture or journal what passes.
|
|
156
|
-
|
|
155
|
+
const started = spawnChild(command, args, {
|
|
157
156
|
cwd: request.cwd,
|
|
158
157
|
env: request.env,
|
|
159
158
|
stdio: "inherit",
|
|
160
159
|
});
|
|
161
|
-
child
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
outcome
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
160
|
+
child = started;
|
|
161
|
+
// Raced inline, in the same synchronous run as the spawn, so both arms are
|
|
162
|
+
// attached before the child can report anything — a spawned race attaches
|
|
163
|
+
// a turn later. Whichever loses is halted, which is what detaches it.
|
|
164
|
+
return yield* race([
|
|
165
|
+
(function* () {
|
|
166
|
+
const [code, signal] = yield* once(started, "exit");
|
|
167
|
+
const outcome = {};
|
|
168
|
+
if (code !== null) {
|
|
169
|
+
outcome.exitCode = code;
|
|
170
|
+
}
|
|
171
|
+
if (signal !== null) {
|
|
172
|
+
outcome.signal = signal;
|
|
173
|
+
}
|
|
174
|
+
return outcome;
|
|
175
|
+
})(),
|
|
176
|
+
(function* () {
|
|
177
|
+
const [error] = yield* once(started, "error");
|
|
178
|
+
throw error;
|
|
179
|
+
})(),
|
|
180
|
+
]);
|
|
173
181
|
});
|
|
174
182
|
}
|
|
175
183
|
/**
|
|
176
184
|
* End one foreground child and wait for it to be gone.
|
|
177
185
|
*
|
|
186
|
+
* Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
|
|
187
|
+
* `mod.ts`: the listener this installs belongs to a bounded Promise, and the
|
|
188
|
+
* only way to observe that it is released on every settlement path is to hold
|
|
189
|
+
* the child.
|
|
190
|
+
*
|
|
178
191
|
* Deliberately one promise rather than an Effection race: this runs while the
|
|
179
192
|
* scope is already being dismantled, and the cheapest correct thing to do
|
|
180
193
|
* there is to wait on the process's own events instead of starting more
|
|
@@ -184,7 +197,7 @@ function runForeground(request) {
|
|
|
184
197
|
* is spent — a native UI holding the terminal is not something a cancelled run
|
|
185
198
|
* can afford to wait on indefinitely.
|
|
186
199
|
*/
|
|
187
|
-
function reap(child) {
|
|
200
|
+
export function reap(child) {
|
|
188
201
|
const pid = child.pid;
|
|
189
202
|
if (pid === undefined || child.exitCode !== null || child.signalCode !== null) {
|
|
190
203
|
return Promise.resolve();
|
|
@@ -206,6 +219,10 @@ function reap(child) {
|
|
|
206
219
|
clearInterval(poll);
|
|
207
220
|
clearTimeout(escalation);
|
|
208
221
|
clearTimeout(deadline);
|
|
222
|
+
// The one funnel every settlement goes through — the exit event, the
|
|
223
|
+
// reachability poll, the escalation deadline, and the refusal that
|
|
224
|
+
// rejects — so the handler comes off however this ends.
|
|
225
|
+
child.off("exit", onExit);
|
|
209
226
|
// Deno's `node:child_process` stops reporting a child's exit once a
|
|
210
227
|
// signal that child ignored has been delivered, and holds the runtime
|
|
211
228
|
// open on the handle it will now never settle. Dropping the reference is
|
|
@@ -223,7 +240,8 @@ function reap(child) {
|
|
|
223
240
|
}
|
|
224
241
|
resolve();
|
|
225
242
|
};
|
|
226
|
-
|
|
243
|
+
const onExit = () => done();
|
|
244
|
+
child.on("exit", onExit);
|
|
227
245
|
// Reachability rather than the exit event, because that is the fact this
|
|
228
246
|
// has to establish and the event is not dependable across runtimes here.
|
|
229
247
|
const poll = setInterval(() => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@executablemd/runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Runtime host APIs for executable.md documents.",
|
|
5
5
|
"homepage": "https://executable.md",
|
|
6
6
|
"repository": {
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"@effectionx/context-api": "0.6.0",
|
|
39
39
|
"@effectionx/fetch": "0.2.1",
|
|
40
40
|
"@effectionx/fs": "0.3.0",
|
|
41
|
+
"@effectionx/node": "0.2.5",
|
|
41
42
|
"@effectionx/process": "0.8.1",
|
|
42
|
-
"effection": "4.1.0"
|
|
43
|
-
"@effectionx/node": "0.2.4"
|
|
43
|
+
"effection": "4.1.0"
|
|
44
44
|
},
|
|
45
45
|
"_generatedBy": "dnt@dev"
|
|
46
46
|
}
|
package/types/launcher.d.ts
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
*/
|
|
29
29
|
import { type Api } from "@effectionx/context-api";
|
|
30
30
|
import type { Operation } from "effection";
|
|
31
|
+
import type { ChildProcess } from "node:child_process";
|
|
31
32
|
/**
|
|
32
33
|
* What a provider asks the host to run.
|
|
33
34
|
*
|
|
@@ -86,6 +87,24 @@ interface ForegroundLauncherOptions {
|
|
|
86
87
|
* own its exit status, or continue after the UI closes.
|
|
87
88
|
*/
|
|
88
89
|
export declare function installForegroundLauncher(options?: ForegroundLauncherOptions): Operation<void>;
|
|
90
|
+
/**
|
|
91
|
+
* End one foreground child and wait for it to be gone.
|
|
92
|
+
*
|
|
93
|
+
* Exported for `packages/runtime/tests/native-launcher.test.ts` and not from
|
|
94
|
+
* `mod.ts`: the listener this installs belongs to a bounded Promise, and the
|
|
95
|
+
* only way to observe that it is released on every settlement path is to hold
|
|
96
|
+
* the child.
|
|
97
|
+
*
|
|
98
|
+
* Deliberately one promise rather than an Effection race: this runs while the
|
|
99
|
+
* scope is already being dismantled, and the cheapest correct thing to do
|
|
100
|
+
* there is to wait on the process's own events instead of starting more
|
|
101
|
+
* structured work beside them.
|
|
102
|
+
*
|
|
103
|
+
* A child that ignores the interrupt is killed outright once the grace period
|
|
104
|
+
* is spent — a native UI holding the terminal is not something a cancelled run
|
|
105
|
+
* can afford to wait on indefinitely.
|
|
106
|
+
*/
|
|
107
|
+
export declare function reap(child: ChildProcess): Promise<void>;
|
|
89
108
|
/**
|
|
90
109
|
* A launcher a host installs when it has no terminal to give away, and no
|
|
91
110
|
* intention of starting a native UI.
|