codebuff 1.0.684 → 1.0.686
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/http.js +275 -12
- package/index.js +24 -938
- package/launcher.js +1469 -0
- package/package.json +4 -4
- package/postinstall.js +0 -34
package/launcher.js
ADDED
|
@@ -0,0 +1,1469 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { spawn } = require('child_process')
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const http = require('http')
|
|
6
|
+
const https = require('https')
|
|
7
|
+
const os = require('os')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const { pipeline } = require('stream/promises')
|
|
10
|
+
const zlib = require('zlib')
|
|
11
|
+
|
|
12
|
+
const tar = require('tar')
|
|
13
|
+
const { createReleaseHttpClient } = require('./http')
|
|
14
|
+
|
|
15
|
+
function createLauncher(productConfig) {
|
|
16
|
+
const {
|
|
17
|
+
packageName,
|
|
18
|
+
displayName,
|
|
19
|
+
wrapperVersion = null,
|
|
20
|
+
includeTreeSitterWasm = true,
|
|
21
|
+
startupBanner = [],
|
|
22
|
+
telemetryEvent = 'cli.update_codebuff_failed',
|
|
23
|
+
telemetryProperties = {},
|
|
24
|
+
tempDownloadDirName = `.${packageName}-download-temp`,
|
|
25
|
+
// Tests only. os.homedir() ignores $HOME under `bun test`, so pointing HOME
|
|
26
|
+
// at a temp dir is not enough to keep a test off the real ~/.config.
|
|
27
|
+
configDir: configDirOverride = null,
|
|
28
|
+
} = productConfig
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Terminal escape sequences to reset terminal state after the child process exits.
|
|
32
|
+
* When the binary is SIGKILL'd, it can't clean up its own terminal state.
|
|
33
|
+
* The wrapper (this process) survives and must reset these modes.
|
|
34
|
+
*/
|
|
35
|
+
const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l'
|
|
36
|
+
const SAFE_TERMINAL_RESET_SEQUENCES =
|
|
37
|
+
'\x1b[?1000l' + // Disable X10 mouse mode
|
|
38
|
+
'\x1b[?1002l' + // Disable button event mouse mode
|
|
39
|
+
'\x1b[?1003l' + // Disable any-event mouse mode (all motion)
|
|
40
|
+
'\x1b[?1006l' + // Disable SGR extended mouse mode
|
|
41
|
+
'\x1b[?1004l' + // Disable focus reporting
|
|
42
|
+
'\x1b[?2004l' + // Disable bracketed paste mode
|
|
43
|
+
'\x1b[<u' + // Pop kitty keyboard protocol flags
|
|
44
|
+
'\x1b[>4;0m' + // Reset modifyOtherKeys
|
|
45
|
+
'\x1b[?25h' // Show cursor
|
|
46
|
+
|
|
47
|
+
const FULL_TERMINAL_RESET_SEQUENCES =
|
|
48
|
+
EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES
|
|
49
|
+
|
|
50
|
+
function resetTerminal(options = {}) {
|
|
51
|
+
const { exitAlternateScreen = false } = options
|
|
52
|
+
|
|
53
|
+
try {
|
|
54
|
+
if (process.stdin.isTTY && process.stdin.setRawMode) {
|
|
55
|
+
process.stdin.setRawMode(false)
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
// stdin may be closed
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
if (process.stdout.isTTY) {
|
|
62
|
+
// Exiting the alternate screen is only safe after an interactive child.
|
|
63
|
+
// Plain CLI paths like --help never enter it, and ?1049l can erase output.
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
exitAlternateScreen
|
|
66
|
+
? FULL_TERMINAL_RESET_SEQUENCES
|
|
67
|
+
: SAFE_TERMINAL_RESET_SEQUENCES,
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
} catch {
|
|
71
|
+
// stdout may be closed
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* How long a binary has to survive before a crash stops looking like a
|
|
77
|
+
* startup failure. Used to bound the STATUS_STACK_BUFFER_OVERRUN heuristic
|
|
78
|
+
* below, which is only trustworthy for deaths during startup.
|
|
79
|
+
*/
|
|
80
|
+
const STARTUP_CRASH_WINDOW_MS = 10000
|
|
81
|
+
|
|
82
|
+
/** Bytes of the binary's stderr kept for the crash report. */
|
|
83
|
+
const STDERR_TAIL_BYTES = 8192
|
|
84
|
+
|
|
85
|
+
function getUnsignedExitCode(code) {
|
|
86
|
+
return code != null && code < 0 ? code >>> 0 : code
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isWindowsNativeCrashCode(code) {
|
|
90
|
+
const unsignedCode = getUnsignedExitCode(code)
|
|
91
|
+
return (
|
|
92
|
+
process.platform === 'win32' &&
|
|
93
|
+
(unsignedCode === 0xc000001d ||
|
|
94
|
+
unsignedCode === 0xc0000005 ||
|
|
95
|
+
unsignedCode === 0xc0000409)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function shouldExitAlternateScreen(code, signal) {
|
|
100
|
+
return Boolean(signal) || isWindowsNativeCrashCode(code)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isIllegalInstructionExit(code, signal) {
|
|
104
|
+
const unsignedCode = getUnsignedExitCode(code)
|
|
105
|
+
return (
|
|
106
|
+
signal === 'SIGILL' ||
|
|
107
|
+
(process.platform === 'win32' && unsignedCode === 0xc000001d)
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* A startup death that is probably this machine failing to run the optimized
|
|
113
|
+
* build, reported under the *other* Windows spelling.
|
|
114
|
+
*
|
|
115
|
+
* Bun is written in Zig, and a Zig panic on Windows reports itself with
|
|
116
|
+
* __fastfail(FAST_FAIL_FATAL_APP_EXIT) — NTSTATUS 0xC0000409
|
|
117
|
+
* (STATUS_STACK_BUFFER_OVERRUN, exit code 3221226505), not
|
|
118
|
+
* STATUS_ILLEGAL_INSTRUCTION. So a CPU without AVX2 does not reliably die on
|
|
119
|
+
* the illegal instruction itself: Bun detects the missing feature, then
|
|
120
|
+
* panics while starting up anyway (oven-sh/bun#28399), and the crash arrives
|
|
121
|
+
* as 0xC0000409. Only the SIGILL spelling was wired to the baseline
|
|
122
|
+
* fallback, which is why these machines crash-looped forever.
|
|
123
|
+
*
|
|
124
|
+
* On its own 0xC0000409 only means "the binary aborted", so this is bounded
|
|
125
|
+
* to deaths during startup. A panic ten minutes into a session is an
|
|
126
|
+
* ordinary bug, and reading it as a missing instruction set would send a
|
|
127
|
+
* perfectly capable machine to the slower build.
|
|
128
|
+
*/
|
|
129
|
+
function isStartupCpuFeatureCrash(code, signal, msAlive) {
|
|
130
|
+
return (
|
|
131
|
+
process.platform === 'win32' &&
|
|
132
|
+
getUnsignedExitCode(code) === 0xc0000409 &&
|
|
133
|
+
!signal &&
|
|
134
|
+
typeof msAlive === 'number' &&
|
|
135
|
+
msAlive < STARTUP_CRASH_WINDOW_MS
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function createConfig(packageName) {
|
|
140
|
+
const homeDir = os.homedir()
|
|
141
|
+
const configDir =
|
|
142
|
+
configDirOverride || path.join(homeDir, '.config', 'manicode')
|
|
143
|
+
const binaryName =
|
|
144
|
+
process.platform === 'win32' ? `${packageName}.exe` : packageName
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
homeDir,
|
|
148
|
+
configDir,
|
|
149
|
+
binaryName,
|
|
150
|
+
binaryPath: path.join(configDir, binaryName),
|
|
151
|
+
metadataPath: path.join(configDir, `${packageName}-metadata.json`),
|
|
152
|
+
tempDownloadDir: path.join(configDir, tempDownloadDirName),
|
|
153
|
+
userAgent: `${packageName}-cli`,
|
|
154
|
+
requestTimeout: 20000,
|
|
155
|
+
downloadRequestTimeout: 120000,
|
|
156
|
+
downloadMaxAttempts: 3,
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const CONFIG = createConfig(packageName)
|
|
161
|
+
const { downloadFile, httpGet, withRetries } = createReleaseHttpClient({
|
|
162
|
+
env: process.env,
|
|
163
|
+
userAgent: CONFIG.userAgent,
|
|
164
|
+
requestTimeout: CONFIG.requestTimeout,
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
function getPostHogConfig() {
|
|
168
|
+
const apiKey =
|
|
169
|
+
process.env.CODEBUFF_POSTHOG_API_KEY ||
|
|
170
|
+
process.env.NEXT_PUBLIC_POSTHOG_API_KEY
|
|
171
|
+
const host =
|
|
172
|
+
process.env.CODEBUFF_POSTHOG_HOST ||
|
|
173
|
+
process.env.NEXT_PUBLIC_POSTHOG_HOST_URL
|
|
174
|
+
|
|
175
|
+
if (!apiKey || !host) {
|
|
176
|
+
return null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { apiKey, host }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Track update failure event to PostHog.
|
|
184
|
+
* Fire-and-forget - errors are silently ignored.
|
|
185
|
+
*/
|
|
186
|
+
function trackUpdateFailed(errorMessage, version, context = {}) {
|
|
187
|
+
try {
|
|
188
|
+
const posthogConfig = getPostHogConfig()
|
|
189
|
+
if (!posthogConfig) {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const payload = JSON.stringify({
|
|
194
|
+
api_key: posthogConfig.apiKey,
|
|
195
|
+
event: telemetryEvent,
|
|
196
|
+
properties: {
|
|
197
|
+
distinct_id: `anonymous-${CONFIG.homeDir}`,
|
|
198
|
+
error: errorMessage,
|
|
199
|
+
version: version || 'unknown',
|
|
200
|
+
platform: process.platform,
|
|
201
|
+
arch: process.arch,
|
|
202
|
+
...telemetryProperties,
|
|
203
|
+
...context,
|
|
204
|
+
},
|
|
205
|
+
timestamp: new Date().toISOString(),
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
const parsedUrl = new URL(`${posthogConfig.host}/capture/`)
|
|
209
|
+
const isHttps = parsedUrl.protocol === 'https:'
|
|
210
|
+
const options = {
|
|
211
|
+
hostname: parsedUrl.hostname,
|
|
212
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
213
|
+
path: parsedUrl.pathname + parsedUrl.search,
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: {
|
|
216
|
+
'Content-Type': 'application/json',
|
|
217
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const transport = isHttps ? https : http
|
|
222
|
+
const req = transport.request(options)
|
|
223
|
+
req.on('error', () => {}) // Silently ignore errors
|
|
224
|
+
req.write(payload)
|
|
225
|
+
req.end()
|
|
226
|
+
} catch (e) {
|
|
227
|
+
// Silently ignore any tracking errors
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const PLATFORM_TARGETS = {
|
|
232
|
+
'linux-x64': `${packageName}-linux-x64.tar.gz`,
|
|
233
|
+
'linux-x64-baseline': `${packageName}-linux-x64-baseline.tar.gz`,
|
|
234
|
+
'linux-arm64': `${packageName}-linux-arm64.tar.gz`,
|
|
235
|
+
'darwin-x64': `${packageName}-darwin-x64.tar.gz`,
|
|
236
|
+
'darwin-arm64': `${packageName}-darwin-arm64.tar.gz`,
|
|
237
|
+
'win32-x64': `${packageName}-win32-x64.tar.gz`,
|
|
238
|
+
'win32-x64-baseline': `${packageName}-win32-x64-baseline.tar.gz`,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const BASELINE_FALLBACK_TARGETS = {
|
|
242
|
+
'linux-x64': 'linux-x64-baseline',
|
|
243
|
+
'win32-x64': 'win32-x64-baseline',
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const term = {
|
|
247
|
+
clearLine: () => {
|
|
248
|
+
if (process.stderr.isTTY) {
|
|
249
|
+
process.stderr.write('\r\x1b[K')
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
write: (text) => {
|
|
253
|
+
term.clearLine()
|
|
254
|
+
process.stderr.write(text)
|
|
255
|
+
},
|
|
256
|
+
writeLine: (text) => {
|
|
257
|
+
term.clearLine()
|
|
258
|
+
process.stderr.write(text + '\n')
|
|
259
|
+
},
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function getPlatformKey() {
|
|
263
|
+
return `${process.platform}-${process.arch}`
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getTargetOverride() {
|
|
267
|
+
const envNames = [
|
|
268
|
+
`${packageName.toUpperCase()}_BINARY_TARGET`,
|
|
269
|
+
'CODEBUFF_BINARY_TARGET',
|
|
270
|
+
'CLI_BINARY_TARGET',
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
for (const envName of envNames) {
|
|
274
|
+
const target = process.env[envName]
|
|
275
|
+
if (target && PLATFORM_TARGETS[target]) {
|
|
276
|
+
return target
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function linuxCpuHasAvx2() {
|
|
284
|
+
try {
|
|
285
|
+
return /\bavx2\b/i.test(fs.readFileSync('/proc/cpuinfo', 'utf8'))
|
|
286
|
+
} catch {
|
|
287
|
+
return true
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
let _hasAvx2Cache
|
|
292
|
+
|
|
293
|
+
function machineHasAvx2() {
|
|
294
|
+
if (_hasAvx2Cache === undefined) {
|
|
295
|
+
_hasAvx2Cache = detectMachineHasAvx2()
|
|
296
|
+
}
|
|
297
|
+
return _hasAvx2Cache
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function detectMachineHasAvx2() {
|
|
301
|
+
if (process.arch !== 'x64') {
|
|
302
|
+
return true
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// A recorded illegal-instruction crash outranks everything below it: the
|
|
306
|
+
// binary actually failed on this machine, which beats any inference we
|
|
307
|
+
// could make about the CPU.
|
|
308
|
+
const recorded = readCachedAvx2()
|
|
309
|
+
if (recorded !== null) {
|
|
310
|
+
return recorded
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Linux can just ask, and a file read is cheap enough not to cache.
|
|
314
|
+
if (process.platform === 'linux') {
|
|
315
|
+
return linuxCpuHasAvx2()
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Everything else assumes AVX2 — true of every x64 CPU since ~2013.
|
|
319
|
+
//
|
|
320
|
+
// Windows is the case that matters, since it's the only other platform with
|
|
321
|
+
// a baseline build. It has no /proc/cpuinfo equivalent we can read without
|
|
322
|
+
// spawning something, and the probe that used to fill the gap asked
|
|
323
|
+
// PowerShell to compile a C# stub and P/Invoke
|
|
324
|
+
// kernel32!IsProcessorFeaturePresent — accurate, but a textbook malware
|
|
325
|
+
// shape that Windows Defender flagged as a "Suspicious PowerShell command
|
|
326
|
+
// line" on real user machines. The illegal-instruction handler corrects us
|
|
327
|
+
// instead: tryFallbackToBaseline() calls recordMachineLacksAvx2(), so a
|
|
328
|
+
// machine without AVX2 pays exactly one failed launch and is answered by
|
|
329
|
+
// the cache read above from then on.
|
|
330
|
+
return true
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Called from the illegal-instruction fallback. Persisting here is what keeps
|
|
334
|
+
// the optimistic assumption above from costing more than a single crash — and
|
|
335
|
+
// it is deliberately separate from the metadata target, so a lost or rewritten
|
|
336
|
+
// metadata file can't resurrect the AVX2 build on a CPU that can't run it.
|
|
337
|
+
function recordMachineLacksAvx2() {
|
|
338
|
+
_hasAvx2Cache = false
|
|
339
|
+
writeCachedAvx2(false)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getCpuFeatureCachePath() {
|
|
343
|
+
return path.join(CONFIG.configDir, 'cpu-features.json')
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function readCachedAvx2() {
|
|
347
|
+
try {
|
|
348
|
+
const cache = JSON.parse(
|
|
349
|
+
fs.readFileSync(getCpuFeatureCachePath(), 'utf8'),
|
|
350
|
+
)
|
|
351
|
+
return typeof cache.avx2 === 'boolean' ? cache.avx2 : null
|
|
352
|
+
} catch {
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function writeCachedAvx2(value) {
|
|
358
|
+
try {
|
|
359
|
+
fs.mkdirSync(CONFIG.configDir, { recursive: true })
|
|
360
|
+
fs.writeFileSync(
|
|
361
|
+
getCpuFeatureCachePath(),
|
|
362
|
+
JSON.stringify({ avx2: value }),
|
|
363
|
+
)
|
|
364
|
+
} catch {
|
|
365
|
+
// Best effort; we'll just re-probe next launch.
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function getDefaultTargetKey() {
|
|
370
|
+
const override = getTargetOverride()
|
|
371
|
+
if (override) {
|
|
372
|
+
return override
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const platformKey = getPlatformKey()
|
|
376
|
+
// Linux still detects up front (reading /proc/cpuinfo is free). Windows
|
|
377
|
+
// cannot without spawning a process, and the PowerShell probe that used to
|
|
378
|
+
// do it tripped Defender, so Windows is optimistic-then-corrected: see
|
|
379
|
+
// detectMachineHasAvx2() and tryFallbackToBaseline(). Once a machine has
|
|
380
|
+
// failed once, readCachedAvx2() answers here and baseline is chosen up front
|
|
381
|
+
// exactly as it used to be.
|
|
382
|
+
//
|
|
383
|
+
// This assumes every baseline target is gated on AVX2 specifically, which
|
|
384
|
+
// holds today (only linux-x64 and win32-x64 have baseline builds, both
|
|
385
|
+
// AVX2-gated). If a baseline build is ever added for a different reason, give
|
|
386
|
+
// BASELINE_FALLBACK_TARGETS a per-target capability and check that instead.
|
|
387
|
+
if (BASELINE_FALLBACK_TARGETS[platformKey] && !machineHasAvx2()) {
|
|
388
|
+
return BASELINE_FALLBACK_TARGETS[platformKey]
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return platformKey
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function getBaselineFallbackTargetKey() {
|
|
395
|
+
// Runtime safety net: if proactive detection was unavailable or wrong and the
|
|
396
|
+
// optimized binary still dies with SIGILL, fall back to baseline.
|
|
397
|
+
return BASELINE_FALLBACK_TARGETS[getPlatformKey()] || null
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function isTargetAllowedForThisMachine(target) {
|
|
401
|
+
const override = getTargetOverride()
|
|
402
|
+
if (override) {
|
|
403
|
+
return target === override
|
|
404
|
+
}
|
|
405
|
+
// Check the baseline fallback first: it's always safe on its platform and
|
|
406
|
+
// avoids running CPU detection when a baseline binary is already installed.
|
|
407
|
+
return (
|
|
408
|
+
target === getBaselineFallbackTargetKey() ||
|
|
409
|
+
target === getDefaultTargetKey()
|
|
410
|
+
)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function getDownloadTargetKey() {
|
|
414
|
+
const override = getTargetOverride()
|
|
415
|
+
if (override) {
|
|
416
|
+
return override
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const metadata = getCurrentMetadata()
|
|
420
|
+
if (metadata?.target && isTargetAllowedForThisMachine(metadata.target)) {
|
|
421
|
+
return metadata.target
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return getDefaultTargetKey()
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function getLatestVersion() {
|
|
428
|
+
try {
|
|
429
|
+
const res = await httpGet(
|
|
430
|
+
`https://registry.npmjs.org/${packageName}/latest`,
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
if (res.statusCode !== 200) return null
|
|
434
|
+
|
|
435
|
+
const body = await streamToString(res)
|
|
436
|
+
const packageData = JSON.parse(body)
|
|
437
|
+
|
|
438
|
+
return packageData.version || null
|
|
439
|
+
} catch (error) {
|
|
440
|
+
return null
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function streamToString(stream) {
|
|
445
|
+
return new Promise((resolve, reject) => {
|
|
446
|
+
let data = ''
|
|
447
|
+
stream.on('data', (chunk) => (data += chunk))
|
|
448
|
+
stream.on('end', () => resolve(data))
|
|
449
|
+
stream.on('error', reject)
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function getCurrentVersion() {
|
|
454
|
+
try {
|
|
455
|
+
const metadata = getCurrentMetadata()
|
|
456
|
+
if (!metadata) {
|
|
457
|
+
return null
|
|
458
|
+
}
|
|
459
|
+
// Also verify the binary still exists
|
|
460
|
+
if (!fs.existsSync(CONFIG.binaryPath)) {
|
|
461
|
+
return null
|
|
462
|
+
}
|
|
463
|
+
const metadataTarget = metadata.target || getPlatformKey()
|
|
464
|
+
if (!isTargetAllowedForThisMachine(metadataTarget)) {
|
|
465
|
+
return null
|
|
466
|
+
}
|
|
467
|
+
return getMetadataVersion(metadata)
|
|
468
|
+
} catch (error) {
|
|
469
|
+
return null
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function getMetadataVersion(metadata) {
|
|
474
|
+
return typeof metadata?.version === 'string' && metadata.version
|
|
475
|
+
? metadata.version
|
|
476
|
+
: null
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function getCurrentMetadata() {
|
|
480
|
+
try {
|
|
481
|
+
if (!fs.existsSync(CONFIG.metadataPath)) {
|
|
482
|
+
return null
|
|
483
|
+
}
|
|
484
|
+
return JSON.parse(fs.readFileSync(CONFIG.metadataPath, 'utf8'))
|
|
485
|
+
} catch {
|
|
486
|
+
return null
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function parseVersion(version) {
|
|
491
|
+
if (typeof version !== 'string') return null
|
|
492
|
+
|
|
493
|
+
const match = version.match(
|
|
494
|
+
/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/,
|
|
495
|
+
)
|
|
496
|
+
if (!match) return null
|
|
497
|
+
|
|
498
|
+
const prerelease = match[4]?.split('.') ?? []
|
|
499
|
+
if (prerelease.some((part) => /^0\d+$/.test(part))) return null
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
main: match.slice(1, 4).map(BigInt),
|
|
503
|
+
prerelease,
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function compareVersions(v1, v2) {
|
|
508
|
+
const p1 = parseVersion(v1)
|
|
509
|
+
const p2 = parseVersion(v2)
|
|
510
|
+
|
|
511
|
+
// Published package versions are valid semver. Treat malformed cached
|
|
512
|
+
// metadata as stale so the wrapper repairs it instead of trusting it.
|
|
513
|
+
if (!p1) return -1
|
|
514
|
+
if (!p2) return 1
|
|
515
|
+
|
|
516
|
+
for (let i = 0; i < p1.main.length; i++) {
|
|
517
|
+
if (p1.main[i] < p2.main[i]) return -1
|
|
518
|
+
if (p1.main[i] > p2.main[i]) return 1
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
if (p1.prerelease.length === 0) {
|
|
522
|
+
return p2.prerelease.length === 0 ? 0 : 1
|
|
523
|
+
}
|
|
524
|
+
if (p2.prerelease.length === 0) return -1
|
|
525
|
+
|
|
526
|
+
for (
|
|
527
|
+
let i = 0;
|
|
528
|
+
i < Math.max(p1.prerelease.length, p2.prerelease.length);
|
|
529
|
+
i++
|
|
530
|
+
) {
|
|
531
|
+
const identifier1 = p1.prerelease[i]
|
|
532
|
+
const identifier2 = p2.prerelease[i]
|
|
533
|
+
if (identifier1 === undefined) return -1
|
|
534
|
+
if (identifier2 === undefined) return 1
|
|
535
|
+
if (identifier1 === identifier2) continue
|
|
536
|
+
|
|
537
|
+
const numeric1 = /^\d+$/.test(identifier1)
|
|
538
|
+
const numeric2 = /^\d+$/.test(identifier2)
|
|
539
|
+
if (numeric1 && numeric2) {
|
|
540
|
+
return BigInt(identifier1) < BigInt(identifier2) ? -1 : 1
|
|
541
|
+
}
|
|
542
|
+
if (numeric1 !== numeric2) return numeric1 ? -1 : 1
|
|
543
|
+
return identifier1 < identifier2 ? -1 : 1
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return 0
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function formatBytes(bytes) {
|
|
550
|
+
if (bytes === 0) return '0 B'
|
|
551
|
+
const k = 1024
|
|
552
|
+
const sizes = ['B', 'KB', 'MB', 'GB']
|
|
553
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
|
554
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function createProgressBar(percentage, width = 30) {
|
|
558
|
+
const filled = Math.round((width * percentage) / 100)
|
|
559
|
+
const empty = width - filled
|
|
560
|
+
return '[' + '█'.repeat(filled) + '░'.repeat(empty) + ']'
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function isRetryableDownloadError(error) {
|
|
564
|
+
if (error && typeof error.retryable === 'boolean') return error.retryable
|
|
565
|
+
return !['EACCES', 'ENOSPC', 'EPERM', 'EROFS'].includes(error?.code)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function getPartialArchivePath(version, targetKey) {
|
|
569
|
+
return path.join(
|
|
570
|
+
CONFIG.configDir,
|
|
571
|
+
`.${packageName}-${version}-${targetKey}.tar.gz.part`,
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function getFileSize(filePath) {
|
|
576
|
+
try {
|
|
577
|
+
return fs.statSync(filePath).size
|
|
578
|
+
} catch (error) {
|
|
579
|
+
if (error.code === 'ENOENT') return 0
|
|
580
|
+
throw error
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function removeFileIfPresent(filePath) {
|
|
585
|
+
try {
|
|
586
|
+
fs.unlinkSync(filePath)
|
|
587
|
+
} catch (error) {
|
|
588
|
+
if (error.code !== 'ENOENT') throw error
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function formatDownloadSource(downloadUrl) {
|
|
593
|
+
try {
|
|
594
|
+
const parsedUrl = new URL(downloadUrl)
|
|
595
|
+
return `${parsedUrl.origin}${parsedUrl.pathname}`
|
|
596
|
+
} catch {
|
|
597
|
+
return downloadUrl
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function printDownloadFailure(error) {
|
|
602
|
+
const code = error.code ? ` (${error.code})` : ''
|
|
603
|
+
console.error(
|
|
604
|
+
`❌ Failed to download ${packageName}: ${error.message}${code}`,
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if (error.requestUrl) {
|
|
608
|
+
console.error(
|
|
609
|
+
`Download source: ${formatDownloadSource(error.requestUrl)}`,
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
if (error.downloadedBytes > 0) {
|
|
613
|
+
const total = error.totalBytes
|
|
614
|
+
? ` of ${formatBytes(error.totalBytes)}`
|
|
615
|
+
: ''
|
|
616
|
+
console.error(
|
|
617
|
+
`Saved ${formatBytes(error.downloadedBytes)}${total}; the next run will resume this download.`,
|
|
618
|
+
)
|
|
619
|
+
} else if (error.requestUrl) {
|
|
620
|
+
console.error(
|
|
621
|
+
'Please retry. The release host may be temporarily unavailable.',
|
|
622
|
+
)
|
|
623
|
+
} else {
|
|
624
|
+
console.error(
|
|
625
|
+
'The downloaded update could not be installed; the existing binary was preserved when possible.',
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function prepareTempDownloadDir() {
|
|
631
|
+
if (fs.existsSync(CONFIG.tempDownloadDir)) {
|
|
632
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
633
|
+
}
|
|
634
|
+
fs.mkdirSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function downloadAndExtract(
|
|
638
|
+
downloadUrl,
|
|
639
|
+
version,
|
|
640
|
+
targetKey,
|
|
641
|
+
{ quiet = false } = {},
|
|
642
|
+
) {
|
|
643
|
+
let attempts = 0
|
|
644
|
+
const partialArchivePath = getPartialArchivePath(version, targetKey)
|
|
645
|
+
let totalBytes = null
|
|
646
|
+
|
|
647
|
+
try {
|
|
648
|
+
return await withRetries(
|
|
649
|
+
async (attempt) => {
|
|
650
|
+
attempts = attempt
|
|
651
|
+
prepareTempDownloadDir()
|
|
652
|
+
if (!quiet) term.write('Downloading...')
|
|
653
|
+
|
|
654
|
+
let lastProgressTime = Date.now()
|
|
655
|
+
const result = await downloadFile(downloadUrl, partialArchivePath, {
|
|
656
|
+
timeout: CONFIG.downloadRequestTimeout,
|
|
657
|
+
onProgress: ({ downloadedBytes, totalBytes: progressTotal }) => {
|
|
658
|
+
totalBytes = progressTotal
|
|
659
|
+
if (quiet) return
|
|
660
|
+
const now = Date.now()
|
|
661
|
+
if (
|
|
662
|
+
now - lastProgressTime < 100 &&
|
|
663
|
+
downloadedBytes !== progressTotal
|
|
664
|
+
) {
|
|
665
|
+
return
|
|
666
|
+
}
|
|
667
|
+
lastProgressTime = now
|
|
668
|
+
if (progressTotal) {
|
|
669
|
+
const pct = Math.round((downloadedBytes / progressTotal) * 100)
|
|
670
|
+
term.write(
|
|
671
|
+
`Downloading... ${createProgressBar(pct)} ${pct}% of ${formatBytes(progressTotal)}`,
|
|
672
|
+
)
|
|
673
|
+
} else {
|
|
674
|
+
term.write(`Downloading... ${formatBytes(downloadedBytes)}`)
|
|
675
|
+
}
|
|
676
|
+
},
|
|
677
|
+
})
|
|
678
|
+
totalBytes = result.totalBytes
|
|
679
|
+
|
|
680
|
+
try {
|
|
681
|
+
await pipeline(
|
|
682
|
+
fs.createReadStream(partialArchivePath),
|
|
683
|
+
zlib.createGunzip(),
|
|
684
|
+
tar.x({ cwd: CONFIG.tempDownloadDir }),
|
|
685
|
+
)
|
|
686
|
+
} catch (error) {
|
|
687
|
+
// A complete archive that cannot be extracted is corrupt. Do not
|
|
688
|
+
// resume it on the next attempt.
|
|
689
|
+
removeFileIfPresent(partialArchivePath)
|
|
690
|
+
throw error
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
const tempBinaryPath = path.join(
|
|
694
|
+
CONFIG.tempDownloadDir,
|
|
695
|
+
CONFIG.binaryName,
|
|
696
|
+
)
|
|
697
|
+
if (!fs.existsSync(tempBinaryPath)) {
|
|
698
|
+
const files = fs.readdirSync(CONFIG.tempDownloadDir)
|
|
699
|
+
removeFileIfPresent(partialArchivePath)
|
|
700
|
+
const error = new Error(
|
|
701
|
+
`Binary not found after extraction. Expected: ${CONFIG.binaryName}, Available files: ${files.join(', ')}`,
|
|
702
|
+
)
|
|
703
|
+
error.retryable = false
|
|
704
|
+
throw error
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
removeFileIfPresent(partialArchivePath)
|
|
708
|
+
return tempBinaryPath
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
maxAttempts: CONFIG.downloadMaxAttempts,
|
|
712
|
+
shouldRetry: isRetryableDownloadError,
|
|
713
|
+
onRetry: ({ nextAttempt, delayMs }) => {
|
|
714
|
+
if (quiet) return
|
|
715
|
+
term.writeLine(
|
|
716
|
+
`Download interrupted. Retrying in ${delayMs / 1000}s (${nextAttempt}/${CONFIG.downloadMaxAttempts})...`,
|
|
717
|
+
)
|
|
718
|
+
},
|
|
719
|
+
},
|
|
720
|
+
)
|
|
721
|
+
} catch (error) {
|
|
722
|
+
try {
|
|
723
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
|
|
724
|
+
} catch {
|
|
725
|
+
// Best effort after a failed download.
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
trackUpdateFailed(error.message, version, {
|
|
729
|
+
stage: 'download',
|
|
730
|
+
errorCode: error.code,
|
|
731
|
+
statusCode: error.statusCode,
|
|
732
|
+
target: targetKey,
|
|
733
|
+
attempts,
|
|
734
|
+
bytesDownloaded: getFileSize(partialArchivePath),
|
|
735
|
+
totalBytes: error.totalBytes || totalBytes,
|
|
736
|
+
})
|
|
737
|
+
error.downloadedBytes = getFileSize(partialArchivePath)
|
|
738
|
+
error.totalBytes ||= totalBytes
|
|
739
|
+
error.requestUrl ||= downloadUrl
|
|
740
|
+
throw error
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
async function stageBinary(
|
|
745
|
+
version,
|
|
746
|
+
targetKey = getDownloadTargetKey(),
|
|
747
|
+
options = {},
|
|
748
|
+
) {
|
|
749
|
+
const fileName = PLATFORM_TARGETS[targetKey]
|
|
750
|
+
|
|
751
|
+
if (!fileName) {
|
|
752
|
+
const error = new Error(
|
|
753
|
+
`Unsupported platform: ${process.platform} ${process.arch}`,
|
|
754
|
+
)
|
|
755
|
+
trackUpdateFailed(error.message, version, {
|
|
756
|
+
stage: 'platform_check',
|
|
757
|
+
target: targetKey,
|
|
758
|
+
})
|
|
759
|
+
throw error
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
const downloadUrl = `${
|
|
763
|
+
process.env.NEXT_PUBLIC_CODEBUFF_APP_URL || 'https://codebuff.com'
|
|
764
|
+
}/api/releases/download/${version}/${fileName}`
|
|
765
|
+
|
|
766
|
+
fs.mkdirSync(CONFIG.configDir, { recursive: true })
|
|
767
|
+
const tempBinaryPath = await downloadAndExtract(
|
|
768
|
+
downloadUrl,
|
|
769
|
+
version,
|
|
770
|
+
targetKey,
|
|
771
|
+
options,
|
|
772
|
+
)
|
|
773
|
+
|
|
774
|
+
try {
|
|
775
|
+
if (process.platform !== 'win32') {
|
|
776
|
+
fs.chmodSync(tempBinaryPath, 0o755)
|
|
777
|
+
}
|
|
778
|
+
} catch (error) {
|
|
779
|
+
try {
|
|
780
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
|
|
781
|
+
} catch {
|
|
782
|
+
// Preserve the original chmod error.
|
|
783
|
+
}
|
|
784
|
+
throw error
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
return { tempBinaryPath, version, targetKey }
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
function replaceFileWithRollback(sourcePath, targetPath, replacements) {
|
|
791
|
+
const backupPath = fs.existsSync(targetPath)
|
|
792
|
+
? `${targetPath}.old.${Date.now()}.${replacements.length}`
|
|
793
|
+
: null
|
|
794
|
+
|
|
795
|
+
if (backupPath) {
|
|
796
|
+
fs.renameSync(targetPath, backupPath)
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
fs.renameSync(sourcePath, targetPath)
|
|
801
|
+
} catch (error) {
|
|
802
|
+
if (backupPath && fs.existsSync(backupPath)) {
|
|
803
|
+
fs.renameSync(backupPath, targetPath)
|
|
804
|
+
}
|
|
805
|
+
throw error
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
replacements.push({ backupPath, targetPath })
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
function rollbackReplacements(replacements) {
|
|
812
|
+
for (const { backupPath, targetPath } of replacements.reverse()) {
|
|
813
|
+
removeFileIfPresent(targetPath)
|
|
814
|
+
if (backupPath && fs.existsSync(backupPath)) {
|
|
815
|
+
fs.renameSync(backupPath, targetPath)
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
function commitReplacements(replacements) {
|
|
821
|
+
for (const { backupPath } of replacements) {
|
|
822
|
+
if (!backupPath) continue
|
|
823
|
+
try {
|
|
824
|
+
removeFileIfPresent(backupPath)
|
|
825
|
+
} catch {
|
|
826
|
+
// The replacement is already committed. A stale backup is safer than
|
|
827
|
+
// rolling back a working install because cleanup failed.
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function installStagedBinary({ tempBinaryPath, version, targetKey }) {
|
|
833
|
+
const replacements = []
|
|
834
|
+
const metadataTempPath = `${CONFIG.metadataPath}.new.${process.pid}`
|
|
835
|
+
|
|
836
|
+
try {
|
|
837
|
+
fs.writeFileSync(
|
|
838
|
+
metadataTempPath,
|
|
839
|
+
JSON.stringify({ version, target: targetKey }, null, 2),
|
|
840
|
+
)
|
|
841
|
+
replaceFileWithRollback(tempBinaryPath, CONFIG.binaryPath, replacements)
|
|
842
|
+
|
|
843
|
+
// Move tree-sitter.wasm next to the binary if the tarball included
|
|
844
|
+
// it. The CLI binary loads this at startup; embedding it inside the
|
|
845
|
+
// binary itself was unreliable on Windows (bun --compile asset
|
|
846
|
+
// bundling silently dropped or unbound it across several attempts),
|
|
847
|
+
// so we ship it as a sibling file instead. Older artifacts that
|
|
848
|
+
// pre-date this change won't have the wasm and will still install —
|
|
849
|
+
// they'll just hit the same crash they had before, which is fine.
|
|
850
|
+
if (includeTreeSitterWasm) {
|
|
851
|
+
const tempWasmPath = path.join(
|
|
852
|
+
CONFIG.tempDownloadDir,
|
|
853
|
+
'tree-sitter.wasm',
|
|
854
|
+
)
|
|
855
|
+
if (fs.existsSync(tempWasmPath)) {
|
|
856
|
+
const targetWasmPath = path.join(
|
|
857
|
+
path.dirname(CONFIG.binaryPath),
|
|
858
|
+
'tree-sitter.wasm',
|
|
859
|
+
)
|
|
860
|
+
replaceFileWithRollback(tempWasmPath, targetWasmPath, replacements)
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
replaceFileWithRollback(
|
|
865
|
+
metadataTempPath,
|
|
866
|
+
CONFIG.metadataPath,
|
|
867
|
+
replacements,
|
|
868
|
+
)
|
|
869
|
+
commitReplacements(replacements)
|
|
870
|
+
} catch (error) {
|
|
871
|
+
rollbackReplacements(replacements)
|
|
872
|
+
throw error
|
|
873
|
+
} finally {
|
|
874
|
+
removeFileIfPresent(metadataTempPath)
|
|
875
|
+
if (fs.existsSync(CONFIG.tempDownloadDir)) {
|
|
876
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true })
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
term.clearLine()
|
|
881
|
+
console.log(`Download complete! Starting ${displayName}...`)
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function downloadBinary(version, targetKey = getDownloadTargetKey()) {
|
|
885
|
+
const stagedBinary = await stageBinary(version, targetKey)
|
|
886
|
+
installStagedBinary(stagedBinary)
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
function getRequiredWrapperVersion(currentVersion) {
|
|
890
|
+
if (
|
|
891
|
+
!wrapperVersion ||
|
|
892
|
+
(currentVersion !== null &&
|
|
893
|
+
compareVersions(currentVersion, wrapperVersion) >= 0)
|
|
894
|
+
) {
|
|
895
|
+
return null
|
|
896
|
+
}
|
|
897
|
+
return wrapperVersion
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
async function ensureBinaryReady() {
|
|
901
|
+
const currentVersion = getCurrentVersion()
|
|
902
|
+
const requiredWrapperVersion = getRequiredWrapperVersion(currentVersion)
|
|
903
|
+
|
|
904
|
+
if (currentVersion !== null && requiredWrapperVersion === null) {
|
|
905
|
+
return
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
// npm installs update this JavaScript wrapper but intentionally preserve the
|
|
909
|
+
// downloaded binary. If that binary exits before the background update
|
|
910
|
+
// check starts, it can otherwise remain stuck forever. The wrapper and its
|
|
911
|
+
// release binary share a version, so repair that stale cache synchronously
|
|
912
|
+
// without adding a registry lookup to healthy launches.
|
|
913
|
+
const version = requiredWrapperVersion ?? (await getLatestVersion())
|
|
914
|
+
if (!version) {
|
|
915
|
+
console.error('❌ Failed to determine latest version')
|
|
916
|
+
console.error('Please check your internet connection and try again')
|
|
917
|
+
process.exit(1)
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
try {
|
|
921
|
+
await downloadBinary(version)
|
|
922
|
+
} catch (error) {
|
|
923
|
+
term.clearLine()
|
|
924
|
+
printDownloadFailure(error)
|
|
925
|
+
if (currentVersion !== null) {
|
|
926
|
+
console.error(
|
|
927
|
+
`Continuing with cached ${packageName} ${currentVersion}.`,
|
|
928
|
+
)
|
|
929
|
+
return
|
|
930
|
+
}
|
|
931
|
+
process.exit(1)
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function stopRunningProcess(runningProcess) {
|
|
936
|
+
return new Promise((resolve, reject) => {
|
|
937
|
+
let forceKillTimer
|
|
938
|
+
let forceKillTimeout
|
|
939
|
+
|
|
940
|
+
const cleanup = () => {
|
|
941
|
+
clearTimeout(forceKillTimer)
|
|
942
|
+
clearTimeout(forceKillTimeout)
|
|
943
|
+
runningProcess.removeListener('exit', handleExit)
|
|
944
|
+
}
|
|
945
|
+
const handleExit = () => {
|
|
946
|
+
cleanup()
|
|
947
|
+
resolve()
|
|
948
|
+
}
|
|
949
|
+
const fail = (error) => {
|
|
950
|
+
cleanup()
|
|
951
|
+
reject(error)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
runningProcess.once('exit', handleExit)
|
|
955
|
+
forceKillTimer = setTimeout(() => {
|
|
956
|
+
forceKillTimeout = setTimeout(() => {
|
|
957
|
+
fail(new Error(`${packageName} did not exit after SIGKILL`))
|
|
958
|
+
}, 1000)
|
|
959
|
+
try {
|
|
960
|
+
runningProcess.kill('SIGKILL')
|
|
961
|
+
} catch (error) {
|
|
962
|
+
fail(error)
|
|
963
|
+
}
|
|
964
|
+
}, 5000)
|
|
965
|
+
try {
|
|
966
|
+
runningProcess.kill('SIGTERM')
|
|
967
|
+
} catch (error) {
|
|
968
|
+
fail(error)
|
|
969
|
+
}
|
|
970
|
+
})
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
async function checkForUpdates(runningProcess, exitListener) {
|
|
974
|
+
// main() schedules this 100ms after launch, so the binary it was handed can
|
|
975
|
+
// already be dead — a startup crash that handed off to the baseline
|
|
976
|
+
// fallback, most of all. Updating around a corpse would race that
|
|
977
|
+
// relaunch's download for the shared temp directory (prepareTempDownloadDir
|
|
978
|
+
// rmSyncs it) and then spend six seconds SIGKILLing a process that has
|
|
979
|
+
// already exited.
|
|
980
|
+
if (runningProcess.exitCode !== null || runningProcess.signalCode !== null) {
|
|
981
|
+
return
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
let stoppedForUpdate = false
|
|
985
|
+
|
|
986
|
+
try {
|
|
987
|
+
const currentVersion = getCurrentVersion()
|
|
988
|
+
|
|
989
|
+
const latestVersion = await getLatestVersion()
|
|
990
|
+
if (!latestVersion) return
|
|
991
|
+
|
|
992
|
+
if (
|
|
993
|
+
// Download new version if current version is unknown or outdated.
|
|
994
|
+
currentVersion === null ||
|
|
995
|
+
compareVersions(currentVersion, latestVersion) < 0
|
|
996
|
+
) {
|
|
997
|
+
const stagedBinary = await stageBinary(
|
|
998
|
+
latestVersion,
|
|
999
|
+
getDownloadTargetKey(),
|
|
1000
|
+
{ quiet: true },
|
|
1001
|
+
)
|
|
1002
|
+
|
|
1003
|
+
term.clearLine()
|
|
1004
|
+
|
|
1005
|
+
runningProcess.removeListener('exit', exitListener)
|
|
1006
|
+
try {
|
|
1007
|
+
await stopRunningProcess(runningProcess)
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
runningProcess.on('exit', exitListener)
|
|
1010
|
+
throw error
|
|
1011
|
+
}
|
|
1012
|
+
stoppedForUpdate = true
|
|
1013
|
+
|
|
1014
|
+
resetTerminal({ exitAlternateScreen: true })
|
|
1015
|
+
console.log(`Update available: ${currentVersion} → ${latestVersion}`)
|
|
1016
|
+
|
|
1017
|
+
installStagedBinary(stagedBinary)
|
|
1018
|
+
|
|
1019
|
+
const newChild = spawnInstalledBinary({ detached: false })
|
|
1020
|
+
attachExitHandler(newChild)
|
|
1021
|
+
|
|
1022
|
+
return new Promise(() => {})
|
|
1023
|
+
}
|
|
1024
|
+
} catch (error) {
|
|
1025
|
+
if (stoppedForUpdate && fs.existsSync(CONFIG.binaryPath)) {
|
|
1026
|
+
console.error(
|
|
1027
|
+
`Update failed; restarting ${packageName} ${getCurrentVersion()}.`,
|
|
1028
|
+
)
|
|
1029
|
+
const child = spawnInstalledBinary({ detached: false })
|
|
1030
|
+
attachExitHandler(child)
|
|
1031
|
+
return new Promise(() => {})
|
|
1032
|
+
}
|
|
1033
|
+
try {
|
|
1034
|
+
fs.rmSync(CONFIG.tempDownloadDir, { recursive: true, force: true })
|
|
1035
|
+
} catch {
|
|
1036
|
+
// Best effort after a failed background update.
|
|
1037
|
+
}
|
|
1038
|
+
// A staging failure leaves the current process and binary untouched.
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Make captured output safe to print back to the terminal.
|
|
1044
|
+
*
|
|
1045
|
+
* The tail is replayed *after* resetTerminal() has put the terminal back in
|
|
1046
|
+
* order, so it must not be able to undo that: a stray \x1b[?1049h or
|
|
1047
|
+
* \x1b[?1003h in what the binary printed would re-enter the alternate screen
|
|
1048
|
+
* or re-enable mouse reporting, hiding the very report it is part of. Strip
|
|
1049
|
+
* the escapes and keep the words.
|
|
1050
|
+
*/
|
|
1051
|
+
function sanitizeForReplay(text) {
|
|
1052
|
+
if (!text) return ''
|
|
1053
|
+
return (
|
|
1054
|
+
text
|
|
1055
|
+
.replace(/\r\n?/g, '\n')
|
|
1056
|
+
// CSI sequences — the ones that actually change terminal state.
|
|
1057
|
+
.replace(/\x1b\[[0-9;:?<>=]*[ -/]*[@-~]/g, '')
|
|
1058
|
+
// Everything else: strip the control bytes and keep the text. An OSC
|
|
1059
|
+
// or a lone escape loses its introducer and degrades to inert
|
|
1060
|
+
// characters, which is all a crash report needs it to be.
|
|
1061
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
1062
|
+
.replace(/\s+$/, '')
|
|
1063
|
+
)
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function printCrashDiagnostics(code, signal, context = {}) {
|
|
1067
|
+
const { msAlive = null, stderrTail = '' } = context
|
|
1068
|
+
// Windows NTSTATUS codes (unsigned DWORD)
|
|
1069
|
+
const unsignedCode = getUnsignedExitCode(code)
|
|
1070
|
+
const isIllegalInstruction = isIllegalInstructionExit(code, signal)
|
|
1071
|
+
const isAccessViolation =
|
|
1072
|
+
signal === 'SIGSEGV' ||
|
|
1073
|
+
(process.platform === 'win32' && unsignedCode === 0xc0000005)
|
|
1074
|
+
const isBusError = signal === 'SIGBUS'
|
|
1075
|
+
const isAbort =
|
|
1076
|
+
signal === 'SIGABRT' ||
|
|
1077
|
+
(process.platform === 'win32' && unsignedCode === 0xc0000409)
|
|
1078
|
+
|
|
1079
|
+
if (!isIllegalInstruction && !isAccessViolation && !isBusError && !isAbort)
|
|
1080
|
+
return
|
|
1081
|
+
|
|
1082
|
+
const exitInfo = signal ? `signal ${signal}` : `code ${code}`
|
|
1083
|
+
console.error('')
|
|
1084
|
+
console.error(`❌ ${packageName} exited immediately (${exitInfo})`)
|
|
1085
|
+
console.error('')
|
|
1086
|
+
|
|
1087
|
+
if (isIllegalInstruction) {
|
|
1088
|
+
console.error(
|
|
1089
|
+
'Your CPU may not support the required instruction set (AVX2).',
|
|
1090
|
+
)
|
|
1091
|
+
console.error('This typically affects CPUs from before 2013.')
|
|
1092
|
+
console.error('')
|
|
1093
|
+
printBaselineOverrideHint()
|
|
1094
|
+
} else if (isAccessViolation) {
|
|
1095
|
+
console.error('The binary crashed with an access violation.')
|
|
1096
|
+
console.error('')
|
|
1097
|
+
} else if (isBusError) {
|
|
1098
|
+
console.error('The binary crashed with a bus error.')
|
|
1099
|
+
console.error('This may indicate a platform compatibility issue.')
|
|
1100
|
+
console.error('')
|
|
1101
|
+
} else if (isAbort) {
|
|
1102
|
+
const startupCpuCrash = isStartupCpuFeatureCrash(code, signal, msAlive)
|
|
1103
|
+
console.error(
|
|
1104
|
+
startupCpuCrash
|
|
1105
|
+
? 'The binary aborted while starting up.'
|
|
1106
|
+
: 'The binary crashed with an abort signal.',
|
|
1107
|
+
)
|
|
1108
|
+
console.error('')
|
|
1109
|
+
// Reached only once the baseline fallback has declined or failed, so name
|
|
1110
|
+
// the case the user can still act on instead of leaving them with an
|
|
1111
|
+
// abort code and a bug-tracker link.
|
|
1112
|
+
if (startupCpuCrash) {
|
|
1113
|
+
if (getCurrentMetadata()?.target === getBaselineFallbackTargetKey()) {
|
|
1114
|
+
console.error(
|
|
1115
|
+
'This is already the older-CPU (baseline) build, so a missing AVX2',
|
|
1116
|
+
)
|
|
1117
|
+
console.error(
|
|
1118
|
+
'instruction set is not the whole story — please include the output',
|
|
1119
|
+
)
|
|
1120
|
+
console.error('below in your report.')
|
|
1121
|
+
console.error('')
|
|
1122
|
+
} else {
|
|
1123
|
+
console.error(
|
|
1124
|
+
'On x64 Windows this is usually a CPU without AVX2 support, which',
|
|
1125
|
+
)
|
|
1126
|
+
console.error('the standard build requires.')
|
|
1127
|
+
console.error('')
|
|
1128
|
+
printBaselineOverrideHint()
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
printSystemInfo()
|
|
1134
|
+
const tailText = sanitizeForReplay(stderrTail)
|
|
1135
|
+
if (tailText) {
|
|
1136
|
+
console.error('')
|
|
1137
|
+
console.error(`Last output from ${packageName}:`)
|
|
1138
|
+
for (const line of tailText.split('\n')) {
|
|
1139
|
+
console.error(` ${line}`)
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
console.error('')
|
|
1143
|
+
console.error('Please report this issue at:')
|
|
1144
|
+
console.error(' https://github.com/CodebuffAI/codebuff/issues')
|
|
1145
|
+
console.error('')
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
function printBaselineOverrideHint() {
|
|
1149
|
+
const fallbackTarget = getBaselineFallbackTargetKey()
|
|
1150
|
+
if (!fallbackTarget) return
|
|
1151
|
+
console.error('To force the baseline (non-AVX2) build, set:')
|
|
1152
|
+
console.error(
|
|
1153
|
+
` ${packageName.toUpperCase()}_BINARY_TARGET=${fallbackTarget}`,
|
|
1154
|
+
)
|
|
1155
|
+
console.error('')
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/**
|
|
1159
|
+
* What we actually know about AVX2, not what we assume.
|
|
1160
|
+
*
|
|
1161
|
+
* The old line printed a flat "yes" from machineHasAvx2(), which on Windows
|
|
1162
|
+
* is an optimistic default and not a measurement (see detectMachineHasAvx2).
|
|
1163
|
+
* Every crash report that reached us therefore claimed AVX2 was present on
|
|
1164
|
+
* machines we had never asked, which is exactly the evidence that would have
|
|
1165
|
+
* pointed at the CPU.
|
|
1166
|
+
*/
|
|
1167
|
+
function describeAvx2Support() {
|
|
1168
|
+
if (readCachedAvx2() === false) return 'no (recorded crash)'
|
|
1169
|
+
if (process.platform === 'linux') return machineHasAvx2() ? 'yes' : 'no'
|
|
1170
|
+
return 'not checked (assumed present)'
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
function printSystemInfo() {
|
|
1174
|
+
const metadata = getCurrentMetadata()
|
|
1175
|
+
console.error('System info:')
|
|
1176
|
+
console.error(` Platform: ${process.platform} ${process.arch}`)
|
|
1177
|
+
console.error(` Node: ${process.version}`)
|
|
1178
|
+
if (process.arch === 'x64') {
|
|
1179
|
+
console.error(` AVX2: ${describeAvx2Support()}`)
|
|
1180
|
+
}
|
|
1181
|
+
console.error(` Target: ${metadata?.target || getDefaultTargetKey()}`)
|
|
1182
|
+
console.error(` Binary: ${CONFIG.binaryPath}`)
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function getInstalledBinaryStatus() {
|
|
1186
|
+
try {
|
|
1187
|
+
const stats = fs.statSync(CONFIG.binaryPath)
|
|
1188
|
+
return stats.isFile() ? `yes (${formatBytes(stats.size)})` : 'no'
|
|
1189
|
+
} catch {
|
|
1190
|
+
return 'no'
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function printSpawnFailure(err) {
|
|
1195
|
+
resetTerminal()
|
|
1196
|
+
const code = err && err.code ? ` (${err.code})` : ''
|
|
1197
|
+
|
|
1198
|
+
console.error(`Failed to start ${packageName}: ${err.message}${code}`)
|
|
1199
|
+
console.error('')
|
|
1200
|
+
printSystemInfo()
|
|
1201
|
+
console.error(` Exists: ${getInstalledBinaryStatus()}`)
|
|
1202
|
+
|
|
1203
|
+
if (process.platform === 'win32') {
|
|
1204
|
+
console.error('')
|
|
1205
|
+
console.error(
|
|
1206
|
+
'On Windows, this can happen when Windows Security or antivirus blocks',
|
|
1207
|
+
)
|
|
1208
|
+
console.error(
|
|
1209
|
+
'or quarantines the downloaded executable, or when the binary requires',
|
|
1210
|
+
)
|
|
1211
|
+
console.error('CPU instructions that are not available on this machine.')
|
|
1212
|
+
console.error('')
|
|
1213
|
+
printBaselineOverrideHint()
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
console.error('')
|
|
1217
|
+
console.error('Try deleting the downloaded files and running again:')
|
|
1218
|
+
console.error(` ${CONFIG.configDir}`)
|
|
1219
|
+
console.error('')
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
function exitOnSpawnFailure(err) {
|
|
1223
|
+
printSpawnFailure(err)
|
|
1224
|
+
process.exit(1)
|
|
1225
|
+
}
|
|
1226
|
+
|
|
1227
|
+
function spawnInstalledBinary(options = {}) {
|
|
1228
|
+
if (!fs.existsSync(CONFIG.binaryPath)) {
|
|
1229
|
+
try {
|
|
1230
|
+
if (fs.existsSync(CONFIG.metadataPath))
|
|
1231
|
+
fs.unlinkSync(CONFIG.metadataPath)
|
|
1232
|
+
} catch {
|
|
1233
|
+
// best effort
|
|
1234
|
+
}
|
|
1235
|
+
const error = new Error(
|
|
1236
|
+
`downloaded binary is missing at ${CONFIG.binaryPath}`,
|
|
1237
|
+
)
|
|
1238
|
+
error.code = 'BINARY_MISSING'
|
|
1239
|
+
exitOnSpawnFailure(error)
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// spawn() only emits 'error' asynchronously for a few errno values
|
|
1243
|
+
// (EACCES, EAGAIN, EMFILE, ENFILE, ENOENT); everything else — notably
|
|
1244
|
+
// UNKNOWN on Windows when antivirus or Smart App Control blocks the
|
|
1245
|
+
// exe or the download is corrupt — is thrown synchronously.
|
|
1246
|
+
let child
|
|
1247
|
+
try {
|
|
1248
|
+
const { env: optionEnv, ...spawnOptions } = options
|
|
1249
|
+
child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
|
|
1250
|
+
// stdin/stdout stay inherited — the TUI owns the terminal and must see
|
|
1251
|
+
// a real tty. stderr is teed on Windows only; see watchLaunch.
|
|
1252
|
+
stdio:
|
|
1253
|
+
process.platform === 'win32'
|
|
1254
|
+
? ['inherit', 'inherit', 'pipe']
|
|
1255
|
+
: ['inherit', 'inherit', 'inherit'],
|
|
1256
|
+
...spawnOptions,
|
|
1257
|
+
env: {
|
|
1258
|
+
...process.env,
|
|
1259
|
+
...optionEnv,
|
|
1260
|
+
CODEBUFF_LAUNCHER_PID: String(process.pid),
|
|
1261
|
+
},
|
|
1262
|
+
})
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
exitOnSpawnFailure(err)
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
child.on('error', exitOnSpawnFailure)
|
|
1268
|
+
child.launch = watchLaunch(child)
|
|
1269
|
+
|
|
1270
|
+
return child
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Watch a launch so its death can be explained: how long the binary lived,
|
|
1275
|
+
* and what it printed on the way out.
|
|
1276
|
+
*
|
|
1277
|
+
* The stderr tee is what makes a crash report self-contained. When the binary
|
|
1278
|
+
* dies natively we reset the terminal, and that reset leaves the alternate
|
|
1279
|
+
* screen with \x1b[?1049l — discarding its contents, including any panic Bun
|
|
1280
|
+
* printed there. (The CLI's own terminal watchdog sends the same sequence, so
|
|
1281
|
+
* dropping it here wouldn't help.) Every report of codebuff#792 is a launcher
|
|
1282
|
+
* message with no panic text above it; keeping a copy means the next one
|
|
1283
|
+
* arrives with Bun's own words in it.
|
|
1284
|
+
*
|
|
1285
|
+
* Only Windows pipes stderr — that's where the reports come from, and
|
|
1286
|
+
* everywhere else stderr stays a real tty.
|
|
1287
|
+
*/
|
|
1288
|
+
function watchLaunch(child) {
|
|
1289
|
+
const startedAt = Date.now()
|
|
1290
|
+
const chunks = []
|
|
1291
|
+
let bufferedBytes = 0
|
|
1292
|
+
|
|
1293
|
+
child.stderr?.on('data', (chunk) => {
|
|
1294
|
+
try {
|
|
1295
|
+
// Straight through: as far as the binary and the user are concerned,
|
|
1296
|
+
// this is still its terminal.
|
|
1297
|
+
process.stderr.write(chunk)
|
|
1298
|
+
} catch {
|
|
1299
|
+
// stderr may be closed
|
|
1300
|
+
}
|
|
1301
|
+
chunks.push(chunk)
|
|
1302
|
+
bufferedBytes += chunk.length
|
|
1303
|
+
while (bufferedBytes > STDERR_TAIL_BYTES && chunks.length > 1) {
|
|
1304
|
+
bufferedBytes -= chunks.shift().length
|
|
1305
|
+
}
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
return {
|
|
1309
|
+
msAlive: () => Date.now() - startedAt,
|
|
1310
|
+
stderrTail: () =>
|
|
1311
|
+
Buffer.concat(chunks).toString('utf8').slice(-STDERR_TAIL_BYTES),
|
|
1312
|
+
// 'exit' can beat the last bytes through the pipe; 'close' is the event
|
|
1313
|
+
// that means the stdio is drained too. The timeout is a bound on a stuck
|
|
1314
|
+
// pipe and is deliberately NOT unref'd — an unref'd timer lets the
|
|
1315
|
+
// process exit before it fires, which would swallow the crash report
|
|
1316
|
+
// entirely. Already-drained streams resolve without waiting for it.
|
|
1317
|
+
drained: () =>
|
|
1318
|
+
child.stderr && !child.stderr.readableEnded
|
|
1319
|
+
? new Promise((resolve) => {
|
|
1320
|
+
child.once('close', resolve)
|
|
1321
|
+
child.stderr.once('end', resolve)
|
|
1322
|
+
setTimeout(resolve, 250)
|
|
1323
|
+
})
|
|
1324
|
+
: Promise.resolve(),
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
async function tryFallbackToBaseline(code, signal, msAlive) {
|
|
1329
|
+
// Two spellings of one failure, at two levels of certainty. SIGILL /
|
|
1330
|
+
// STATUS_ILLEGAL_INSTRUCTION is proof this CPU cannot run this build; a
|
|
1331
|
+
// Windows startup abort is a strong suspicion (see isStartupCpuFeatureCrash).
|
|
1332
|
+
const confirmed = isIllegalInstructionExit(code, signal)
|
|
1333
|
+
if (!confirmed && !isStartupCpuFeatureCrash(code, signal, msAlive)) {
|
|
1334
|
+
return false
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
const fallbackTarget = getBaselineFallbackTargetKey()
|
|
1338
|
+
if (!fallbackTarget) {
|
|
1339
|
+
return false
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// An explicit target is the user's decision; don't download over it.
|
|
1343
|
+
if (getTargetOverride()) {
|
|
1344
|
+
return false
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
const metadata = getCurrentMetadata()
|
|
1348
|
+
const currentTarget = metadata?.target || getDefaultTargetKey()
|
|
1349
|
+
if (currentTarget === fallbackTarget) {
|
|
1350
|
+
return false
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// Only a confirmed illegal instruction gets written down. Persisting it
|
|
1354
|
+
// before the download is what caps the cost at one crash: even if the
|
|
1355
|
+
// download or the relaunch fails, we never optimistically pick the AVX2
|
|
1356
|
+
// build again. A suspected crash records nothing — installing the baseline
|
|
1357
|
+
// already keeps this machine on it, so a guess that turns out to be wrong
|
|
1358
|
+
// costs the slower build instead of leaving cpu-features.json asserting a
|
|
1359
|
+
// CPU limitation we never observed.
|
|
1360
|
+
if (confirmed) {
|
|
1361
|
+
recordMachineLacksAvx2()
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const version = metadata?.version || (await getLatestVersion())
|
|
1365
|
+
if (!version) {
|
|
1366
|
+
return false
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
resetTerminal({
|
|
1370
|
+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
|
|
1371
|
+
})
|
|
1372
|
+
console.error('')
|
|
1373
|
+
console.error(
|
|
1374
|
+
confirmed
|
|
1375
|
+
? `${packageName} is switching to the older-CPU binary for this machine.`
|
|
1376
|
+
: `${packageName} crashed on startup; trying the older-CPU binary.`,
|
|
1377
|
+
)
|
|
1378
|
+
|
|
1379
|
+
try {
|
|
1380
|
+
await downloadBinary(version, fallbackTarget)
|
|
1381
|
+
} catch (error) {
|
|
1382
|
+
term.clearLine()
|
|
1383
|
+
console.error(`Failed to download ${fallbackTarget}: ${error.message}`)
|
|
1384
|
+
return false
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
const child = spawnInstalledBinary({ detached: false })
|
|
1388
|
+
attachExitHandler(child, false)
|
|
1389
|
+
return true
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function attachExitHandler(child, allowBaselineFallback = true) {
|
|
1393
|
+
const exitListener = async (code, signal) => {
|
|
1394
|
+
// A child we never watched (only reachable from a test) reports no age
|
|
1395
|
+
// rather than a suspiciously young one: absent evidence must not be read
|
|
1396
|
+
// as a startup crash and trigger a download.
|
|
1397
|
+
const msAlive = child.launch ? child.launch.msAlive() : Infinity
|
|
1398
|
+
|
|
1399
|
+
let stderrTail = ''
|
|
1400
|
+
if (child.launch && (isWindowsNativeCrashCode(code) || signal)) {
|
|
1401
|
+
await child.launch.drained()
|
|
1402
|
+
stderrTail = child.launch.stderrTail()
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
if (
|
|
1406
|
+
allowBaselineFallback &&
|
|
1407
|
+
(await tryFallbackToBaseline(code, signal, msAlive))
|
|
1408
|
+
) {
|
|
1409
|
+
return
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
resetTerminal({
|
|
1413
|
+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
|
|
1414
|
+
})
|
|
1415
|
+
printCrashDiagnostics(code, signal, { msAlive, stderrTail })
|
|
1416
|
+
process.exit(signal ? 1 : code || 0)
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
child.on('exit', exitListener)
|
|
1420
|
+
return exitListener
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
async function main() {
|
|
1424
|
+
for (const line of startupBanner) {
|
|
1425
|
+
console.log(line)
|
|
1426
|
+
}
|
|
1427
|
+
if (startupBanner.length > 0) console.log('')
|
|
1428
|
+
|
|
1429
|
+
await ensureBinaryReady()
|
|
1430
|
+
|
|
1431
|
+
const child = spawnInstalledBinary()
|
|
1432
|
+
const exitListener = attachExitHandler(child)
|
|
1433
|
+
|
|
1434
|
+
setTimeout(() => {
|
|
1435
|
+
checkForUpdates(child, exitListener)
|
|
1436
|
+
}, 100)
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return {
|
|
1440
|
+
config: productConfig,
|
|
1441
|
+
main,
|
|
1442
|
+
stopRunningProcess,
|
|
1443
|
+
// Internals exposed for tests only. The AVX2 path is optimistic-then-
|
|
1444
|
+
// corrected (see detectMachineHasAvx2), so the correction has to be
|
|
1445
|
+
// exercised directly — there is no way to make a CI runner lack AVX2.
|
|
1446
|
+
__testing: {
|
|
1447
|
+
detectMachineHasAvx2,
|
|
1448
|
+
recordMachineLacksAvx2,
|
|
1449
|
+
readCachedAvx2,
|
|
1450
|
+
isIllegalInstructionExit,
|
|
1451
|
+
isStartupCpuFeatureCrash,
|
|
1452
|
+
tryFallbackToBaseline,
|
|
1453
|
+
printCrashDiagnostics,
|
|
1454
|
+
checkForUpdates,
|
|
1455
|
+
spawnInstalledBinary,
|
|
1456
|
+
attachExitHandler,
|
|
1457
|
+
getDefaultTargetKey,
|
|
1458
|
+
getCpuFeatureCachePath,
|
|
1459
|
+
getCurrentVersion,
|
|
1460
|
+
getMetadataVersion,
|
|
1461
|
+
getRequiredWrapperVersion,
|
|
1462
|
+
ensureBinaryReady,
|
|
1463
|
+
isTargetAllowedForThisMachine,
|
|
1464
|
+
CONFIG,
|
|
1465
|
+
},
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
module.exports = { createLauncher }
|