@bitmagic/cli 0.1.60-dev.2 → 0.1.60-dev.4
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/README.md +67 -3
- package/dist/atomic-json.d.ts +18 -0
- package/dist/atomic-json.js +39 -0
- package/dist/atomic-json.js.map +1 -0
- package/dist/cli.d.ts +1 -1
- package/dist/commands/cover.js +2 -1
- package/dist/commands/cover.js.map +1 -1
- package/dist/commands/dev.js +79 -35
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/publish.d.ts +2 -0
- package/dist/commands/publish.js +1 -1
- package/dist/commands/publish.js.map +1 -1
- package/dist/commands/reload.d.ts +1 -1
- package/dist/commands/reload.js +14 -5
- package/dist/commands/reload.js.map +1 -1
- package/dist/commands/verify.js +134 -118
- package/dist/commands/verify.js.map +1 -1
- package/dist/editor/motion-panel.d.ts +2 -2
- package/dist/editor/motion-panel.js +27 -5
- package/dist/editor/motion-panel.js.map +1 -1
- package/dist/editor/reload-bus.d.ts +11 -23
- package/dist/editor/reload-bus.js +55 -8
- package/dist/editor/reload-bus.js.map +1 -1
- package/dist/editor/server.d.ts +10 -0
- package/dist/editor/server.js +46 -0
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.d.ts +10 -0
- package/dist/editor/shell-page.js +58 -3
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/editor/tab-reuse.d.ts +23 -0
- package/dist/editor/tab-reuse.js +61 -0
- package/dist/editor/tab-reuse.js.map +1 -0
- package/dist/local-port.d.ts +8 -1
- package/dist/local-port.js +9 -2
- package/dist/local-port.js.map +1 -1
- package/dist/machine-lock.d.ts +56 -0
- package/dist/machine-lock.js +213 -0
- package/dist/machine-lock.js.map +1 -0
- package/dist/port-lane.d.ts +27 -0
- package/dist/port-lane.js +36 -1
- package/dist/port-lane.js.map +1 -1
- package/dist/process-alive.d.ts +13 -0
- package/dist/process-alive.js +24 -0
- package/dist/process-alive.js.map +1 -0
- package/dist/project/dev-handle.d.ts +6 -34
- package/dist/project/dev-handle.js +9 -59
- package/dist/project/dev-handle.js.map +1 -1
- package/dist/project/dev-ports.d.ts +67 -0
- package/dist/project/dev-ports.js +204 -0
- package/dist/project/dev-ports.js.map +1 -0
- package/dist/project/game-name.d.ts +9 -0
- package/dist/project/game-name.js +21 -0
- package/dist/project/game-name.js.map +1 -0
- package/dist/project/jobs.js +2 -1
- package/dist/project/jobs.js.map +1 -1
- package/dist/scaffold/project-files.js +8 -8
- package/dist/scaffold/project-files.js.map +1 -1
- package/dist/smoke/run.d.ts +4 -0
- package/dist/smoke/run.js +5 -2
- package/dist/smoke/run.js.map +1 -1
- package/dist/verify/iterate.d.ts +5 -0
- package/dist/verify/iterate.js +23 -20
- package/dist/verify/iterate.js.map +1 -1
- package/dist/verify/lock.d.ts +32 -0
- package/dist/verify/lock.js +54 -0
- package/dist/verify/lock.js.map +1 -0
- package/package.json +4 -4
|
@@ -105,11 +105,16 @@ function escapeAttr(value) {
|
|
|
105
105
|
export function renderEditorShell(options) {
|
|
106
106
|
const gameUrl = `http://localhost:${options.gamePort}/?source=creator&gameId=${encodeURIComponent(options.gameId)}`;
|
|
107
107
|
const recording = options.recording;
|
|
108
|
+
const devLine = options.cliLine === 'dev';
|
|
109
|
+
const lineTitle = devLine
|
|
110
|
+
? `bitmagic ${options.cliVersion} — a prerelease from the dev line (@bitmagic/cli@dev)`
|
|
111
|
+
: `bitmagic ${options.cliVersion} — the release line (@bitmagic/cli)`;
|
|
112
|
+
const title = `${options.gameName} — bitmagic dev${devLine ? ' (dev build)' : ''}`;
|
|
108
113
|
return `<!doctype html>
|
|
109
114
|
<html lang="en">
|
|
110
115
|
<head>
|
|
111
116
|
<meta charset="utf-8">
|
|
112
|
-
<title
|
|
117
|
+
<title>${escapeAttr(title)}</title>
|
|
113
118
|
${BITMAGIC_FONT_LINK}
|
|
114
119
|
<style>
|
|
115
120
|
${BITMAGIC_TOKENS_CSS}
|
|
@@ -125,6 +130,11 @@ ${BITMAGIC_CONTROLS_CSS}
|
|
|
125
130
|
#bar .wordmark { font-size: 14px; font-weight: 700; text-transform: uppercase;
|
|
126
131
|
letter-spacing: 1.6px; }
|
|
127
132
|
#bar .id { color: var(--bm-text-dim); font-family: var(--bm-font-mono); font-size: 12px; }
|
|
133
|
+
/* Who this is: the build's badge, then the game by name, then its id and environment dimmed. */
|
|
134
|
+
#bar .game-name { font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
135
|
+
max-width: 22vw; }
|
|
136
|
+
#bar .env { color: var(--bm-text-dim); font-family: var(--bm-font-mono); font-size: 12px;
|
|
137
|
+
white-space: nowrap; }
|
|
128
138
|
#bar .spacer { flex: 1; }
|
|
129
139
|
#tabs { display: flex; gap: 4px; background: var(--bm-surface-alt); border-radius: 9999px;
|
|
130
140
|
padding: 3px; }
|
|
@@ -248,7 +258,10 @@ ${BOOT_LOADER_CSS}
|
|
|
248
258
|
<body>
|
|
249
259
|
<div id="bar">
|
|
250
260
|
<span class="wordmark">bitmagic dev</span>
|
|
251
|
-
<span class="
|
|
261
|
+
<span id="cli-line" class="bm-badge${devLine ? ' bm-badge--warning' : ''}" title="${escapeAttr(lineTitle)}">${devLine ? 'DEV BUILD' : 'RELEASE'}</span>
|
|
262
|
+
<span id="game-name" class="game-name" title="gameName in src/work/game.json">${escapeAttr(options.gameName)}</span>
|
|
263
|
+
<span class="id" title="Game id, from bitmagic.json">${escapeAttr(options.gameId)}</span>
|
|
264
|
+
<span id="env" class="env" title="The api-server this project talks to">env ${escapeAttr(options.environment)}</span>
|
|
252
265
|
<div id="tabs">
|
|
253
266
|
<button class="tab bm-pill active" type="button" data-tab="game">Game</button>
|
|
254
267
|
<button class="tab bm-pill" type="button" data-tab="editor">Editor</button>
|
|
@@ -378,6 +391,16 @@ ${ASSET_DETAIL_HTML}
|
|
|
378
391
|
|
|
379
392
|
var loaded = false;
|
|
380
393
|
var saving = false;
|
|
394
|
+
/**
|
|
395
|
+
* Which bitmagic dev process rendered this page, as first reported by its poll or its stream.
|
|
396
|
+
* A different id later means the server was restarted under this tab — and this page, its
|
|
397
|
+
* baked ports and quite possibly its JavaScript are from the OLD one. staleShell makes the
|
|
398
|
+
* next start() a full page reload instead of an iframe reboot; the reload itself goes through
|
|
399
|
+
* onChangeSignal so a sculpt in progress is asked, not destroyed.
|
|
400
|
+
*/
|
|
401
|
+
var sessionId = null;
|
|
402
|
+
var sessionChanged = false;
|
|
403
|
+
var staleShell = false;
|
|
381
404
|
var markersDirty = false;
|
|
382
405
|
var lastKnownMtime = 0;
|
|
383
406
|
var pollTimer = null;
|
|
@@ -1086,7 +1109,10 @@ ${ASSET_DETAIL_HTML}
|
|
|
1086
1109
|
async function checkExternalEdits() {
|
|
1087
1110
|
if (saving) return;
|
|
1088
1111
|
try {
|
|
1089
|
-
|
|
1112
|
+
// from=shell is how the server tells this page's poll from the CLI's own probes when it
|
|
1113
|
+
// asks "is a tab watching?" before opening another (tab-reuse.ts).
|
|
1114
|
+
var state = await (await fetch('/api/state?from=shell')).json();
|
|
1115
|
+
noteSession(state.session, state.ready === true);
|
|
1090
1116
|
showCliUpdate(state.cliUpdate);
|
|
1091
1117
|
showPublish(state.publish);
|
|
1092
1118
|
// Only the mtime comparison below needs the game to have booted — it is what establishes
|
|
@@ -1256,6 +1282,20 @@ ${ASSET_DETAIL_HTML}
|
|
|
1256
1282
|
* button on it. What changed is who is usually writing — an agent, not the human — so the
|
|
1257
1283
|
* default flipped from "always ask" to "ask only when the human is in the middle of something".
|
|
1258
1284
|
*/
|
|
1285
|
+
/**
|
|
1286
|
+
* A restarted bitmagic dev behind this tab. Noticed from either channel, acted on once the
|
|
1287
|
+
* new server's vite is up — rebooting against one that is not would only show a boot failure.
|
|
1288
|
+
*/
|
|
1289
|
+
function noteSession(id, ready) {
|
|
1290
|
+
if (typeof id !== 'string' || id === '') return;
|
|
1291
|
+
if (sessionId === null) { sessionId = id; return; }
|
|
1292
|
+
if (id !== sessionId) sessionChanged = true;
|
|
1293
|
+
if (sessionChanged && ready && !staleShell) {
|
|
1294
|
+
staleShell = true;
|
|
1295
|
+
onChangeSignal('dev server restarted');
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1259
1299
|
function onChangeSignal(reason) {
|
|
1260
1300
|
// Logged because "why did my game just reload?" is otherwise unanswerable from the browser.
|
|
1261
1301
|
console.log('[bitmagic dev] change signalled:', reason);
|
|
@@ -2297,6 +2337,9 @@ ${ASSET_DETAIL_HTML}
|
|
|
2297
2337
|
}
|
|
2298
2338
|
|
|
2299
2339
|
function start() {
|
|
2340
|
+
// The page itself is from a previous server: reload it whole rather than reboot the iframe
|
|
2341
|
+
// under markup and script the new server did not render. See noteSession.
|
|
2342
|
+
if (staleShell) { window.location.reload(); return; }
|
|
2300
2343
|
stopLoop();
|
|
2301
2344
|
var gen = ++generation;
|
|
2302
2345
|
boot(gen).then(function () {
|
|
@@ -2323,6 +2366,14 @@ ${ASSET_DETAIL_HTML}
|
|
|
2323
2366
|
try { return JSON.parse(event.data); } catch (error) { return {}; }
|
|
2324
2367
|
}
|
|
2325
2368
|
var events = new EventSource('/api/events/stream');
|
|
2369
|
+
// Sent first on every (re)connect. A reconnect that lands on a different session is a
|
|
2370
|
+
// restarted server; the poll it triggers below reads whether that server is ready yet.
|
|
2371
|
+
events.addEventListener('session', function (event) {
|
|
2372
|
+
noteSession(payloadOf(event).id, false);
|
|
2373
|
+
});
|
|
2374
|
+
// The stream is back — after a restart, or after the browser throttled this hidden tab. Ask
|
|
2375
|
+
// now rather than on the next tick of a poll that may be a minute away.
|
|
2376
|
+
events.addEventListener('open', function () { void checkExternalEdits(); });
|
|
2326
2377
|
events.addEventListener('reload', function (event) {
|
|
2327
2378
|
onChangeSignal(payloadOf(event).reason || 'change');
|
|
2328
2379
|
});
|
|
@@ -2364,6 +2415,10 @@ ${ASSET_DETAIL_HTML}
|
|
|
2364
2415
|
}
|
|
2365
2416
|
|
|
2366
2417
|
shotButton.addEventListener('click', takeScreenshot);
|
|
2418
|
+
// A tab that was hidden while the server restarted finds out the moment it is looked at.
|
|
2419
|
+
document.addEventListener('visibilitychange', function () {
|
|
2420
|
+
if (document.visibilityState === 'visible') void checkExternalEdits();
|
|
2421
|
+
});
|
|
2367
2422
|
document.getElementById('reload').addEventListener('click', reloadUnlessSculpting);
|
|
2368
2423
|
// Same thing the bar's Reload does, put where a creator is already looking when a boot fails.
|
|
2369
2424
|
// Unguarded on purpose: a failed boot has no session left to protect, and this is the way out.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AAEH,OAAO,EACL,qBAAqB,EAAE,kBAAkB,GAC1C,MAAM,qDAAqD,CAAC;AAC7D,OAAO,EAAE,iBAAiB,EAAE,MAAM,+CAA+C,CAAC;AAClF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAC9E,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAC3E,OAAO,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjF,OAAO,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACzF,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACrE,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAE7F,oGAAoG;AACpG,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;GAMG;AACH,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAE7B;;;;;;;;;;;;GAYG;AACH,SAAS,sBAAsB;IAC7B,OAAO;QACL,6BAA6B,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,GAAG;QACjE,iCAAiC,qBAAqB,CAAC,QAAQ,EAAE,GAAG;QACpE,8BAA8B,kBAAkB,CAAC,QAAQ,EAAE,GAAG;KAC/D,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACjB,CAAC;AAED;;;;;;GAMG;AACH,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC,kGAAkG;AAClG,SAAS,UAAU,CAAC,KAAa;IAC/B,OAAO,KAAK;SACT,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC;SACtB,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC;SACvB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC;SACrB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AAC3B,CAAC;
|
|
1
|
+
{"version":3,"file":"shell-page.js","sourceRoot":"","sources":["../../src/editor/shell-page.ts"],"names":[],"mappings":"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B;;IAEvC,sBAAsB,EAAE;;;;;;;;4BAQA,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAyjEtD,eAAe;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAqChB,CAAC;AACF,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Comfortably past Firefox's 5 s reconnect, short enough that a closed tab costs one breath. */
|
|
2
|
+
export declare const TAB_REUSE_WINDOW_MS = 7000;
|
|
3
|
+
export interface DevViewRecord {
|
|
4
|
+
/** Epoch ms when a tab last attached to a `dev` on this port. */
|
|
5
|
+
watcherSeenAt: number;
|
|
6
|
+
editorPort: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function devViewRecordPath(root: string): string;
|
|
9
|
+
export declare function readDevViewRecord(root: string): DevViewRecord | null;
|
|
10
|
+
export declare function writeDevViewRecord(root: string, record: DevViewRecord): void;
|
|
11
|
+
export interface TabDecisionInput {
|
|
12
|
+
/** A tab attached to a previous `dev` on this very port — worth waiting for. */
|
|
13
|
+
evidence: boolean;
|
|
14
|
+
/** Epoch ms when the sidecar began listening; the wait is measured from here, not from now. */
|
|
15
|
+
listeningAt: number;
|
|
16
|
+
hasWatcher: () => boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface TabDecisionDeps {
|
|
19
|
+
now?: () => number;
|
|
20
|
+
sleep?: (ms: number) => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/** True when a tab should be opened; false when one is already watching this server. */
|
|
23
|
+
export declare function shouldOpenTab(input: TabDecisionInput, deps?: TabDecisionDeps): Promise<boolean>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether `bitmagic dev` should open a browser tab, or whether one is already there.
|
|
3
|
+
*
|
|
4
|
+
* `dev` opens its page itself (open-browser.ts), and it is restarted often — agents start it per
|
|
5
|
+
* session, a background task gets killed, a creator Ctrl-C's it to add `--mobile`. Every restart
|
|
6
|
+
* used to open another tab while the previous one was still there: that tab polls `/api/state`
|
|
7
|
+
* every half second and holds an `EventSource` that reconnects on its own, so it comes back to the
|
|
8
|
+
* new server by itself — and then there were two. After a few prompts, ten.
|
|
9
|
+
*
|
|
10
|
+
* The signal is the SSE reconnect, not the poll. A background tab's `setTimeout` chain is throttled
|
|
11
|
+
* by Chrome to once a second, and to once a MINUTE after five minutes hidden; `EventSource`
|
|
12
|
+
* reconnects on a browser network timer — about 3 s in Chromium, 5 s in Firefox — that is not.
|
|
13
|
+
*
|
|
14
|
+
* The wait is paid only when there is EVIDENCE a tab existed: the previous server wrote
|
|
15
|
+
* `.bitmagic/dev-view.json` the first time a tab attached. A fresh project opens instantly. A
|
|
16
|
+
* restart waits until a tab reconnects, or until `TAB_REUSE_WINDOW_MS` have passed since the
|
|
17
|
+
* sidecar started listening — and the sidecar listens from before the build, so in practice the
|
|
18
|
+
* build has already spent that window and the decision is immediate either way.
|
|
19
|
+
*/
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { readJsonFile, writeJsonAtomic } from '../atomic-json.js';
|
|
22
|
+
/** Comfortably past Firefox's 5 s reconnect, short enough that a closed tab costs one breath. */
|
|
23
|
+
export const TAB_REUSE_WINDOW_MS = 7_000;
|
|
24
|
+
const POLL_MS = 100;
|
|
25
|
+
export function devViewRecordPath(root) {
|
|
26
|
+
return path.join(root, '.bitmagic', 'dev-view.json');
|
|
27
|
+
}
|
|
28
|
+
export function readDevViewRecord(root) {
|
|
29
|
+
const parsed = readJsonFile(devViewRecordPath(root));
|
|
30
|
+
if (typeof parsed !== 'object' || parsed === null)
|
|
31
|
+
return null;
|
|
32
|
+
const record = parsed;
|
|
33
|
+
if (typeof record.watcherSeenAt !== 'number' || typeof record.editorPort !== 'number')
|
|
34
|
+
return null;
|
|
35
|
+
return { watcherSeenAt: record.watcherSeenAt, editorPort: record.editorPort };
|
|
36
|
+
}
|
|
37
|
+
export function writeDevViewRecord(root, record) {
|
|
38
|
+
try {
|
|
39
|
+
writeJsonAtomic(devViewRecordPath(root), record);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// A record we cannot write costs one extra tab on the next restart, nothing else.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** True when a tab should be opened; false when one is already watching this server. */
|
|
46
|
+
export async function shouldOpenTab(input, deps = {}) {
|
|
47
|
+
const now = deps.now ?? Date.now;
|
|
48
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
49
|
+
if (input.hasWatcher())
|
|
50
|
+
return false;
|
|
51
|
+
if (!input.evidence)
|
|
52
|
+
return true;
|
|
53
|
+
const deadline = input.listeningAt + TAB_REUSE_WINDOW_MS;
|
|
54
|
+
while (now() < deadline) {
|
|
55
|
+
await sleep(POLL_MS);
|
|
56
|
+
if (input.hasWatcher())
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=tab-reuse.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tab-reuse.js","sourceRoot":"","sources":["../../src/editor/tab-reuse.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,IAAI,MAAM,MAAM,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAElE,iGAAiG;AACjG,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,CAAC;AACzC,MAAM,OAAO,GAAG,GAAG,CAAC;AAQpB,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE,eAAe,CAAC,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,IAAY;IAC5C,MAAM,MAAM,GAAG,YAAY,CAAC,iBAAiB,CAAC,IAAI,CAAC,CAAC,CAAC;IACrD,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAC/D,MAAM,MAAM,GAAG,MAAiC,CAAC;IACjD,IAAI,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IACnG,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,aAAa,EAAE,UAAU,EAAE,MAAM,CAAC,UAAU,EAAE,CAAC;AAChF,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,IAAY,EAAE,MAAqB;IACpE,IAAI,CAAC;QACH,eAAe,CAAC,iBAAiB,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC;IACnD,CAAC;IAAC,MAAM,CAAC;QACP,kFAAkF;IACpF,CAAC;AACH,CAAC;AAeD,wFAAwF;AACxF,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAuB,EAAE,OAAwB,EAAE;IACrF,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACjC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAU,EAAE,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC;IACtG,IAAI,KAAK,CAAC,UAAU,EAAE;QAAE,OAAO,KAAK,CAAC;IACrC,IAAI,CAAC,KAAK,CAAC,QAAQ;QAAE,OAAO,IAAI,CAAC;IACjC,MAAM,QAAQ,GAAG,KAAK,CAAC,WAAW,GAAG,mBAAmB,CAAC;IACzD,OAAO,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QACxB,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;QACrB,IAAI,KAAK,CAAC,UAAU,EAAE;YAAE,OAAO,KAAK,CAAC;IACvC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC"}
|
package/dist/local-port.d.ts
CHANGED
|
@@ -82,8 +82,15 @@ export declare function laneExhaustedMessage(lane: PortLane, remedy: string): st
|
|
|
82
82
|
* and `forge` alike need a port inside the CDN's CORS window (see the file header) and have
|
|
83
83
|
* nothing useful to do with a `null`. Bundling the lane lookup, the scan and the refusal here
|
|
84
84
|
* keeps the three from drifting apart — a caller varies only what it tells the creator to do.
|
|
85
|
+
*
|
|
86
|
+
* Every port a project on this machine has been promised (project/dev-ports.ts) is skipped as
|
|
87
|
+
* well, whether or not that project's `dev` is up right now. A verify sitting on 3021 while
|
|
88
|
+
* project B's dev happened to be down would push B's next `dev` into a drift, which is exactly the
|
|
89
|
+
* instability the registry ends. At most 27 of the lane's hundred ports, so the scan loses nothing.
|
|
85
90
|
*/
|
|
86
|
-
export declare function reserveLanePort(remedy: string, exclude?: ReadonlySet<number
|
|
91
|
+
export declare function reserveLanePort(remedy: string, exclude?: ReadonlySet<number>, deps?: {
|
|
92
|
+
registryDir?: string;
|
|
93
|
+
}): Promise<number>;
|
|
87
94
|
/**
|
|
88
95
|
* The port to actually bind: `preferred` when it is free, otherwise another one in the window.
|
|
89
96
|
*
|
package/dist/local-port.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as net from 'net';
|
|
2
2
|
import { CliError } from './errors.js';
|
|
3
3
|
import { currentPortLane } from './port-lane.js';
|
|
4
|
+
import { registeredSlotPorts } from './project/dev-ports.js';
|
|
4
5
|
/**
|
|
5
6
|
* The port window the platform's bucket CORS policy allowlists as an origin.
|
|
6
7
|
*
|
|
@@ -131,10 +132,16 @@ export function laneExhaustedMessage(lane, remedy) {
|
|
|
131
132
|
* and `forge` alike need a port inside the CDN's CORS window (see the file header) and have
|
|
132
133
|
* nothing useful to do with a `null`. Bundling the lane lookup, the scan and the refusal here
|
|
133
134
|
* keeps the three from drifting apart — a caller varies only what it tells the creator to do.
|
|
135
|
+
*
|
|
136
|
+
* Every port a project on this machine has been promised (project/dev-ports.ts) is skipped as
|
|
137
|
+
* well, whether or not that project's `dev` is up right now. A verify sitting on 3021 while
|
|
138
|
+
* project B's dev happened to be down would push B's next `dev` into a drift, which is exactly the
|
|
139
|
+
* instability the registry ends. At most 27 of the lane's hundred ports, so the scan loses nothing.
|
|
134
140
|
*/
|
|
135
|
-
export async function reserveLanePort(remedy, exclude = new Set()) {
|
|
141
|
+
export async function reserveLanePort(remedy, exclude = new Set(), deps = {}) {
|
|
136
142
|
const lane = currentPortLane();
|
|
137
|
-
const
|
|
143
|
+
const promised = registeredSlotPorts(lane, { baseDir: deps.registryDir });
|
|
144
|
+
const port = await reservePortInLane(lane, new Set([...exclude, ...promised]));
|
|
138
145
|
if (port === null)
|
|
139
146
|
throw new CliError(laneExhaustedMessage(lane, remedy));
|
|
140
147
|
return port;
|
package/dist/local-port.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"local-port.js","sourceRoot":"","sources":["../src/local-port.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,KAAK,CAAC;AAC3B,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,eAAe,EAAiB,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"local-port.js","sourceRoot":"","sources":["../src/local-port.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export interface LockHolder {
|
|
2
|
+
pid: number;
|
|
3
|
+
/** Epoch ms when the holder took the lock. */
|
|
4
|
+
startedAt: number;
|
|
5
|
+
/** The project the holder was working on, for the waiting line. */
|
|
6
|
+
root: string;
|
|
7
|
+
/** The command that took it — `verify`, `dev` — for the same line. */
|
|
8
|
+
command: string;
|
|
9
|
+
}
|
|
10
|
+
export interface MachineLockDeps {
|
|
11
|
+
baseDir?: string;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
sleep?: (ms: number) => Promise<void>;
|
|
14
|
+
isAlive?: (pid: number) => boolean;
|
|
15
|
+
/** Epoch ms of this machine's boot, below which a holder cannot be alive. */
|
|
16
|
+
bootTime?: () => number;
|
|
17
|
+
pid?: number;
|
|
18
|
+
pollMs?: number;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
maxAgeMs?: number;
|
|
21
|
+
/** Called once, the first time the lock turns out to be held by someone else. */
|
|
22
|
+
onWait?: (holder: LockHolder) => void;
|
|
23
|
+
/** Called when a lock is reclaimed for being too old — the one stale case worth a warning. */
|
|
24
|
+
onStale?: (holder: LockHolder) => void;
|
|
25
|
+
}
|
|
26
|
+
export interface MachineLock {
|
|
27
|
+
/** Whether acquiring involved waiting for another holder at all. */
|
|
28
|
+
readonly waited: boolean;
|
|
29
|
+
/** Give it back. Idempotent; never removes a lock someone else has since taken. */
|
|
30
|
+
release(): void;
|
|
31
|
+
}
|
|
32
|
+
/** A poll, not a watch: cheap, portable, and a released lock is noticed within this. */
|
|
33
|
+
export declare const LOCK_POLL_MS = 500;
|
|
34
|
+
/** How long a waiter queues before giving up with the holder's name. */
|
|
35
|
+
export declare const LOCK_TIMEOUT_MS: number;
|
|
36
|
+
/** No holder legitimately keeps a lock this long; beyond it the record is treated as abandoned. */
|
|
37
|
+
export declare const LOCK_MAX_AGE_MS: number;
|
|
38
|
+
export declare function lockPath(name: string, baseDir?: string): string;
|
|
39
|
+
/** The recorded holder, or null for a file that is missing, empty or garbage. */
|
|
40
|
+
export declare function readLockHolder(file: string): LockHolder | null;
|
|
41
|
+
/** Whether the holder is one no longer worth waiting for, and why. */
|
|
42
|
+
export declare function staleReason(holder: LockHolder, deps: {
|
|
43
|
+
now: number;
|
|
44
|
+
isAlive: (pid: number) => boolean;
|
|
45
|
+
bootTime: number;
|
|
46
|
+
maxAgeMs: number;
|
|
47
|
+
}): 'dead' | 'pre-boot' | 'too-old' | null;
|
|
48
|
+
export declare function acquireMachineLock(name: string, info: {
|
|
49
|
+
root: string;
|
|
50
|
+
command: string;
|
|
51
|
+
}, deps?: MachineLockDeps): Promise<MachineLock>;
|
|
52
|
+
/** Hold `name` for the duration of `fn`, releasing however it ends. */
|
|
53
|
+
export declare function withMachineLock<T>(name: string, info: {
|
|
54
|
+
root: string;
|
|
55
|
+
command: string;
|
|
56
|
+
}, fn: (lock: MachineLock) => Promise<T>, deps?: MachineLockDeps): Promise<T>;
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lock shared by every `bitmagic` process on this machine — the one piece of cross-process
|
|
3
|
+
* coordination the CLI has.
|
|
4
|
+
*
|
|
5
|
+
* Two things need it. `bitmagic verify` measures the game's frame rate in a real browser, and two
|
|
6
|
+
* verifies at once on one laptop starve each other into a verdict about the machine rather than
|
|
7
|
+
* about the game; so verify runs one at a time, and the others wait (verify/lock.ts). The
|
|
8
|
+
* per-project port registry (project/dev-ports.ts) is a read-modify-write of one file that two
|
|
9
|
+
* `bitmagic dev`s starting in two folders at the same second must not both perform.
|
|
10
|
+
*
|
|
11
|
+
* It lives in `~/.bitmagic/locks/<name>.lock`, the directory both published lines already share
|
|
12
|
+
* for credentials, so a dev-line and a release-line install queue behind each other too — the fps
|
|
13
|
+
* problem is about the machine, not about which build is measuring.
|
|
14
|
+
*
|
|
15
|
+
* ── Why a file, and why this shape ────────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* No dependency, because this package deliberately keeps `dev` and `verify` offline and light, and
|
|
18
|
+
* the primitives are enough: `open(..., 'wx')` is atomic on every platform Node runs on (it is
|
|
19
|
+
* CREATE_NEW on Windows), so the file's EXISTENCE is the lock. The descriptor is closed the moment
|
|
20
|
+
* the body is written — Windows will not unlink a file another process holds open, and a lock that
|
|
21
|
+
* cannot be reclaimed after its owner dies is worse than no lock.
|
|
22
|
+
*
|
|
23
|
+
* The body names the holder, for two reasons: so a waiter can say WHO it is waiting for (a line
|
|
24
|
+
* that turns "verify hangs" into "verify is queued behind the other project"), and so a stale lock
|
|
25
|
+
* can be told from a live one. Stale means the pid is gone, or the record predates this boot (pids
|
|
26
|
+
* are reused after a restart), or it is simply too old — no verify holds it for half an hour.
|
|
27
|
+
*
|
|
28
|
+
* Reclaiming a stale lock is a rename, then an unlink of the renamed file. Two waiters that both
|
|
29
|
+
* decide "stale" would otherwise race: one unlinks, the other creates a fresh lock, and the first
|
|
30
|
+
* unlinks THAT. Rename is atomic, so exactly one of them moves the file aside; the other gets
|
|
31
|
+
* ENOENT and simply tries `wx` again.
|
|
32
|
+
*
|
|
33
|
+
* A holder interrupted with Ctrl-C leaves its file behind — Node's default SIGINT exit runs no
|
|
34
|
+
* `finally` — and that is fine: its pid is dead, so the next waiter reclaims it on the first poll.
|
|
35
|
+
*/
|
|
36
|
+
import * as fs from 'fs';
|
|
37
|
+
import * as os from 'os';
|
|
38
|
+
import * as path from 'path';
|
|
39
|
+
import { CliError } from './errors.js';
|
|
40
|
+
import { defaultBaseDir } from './config/credentials.js';
|
|
41
|
+
import { isProcessAlive } from './process-alive.js';
|
|
42
|
+
import { readJsonFile } from './atomic-json.js';
|
|
43
|
+
/** A poll, not a watch: cheap, portable, and a released lock is noticed within this. */
|
|
44
|
+
export const LOCK_POLL_MS = 500;
|
|
45
|
+
/** How long a waiter queues before giving up with the holder's name. */
|
|
46
|
+
export const LOCK_TIMEOUT_MS = 15 * 60 * 1000;
|
|
47
|
+
/** No holder legitimately keeps a lock this long; beyond it the record is treated as abandoned. */
|
|
48
|
+
export const LOCK_MAX_AGE_MS = 30 * 60 * 1000;
|
|
49
|
+
/**
|
|
50
|
+
* Slack on the boot-time rule. `os.uptime()` may or may not count time spent suspended, so a lock
|
|
51
|
+
* taken just before a laptop lid closed could otherwise read as "from before this boot".
|
|
52
|
+
*/
|
|
53
|
+
const BOOT_TOLERANCE_MS = 60 * 1000;
|
|
54
|
+
export function lockPath(name, baseDir = defaultBaseDir()) {
|
|
55
|
+
return path.join(baseDir, 'locks', `${name}.lock`);
|
|
56
|
+
}
|
|
57
|
+
function defaultBootTime() {
|
|
58
|
+
return Date.now() - os.uptime() * 1000;
|
|
59
|
+
}
|
|
60
|
+
function isHolder(value) {
|
|
61
|
+
if (typeof value !== 'object' || value === null)
|
|
62
|
+
return false;
|
|
63
|
+
const v = value;
|
|
64
|
+
return typeof v.pid === 'number' && typeof v.startedAt === 'number'
|
|
65
|
+
&& typeof v.root === 'string' && typeof v.command === 'string';
|
|
66
|
+
}
|
|
67
|
+
/** The recorded holder, or null for a file that is missing, empty or garbage. */
|
|
68
|
+
export function readLockHolder(file) {
|
|
69
|
+
const parsed = readJsonFile(file);
|
|
70
|
+
return isHolder(parsed) ? parsed : null;
|
|
71
|
+
}
|
|
72
|
+
/** Whether the holder is one no longer worth waiting for, and why. */
|
|
73
|
+
export function staleReason(holder, deps) {
|
|
74
|
+
if (!deps.isAlive(holder.pid))
|
|
75
|
+
return 'dead';
|
|
76
|
+
if (holder.startedAt < deps.bootTime - BOOT_TOLERANCE_MS)
|
|
77
|
+
return 'pre-boot';
|
|
78
|
+
if (deps.now - holder.startedAt > deps.maxAgeMs)
|
|
79
|
+
return 'too-old';
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
/** Move a stale lock aside and delete it. False when another waiter got there first. */
|
|
83
|
+
function reclaim(file, pid, now) {
|
|
84
|
+
const aside = `${file}.stale.${pid}.${now}`;
|
|
85
|
+
try {
|
|
86
|
+
fs.renameSync(file, aside);
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
fs.rmSync(aside, { force: true });
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
/** Try once to create the lock. True when this process now holds it. */
|
|
95
|
+
function tryCreate(file, holder) {
|
|
96
|
+
let fd;
|
|
97
|
+
try {
|
|
98
|
+
fd = fs.openSync(file, 'wx');
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'EEXIST')
|
|
102
|
+
return false;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
fs.writeSync(fd, `${JSON.stringify(holder)}\n`);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
fs.closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* How long an unreadable lock file is given to become readable.
|
|
115
|
+
*
|
|
116
|
+
* Between `open('wx')` and the write that follows it, the file exists and is empty; a waiter
|
|
117
|
+
* polling at that instant must not mistake a lock being taken for a corrupt one and delete it.
|
|
118
|
+
* Beyond this, an empty file is a crashed writer's, and is reclaimed.
|
|
119
|
+
*/
|
|
120
|
+
const UNREADABLE_GRACE_MS = 5_000;
|
|
121
|
+
function fileAgeMs(file, now) {
|
|
122
|
+
try {
|
|
123
|
+
return now - fs.statSync(file).mtimeMs;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
export async function acquireMachineLock(name, info, deps = {}) {
|
|
130
|
+
const now = deps.now ?? Date.now;
|
|
131
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
132
|
+
const isAlive = deps.isAlive ?? isProcessAlive;
|
|
133
|
+
const bootTime = deps.bootTime ?? defaultBootTime;
|
|
134
|
+
const pid = deps.pid ?? process.pid;
|
|
135
|
+
const pollMs = deps.pollMs ?? LOCK_POLL_MS;
|
|
136
|
+
const timeoutMs = deps.timeoutMs ?? LOCK_TIMEOUT_MS;
|
|
137
|
+
const maxAgeMs = deps.maxAgeMs ?? LOCK_MAX_AGE_MS;
|
|
138
|
+
const file = lockPath(name, deps.baseDir);
|
|
139
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
140
|
+
const startedWaitingAt = now();
|
|
141
|
+
let waited = false;
|
|
142
|
+
let lastHolder = null;
|
|
143
|
+
for (;;) {
|
|
144
|
+
const holder = { pid, startedAt: now(), root: info.root, command: info.command };
|
|
145
|
+
if (tryCreate(file, holder)) {
|
|
146
|
+
return makeLock(file, pid, waited);
|
|
147
|
+
}
|
|
148
|
+
const current = readLockHolder(file);
|
|
149
|
+
if (current === null) {
|
|
150
|
+
// Empty or garbage. Being written right now, or left by a writer that died mid-write.
|
|
151
|
+
const age = fileAgeMs(file, now());
|
|
152
|
+
if (age === null)
|
|
153
|
+
continue; // gone between the failed create and the stat: try again.
|
|
154
|
+
if (age > UNREADABLE_GRACE_MS) {
|
|
155
|
+
reclaim(file, pid, now());
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const reason = staleReason(current, { now: now(), isAlive, bootTime: bootTime(), maxAgeMs });
|
|
161
|
+
if (reason !== null) {
|
|
162
|
+
if (reason === 'too-old')
|
|
163
|
+
deps.onStale?.(current);
|
|
164
|
+
reclaim(file, pid, now());
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (!waited || lastHolder === null || lastHolder.pid !== current.pid) {
|
|
168
|
+
// Once per holder: a queue that moves — A finishes, B takes over — is worth a second line,
|
|
169
|
+
// a holder that is merely slow is not.
|
|
170
|
+
deps.onWait?.(current);
|
|
171
|
+
}
|
|
172
|
+
lastHolder = current;
|
|
173
|
+
}
|
|
174
|
+
waited = true;
|
|
175
|
+
if (now() - startedWaitingAt >= timeoutMs) {
|
|
176
|
+
const who = lastHolder === null
|
|
177
|
+
? 'an unreadable lock file'
|
|
178
|
+
: `\`bitmagic ${lastHolder.command}\` (pid ${lastHolder.pid}) in ${lastHolder.root}`;
|
|
179
|
+
throw new CliError(`Gave up waiting for ${who} after ${Math.round(timeoutMs / 60_000)} minutes. `
|
|
180
|
+
+ `If nothing is actually running, delete ${file} and try again.`);
|
|
181
|
+
}
|
|
182
|
+
await sleep(pollMs);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
function makeLock(file, pid, waited) {
|
|
186
|
+
let released = false;
|
|
187
|
+
const release = () => {
|
|
188
|
+
if (released)
|
|
189
|
+
return;
|
|
190
|
+
released = true;
|
|
191
|
+
process.off('exit', release);
|
|
192
|
+
// Only ours. A lock this process lost to the staleness rules — a laptop asleep for an hour —
|
|
193
|
+
// may by now belong to someone else, and removing theirs would let a third run in beside them.
|
|
194
|
+
const current = readLockHolder(file);
|
|
195
|
+
if (current !== null && current.pid !== pid)
|
|
196
|
+
return;
|
|
197
|
+
fs.rmSync(file, { force: true });
|
|
198
|
+
};
|
|
199
|
+
// Best-effort on a normal exit; a signal death is covered by the dead-pid rule instead.
|
|
200
|
+
process.on('exit', release);
|
|
201
|
+
return { waited, release };
|
|
202
|
+
}
|
|
203
|
+
/** Hold `name` for the duration of `fn`, releasing however it ends. */
|
|
204
|
+
export async function withMachineLock(name, info, fn, deps = {}) {
|
|
205
|
+
const lock = await acquireMachineLock(name, info, deps);
|
|
206
|
+
try {
|
|
207
|
+
return await fn(lock);
|
|
208
|
+
}
|
|
209
|
+
finally {
|
|
210
|
+
lock.release();
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
//# sourceMappingURL=machine-lock.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"machine-lock.js","sourceRoot":"","sources":["../src/machine-lock.ts"],"names":[],"mappings":"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"}
|
package/dist/port-lane.d.ts
CHANGED
|
@@ -45,6 +45,31 @@ export declare const RELEASE_LANE: PortLane;
|
|
|
45
45
|
export declare const DEV_LANE: PortLane;
|
|
46
46
|
/** The lane belonging to a published line. */
|
|
47
47
|
export declare function portLaneFor(tag: UpdateTag): PortLane;
|
|
48
|
+
/**
|
|
49
|
+
* How many projects a lane holds at fixed addresses — see `slotPorts`.
|
|
50
|
+
*
|
|
51
|
+
* Nine, because a slot is ten ports wide and the lane's defaults sit at x10: slots 0-8 span
|
|
52
|
+
* x10-x92, and a tenth would run past the lane's end. Nine games being developed at once on one
|
|
53
|
+
* machine is a lot; a tenth falls back to the scan, and says so.
|
|
54
|
+
*/
|
|
55
|
+
export declare const DEV_PORT_SLOTS = 9;
|
|
56
|
+
/**
|
|
57
|
+
* The three ports a project in `slot` of `lane` serves on.
|
|
58
|
+
*
|
|
59
|
+
* `bitmagic dev` used to start every project at the lane's defaults and drift to a random port in
|
|
60
|
+
* the lane when they were busy — so which project answered on 3011 depended on start order,
|
|
61
|
+
* bookmarks went stale, and a `bitmagic reload` falling back to the default could reach the wrong
|
|
62
|
+
* project. A slot is the same numbers, one tens-digit further along per project: slot 0 is the
|
|
63
|
+
* defaults every doc names (3010/3011/3012), slot 1 is 3020/3021/3022, and so on. The hundreds
|
|
64
|
+
* digit still says which install, the tens digit now says which project, and the units digit still
|
|
65
|
+
* reads game / shell / phone. Which slot a project holds is recorded per machine in
|
|
66
|
+
* `~/.bitmagic/dev-ports.json` (project/dev-ports.ts).
|
|
67
|
+
*/
|
|
68
|
+
export declare function slotPorts(lane: PortLane, slot: number): {
|
|
69
|
+
gamePort: number;
|
|
70
|
+
editorPort: number;
|
|
71
|
+
mobilePort: number;
|
|
72
|
+
};
|
|
48
73
|
/**
|
|
49
74
|
* This install's lane.
|
|
50
75
|
*
|
|
@@ -57,3 +82,5 @@ export declare function portLaneFor(tag: UpdateTag): PortLane;
|
|
|
57
82
|
* before this existed. Failing closed here keeps the numbers the docs promise.
|
|
58
83
|
*/
|
|
59
84
|
export declare function currentPortLane(version?: string): PortLane;
|
|
85
|
+
/** Which published line this install is on — the key the port registry files a project under. */
|
|
86
|
+
export declare function currentLineTag(version?: string): UpdateTag;
|