feat(audit): Batch 15 live-verify — download-engine lifecycle (R4, L65, L139, B2, B6, L143)
The watched live session for the six deferred lifecycle items, each verified against real yt-dlp/Electron behavior on Windows: - R4: cancel now deletes the download's orphaned partials. The engine captures the final output path via a new `--print before_dl:dest|%(filename)s` (empirically %(filepath)s is still 'NA' that early) and the close handler sweeps only unambiguous intermediates (.part / .part-Frag* / .ytdl / <stem>.fNNN.<ext>) via the pure, unit-tested lib/partials.ts — never a bare <stem>.<ext> or another download's files. Live-verified with decoys. - L65: verified-acceptable, no code change — a taskkill /F pause leaves the .part byte-intact and yt-dlp --continue resumes at the exact byte offset. - L139: spawn↔self-update interlock (new ytdlpLock.ts). The updater refuses while any yt-dlp spawn is live; download/terminal IPC handlers hold (await) behind an in-progress exe rewrite instead of failing. - B2: source indexing is cancellable — AbortSignal through the probe walk (kills the in-flight child, live-verified via tasklist), sources:index-cancel IPC, and a Cancel button in the Library add-source row. - B6: app-update download is cancellable — app:update-cancel routes through the existing finish() teardown (abort + destroy + unlink, live-verified on Electron net.request); Cancel button under the update progress bar. The checksum phase is cancelable too. - L143: closing the window mid-download (non-tray mode) now offers a native tray-independent "Quit anyway / Keep downloading" dialog, replacing the passive "use the tray to quit" notification. Peer-review pass caught and fixed: a non-reentrant update lock (concurrent begin clobbered the settle promise), a dead Cancel window during the B6 checksum fetch + a canceller slot-ownership bug (L140 shape), and a persist-after-cancel hole in the index walk. typecheck + 304 tests + eslint + production build green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+35
-9
@@ -6,6 +6,7 @@ import { cleanError } from './log'
|
||||
import { VERSION_TIMEOUT_MS, YTDLP_UPDATE_TIMEOUT_MS } from './constants'
|
||||
import { getSettings, setSettings } from './settings'
|
||||
import { shouldAutoCheckYtdlp } from './ytdlpPolicy'
|
||||
import { beginYtdlpUpdate, endYtdlpUpdate, liveSpawnCount } from './ytdlpLock'
|
||||
import {
|
||||
isYtdlpUpdateChannel,
|
||||
type YtdlpVersionResult,
|
||||
@@ -78,16 +79,31 @@ export async function updateYtdlp(channel: YtdlpUpdateChannel): Promise<YtdlpUpd
|
||||
return { ok: false, error: YTDLP_MISSING_MSG }
|
||||
}
|
||||
|
||||
const r = await execFileAsync(ytdlpPath, ['--update-to', channel], {
|
||||
timeout: YTDLP_UPDATE_TIMEOUT_MS
|
||||
})
|
||||
if (!r.ok) {
|
||||
const msg = r.timedOut
|
||||
? 'Timed out updating yt-dlp.'
|
||||
: cleanError(r.stderr) || r.error?.message || ''
|
||||
return { ok: false, error: msg }
|
||||
// L139: `--update-to` rewrites yt-dlp.exe in place. Take the spawn interlock so a
|
||||
// download/terminal can't execute the exe mid-swap (Windows sharing violation /
|
||||
// half-written binary). If a spawn (or another update) is already live, defer
|
||||
// rather than fight it — the update is best-effort; the startup path retries next
|
||||
// launch and the Settings "Update now" button surfaces this message for a manual retry.
|
||||
if (!beginYtdlpUpdate()) {
|
||||
return {
|
||||
ok: false,
|
||||
error: 'yt-dlp is busy (a download or another update is running) — try again shortly.'
|
||||
}
|
||||
}
|
||||
try {
|
||||
const r = await execFileAsync(ytdlpPath, ['--update-to', channel], {
|
||||
timeout: YTDLP_UPDATE_TIMEOUT_MS
|
||||
})
|
||||
if (!r.ok) {
|
||||
const msg = r.timedOut
|
||||
? 'Timed out updating yt-dlp.'
|
||||
: cleanError(r.stderr) || r.error?.message || ''
|
||||
return { ok: false, error: msg }
|
||||
}
|
||||
return { ok: true, output: r.stdout.trim() }
|
||||
} finally {
|
||||
endYtdlpUpdate()
|
||||
}
|
||||
return { ok: true, output: r.stdout.trim() }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -109,11 +125,21 @@ export async function runStartupYtdlpAutoUpdate(
|
||||
if (!shouldAutoCheckYtdlp(settings.autoUpdateYtdlp, settings.ytdlpLastUpdateCheck, Date.now())) {
|
||||
return
|
||||
}
|
||||
// L139: never rewrite the exe while a download/terminal is live (e.g. an auto-
|
||||
// resumed persisted-queue item). Skip WITHOUT recording the check so the daily
|
||||
// throttle isn't burned on a deferral — retry next launch. Re-checked right before
|
||||
// the rewrite too, since the version probe below awaits.
|
||||
if (liveSpawnCount() > 0) return
|
||||
|
||||
const channel = settings.ytdlpChannel
|
||||
send({ phase: 'checking', channel })
|
||||
|
||||
const before = await getYtdlpVersion()
|
||||
if (liveSpawnCount() > 0) {
|
||||
// A spawn started during the version probe — defer to next launch (no record).
|
||||
send({ phase: 'current', channel, version: before.ok ? before.version : undefined })
|
||||
return
|
||||
}
|
||||
const result = await updateYtdlp(channel)
|
||||
setSettings({ ytdlpLastUpdateCheck: Date.now() })
|
||||
|
||||
|
||||
Reference in New Issue
Block a user