@volter/world-runtime 2.0.23 → 2.0.24
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/dist/src/runtime.js +55 -16
- package/package.json +1 -1
- package/src/runtime.ts +42 -11
package/dist/src/runtime.js
CHANGED
|
@@ -114,11 +114,13 @@ async function untilReadyOrEnded(wait, ended, boot, what, log) {
|
|
|
114
114
|
* always runs in full: one port's answer says nothing of the others'. Programs that are not twins are never asked. */
|
|
115
115
|
async function confirmOwnPort(port, boot, ended, twin, what, log, early = false) {
|
|
116
116
|
// A twin is asked at once, raced with the settle: a server slow to answer costs no more than the settle did.
|
|
117
|
-
|
|
117
|
+
boot.signal.throwIfAborted();
|
|
118
|
+
const answer = twin ? worldBootOf(port, boot.signal) : Promise.resolve(null);
|
|
118
119
|
const own = early && twin ? answer.then((a) => (a && a.boot === boot.bootId ? 'own' : new Promise(() => undefined))) : new Promise(() => undefined);
|
|
119
120
|
// `ended` absent: the process was already settled (a co-located host settles once for all its twins).
|
|
120
121
|
if (ended) {
|
|
121
122
|
const settled = await Promise.race([own, ended.then(() => 'ended'), new Promise((resolveAlive) => setTimeout(() => resolveAlive('alive'), 300))]);
|
|
123
|
+
boot.signal.throwIfAborted();
|
|
122
124
|
if (settled === 'own')
|
|
123
125
|
return;
|
|
124
126
|
if (settled === 'ended')
|
|
@@ -127,28 +129,65 @@ async function confirmOwnPort(port, boot, ended, twin, what, log, early = false)
|
|
|
127
129
|
if (!twin)
|
|
128
130
|
return;
|
|
129
131
|
const answered = await answer;
|
|
132
|
+
boot.signal.throwIfAborted();
|
|
130
133
|
if (answered && typeof answered.boot === 'string' && answered.boot !== boot.bootId) {
|
|
131
134
|
throw new Error(`${what}: its port is answered by a twin of another World boot (${answered.boot}), not this one; a leftover World holds the port. Stop it (\`volter-world list\`, then \`down\`) and retry.\nLog: ${log}`);
|
|
132
135
|
}
|
|
133
136
|
}
|
|
134
137
|
/** What the server on a loopback port says its World boot is (WORLD_BOOT_PATH), or null. A direct loopback request:
|
|
135
138
|
* never through a proxy an activated shell's environment names. */
|
|
136
|
-
function worldBootOf(port) {
|
|
139
|
+
function worldBootOf(port, abort) {
|
|
137
140
|
return new Promise((resolveBoot) => {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
141
|
+
let request;
|
|
142
|
+
let deadline;
|
|
143
|
+
let settled = false;
|
|
144
|
+
let responseStarted = false;
|
|
145
|
+
const finish = (value) => {
|
|
146
|
+
if (settled)
|
|
147
|
+
return;
|
|
148
|
+
settled = true;
|
|
149
|
+
if (deadline !== undefined)
|
|
150
|
+
clearTimeout(deadline);
|
|
151
|
+
abort.removeEventListener('abort', cancel);
|
|
152
|
+
resolveBoot(value);
|
|
153
|
+
};
|
|
154
|
+
const cancel = () => { finish(null); request?.destroy(); };
|
|
155
|
+
if (abort.aborted) {
|
|
156
|
+
finish(null);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
abort.addEventListener('abort', cancel, { once: true });
|
|
160
|
+
// A socket timeout alone is insufficient: destroying a request after
|
|
161
|
+
// headers can close/abort its response without emitting request 'error'.
|
|
162
|
+
// Bound the whole probe, including a peer that dribbles bytes forever.
|
|
163
|
+
deadline = setTimeout(cancel, 1000);
|
|
164
|
+
try {
|
|
165
|
+
request = httpGet({ host: '127.0.0.1', port, path: WORLD_BOOT_PATH, timeout: 1000, agent: false }, (response) => {
|
|
166
|
+
responseStarted = true;
|
|
167
|
+
let body = '';
|
|
168
|
+
response.on('error', cancel);
|
|
169
|
+
response.on('aborted', cancel);
|
|
170
|
+
response.on('close', () => finish(null));
|
|
171
|
+
response.setEncoding('utf8');
|
|
172
|
+
response.on('data', (chunk) => { body += chunk; if (body.length > 4096)
|
|
173
|
+
cancel(); });
|
|
174
|
+
response.on('end', () => {
|
|
175
|
+
try {
|
|
176
|
+
finish(response.statusCode === 200 ? JSON.parse(body) : null);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
finish(null);
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
request.on('timeout', cancel);
|
|
184
|
+
request.on('error', () => finish(null));
|
|
185
|
+
request.on('close', () => { if (!responseStarted)
|
|
186
|
+
finish(null); });
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
cancel();
|
|
190
|
+
}
|
|
152
191
|
});
|
|
153
192
|
}
|
|
154
193
|
/** A stopped service's own account from its log: how it ended and the last lines it wrote. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volter/world-runtime",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.24",
|
|
4
4
|
"description": "World configs for twins: boot named local runtimes, allocate ports, generate world.env/instance.json, and run apps against fake-key twin worlds.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"twin",
|
package/src/runtime.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { offSignal } from './signals.ts';
|
|
|
3
3
|
import { assertWorldStateRemovable, removeWorldState } from './ancestry.ts';
|
|
4
4
|
import { packCli, packEntry } from './catalog.ts';
|
|
5
5
|
import { spawn, spawnSync } from 'node:child_process';
|
|
6
|
-
import { get as httpGet } from 'node:http';
|
|
6
|
+
import { get as httpGet, type ClientRequest } from 'node:http';
|
|
7
7
|
import { siblingScript } from './sibling.ts';
|
|
8
8
|
import { commandLifetime, commandProcessGroup, CommandRetirementError } from './command-lifetime.ts';
|
|
9
9
|
import { superviseWorldRun } from './run-task.ts';
|
|
@@ -108,16 +108,19 @@ async function untilReadyOrEnded<T>(wait: (abort: AbortSignal) => Promise<T>, en
|
|
|
108
108
|
* always runs in full: one port's answer says nothing of the others'. Programs that are not twins are never asked. */
|
|
109
109
|
async function confirmOwnPort(port: number, boot: BootContext, ended: Promise<void> | undefined, twin: boolean, what: string, log: string, early = false): Promise<void> {
|
|
110
110
|
// A twin is asked at once, raced with the settle: a server slow to answer costs no more than the settle did.
|
|
111
|
-
|
|
111
|
+
boot.signal.throwIfAborted();
|
|
112
|
+
const answer = twin ? worldBootOf(port, boot.signal) : Promise.resolve(null);
|
|
112
113
|
const own = early && twin ? answer.then((a) => (a && a.boot === boot.bootId ? 'own' as const : new Promise<never>(() => undefined))) : new Promise<never>(() => undefined);
|
|
113
114
|
// `ended` absent: the process was already settled (a co-located host settles once for all its twins).
|
|
114
115
|
if (ended) {
|
|
115
116
|
const settled = await Promise.race([own, ended.then(() => 'ended' as const), new Promise<'alive'>((resolveAlive) => setTimeout(() => resolveAlive('alive'), 300))]);
|
|
117
|
+
boot.signal.throwIfAborted();
|
|
116
118
|
if (settled === 'own') return;
|
|
117
119
|
if (settled === 'ended') throw new Error(`${what} ended right after its port first answered, so another process probably holds the port: the service ${endedAccount(log)}\nLog: ${log}`);
|
|
118
120
|
}
|
|
119
121
|
if (!twin) return;
|
|
120
122
|
const answered = await answer;
|
|
123
|
+
boot.signal.throwIfAborted();
|
|
121
124
|
if (answered && typeof answered.boot === 'string' && answered.boot !== boot.bootId) {
|
|
122
125
|
throw new Error(`${what}: its port is answered by a twin of another World boot (${answered.boot}), not this one; a leftover World holds the port. Stop it (\`volter-world list\`, then \`down\`) and retry.\nLog: ${log}`);
|
|
123
126
|
}
|
|
@@ -125,16 +128,44 @@ async function confirmOwnPort(port: number, boot: BootContext, ended: Promise<vo
|
|
|
125
128
|
|
|
126
129
|
/** What the server on a loopback port says its World boot is (WORLD_BOOT_PATH), or null. A direct loopback request:
|
|
127
130
|
* never through a proxy an activated shell's environment names. */
|
|
128
|
-
function worldBootOf(port: number): Promise<{ boot?: unknown } | null> {
|
|
131
|
+
function worldBootOf(port: number, abort: AbortSignal): Promise<{ boot?: unknown } | null> {
|
|
129
132
|
return new Promise((resolveBoot) => {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
133
|
+
let request: ClientRequest | undefined;
|
|
134
|
+
let deadline: ReturnType<typeof setTimeout> | undefined;
|
|
135
|
+
let settled = false;
|
|
136
|
+
let responseStarted = false;
|
|
137
|
+
const finish = (value: { boot?: unknown } | null): void => {
|
|
138
|
+
if (settled) return;
|
|
139
|
+
settled = true;
|
|
140
|
+
if (deadline !== undefined) clearTimeout(deadline);
|
|
141
|
+
abort.removeEventListener('abort', cancel);
|
|
142
|
+
resolveBoot(value);
|
|
143
|
+
};
|
|
144
|
+
const cancel = (): void => { finish(null); request?.destroy(); };
|
|
145
|
+
if (abort.aborted) { finish(null); return; }
|
|
146
|
+
abort.addEventListener('abort', cancel, { once: true });
|
|
147
|
+
// A socket timeout alone is insufficient: destroying a request after
|
|
148
|
+
// headers can close/abort its response without emitting request 'error'.
|
|
149
|
+
// Bound the whole probe, including a peer that dribbles bytes forever.
|
|
150
|
+
deadline = setTimeout(cancel, 1000);
|
|
151
|
+
try {
|
|
152
|
+
request = httpGet({ host: '127.0.0.1', port, path: WORLD_BOOT_PATH, timeout: 1000, agent: false }, (response) => {
|
|
153
|
+
responseStarted = true;
|
|
154
|
+
let body = '';
|
|
155
|
+
response.on('error', cancel);
|
|
156
|
+
response.on('aborted', cancel);
|
|
157
|
+
response.on('close', () => finish(null));
|
|
158
|
+
response.setEncoding('utf8');
|
|
159
|
+
response.on('data', (chunk: string) => { body += chunk; if (body.length > 4096) cancel(); });
|
|
160
|
+
response.on('end', () => {
|
|
161
|
+
try { finish(response.statusCode === 200 ? JSON.parse(body) as { boot?: unknown } : null); }
|
|
162
|
+
catch { finish(null); }
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
request.on('timeout', cancel);
|
|
166
|
+
request.on('error', () => finish(null));
|
|
167
|
+
request.on('close', () => { if (!responseStarted) finish(null); });
|
|
168
|
+
} catch { cancel(); }
|
|
138
169
|
});
|
|
139
170
|
}
|
|
140
171
|
|