codebuff 1.0.685 → 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/index.js +1 -0
- package/launcher.js +372 -120
- package/package.json +1 -1
package/index.js
CHANGED
package/launcher.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const { spawn
|
|
3
|
+
const { spawn } = require('child_process')
|
|
4
4
|
const fs = require('fs')
|
|
5
5
|
const http = require('http')
|
|
6
6
|
const https = require('https')
|
|
@@ -16,11 +16,15 @@ function createLauncher(productConfig) {
|
|
|
16
16
|
const {
|
|
17
17
|
packageName,
|
|
18
18
|
displayName,
|
|
19
|
+
wrapperVersion = null,
|
|
19
20
|
includeTreeSitterWasm = true,
|
|
20
21
|
startupBanner = [],
|
|
21
22
|
telemetryEvent = 'cli.update_codebuff_failed',
|
|
22
23
|
telemetryProperties = {},
|
|
23
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,
|
|
24
28
|
} = productConfig
|
|
25
29
|
|
|
26
30
|
/**
|
|
@@ -68,6 +72,16 @@ function createLauncher(productConfig) {
|
|
|
68
72
|
}
|
|
69
73
|
}
|
|
70
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
|
+
|
|
71
85
|
function getUnsignedExitCode(code) {
|
|
72
86
|
return code != null && code < 0 ? code >>> 0 : code
|
|
73
87
|
}
|
|
@@ -94,9 +108,38 @@ function createLauncher(productConfig) {
|
|
|
94
108
|
)
|
|
95
109
|
}
|
|
96
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
|
+
|
|
97
139
|
function createConfig(packageName) {
|
|
98
140
|
const homeDir = os.homedir()
|
|
99
|
-
const configDir =
|
|
141
|
+
const configDir =
|
|
142
|
+
configDirOverride || path.join(homeDir, '.config', 'manicode')
|
|
100
143
|
const binaryName =
|
|
101
144
|
process.platform === 'win32' ? `${packageName}.exe` : packageName
|
|
102
145
|
|
|
@@ -245,35 +288,6 @@ function createLauncher(productConfig) {
|
|
|
245
288
|
}
|
|
246
289
|
}
|
|
247
290
|
|
|
248
|
-
// Returns true (AVX2 present), false (absent), or null (couldn't determine).
|
|
249
|
-
// Ask the OS directly via IsProcessorFeaturePresent (kernel32), which is
|
|
250
|
-
// backed by CPUID — far more reliable than matching CPU model names, and it
|
|
251
|
-
// works on the stock Windows PowerShell that ships with every supported
|
|
252
|
-
// Windows version. Feature 40 = PF_AVX2_INSTRUCTIONS_AVAILABLE.
|
|
253
|
-
function probeWindowsAvx2() {
|
|
254
|
-
const script =
|
|
255
|
-
'$f = Add-Type -MemberDefinition \'[DllImport("kernel32.dll")] ' +
|
|
256
|
-
"public static extern bool IsProcessorFeaturePresent(uint feature);' " +
|
|
257
|
-
'-Name Cpu -Namespace Win32 -PassThru; $f::IsProcessorFeaturePresent(40)'
|
|
258
|
-
try {
|
|
259
|
-
const out = execFileSync(
|
|
260
|
-
'powershell.exe',
|
|
261
|
-
['-NoProfile', '-NonInteractive', '-Command', script],
|
|
262
|
-
{
|
|
263
|
-
encoding: 'utf8',
|
|
264
|
-
timeout: 5000,
|
|
265
|
-
stdio: ['ignore', 'pipe', 'ignore'],
|
|
266
|
-
},
|
|
267
|
-
).trim()
|
|
268
|
-
if (out === 'True') return true
|
|
269
|
-
if (out === 'False') return false
|
|
270
|
-
return null
|
|
271
|
-
} catch {
|
|
272
|
-
// No PowerShell, locked-down policy, timeout, etc. — inconclusive.
|
|
273
|
-
return null
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
291
|
let _hasAvx2Cache
|
|
278
292
|
|
|
279
293
|
function machineHasAvx2() {
|
|
@@ -288,31 +302,41 @@ function createLauncher(productConfig) {
|
|
|
288
302
|
return true
|
|
289
303
|
}
|
|
290
304
|
|
|
291
|
-
//
|
|
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.
|
|
292
314
|
if (process.platform === 'linux') {
|
|
293
315
|
return linuxCpuHasAvx2()
|
|
294
316
|
}
|
|
295
317
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
}
|
|
299
332
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const detected = probeWindowsAvx2()
|
|
308
|
-
if (detected === null) {
|
|
309
|
-
// Inconclusive probe: assume AVX2 for this launch and rely on the SIGILL
|
|
310
|
-
// fallback, but don't persist it — a transient failure must not lock in a
|
|
311
|
-
// wrong answer for the lifetime of the install. We'll re-probe next launch.
|
|
312
|
-
return true
|
|
313
|
-
}
|
|
314
|
-
writeCachedAvx2(detected)
|
|
315
|
-
return detected
|
|
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)
|
|
316
340
|
}
|
|
317
341
|
|
|
318
342
|
function getCpuFeatureCachePath() {
|
|
@@ -349,11 +373,12 @@ function createLauncher(productConfig) {
|
|
|
349
373
|
}
|
|
350
374
|
|
|
351
375
|
const platformKey = getPlatformKey()
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
//
|
|
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.
|
|
357
382
|
//
|
|
358
383
|
// This assumes every baseline target is gated on AVX2 specifically, which
|
|
359
384
|
// holds today (only linux-x64 and win32-x64 have baseline builds, both
|
|
@@ -439,12 +464,18 @@ function createLauncher(productConfig) {
|
|
|
439
464
|
if (!isTargetAllowedForThisMachine(metadataTarget)) {
|
|
440
465
|
return null
|
|
441
466
|
}
|
|
442
|
-
return metadata
|
|
467
|
+
return getMetadataVersion(metadata)
|
|
443
468
|
} catch (error) {
|
|
444
469
|
return null
|
|
445
470
|
}
|
|
446
471
|
}
|
|
447
472
|
|
|
473
|
+
function getMetadataVersion(metadata) {
|
|
474
|
+
return typeof metadata?.version === 'string' && metadata.version
|
|
475
|
+
? metadata.version
|
|
476
|
+
: null
|
|
477
|
+
}
|
|
478
|
+
|
|
448
479
|
function getCurrentMetadata() {
|
|
449
480
|
try {
|
|
450
481
|
if (!fs.existsSync(CONFIG.metadataPath)) {
|
|
@@ -456,68 +487,63 @@ function createLauncher(productConfig) {
|
|
|
456
487
|
}
|
|
457
488
|
}
|
|
458
489
|
|
|
459
|
-
function
|
|
460
|
-
if (
|
|
490
|
+
function parseVersion(version) {
|
|
491
|
+
if (typeof version !== 'string') return null
|
|
461
492
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
|
467
500
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const prereleaseParts = parts[1] ? parts[1].split('.') : []
|
|
472
|
-
return { main: mainParts, prerelease: prereleaseParts }
|
|
501
|
+
return {
|
|
502
|
+
main: match.slice(1, 4).map(BigInt),
|
|
503
|
+
prerelease,
|
|
473
504
|
}
|
|
505
|
+
}
|
|
474
506
|
|
|
507
|
+
function compareVersions(v1, v2) {
|
|
475
508
|
const p1 = parseVersion(v1)
|
|
476
509
|
const p2 = parseVersion(v2)
|
|
477
510
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
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
|
|
481
515
|
|
|
482
|
-
|
|
483
|
-
if (
|
|
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
|
|
484
519
|
}
|
|
485
520
|
|
|
486
|
-
if (p1.prerelease.length === 0
|
|
487
|
-
return 0
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const num2 = parseInt(pr2)
|
|
507
|
-
if (num1 < num2) return -1
|
|
508
|
-
if (num1 > num2) return 1
|
|
509
|
-
} else if (isNum1 && !isNum2) {
|
|
510
|
-
return 1
|
|
511
|
-
} else if (!isNum1 && isNum2) {
|
|
512
|
-
return -1
|
|
513
|
-
} else if (pr1 < pr2) {
|
|
514
|
-
return -1
|
|
515
|
-
} else if (pr1 > pr2) {
|
|
516
|
-
return 1
|
|
517
|
-
}
|
|
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
|
|
518
541
|
}
|
|
519
|
-
return
|
|
542
|
+
if (numeric1 !== numeric2) return numeric1 ? -1 : 1
|
|
543
|
+
return identifier1 < identifier2 ? -1 : 1
|
|
520
544
|
}
|
|
545
|
+
|
|
546
|
+
return 0
|
|
521
547
|
}
|
|
522
548
|
|
|
523
549
|
function formatBytes(bytes) {
|
|
@@ -860,13 +886,31 @@ function createLauncher(productConfig) {
|
|
|
860
886
|
installStagedBinary(stagedBinary)
|
|
861
887
|
}
|
|
862
888
|
|
|
863
|
-
|
|
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() {
|
|
864
901
|
const currentVersion = getCurrentVersion()
|
|
865
|
-
|
|
902
|
+
const requiredWrapperVersion = getRequiredWrapperVersion(currentVersion)
|
|
903
|
+
|
|
904
|
+
if (currentVersion !== null && requiredWrapperVersion === null) {
|
|
866
905
|
return
|
|
867
906
|
}
|
|
868
907
|
|
|
869
|
-
|
|
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())
|
|
870
914
|
if (!version) {
|
|
871
915
|
console.error('❌ Failed to determine latest version')
|
|
872
916
|
console.error('Please check your internet connection and try again')
|
|
@@ -878,6 +922,12 @@ function createLauncher(productConfig) {
|
|
|
878
922
|
} catch (error) {
|
|
879
923
|
term.clearLine()
|
|
880
924
|
printDownloadFailure(error)
|
|
925
|
+
if (currentVersion !== null) {
|
|
926
|
+
console.error(
|
|
927
|
+
`Continuing with cached ${packageName} ${currentVersion}.`,
|
|
928
|
+
)
|
|
929
|
+
return
|
|
930
|
+
}
|
|
881
931
|
process.exit(1)
|
|
882
932
|
}
|
|
883
933
|
}
|
|
@@ -921,6 +971,16 @@ function createLauncher(productConfig) {
|
|
|
921
971
|
}
|
|
922
972
|
|
|
923
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
|
+
|
|
924
984
|
let stoppedForUpdate = false
|
|
925
985
|
|
|
926
986
|
try {
|
|
@@ -979,7 +1039,32 @@ function createLauncher(productConfig) {
|
|
|
979
1039
|
}
|
|
980
1040
|
}
|
|
981
1041
|
|
|
982
|
-
|
|
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
|
|
983
1068
|
// Windows NTSTATUS codes (unsigned DWORD)
|
|
984
1069
|
const unsignedCode = getUnsignedExitCode(code)
|
|
985
1070
|
const isIllegalInstruction = isIllegalInstructionExit(code, signal)
|
|
@@ -1014,11 +1099,46 @@ function createLauncher(productConfig) {
|
|
|
1014
1099
|
console.error('This may indicate a platform compatibility issue.')
|
|
1015
1100
|
console.error('')
|
|
1016
1101
|
} else if (isAbort) {
|
|
1017
|
-
|
|
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
|
+
)
|
|
1018
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
|
+
}
|
|
1019
1131
|
}
|
|
1020
1132
|
|
|
1021
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
|
+
}
|
|
1022
1142
|
console.error('')
|
|
1023
1143
|
console.error('Please report this issue at:')
|
|
1024
1144
|
console.error(' https://github.com/CodebuffAI/codebuff/issues')
|
|
@@ -1035,13 +1155,28 @@ function createLauncher(productConfig) {
|
|
|
1035
1155
|
console.error('')
|
|
1036
1156
|
}
|
|
1037
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
|
+
|
|
1038
1173
|
function printSystemInfo() {
|
|
1039
1174
|
const metadata = getCurrentMetadata()
|
|
1040
1175
|
console.error('System info:')
|
|
1041
1176
|
console.error(` Platform: ${process.platform} ${process.arch}`)
|
|
1042
1177
|
console.error(` Node: ${process.version}`)
|
|
1043
1178
|
if (process.arch === 'x64') {
|
|
1044
|
-
console.error(` AVX2: ${
|
|
1179
|
+
console.error(` AVX2: ${describeAvx2Support()}`)
|
|
1045
1180
|
}
|
|
1046
1181
|
console.error(` Target: ${metadata?.target || getDefaultTargetKey()}`)
|
|
1047
1182
|
console.error(` Binary: ${CONFIG.binaryPath}`)
|
|
@@ -1112,7 +1247,12 @@ function createLauncher(productConfig) {
|
|
|
1112
1247
|
try {
|
|
1113
1248
|
const { env: optionEnv, ...spawnOptions } = options
|
|
1114
1249
|
child = spawn(CONFIG.binaryPath, process.argv.slice(2), {
|
|
1115
|
-
|
|
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'],
|
|
1116
1256
|
...spawnOptions,
|
|
1117
1257
|
env: {
|
|
1118
1258
|
...process.env,
|
|
@@ -1125,12 +1265,72 @@ function createLauncher(productConfig) {
|
|
|
1125
1265
|
}
|
|
1126
1266
|
|
|
1127
1267
|
child.on('error', exitOnSpawnFailure)
|
|
1268
|
+
child.launch = watchLaunch(child)
|
|
1128
1269
|
|
|
1129
1270
|
return child
|
|
1130
1271
|
}
|
|
1131
1272
|
|
|
1132
|
-
|
|
1133
|
-
|
|
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)) {
|
|
1134
1334
|
return false
|
|
1135
1335
|
}
|
|
1136
1336
|
|
|
@@ -1139,12 +1339,28 @@ function createLauncher(productConfig) {
|
|
|
1139
1339
|
return false
|
|
1140
1340
|
}
|
|
1141
1341
|
|
|
1342
|
+
// An explicit target is the user's decision; don't download over it.
|
|
1343
|
+
if (getTargetOverride()) {
|
|
1344
|
+
return false
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1142
1347
|
const metadata = getCurrentMetadata()
|
|
1143
1348
|
const currentTarget = metadata?.target || getDefaultTargetKey()
|
|
1144
1349
|
if (currentTarget === fallbackTarget) {
|
|
1145
1350
|
return false
|
|
1146
1351
|
}
|
|
1147
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
|
+
|
|
1148
1364
|
const version = metadata?.version || (await getLatestVersion())
|
|
1149
1365
|
if (!version) {
|
|
1150
1366
|
return false
|
|
@@ -1155,7 +1371,9 @@ function createLauncher(productConfig) {
|
|
|
1155
1371
|
})
|
|
1156
1372
|
console.error('')
|
|
1157
1373
|
console.error(
|
|
1158
|
-
|
|
1374
|
+
confirmed
|
|
1375
|
+
? `${packageName} is switching to the older-CPU binary for this machine.`
|
|
1376
|
+
: `${packageName} crashed on startup; trying the older-CPU binary.`,
|
|
1159
1377
|
)
|
|
1160
1378
|
|
|
1161
1379
|
try {
|
|
@@ -1173,9 +1391,20 @@ function createLauncher(productConfig) {
|
|
|
1173
1391
|
|
|
1174
1392
|
function attachExitHandler(child, allowBaselineFallback = true) {
|
|
1175
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
|
+
|
|
1176
1405
|
if (
|
|
1177
1406
|
allowBaselineFallback &&
|
|
1178
|
-
(await tryFallbackToBaseline(code, signal))
|
|
1407
|
+
(await tryFallbackToBaseline(code, signal, msAlive))
|
|
1179
1408
|
) {
|
|
1180
1409
|
return
|
|
1181
1410
|
}
|
|
@@ -1183,7 +1412,7 @@ function createLauncher(productConfig) {
|
|
|
1183
1412
|
resetTerminal({
|
|
1184
1413
|
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
|
|
1185
1414
|
})
|
|
1186
|
-
printCrashDiagnostics(code, signal)
|
|
1415
|
+
printCrashDiagnostics(code, signal, { msAlive, stderrTail })
|
|
1187
1416
|
process.exit(signal ? 1 : code || 0)
|
|
1188
1417
|
}
|
|
1189
1418
|
|
|
@@ -1197,7 +1426,7 @@ function createLauncher(productConfig) {
|
|
|
1197
1426
|
}
|
|
1198
1427
|
if (startupBanner.length > 0) console.log('')
|
|
1199
1428
|
|
|
1200
|
-
await
|
|
1429
|
+
await ensureBinaryReady()
|
|
1201
1430
|
|
|
1202
1431
|
const child = spawnInstalledBinary()
|
|
1203
1432
|
const exitListener = attachExitHandler(child)
|
|
@@ -1211,6 +1440,29 @@ function createLauncher(productConfig) {
|
|
|
1211
1440
|
config: productConfig,
|
|
1212
1441
|
main,
|
|
1213
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
|
+
},
|
|
1214
1466
|
}
|
|
1215
1467
|
}
|
|
1216
1468
|
|