@remix_labs/machine-starter 2.12816.0 → 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 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
- ffiLoop.terminate();
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
- }).then(_ => worker.terminate())
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
- let channel = await hub.newChannel();
137
- // injected by either index.js or node.js
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
  }
@@ -1,2 +1,2 @@
1
- var GROOVEBOX_BUILD = "12816";
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.12816.0",
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.12816.0",
14
+ "@remix_labs/hub-client": "2.12820.0-dev",
15
15
  "nanoid": "^5.0.2",
16
16
  "web-worker": "^1.2.0"
17
17
  },