@centralu/darwin-arm64 0.1.0-beta.14 → 0.1.0-beta.15
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/Centralu.app/Contents/Info.plist +2 -2
- package/Centralu.app/Contents/MacOS/centralu +0 -0
- package/Centralu.app/Contents/MacOS/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/centralu-keeper +0 -0
- package/Centralu.app/Contents/Resources/content/content-manifest.json +8 -8
- package/Centralu.app/Contents/Resources/content/content-manifest.json.sig +1 -1
- package/Centralu.app/Contents/Resources/content/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/content/host/main.mjs +364 -55
- package/Centralu.app/Contents/Resources/content/host/remote-install.mjs +105 -19
- package/Centralu.app/Contents/Resources/resources/host/bundle-info.json +2 -2
- package/Centralu.app/Contents/Resources/resources/host/main.mjs +364 -55
- package/Centralu.app/Contents/Resources/resources/host/remote-install.mjs +105 -19
- package/Centralu.app/Contents/_CodeSignature/CodeResources +20 -20
- package/package.json +1 -1
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
// not survive the way back from Windows; tunnel.ts). The sentences for the person are the hub's.
|
|
10
10
|
import { spawnSync } from 'node:child_process'
|
|
11
11
|
import { createHash, randomBytes } from 'node:crypto'
|
|
12
|
-
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
|
12
|
+
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
13
13
|
import { homedir } from 'node:os'
|
|
14
|
-
import { join } from 'node:path'
|
|
14
|
+
import { basename, join } from 'node:path'
|
|
15
15
|
import { pathToFileURL } from 'node:url'
|
|
16
16
|
|
|
17
17
|
const WORD = /^[0-9A-Za-z][0-9A-Za-z.+-]{0,63}$/
|
|
@@ -86,27 +86,79 @@ export function prune(l) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
sweep(l.versions, new Set(keep.map((p) => p.version)))
|
|
89
|
+
// What `placeVersion` moved aside and could not remove then
|
|
90
|
+
try {
|
|
91
|
+
for (const name of readdirSync(l.root).filter((n) => n.startsWith('.replaced-'))) {
|
|
92
|
+
try {
|
|
93
|
+
rmSync(join(l.root, name), { recursive: true, force: true })
|
|
94
|
+
} catch {
|
|
95
|
+
left.push(name)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {}
|
|
89
99
|
sweep(l.node, new Set(keep.map((p) => `v${p.node}`)))
|
|
90
100
|
return { removed, left }
|
|
91
101
|
}
|
|
92
102
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
103
|
+
/** A lock folder with no pid in it, younger than this, is held: its installer has made it and not yet written the pid */
|
|
104
|
+
const LOCK_YOUNG_MS = 60_000
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* One installer at a time per data folder; a lock whose process is gone is taken over.
|
|
108
|
+
*
|
|
109
|
+
* Taking it is `mkdir`, which only one installer can do; the pid follows. A lock found with no pid is
|
|
110
|
+
* held while it is young (another installer between the two, which reading it as pid 0 once took
|
|
111
|
+
* over), and stale only after `LOCK_YOUNG_MS` (an installer that died between them). A stale lock is
|
|
112
|
+
* taken over by moving it aside first, so of two installers that both found it stale only one removes
|
|
113
|
+
* it, and one that moved a lock just replaced by a live one puts it back. Unlock removes the lock only
|
|
114
|
+
* while it is still this run's own (its `token`).
|
|
115
|
+
*/
|
|
116
|
+
export function lock(l) {
|
|
117
|
+
const token = `${process.pid}-${randomBytes(8).toString('hex')}`
|
|
118
|
+
for (let i = 0; i < 3; i++) {
|
|
96
119
|
try {
|
|
97
120
|
mkdirSync(l.lock)
|
|
98
121
|
writeFileSync(join(l.lock, 'pid'), String(process.pid))
|
|
99
|
-
|
|
122
|
+
writeFileSync(join(l.lock, 'token'), token)
|
|
123
|
+
return () => {
|
|
124
|
+
if (readFileSafe(join(l.lock, 'token')) === token) rmSync(l.lock, { recursive: true, force: true })
|
|
125
|
+
}
|
|
100
126
|
} catch (err) {
|
|
101
127
|
if (err.code !== 'EEXIST') throw err
|
|
102
|
-
const pid = Number(readFileSafe(join(l.lock, 'pid')))
|
|
103
|
-
if (pid > 0 && alive(pid)) throw new Refusal('busy', String(pid))
|
|
104
|
-
rmSync(l.lock, { recursive: true, force: true })
|
|
105
128
|
}
|
|
129
|
+
const held = readFileSafe(join(l.lock, 'pid'))
|
|
130
|
+
if (!lockIsStale(l.lock, held)) throw new Refusal('busy', held.trim())
|
|
131
|
+
const aside = `${l.lock}.${token}.stale`
|
|
132
|
+
try {
|
|
133
|
+
renameSync(l.lock, aside)
|
|
134
|
+
} catch {
|
|
135
|
+
continue // gone already, or another installer moved it first
|
|
136
|
+
}
|
|
137
|
+
const moved = readFileSafe(join(aside, 'pid'))
|
|
138
|
+
if (moved !== held) {
|
|
139
|
+
// Replaced by a live lock between reading it and moving it: not ours to remove
|
|
140
|
+
try {
|
|
141
|
+
renameSync(aside, l.lock)
|
|
142
|
+
} catch {
|
|
143
|
+
// Someone holds a newer one; this one stays aside, named, and harmless
|
|
144
|
+
}
|
|
145
|
+
throw new Refusal('busy', moved.trim())
|
|
146
|
+
}
|
|
147
|
+
rmSync(aside, { recursive: true, force: true })
|
|
106
148
|
}
|
|
107
149
|
throw new Refusal('busy')
|
|
108
150
|
}
|
|
109
151
|
|
|
152
|
+
function lockIsStale(dir, pidText) {
|
|
153
|
+
const pid = Number(pidText.trim())
|
|
154
|
+
if (pidText.trim() && pid > 0) return !alive(pid)
|
|
155
|
+
try {
|
|
156
|
+
return Date.now() - statSync(dir).mtimeMs > LOCK_YOUNG_MS
|
|
157
|
+
} catch {
|
|
158
|
+
return true // gone: the next turn takes the lock
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
110
162
|
function readFileSafe(file) {
|
|
111
163
|
try {
|
|
112
164
|
return readFileSync(file, 'utf8')
|
|
@@ -144,10 +196,17 @@ export async function fetchChecked(pkg) {
|
|
|
144
196
|
return bytes
|
|
145
197
|
}
|
|
146
198
|
|
|
147
|
-
/**
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
199
|
+
/**
|
|
200
|
+
* Which package brings the host, where the host sits in it, and what of it a remote never runs (plan
|
|
201
|
+
* S6). The host-only package (`@centralu/host-<platform>`) is the host and nothing else; a version
|
|
202
|
+
* published before it existed comes as its platform package, whose window is thrown away
|
|
203
|
+
*/
|
|
204
|
+
function hostParts(platform, packages) {
|
|
205
|
+
const hostOnly = `@centralu/host-${platform}`
|
|
206
|
+
if (packages.some((pkg) => pkg.name === hostOnly)) return { pkg: hostOnly, host: join('host', 'main.mjs'), drop: [] }
|
|
207
|
+
const pkg = `@centralu/${platform}`
|
|
208
|
+
if (platform === 'win32-x64') return { pkg, host: join('Centralu', 'resources', 'host', 'main.mjs'), drop: [join('Centralu', 'centralu.exe')] }
|
|
209
|
+
return { pkg, host: join('host', 'main.mjs'), drop: ['Centralu.AppImage'] }
|
|
151
210
|
}
|
|
152
211
|
|
|
153
212
|
function sameInstall(dir, p) {
|
|
@@ -159,6 +218,32 @@ function sameInstall(dir, p) {
|
|
|
159
218
|
}
|
|
160
219
|
}
|
|
161
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Puts the unpacked `partial` folder in place as `dir`. A `dir` already there (the same version
|
|
223
|
+
* installed again with other bytes, possibly the one `current` runs) is moved aside first and removed
|
|
224
|
+
* only once the new one is in place, and put back if that fails: removing it first would leave
|
|
225
|
+
* `current` naming nothing after a failed rename, and on Windows a removal can stop halfway on the
|
|
226
|
+
* files of a host running from it (moving that folder is allowed, plan §10.5). What cannot be removed
|
|
227
|
+
* stays aside under a dot name, and the next prune tries again
|
|
228
|
+
*/
|
|
229
|
+
export function placeVersion(l, partial, dir) {
|
|
230
|
+
const aside = existsSync(dir) ? join(l.root, `.replaced-${basename(dir)}-${process.pid}-${randomBytes(4).toString('hex')}`) : null
|
|
231
|
+
if (aside) renameSync(dir, aside)
|
|
232
|
+
try {
|
|
233
|
+
renameSync(partial, dir)
|
|
234
|
+
} catch (err) {
|
|
235
|
+
if (aside) renameSync(aside, dir)
|
|
236
|
+
throw err
|
|
237
|
+
}
|
|
238
|
+
if (aside) {
|
|
239
|
+
try {
|
|
240
|
+
rmSync(aside, { recursive: true, force: true })
|
|
241
|
+
} catch {
|
|
242
|
+
// A Windows program still running from it; harmless where it is
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
162
247
|
/** Installs `p.version` beside what is there, switches `current` to it, and keeps `previous` */
|
|
163
248
|
export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(homedir(), '.centralu'), platform = process.platform } = {}) {
|
|
164
249
|
if (!WORD.test(p.version) || !WORD.test(p.node)) throw new Refusal('params', 'version')
|
|
@@ -167,7 +252,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
|
|
|
167
252
|
const unlock = lock(l)
|
|
168
253
|
try {
|
|
169
254
|
const dir = join(l.versions, p.version)
|
|
170
|
-
const parts =
|
|
255
|
+
const parts = hostParts(p.platform, p.packages)
|
|
171
256
|
if (!sameInstall(dir, p)) {
|
|
172
257
|
// Everything lands in a folder of its own first; the version's folder appears by one rename
|
|
173
258
|
const partial = join(l.root, `.partial-${p.version}-${process.pid}`)
|
|
@@ -183,7 +268,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
|
|
|
183
268
|
if (r.status !== 0) throw new Refusal('unpack', `${pkg.name}: ${(r.stderr || r.error?.message || '').trim().split('\n').at(-1)}`)
|
|
184
269
|
rmSync(file, { force: true })
|
|
185
270
|
}
|
|
186
|
-
const pkgDir = join(partial, 'node_modules', '
|
|
271
|
+
const pkgDir = join(partial, 'node_modules', ...parts.pkg.split('/'))
|
|
187
272
|
if (!existsSync(join(pkgDir, parts.host))) throw new Refusal('no_host', p.platform)
|
|
188
273
|
for (const d of parts.drop) rmSync(join(pkgDir, d), { force: true })
|
|
189
274
|
writeFileSync(
|
|
@@ -191,8 +276,7 @@ export async function install(p, { dataDir = process.env.CC_DATA_DIR || join(hom
|
|
|
191
276
|
JSON.stringify({ version: p.version, node: p.node, platform: p.platform, packages: p.packages.map(({ name, integrity }) => ({ name, integrity })), installedAt: new Date().toISOString(), by: p.hub ?? null }, null, 2) + '\n',
|
|
192
277
|
)
|
|
193
278
|
mkdirSync(l.versions, { recursive: true })
|
|
194
|
-
|
|
195
|
-
renameSync(partial, dir)
|
|
279
|
+
placeVersion(l, partial, dir)
|
|
196
280
|
} finally {
|
|
197
281
|
rmSync(partial, { recursive: true, force: true })
|
|
198
282
|
}
|
|
@@ -249,8 +333,10 @@ export async function setPointers(p, { dataDir = process.env.CC_DATA_DIR || join
|
|
|
249
333
|
return { current: null, previous: null }
|
|
250
334
|
}
|
|
251
335
|
const dir = join(l.versions, current.version)
|
|
252
|
-
|
|
253
|
-
|
|
336
|
+
// Both must be there: a `previous` naming nothing would offer a rollback to a version that is gone
|
|
337
|
+
for (const v of [current, previous].filter(Boolean)) {
|
|
338
|
+
if (!existsSync(join(l.versions, v.version, 'install.json')) || !existsSync(join(l.node, `v${v.node}`))) throw new Refusal('missing', `${v.version} ${v.node}`)
|
|
339
|
+
}
|
|
254
340
|
await ensureManagedLauncher(l, dir, current.version, platform)
|
|
255
341
|
if (previous) writeAtomic(l.previous, `${previous.version} ${previous.node}\n`)
|
|
256
342
|
else rmSync(l.previous, { force: true })
|