@centralu/win32-x64 0.1.0-beta.45 → 0.1.0-beta.47
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.
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
// Centralu's remote installer, step 2 (docs/plans/remote-hub.md §10.2). The hub sends this file over
|
|
2
2
|
// ssh and runs it on the pinned Node that step 1 placed; it never runs on the hub. It imports Node's
|
|
3
3
|
// own modules only, and reads one argument: the base64 of a JSON object the hub wrote
|
|
4
|
-
// ({ version, node, platform, hub, packages: [{ name, tarball, integrity }], activate? }), or
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
// and the copy of the store taken before it first runs ({ action: 'store-backup' | 'store-restore' }).
|
|
4
|
+
// ({ version, node, platform, hub, packages: [{ name, tarball, integrity }], activate? }), or a dev build of the host
|
|
5
|
+
// from a git commit in a working copy there ({ action: 'dev', repo, commit, … }; `pnpm remote-dev`, #1036). What an
|
|
6
|
+
// update does after placing (stop, switch, start, check, put back, prune) is the new version's own `serve --switch`.
|
|
8
7
|
//
|
|
9
8
|
// It prints one line for the hub to read, `CENTRALU-INSTALL done <json>` or
|
|
10
9
|
// `CENTRALU-INSTALL fail <code> <detail>`, and nothing that matters on its exit code (exit codes do
|
|
@@ -14,6 +13,7 @@ import { createHash, randomBytes } from 'node:crypto'
|
|
|
14
13
|
import { chmodSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
15
14
|
import { homedir } from 'node:os'
|
|
16
15
|
import { basename, isAbsolute, join, win32 } from 'node:path'
|
|
16
|
+
import { createRequire } from 'node:module'
|
|
17
17
|
import { pathToFileURL } from 'node:url'
|
|
18
18
|
|
|
19
19
|
const WORD = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/
|
|
@@ -37,7 +37,6 @@ export function remoteLayout(dataDir) {
|
|
|
37
37
|
node: join(root, 'node'),
|
|
38
38
|
bin: join(root, 'bin'),
|
|
39
39
|
lock: join(root, 'install.lock'),
|
|
40
|
-
storeBackup: join(root, 'store-backup'),
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
@@ -71,53 +70,6 @@ export function switchTo(l, next) {
|
|
|
71
70
|
return { current: next, previous: cur ?? readPointer(l.previous) }
|
|
72
71
|
}
|
|
73
72
|
|
|
74
|
-
/**
|
|
75
|
-
* Removes every version that is neither `current` nor `previous`, and every Node neither uses. A
|
|
76
|
-
* folder that cannot go (a Windows program still running from it) stays and is named, never fatal
|
|
77
|
-
*/
|
|
78
|
-
export function prune(l) {
|
|
79
|
-
const keep = [readPointer(l.current), readPointer(l.previous)].filter(Boolean)
|
|
80
|
-
const removed = []
|
|
81
|
-
const left = []
|
|
82
|
-
const sweep = (dir, kept) => {
|
|
83
|
-
let names = []
|
|
84
|
-
try {
|
|
85
|
-
names = readdirSync(dir)
|
|
86
|
-
} catch {
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
for (const name of names) {
|
|
90
|
-
if (kept.has(name) || name.startsWith('.')) continue
|
|
91
|
-
try {
|
|
92
|
-
rmSync(join(dir, name), { recursive: true, force: true })
|
|
93
|
-
removed.push(name)
|
|
94
|
-
} catch {
|
|
95
|
-
left.push(name)
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
sweep(l.versions, new Set(keep.map((p) => p.version)))
|
|
100
|
-
// What `placeVersion` moved aside and could not remove then
|
|
101
|
-
try {
|
|
102
|
-
for (const name of readdirSync(l.root).filter((n) => n.startsWith('.replaced-'))) {
|
|
103
|
-
try {
|
|
104
|
-
rmSync(join(l.root, name), { recursive: true, force: true })
|
|
105
|
-
} catch {
|
|
106
|
-
left.push(name)
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
} catch {}
|
|
110
|
-
sweep(l.node, new Set(keep.map((p) => `v${p.node}`)))
|
|
111
|
-
// The store copies of versions that are gone (#1036): nothing can start them to need the copy any more. Not reported:
|
|
112
|
-
// `removed` names versions and Nodes
|
|
113
|
-
try {
|
|
114
|
-
for (const name of readdirSync(l.storeBackup).filter((n) => !keep.some((p) => p.version === n))) rmSync(join(l.storeBackup, name), { recursive: true, force: true })
|
|
115
|
-
} catch {
|
|
116
|
-
// none, or one still open; the next prune tries again
|
|
117
|
-
}
|
|
118
|
-
return { removed, left }
|
|
119
|
-
}
|
|
120
|
-
|
|
121
73
|
/** A lock folder with no pid in it, younger than this, is held: its installer has made it and not yet written the pid */
|
|
122
74
|
const LOCK_YOUNG_MS = 60_000
|
|
123
75
|
|
|
@@ -312,8 +264,8 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
|
|
|
312
264
|
return { installed: { version: p.version, node: p.node }, current: readPointer(l.current), previous: readPointer(l.previous), removed: [], left: [] }
|
|
313
265
|
}
|
|
314
266
|
const launcher = await ensureManagedLauncher(l, dir, p.version, platform)
|
|
315
|
-
|
|
316
|
-
return { ...
|
|
267
|
+
// Nothing is pruned here: the switch that replaces a version prunes (the launcher's serve --switch, plan §10.5a)
|
|
268
|
+
return { ...switchTo(l, { version: p.version, node: p.node }), removed: [], left: [], launcher }
|
|
317
269
|
} finally {
|
|
318
270
|
unlock()
|
|
319
271
|
}
|
|
@@ -336,121 +288,6 @@ async function ensureManagedLauncher(l, dir, version, platform) {
|
|
|
336
288
|
return launcher
|
|
337
289
|
}
|
|
338
290
|
|
|
339
|
-
const pointerOf = (v) => (v && WORD.test(v.version) && WORD.test(v.node) ? { version: v.version, node: v.node } : null)
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* Writes both pointers as given (plan §10.5 steps 3 and 5, and rollback): `previous` first, then
|
|
343
|
-
* `current`, each by rename; a null `previous` removes that file. `current` must name a version and a
|
|
344
|
-
* Node that are both there. A null `current` puts the machine back to having no managed version: the
|
|
345
|
-
* pointers and the launcher go, so the lookup finds the Centralu it ran before (an npm install).
|
|
346
|
-
* Nothing is removed from `versions/` or `node/`
|
|
347
|
-
*/
|
|
348
|
-
export async function setPointers(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform } = {}) {
|
|
349
|
-
const l = remoteLayout(dataDir)
|
|
350
|
-
mkdirSync(l.root, { recursive: true })
|
|
351
|
-
const unlock = lock(l)
|
|
352
|
-
try {
|
|
353
|
-
const current = pointerOf(p.current)
|
|
354
|
-
const previous = pointerOf(p.previous)
|
|
355
|
-
if (!current) {
|
|
356
|
-
for (const f of [l.previous, l.current, launcherPath(l, platform)]) rmSync(f, { force: true })
|
|
357
|
-
return { current: null, previous: null }
|
|
358
|
-
}
|
|
359
|
-
const dir = join(l.versions, current.version)
|
|
360
|
-
// Both must be there: a `previous` naming nothing would offer a rollback to a version that is gone
|
|
361
|
-
for (const v of [current, previous].filter(Boolean)) {
|
|
362
|
-
if (!existsSync(join(l.versions, v.version, 'install.json')) || !existsSync(join(l.node, `v${v.node}`))) throw new Refusal('missing', `${v.version} ${v.node}`)
|
|
363
|
-
}
|
|
364
|
-
await ensureManagedLauncher(l, dir, current.version, platform)
|
|
365
|
-
if (previous) writeAtomic(l.previous, `${previous.version} ${previous.node}\n`)
|
|
366
|
-
else rmSync(l.previous, { force: true })
|
|
367
|
-
writeAtomic(l.current, `${current.version} ${current.node}\n`)
|
|
368
|
-
return { current, previous }
|
|
369
|
-
} finally {
|
|
370
|
-
unlock()
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/** Plan §10.5 step 6, on its own: what is neither `current` nor `previous` goes */
|
|
375
|
-
export function pruneNow({ dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu') } = {}) {
|
|
376
|
-
const l = remoteLayout(dataDir)
|
|
377
|
-
mkdirSync(l.root, { recursive: true })
|
|
378
|
-
const unlock = lock(l)
|
|
379
|
-
try {
|
|
380
|
-
return { current: readPointer(l.current), previous: readPointer(l.previous), ...prune(l) }
|
|
381
|
-
} finally {
|
|
382
|
-
unlock()
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/** The files of the host's store in the data folder: the database and SQLite's write-ahead log beside it */
|
|
387
|
-
const STORE_FILES = ['store.db', 'store.db-wal', 'store.db-shm']
|
|
388
|
-
|
|
389
|
-
/**
|
|
390
|
-
* A copy of the store taken while no host runs, before a dev build first opens it (#1036): a dev build may migrate the
|
|
391
|
-
* store past what the version before it reads (a step that raises `min_reader_version`), and that version would then
|
|
392
|
-
* refuse to start (`StoreTooNewError`), leaving the machine with no host. The copy is kept per `owner`, the version
|
|
393
|
-
* that left the store so (`store-backup/<owner>/`), replaced whole, and `before` names the build it was taken before.
|
|
394
|
-
* A copy goes with its version (`prune`)
|
|
395
|
-
*/
|
|
396
|
-
export function storeBackup(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu') } = {}) {
|
|
397
|
-
if (!WORD.test(String(p.owner)) || !WORD.test(String(p.before))) throw new Refusal('params', 'store backup')
|
|
398
|
-
const l = remoteLayout(dataDir)
|
|
399
|
-
mkdirSync(l.storeBackup, { recursive: true })
|
|
400
|
-
const unlock = lock(l)
|
|
401
|
-
try {
|
|
402
|
-
const partial = join(l.storeBackup, `.partial-${process.pid}`)
|
|
403
|
-
rmSync(partial, { recursive: true, force: true })
|
|
404
|
-
try {
|
|
405
|
-
mkdirSync(partial, { recursive: true })
|
|
406
|
-
const files = STORE_FILES.filter((f) => existsSync(join(dataDir, f)))
|
|
407
|
-
for (const f of files) cpSync(join(dataDir, f), join(partial, f))
|
|
408
|
-
writeFileSync(join(partial, 'backup.json'), JSON.stringify({ owner: p.owner, before: p.before, files, at: new Date().toISOString() }, null, 2) + '\n')
|
|
409
|
-
rmSync(join(l.storeBackup, p.owner), { recursive: true, force: true })
|
|
410
|
-
renameSync(partial, join(l.storeBackup, p.owner))
|
|
411
|
-
return { saved: files }
|
|
412
|
-
} catch (err) {
|
|
413
|
-
throw err instanceof Refusal ? err : new Refusal('store_backup', err.message)
|
|
414
|
-
} finally {
|
|
415
|
-
rmSync(partial, { recursive: true, force: true })
|
|
416
|
-
}
|
|
417
|
-
} finally {
|
|
418
|
-
unlock()
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* Puts back the copy `storeBackup` took for `owner` (the version about to start), with no host running; without one,
|
|
424
|
-
* touches nothing and says so. A write-ahead log the copy did not have is removed, so SQLite does not replay the dev
|
|
425
|
-
* build's changes onto the restored database. What the dev build wrote to the store since is gone; nothing else in the
|
|
426
|
-
* data folder is touched. The copy stays, so a second attempt finds it again
|
|
427
|
-
*/
|
|
428
|
-
export function storeRestore(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu') } = {}) {
|
|
429
|
-
if (!WORD.test(String(p.owner))) throw new Refusal('params', 'store restore')
|
|
430
|
-
const l = remoteLayout(dataDir)
|
|
431
|
-
mkdirSync(l.root, { recursive: true })
|
|
432
|
-
const unlock = lock(l)
|
|
433
|
-
try {
|
|
434
|
-
const dir = join(l.storeBackup, p.owner)
|
|
435
|
-
let meta
|
|
436
|
-
try {
|
|
437
|
-
meta = JSON.parse(readFileSync(join(dir, 'backup.json'), 'utf8'))
|
|
438
|
-
} catch {
|
|
439
|
-
return { restored: false }
|
|
440
|
-
}
|
|
441
|
-
if (meta.owner !== p.owner || !Array.isArray(meta.files)) return { restored: false }
|
|
442
|
-
try {
|
|
443
|
-
for (const f of STORE_FILES) rmSync(join(dataDir, f), { force: true })
|
|
444
|
-
for (const f of meta.files.filter((f) => STORE_FILES.includes(f))) cpSync(join(dir, f), join(dataDir, f))
|
|
445
|
-
} catch (err) {
|
|
446
|
-
throw new Refusal('store_restore', err.message)
|
|
447
|
-
}
|
|
448
|
-
return { restored: true, before: meta.before ?? null, at: meta.at ?? null }
|
|
449
|
-
} finally {
|
|
450
|
-
unlock()
|
|
451
|
-
}
|
|
452
|
-
}
|
|
453
|
-
|
|
454
291
|
/** `a` at least `b`, both plain `x.y.z` (a prerelease or build suffix is ignored) */
|
|
455
292
|
function atLeast(a, b) {
|
|
456
293
|
const n = (v) => String(v).replace(/^v/, '').split(/[-+]/)[0].split('.').map((x) => Number(x) || 0)
|
|
@@ -499,8 +336,8 @@ function lastSaid(...texts) {
|
|
|
499
336
|
|
|
500
337
|
/**
|
|
501
338
|
* A dev build of the host from a git commit (`pnpm remote-dev`, #1036; docs/plans/remote-hub.md §10.11), placed beside
|
|
502
|
-
* what is there as `<base>.local.N` with nothing switched: the
|
|
503
|
-
* (update.ts `
|
|
339
|
+
* what is there as `<base>.local.N` with nothing switched: the dev build's own `serve --switch` stops, switches, starts
|
|
340
|
+
* and checks it (update.ts `handOver`).
|
|
504
341
|
*
|
|
505
342
|
* Checked before anything is built: the working copy's HEAD is exactly `p.commit`, it has no changes unless
|
|
506
343
|
* `p.allowDirty`, it speaks `p.protocol` (the protocol the hub links on), and this Node (the pinned one `current` runs
|
|
@@ -532,7 +369,12 @@ export function devBuild(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(
|
|
|
532
369
|
if (protocol !== p.protocol) throw new Refusal('dev_protocol', `${protocol} ${p.protocol}`)
|
|
533
370
|
const floor = /(\d+\.\d+\.\d+)/.exec(String(shim.engines?.node ?? ''))?.[1]
|
|
534
371
|
if (floor && !atLeast(process.versions.node, floor)) throw new Refusal('dev_node', `${process.versions.node} ${floor}`)
|
|
535
|
-
|
|
372
|
+
// Found the way the bundler finds it (its own folder, then up), so a hoisted install passes too
|
|
373
|
+
try {
|
|
374
|
+
createRequire(join(repo, 'packages', 'agent-host', 'package.json')).resolve('esbuild')
|
|
375
|
+
} catch {
|
|
376
|
+
throw new Refusal('dev_deps', repo)
|
|
377
|
+
}
|
|
536
378
|
// The next number after every dev build of this release still there, and any a pointer names
|
|
537
379
|
const local = new RegExp(`^${base.replace(/[.+-]/g, '\\$&')}\\.local\\.([1-9]\\d*)$`)
|
|
538
380
|
let names = []
|
|
@@ -584,18 +426,7 @@ export function devBuild(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(
|
|
|
584
426
|
async function main() {
|
|
585
427
|
try {
|
|
586
428
|
const p = JSON.parse(Buffer.from(process.argv[2] ?? '', 'base64').toString('utf8'))
|
|
587
|
-
const r =
|
|
588
|
-
p.action === 'pointers'
|
|
589
|
-
? await setPointers(p)
|
|
590
|
-
: p.action === 'prune'
|
|
591
|
-
? pruneNow()
|
|
592
|
-
: p.action === 'dev'
|
|
593
|
-
? devBuild(p)
|
|
594
|
-
: p.action === 'store-backup'
|
|
595
|
-
? storeBackup(p)
|
|
596
|
-
: p.action === 'store-restore'
|
|
597
|
-
? storeRestore(p)
|
|
598
|
-
: await install(p)
|
|
429
|
+
const r = p.action === 'dev' ? devBuild(p) : await install(p)
|
|
599
430
|
process.stdout.write(`CENTRALU-INSTALL done ${JSON.stringify(r)}\n`)
|
|
600
431
|
} catch (err) {
|
|
601
432
|
const code = err instanceof Refusal ? err.code : 'error'
|