Skip to content

Commit e87f7a7

Browse files
authored
Fix wrapper terminal reset for help output (#739)
1 parent 250d4aa commit e87f7a7

3 files changed

Lines changed: 123 additions & 30 deletions

File tree

cli/release-staging/index.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ const packageName = 'codecane'
1717
* Terminal escape sequences to reset terminal state after the child process exits.
1818
* When the binary is SIGKILL'd, it can't clean up its own terminal state.
1919
* The wrapper (this process) survives and must reset these modes.
20-
*
21-
* Keep in sync with TERMINAL_RESET_SEQUENCES in cli/src/utils/renderer-cleanup.ts
2220
*/
23-
const TERMINAL_RESET_SEQUENCES =
24-
'\x1b[?1049l' + // Exit alternate screen buffer
21+
const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l'
22+
const SAFE_TERMINAL_RESET_SEQUENCES =
2523
'\x1b[?1000l' + // Disable X10 mouse mode
2624
'\x1b[?1002l' + // Disable button event mouse mode
2725
'\x1b[?1003l' + // Disable any-event mouse mode (all motion)
@@ -30,7 +28,12 @@ const TERMINAL_RESET_SEQUENCES =
3028
'\x1b[?2004l' + // Disable bracketed paste mode
3129
'\x1b[?25h' // Show cursor
3230

33-
function resetTerminal() {
31+
const FULL_TERMINAL_RESET_SEQUENCES =
32+
EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES
33+
34+
function resetTerminal(options = {}) {
35+
const { exitAlternateScreen = false } = options
36+
3437
try {
3538
if (process.stdin.isTTY && process.stdin.setRawMode) {
3639
process.stdin.setRawMode(false)
@@ -40,13 +43,37 @@ function resetTerminal() {
4043
}
4144
try {
4245
if (process.stdout.isTTY) {
43-
process.stdout.write(TERMINAL_RESET_SEQUENCES)
46+
// Exiting the alternate screen is only safe after an interactive child.
47+
// Plain CLI paths like --help never enter it, and ?1049l can erase output.
48+
process.stdout.write(
49+
exitAlternateScreen
50+
? FULL_TERMINAL_RESET_SEQUENCES
51+
: SAFE_TERMINAL_RESET_SEQUENCES,
52+
)
4453
}
4554
} catch {
4655
// stdout may be closed
4756
}
4857
}
4958

59+
function getUnsignedExitCode(code) {
60+
return code != null && code < 0 ? (code >>> 0) : code
61+
}
62+
63+
function isWindowsNativeCrashCode(code) {
64+
const unsignedCode = getUnsignedExitCode(code)
65+
return (
66+
process.platform === 'win32' &&
67+
(unsignedCode === 0xC000001D ||
68+
unsignedCode === 0xC0000005 ||
69+
unsignedCode === 0xC0000409)
70+
)
71+
}
72+
73+
function shouldExitAlternateScreen(code, signal) {
74+
return Boolean(signal) || isWindowsNativeCrashCode(code)
75+
}
76+
5077
function createConfig(packageName) {
5178
const homeDir = os.homedir()
5279
const configDir = path.join(homeDir, '.config', 'manicode')
@@ -465,7 +492,7 @@ async function checkForUpdates(runningProcess, exitListener) {
465492
}, 5000)
466493
})
467494

468-
resetTerminal()
495+
resetTerminal({ exitAlternateScreen: true })
469496
console.log(`Update available: ${currentVersion}${latestVersion}`)
470497

471498
await downloadBinary(latestVersion)
@@ -476,7 +503,9 @@ async function checkForUpdates(runningProcess, exitListener) {
476503
})
477504

478505
newChild.on('exit', (code, signal) => {
479-
resetTerminal()
506+
resetTerminal({
507+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
508+
})
480509
printCrashDiagnostics(code, signal)
481510
process.exit(signal ? 1 : (code || 0))
482511
})
@@ -495,7 +524,7 @@ async function checkForUpdates(runningProcess, exitListener) {
495524

496525
function printCrashDiagnostics(code, signal) {
497526
// Windows NTSTATUS codes (unsigned DWORD)
498-
const unsignedCode = code != null && code < 0 ? (code >>> 0) : code
527+
const unsignedCode = getUnsignedExitCode(code)
499528
const isIllegalInstruction =
500529
signal === 'SIGILL' ||
501530
(process.platform === 'win32' && unsignedCode === 0xC000001D)
@@ -557,7 +586,9 @@ async function main() {
557586
})
558587

559588
const exitListener = (code, signal) => {
560-
resetTerminal()
589+
resetTerminal({
590+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
591+
})
561592
printCrashDiagnostics(code, signal)
562593
process.exit(signal ? 1 : (code || 0))
563594
}

cli/release/index.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ const packageName = 'codebuff'
1717
* Terminal escape sequences to reset terminal state after the child process exits.
1818
* When the binary is SIGKILL'd, it can't clean up its own terminal state.
1919
* The wrapper (this process) survives and must reset these modes.
20-
*
21-
* Keep in sync with TERMINAL_RESET_SEQUENCES in cli/src/utils/renderer-cleanup.ts
2220
*/
23-
const TERMINAL_RESET_SEQUENCES =
24-
'\x1b[?1049l' + // Exit alternate screen buffer
21+
const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l'
22+
const SAFE_TERMINAL_RESET_SEQUENCES =
2523
'\x1b[?1000l' + // Disable X10 mouse mode
2624
'\x1b[?1002l' + // Disable button event mouse mode
2725
'\x1b[?1003l' + // Disable any-event mouse mode (all motion)
@@ -30,7 +28,12 @@ const TERMINAL_RESET_SEQUENCES =
3028
'\x1b[?2004l' + // Disable bracketed paste mode
3129
'\x1b[?25h' // Show cursor
3230

33-
function resetTerminal() {
31+
const FULL_TERMINAL_RESET_SEQUENCES =
32+
EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES
33+
34+
function resetTerminal(options = {}) {
35+
const { exitAlternateScreen = false } = options
36+
3437
try {
3538
if (process.stdin.isTTY && process.stdin.setRawMode) {
3639
process.stdin.setRawMode(false)
@@ -40,13 +43,37 @@ function resetTerminal() {
4043
}
4144
try {
4245
if (process.stdout.isTTY) {
43-
process.stdout.write(TERMINAL_RESET_SEQUENCES)
46+
// Exiting the alternate screen is only safe after an interactive child.
47+
// Plain CLI paths like --help never enter it, and ?1049l can erase output.
48+
process.stdout.write(
49+
exitAlternateScreen
50+
? FULL_TERMINAL_RESET_SEQUENCES
51+
: SAFE_TERMINAL_RESET_SEQUENCES,
52+
)
4453
}
4554
} catch {
4655
// stdout may be closed
4756
}
4857
}
4958

59+
function getUnsignedExitCode(code) {
60+
return code != null && code < 0 ? (code >>> 0) : code
61+
}
62+
63+
function isWindowsNativeCrashCode(code) {
64+
const unsignedCode = getUnsignedExitCode(code)
65+
return (
66+
process.platform === 'win32' &&
67+
(unsignedCode === 0xC000001D ||
68+
unsignedCode === 0xC0000005 ||
69+
unsignedCode === 0xC0000409)
70+
)
71+
}
72+
73+
function shouldExitAlternateScreen(code, signal) {
74+
return Boolean(signal) || isWindowsNativeCrashCode(code)
75+
}
76+
5077
function createConfig(packageName) {
5178
const homeDir = os.homedir()
5279
const configDir = path.join(homeDir, '.config', 'manicode')
@@ -485,15 +512,17 @@ async function checkForUpdates(runningProcess, exitListener) {
485512
}, 5000)
486513
})
487514

488-
resetTerminal()
515+
resetTerminal({ exitAlternateScreen: true })
489516
console.log(`Update available: ${currentVersion}${latestVersion}`)
490517

491518
await downloadBinary(latestVersion)
492519

493520
const newChild = spawnInstalledBinary({ detached: false })
494521

495522
newChild.on('exit', (code, signal) => {
496-
resetTerminal()
523+
resetTerminal({
524+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
525+
})
497526
printCrashDiagnostics(code, signal)
498527
process.exit(signal ? 1 : (code || 0))
499528
})
@@ -507,7 +536,7 @@ async function checkForUpdates(runningProcess, exitListener) {
507536

508537
function printCrashDiagnostics(code, signal) {
509538
// Windows NTSTATUS codes (unsigned DWORD)
510-
const unsignedCode = code != null && code < 0 ? (code >>> 0) : code
539+
const unsignedCode = getUnsignedExitCode(code)
511540
const isIllegalInstruction =
512541
signal === 'SIGILL' ||
513542
(process.platform === 'win32' && unsignedCode === 0xC000001D)
@@ -625,7 +654,9 @@ async function main() {
625654
const child = spawnInstalledBinary()
626655

627656
const exitListener = (code, signal) => {
628-
resetTerminal()
657+
resetTerminal({
658+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
659+
})
629660
printCrashDiagnostics(code, signal)
630661
process.exit(signal ? 1 : (code || 0))
631662
}

freebuff/cli/release/index.js

Lines changed: 41 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,9 @@ const packageName = 'freebuff'
1717
* Terminal escape sequences to reset terminal state after the child process exits.
1818
* When the binary is SIGKILL'd, it can't clean up its own terminal state.
1919
* The wrapper (this process) survives and must reset these modes.
20-
*
21-
* Keep in sync with TERMINAL_RESET_SEQUENCES in cli/src/utils/renderer-cleanup.ts
2220
*/
23-
const TERMINAL_RESET_SEQUENCES =
24-
'\x1b[?1049l' + // Exit alternate screen buffer
21+
const EXIT_ALTERNATE_SCREEN_SEQUENCE = '\x1b[?1049l'
22+
const SAFE_TERMINAL_RESET_SEQUENCES =
2523
'\x1b[?1000l' + // Disable X10 mouse mode
2624
'\x1b[?1002l' + // Disable button event mouse mode
2725
'\x1b[?1003l' + // Disable any-event mouse mode (all motion)
@@ -30,7 +28,12 @@ const TERMINAL_RESET_SEQUENCES =
3028
'\x1b[?2004l' + // Disable bracketed paste mode
3129
'\x1b[?25h' // Show cursor
3230

33-
function resetTerminal() {
31+
const FULL_TERMINAL_RESET_SEQUENCES =
32+
EXIT_ALTERNATE_SCREEN_SEQUENCE + SAFE_TERMINAL_RESET_SEQUENCES
33+
34+
function resetTerminal(options = {}) {
35+
const { exitAlternateScreen = false } = options
36+
3437
try {
3538
if (process.stdin.isTTY && process.stdin.setRawMode) {
3639
process.stdin.setRawMode(false)
@@ -40,13 +43,37 @@ function resetTerminal() {
4043
}
4144
try {
4245
if (process.stdout.isTTY) {
43-
process.stdout.write(TERMINAL_RESET_SEQUENCES)
46+
// Exiting the alternate screen is only safe after an interactive child.
47+
// Plain CLI paths like --help never enter it, and ?1049l can erase output.
48+
process.stdout.write(
49+
exitAlternateScreen
50+
? FULL_TERMINAL_RESET_SEQUENCES
51+
: SAFE_TERMINAL_RESET_SEQUENCES,
52+
)
4453
}
4554
} catch {
4655
// stdout may be closed
4756
}
4857
}
4958

59+
function getUnsignedExitCode(code) {
60+
return code != null && code < 0 ? (code >>> 0) : code
61+
}
62+
63+
function isWindowsNativeCrashCode(code) {
64+
const unsignedCode = getUnsignedExitCode(code)
65+
return (
66+
process.platform === 'win32' &&
67+
(unsignedCode === 0xC000001D ||
68+
unsignedCode === 0xC0000005 ||
69+
unsignedCode === 0xC0000409)
70+
)
71+
}
72+
73+
function shouldExitAlternateScreen(code, signal) {
74+
return Boolean(signal) || isWindowsNativeCrashCode(code)
75+
}
76+
5077
function createConfig(packageName) {
5178
const homeDir = os.homedir()
5279
const configDir = path.join(homeDir, '.config', 'manicode')
@@ -472,15 +499,17 @@ async function checkForUpdates(runningProcess, exitListener) {
472499
}, 5000)
473500
})
474501

475-
resetTerminal()
502+
resetTerminal({ exitAlternateScreen: true })
476503
console.log(`Update available: ${currentVersion}${latestVersion}`)
477504

478505
await downloadBinary(latestVersion)
479506

480507
const newChild = spawnInstalledBinary({ detached: false })
481508

482509
newChild.on('exit', (code, signal) => {
483-
resetTerminal()
510+
resetTerminal({
511+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
512+
})
484513
printCrashDiagnostics(code, signal)
485514
process.exit(signal ? 1 : (code || 0))
486515
})
@@ -494,7 +523,7 @@ async function checkForUpdates(runningProcess, exitListener) {
494523

495524
function printCrashDiagnostics(code, signal) {
496525
// Windows NTSTATUS codes (unsigned DWORD)
497-
const unsignedCode = code != null && code < 0 ? (code >>> 0) : code
526+
const unsignedCode = getUnsignedExitCode(code)
498527
const isIllegalInstruction =
499528
signal === 'SIGILL' ||
500529
(process.platform === 'win32' && unsignedCode === 0xC000001D)
@@ -612,7 +641,9 @@ async function main() {
612641
const child = spawnInstalledBinary()
613642

614643
const exitListener = (code, signal) => {
615-
resetTerminal()
644+
resetTerminal({
645+
exitAlternateScreen: shouldExitAlternateScreen(code, signal),
646+
})
616647
printCrashDiagnostics(code, signal)
617648
process.exit(signal ? 1 : (code || 0))
618649
}

0 commit comments

Comments
 (0)