fix: batch of review findings — dead install button, history pollution, TUI completion interference, ssh split session kill, scrollback live apply, zmodem second transfer, sftp shell quoting, replay buffer leak, release guards

This commit is contained in:
Bill committed 2026-09-14 02:20:40 +08:00
1 parent e6fc021903
commit 6c100542c5
11 files changed
+130 -19

No files matched your search

+4
View File
@@ -173,6 +173,9 @@ export function createPty(opts: PtyCreateOptions = {}): PtyCreateResult {
pty.onExit(({ exitCode }) => {
try {
sessions.delete(id)
// The session is gone: drop its replay buffer too (killPty was the only
// path that did, so naturally-exiting shells leaked up to 64KB each).
replayBuffers.delete(id)
safeStopLog(id)
broadcast(Ipc.PTY_EXIT, { id, exitCode })
} catch {
@@ -258,6 +261,7 @@ export async function openSession(opts: SessionOpenOptions): Promise<{ id: strin
detachZmodem(handle.id)
safeStopLog(handle.id)
sessions.delete(handle.id)
replayBuffers.delete(handle.id)
deps.broadcast(Ipc.PTY_EXIT, { id: handle.id, exitCode: 0 })
} catch {
// never crash the event loop
+7 -3
View File
@@ -21,7 +21,9 @@ const DEFAULT_SYSTEM: SystemSettings = {
launchAtLogin: false,
preventSleep: false,
globalShowHide: '',
closeAction: 'ask',
// Must match DEFAULT_SETTINGS.system in @shared/settings — an "ask" here made
// a fresh install prompt on close while the docs and UI promised tray.
closeAction: 'tray',
autoCheckUpdate: true
}
@@ -43,9 +45,11 @@ function isHighlightRule(value: unknown): value is HighlightRule {
}
function sanitizeRules(value: unknown): HighlightRule[] {
// An explicit empty array is a valid choice ("no highlighting"); only
// malformed data falls back to the built-in rules. Returning the defaults for
// [] made deleting the last rule look like it silently failed.
if (!Array.isArray(value)) return DEFAULT_HIGHLIGHT_RULES
const rules = value.filter(isHighlightRule)
return rules.length > 0 ? rules : DEFAULT_HIGHLIGHT_RULES
return value.filter(isHighlightRule)
}
function deepMerge(raw: unknown): { settings: AppSettings; errors: string[] } {
+12 -2
View File
@@ -195,13 +195,23 @@ export function deleteRemote(sessionId: string, paths: string[]): Promise<void>
* chmod/chown run over an exec channel: the JD test server's sftp subsystem
* accepts SETSTAT but silently ignores it, while shell chmod/chown work.
*/
/**
* POSIX single-quote escaping. `JSON.stringify` only escapes `"`, so a remote
* file name containing `$`, a backtick or a quote would still be expanded by the
* far-side shell — that is remote command execution triggered by a file name.
*/
function shQuote(value: string): string {
return `'${value.replace(/'/g, `'\\''`)}'`
}
export function chmodRemote(sessionId: string, path: string, mode: string): Promise<void> {
if (!/^[0-7]{1,4}$/.test(mode)) throw new Error(`非法权限值: ${mode}`)
return execQuiet(sessionId, `chmod ${mode} ${JSON.stringify(path)}`)
return execQuiet(sessionId, `chmod ${mode} ${shQuote(path)}`)
}
export function chownRemote(sessionId: string, path: string, uid: number, gid: number): Promise<void> {
return execQuiet(sessionId, `chown ${uid}:${gid} ${JSON.stringify(path)}`)
if (!Number.isInteger(uid) || !Number.isInteger(gid)) throw new Error('非法 uid/gid')
return execQuiet(sessionId, `chown ${uid}:${gid} ${shQuote(path)}`)
}
/** Run a command on the session's shell channel and wait for it to finish. */
+11
View File
@@ -309,6 +309,17 @@ export function attachZmodem(sessionId: string, deps: ZmodemDeps): void {
engine.mode = role === 'receive' ? 'receive' : 'send'
engine.detection = detection
engine.active = true
// A detect is the start of a fresh transfer on this session, so the
// one-shot flags of the previous one must go with it. Leaving `confirmed`
// set made the *second* `sz`/`rz` on a session a no-op: respondZmodem
// bailed out, the engine stayed `active` (swallowing every keystroke and
// all terminal data) until the stall timer finally fired.
engine.confirmed = false
engine.progressEmitted = false
engine.doneEmitted = false
engine.dir = null
engine.session = null
engine.receiveStream = null
engine.transferId = `zm-${sessionId}`
emitProgress(engine, { file: '', bytes: 0, totalBytes: 0 })
try {