@remix_labs/machine-starter 2.12814.0-dev → 2.12820.0-dev
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/common.js +151 -9
- package/groovebox_build.js +1 -1
- package/package.json +2 -2
package/common.js
CHANGED
|
@@ -8,10 +8,32 @@ let last_hub = null;
|
|
|
8
8
|
|
|
9
9
|
let workers = {};
|
|
10
10
|
let worker_topics = {};
|
|
11
|
+
// hub channel handed to each VM worker; the hub's end must be released when the worker dies
|
|
12
|
+
let worker_chans = {};
|
|
11
13
|
let children = {};
|
|
12
14
|
let ffiLoops = {};
|
|
13
15
|
let blobs = {};
|
|
14
16
|
let blobsLoops = {};
|
|
17
|
+
// vmID -> { cancelled } for a StartWASM2 that has not finished setting itself
|
|
18
|
+
// up. terminateVM marks it cancelled so the start can unwind instead of
|
|
19
|
+
// registering loops for a VM that was already stopped.
|
|
20
|
+
let starting = {};
|
|
21
|
+
|
|
22
|
+
// Diagnostics for slowdowns and "the ID is used" errors. Call
|
|
23
|
+
// globalThis.__rmxMachineDiag() in the console: every map below should be empty
|
|
24
|
+
// when no VM is running. A non-empty ffiLoops/blobsLoops/worker_topics entry
|
|
25
|
+
// without a worker is a leak.
|
|
26
|
+
const diag = { started: 0, terminated: 0, cancelledStarts: 0, terminateWhileStarting: 0, loopsClosed: 0, killPublishFailed: 0 };
|
|
27
|
+
globalThis.__rmxMachineDiag = () => ({
|
|
28
|
+
...diag,
|
|
29
|
+
workers: Object.keys(workers),
|
|
30
|
+
worker_topics: Object.keys(worker_topics),
|
|
31
|
+
ffiLoops: Object.keys(ffiLoops),
|
|
32
|
+
blobsLoops: Object.keys(blobsLoops),
|
|
33
|
+
children: Object.keys(children),
|
|
34
|
+
blobs: Object.keys(blobs),
|
|
35
|
+
starting: Object.keys(starting),
|
|
36
|
+
});
|
|
15
37
|
|
|
16
38
|
function terminateAll() {
|
|
17
39
|
console.log("[machine-starter] terminate all VMs");
|
|
@@ -40,6 +62,19 @@ function terminateAll() {
|
|
|
40
62
|
function terminateVM(vmID) {
|
|
41
63
|
console.log("[machine-starter] terminate VM", vmID);
|
|
42
64
|
let worker = workers[vmID];
|
|
65
|
+
if (starting[vmID]) {
|
|
66
|
+
// Stopped while StartWASM2 was still awaiting (fetching/compiling mixrt,
|
|
67
|
+
// creating the starter mixcore). Tell it to unwind; it removes what it
|
|
68
|
+
// registered. Without this it registered ffiLoops[vmID] after this
|
|
69
|
+
// call and every later start of the same vmID failed with
|
|
70
|
+
// "Cannot start VM, the ID is used".
|
|
71
|
+
starting[vmID].cancelled = true;
|
|
72
|
+
// free the id now: a start that follows this stop must not see the cancelled one
|
|
73
|
+
// (the cancelled start still holds its token and unwinds itself)
|
|
74
|
+
delete starting[vmID];
|
|
75
|
+
diag.terminateWhileStarting++;
|
|
76
|
+
console.warn("[machine-starter] terminate VM while it is still starting", vmID);
|
|
77
|
+
}
|
|
43
78
|
if (worker) {
|
|
44
79
|
let vmChildren = children[vmID];
|
|
45
80
|
let blobList = blobs[vmID] || [];
|
|
@@ -48,8 +83,12 @@ function terminateVM(vmID) {
|
|
|
48
83
|
delete children[vmID];
|
|
49
84
|
delete ffiLoops[vmID];
|
|
50
85
|
delete blobsLoops[vmID];
|
|
86
|
+
delete blobs[vmID];
|
|
51
87
|
killVM(vmID);
|
|
52
|
-
|
|
88
|
+
diag.terminated++;
|
|
89
|
+
if (ffiLoop) {
|
|
90
|
+
ffiLoop.terminate();
|
|
91
|
+
}
|
|
53
92
|
if (blobsLoop) {
|
|
54
93
|
blobsLoop.terminate();
|
|
55
94
|
}
|
|
@@ -72,15 +111,39 @@ function killVM(vmID) {
|
|
|
72
111
|
delete worker_topics[vmID];
|
|
73
112
|
if (topic && last_hub) {
|
|
74
113
|
let payload = { _rmx_type:"msg_vm_register", id: vmID, alive: false };
|
|
114
|
+
// The worker must be terminated even when the unannounce cannot be published:
|
|
115
|
+
// a chain of .then() skipped worker.terminate() on any failure, leaking a Web Worker
|
|
116
|
+
// (with its wasm instance) for every VM stopped that way.
|
|
75
117
|
last_hub.newChannel().then(channel => {
|
|
76
118
|
let msg = channel.newJSONMessage("msg_vm_register", "vm", payload);
|
|
77
|
-
channel.publish(topic + "/announce", msg, true)
|
|
78
|
-
}).
|
|
119
|
+
return channel.publish(topic + "/announce", msg, true).finally(() => closeChannel(channel));
|
|
120
|
+
}).catch(e => {
|
|
121
|
+
diag.killPublishFailed++;
|
|
122
|
+
console.warn("[machine-starter] could not announce VM stop", vmID, e);
|
|
123
|
+
}).finally(() => { worker.terminate(); releaseWorkerChannel(last_hub, vmID); });
|
|
79
124
|
} else {
|
|
80
|
-
worker.terminate()
|
|
125
|
+
worker.terminate();
|
|
126
|
+
releaseWorkerChannel(last_hub, vmID);
|
|
81
127
|
}
|
|
82
128
|
}
|
|
83
129
|
|
|
130
|
+
// A hub channel has no explicit close: drop its MessagePort so the hub worker can release it.
|
|
131
|
+
function closeChannel(channel) {
|
|
132
|
+
try {
|
|
133
|
+
if (typeof channel.close === "function") channel.close();
|
|
134
|
+
else channel.port && channel.port.close && channel.port.close();
|
|
135
|
+
// a message sent just before close() may not arrive: ask via the hub itself as well
|
|
136
|
+
if (last_hub && last_hub.closeChannelById) last_hub.closeChannelById(channel.chanId);
|
|
137
|
+
} catch (_) {}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The channel given to a VM worker was transferred, so only the hub can release its end.
|
|
141
|
+
function releaseWorkerChannel(hub, vmID) {
|
|
142
|
+
let chanId = worker_chans[vmID];
|
|
143
|
+
delete worker_chans[vmID];
|
|
144
|
+
try { if (hub && hub.closeChannelById) hub.closeChannelById(chanId); } catch (_) {}
|
|
145
|
+
}
|
|
146
|
+
|
|
84
147
|
if (globalThis.Process) {
|
|
85
148
|
Process.on("exit", () => {
|
|
86
149
|
terminateAll()
|
|
@@ -133,12 +196,60 @@ async function StartWASM2(hub, config, getWorker) {
|
|
|
133
196
|
console.log("[machine-starter] Call MixExplain() for explaining debug levels, and MixSetMask(m) for changing these")
|
|
134
197
|
};
|
|
135
198
|
last_hub = hub;
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
let worker = await getWorker();
|
|
139
|
-
if (workers[config.vmID] || ffiLoops[config.vmID])
|
|
199
|
+
if (starting[config.vmID]) {
|
|
200
|
+
// another start of the same id has not finished setting itself up
|
|
140
201
|
throw new Error("Cannot start VM, the ID is used: " + config.vmID);
|
|
202
|
+
}
|
|
203
|
+
// Registered before the first await: a terminateVM that arrives while the channel and worker
|
|
204
|
+
// are still being created finds the token and cancels this start. Without it that stop was a
|
|
205
|
+
// no-op, the VM registered and ran with no owner, and every later start failed with
|
|
206
|
+
// "Cannot start VM, the ID is used".
|
|
207
|
+
const startToken = { cancelled: false };
|
|
208
|
+
starting[config.vmID] = startToken;
|
|
209
|
+
let channel;
|
|
210
|
+
let worker;
|
|
211
|
+
try {
|
|
212
|
+
channel = await hub.newChannel();
|
|
213
|
+
// injected by either index.js or node.js
|
|
214
|
+
worker = await getWorker();
|
|
215
|
+
} catch (e) {
|
|
216
|
+
if (starting[config.vmID] === startToken) delete starting[config.vmID];
|
|
217
|
+
if (channel) closeChannel(channel);
|
|
218
|
+
throw e;
|
|
219
|
+
}
|
|
220
|
+
if (startToken.cancelled) {
|
|
221
|
+
diag.cancelledStarts++;
|
|
222
|
+
console.warn("[machine-starter] start cancelled before the VM was registered", config.vmID);
|
|
223
|
+
if (starting[config.vmID] === startToken) delete starting[config.vmID];
|
|
224
|
+
closeChannel(channel);
|
|
225
|
+
try { worker.terminate(); } catch (_) {}
|
|
226
|
+
return worker;
|
|
227
|
+
}
|
|
228
|
+
if (workers[config.vmID] || ffiLoops[config.vmID]) {
|
|
229
|
+
if (starting[config.vmID] === startToken) delete starting[config.vmID];
|
|
230
|
+
closeChannel(channel);
|
|
231
|
+
throw new Error("Cannot start VM, the ID is used: " + config.vmID);
|
|
232
|
+
}
|
|
141
233
|
workers[config.vmID] = worker;
|
|
234
|
+
worker_chans[config.vmID] = channel.chanId;
|
|
235
|
+
diag.started++;
|
|
236
|
+
// Called after every await below: if terminateVM ran meanwhile, undo and stop.
|
|
237
|
+
const abortIfCancelled = () => {
|
|
238
|
+
if (!startToken.cancelled) return false;
|
|
239
|
+
diag.cancelledStarts++;
|
|
240
|
+
console.warn("[machine-starter] start cancelled, VM was terminated while starting", config.vmID);
|
|
241
|
+
if (starting[config.vmID] === startToken) delete starting[config.vmID];
|
|
242
|
+
// only undo what this start registered: a newer start of the same id may own the maps now
|
|
243
|
+
if (workers[config.vmID] === worker) {
|
|
244
|
+
delete ffiLoops[config.vmID];
|
|
245
|
+
delete blobsLoops[config.vmID];
|
|
246
|
+
delete worker_topics[config.vmID];
|
|
247
|
+
delete workers[config.vmID];
|
|
248
|
+
releaseWorkerChannel(hub, config.vmID);
|
|
249
|
+
}
|
|
250
|
+
try { worker.terminate(); } catch (_) {}
|
|
251
|
+
return true;
|
|
252
|
+
};
|
|
142
253
|
if (config.topicPrefix != undefined && config.topicPrefix != "") {
|
|
143
254
|
worker_topics[config.vmID] = config.topicPrefix + "/vm/" + config.vmID;
|
|
144
255
|
} else {
|
|
@@ -148,9 +259,11 @@ async function StartWASM2(hub, config, getWorker) {
|
|
|
148
259
|
// from now on, the "local" FFIs are called "starter" FFIs, because this
|
|
149
260
|
// distinguishes these from other locally defined FFIs
|
|
150
261
|
let mixcore = await createStarterMixcore(config.mixcore);
|
|
262
|
+
if (abortIfCancelled()) return worker;
|
|
151
263
|
if (mixcore) {
|
|
152
264
|
// merge the ffis and store them back into the config so sub vms also get them
|
|
153
265
|
config.localFFIs = Object.assign(starterFFIs, await mixcoreStarterFFIs(mixcore));
|
|
266
|
+
if (abortIfCancelled()) return worker;
|
|
154
267
|
// delete the mixcore config to avoid being handled further by the worker
|
|
155
268
|
delete config.mixcore;
|
|
156
269
|
}
|
|
@@ -184,7 +297,9 @@ async function StartWASM2(hub, config, getWorker) {
|
|
|
184
297
|
globalThis.mixrtCodeURL = url;
|
|
185
298
|
globalThis.mixrtCodeModule = mixrtCode;
|
|
186
299
|
}
|
|
300
|
+
if (abortIfCancelled()) return worker;
|
|
187
301
|
};
|
|
302
|
+
if (abortIfCancelled()) return worker;
|
|
188
303
|
ffiLoops[config.vmID] = setupStarterFFIs(hub, config, starterFFIfuns);
|
|
189
304
|
blobsLoops[config.vmID] = setupBlobs(hub, config);
|
|
190
305
|
let config_msg =
|
|
@@ -210,6 +325,7 @@ async function StartWASM2(hub, config, getWorker) {
|
|
|
210
325
|
// (http, https, data)
|
|
211
326
|
};
|
|
212
327
|
worker.postMessage(config_msg, [ channel.port ]);
|
|
328
|
+
if (starting[config.vmID] === startToken) delete starting[config.vmID];
|
|
213
329
|
return worker;
|
|
214
330
|
}
|
|
215
331
|
|
|
@@ -234,6 +350,7 @@ async function ffi_vm_start(conn, args, getWorker) {
|
|
|
234
350
|
let vmChildren = children[conn.config.vmID];
|
|
235
351
|
if (!vmChildren) vmChildren = [];
|
|
236
352
|
vmChildren.push(vm_id);
|
|
353
|
+
children[conn.config.vmID] = vmChildren; // was never stored: embedded VMs were not stopped with their parent
|
|
237
354
|
let arg_config = args[0];
|
|
238
355
|
if (arg_config.interceptFFI !== undefined)
|
|
239
356
|
config["interceptFFI"] = arg_config.interceptFFI;
|
|
@@ -247,6 +364,10 @@ async function ffi_vm_stop(conn, args) {
|
|
|
247
364
|
let vm_id = args[0];
|
|
248
365
|
let worker = workers[vm_id];
|
|
249
366
|
if (!worker) throw new Error("no such VM: " + vm_id);
|
|
367
|
+
for (let parent of Object.keys(children)) {
|
|
368
|
+
let i = children[parent].indexOf(vm_id);
|
|
369
|
+
if (i >= 0) children[parent].splice(i, 1);
|
|
370
|
+
}
|
|
250
371
|
terminateVM(vm_id);
|
|
251
372
|
return null;
|
|
252
373
|
}
|
|
@@ -389,6 +510,11 @@ async function starterFFIsMessageLoop(hub, config, starterFFIs, terminate) {
|
|
|
389
510
|
]);
|
|
390
511
|
if (terminated) {
|
|
391
512
|
// console.log("[machine-starter] terminate FFI loop", vmID);
|
|
513
|
+
// Release the hub subscription and channel; without this each VM (one per build in the
|
|
514
|
+
// builder) left both behind for the lifetime of the page.
|
|
515
|
+
await sub.unsubscribe().catch(() => {});
|
|
516
|
+
closeChannel(channel);
|
|
517
|
+
diag.loopsClosed++;
|
|
392
518
|
return;
|
|
393
519
|
}
|
|
394
520
|
let name = msg.payload.name;
|
|
@@ -449,6 +575,9 @@ async function blobsMessageLoop(hub, config, terminate) {
|
|
|
449
575
|
]);
|
|
450
576
|
if (terminated) {
|
|
451
577
|
// console.log("[machine-starter] terminate blobs loop", vmID);
|
|
578
|
+
await sub.unsubscribe().catch(() => {});
|
|
579
|
+
closeChannel(channel);
|
|
580
|
+
diag.loopsClosed++;
|
|
452
581
|
return;
|
|
453
582
|
}
|
|
454
583
|
let name = msg.payload.name;
|
|
@@ -516,7 +645,20 @@ class FFIComm {
|
|
|
516
645
|
}
|
|
517
646
|
}
|
|
518
647
|
|
|
648
|
+
// Stops every VM that `parentVmID` started (through $vm_start) and has not stopped itself, including
|
|
649
|
+
// ones that are still starting. Returns how many were stopped. A parent that outlives its children
|
|
650
|
+
// (the L2 session agent starts one embedded VM per session) can leave one running when its owner
|
|
651
|
+
// goes away before the session finished starting; the caller decides when that has happened.
|
|
652
|
+
function terminateChildren(parentVmID) {
|
|
653
|
+
let list = children[parentVmID];
|
|
654
|
+
if (!list || list.length === 0) return 0;
|
|
655
|
+
let ids = list.slice();
|
|
656
|
+
children[parentVmID] = [];
|
|
657
|
+
for (let id of ids) terminateVM(id);
|
|
658
|
+
return ids.length;
|
|
659
|
+
}
|
|
660
|
+
|
|
519
661
|
// use terminateVM to shut a worker down!
|
|
520
|
-
export { StartWASM, StartWASM2, terminateAll, terminateVM,
|
|
662
|
+
export { StartWASM, StartWASM2, terminateAll, terminateVM, terminateChildren,
|
|
521
663
|
Token, Case, Opaque
|
|
522
664
|
}
|
package/groovebox_build.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var GROOVEBOX_BUILD = "
|
|
1
|
+
var GROOVEBOX_BUILD = "12820";
|
|
2
2
|
export { GROOVEBOX_BUILD }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remix_labs/machine-starter",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.12820.0-dev",
|
|
4
4
|
"description": "start the groove",
|
|
5
5
|
"main": "node.js",
|
|
6
6
|
"browser": "index.js",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"author": "Remixlabs staff",
|
|
12
12
|
"license": "ISC",
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@remix_labs/hub-client": "2.
|
|
14
|
+
"@remix_labs/hub-client": "2.12820.0-dev",
|
|
15
15
|
"nanoid": "^5.0.2",
|
|
16
16
|
"web-worker": "^1.2.0"
|
|
17
17
|
},
|