fix(windows): tighten the install-dir clearing per review

- An image that fails to canonicalize stays in the lock check instead of
  being silently skipped; only a positive match against the caller's own
  running image is excluded.
- reap_recorded_daemon shares one deadline across the whole tree via a
  new winproc::terminate_and_wait_all, which stop_for_update and
  reap_descendants_of now use too — one implementation of "terminate,
  then wait, bounded overall" instead of three.
- [UninstallRun] passes --update-install-dir "{app}" like
  PrepareToInstall, so uninstalling after a daemon crash gets the same
  orphaned-ConPTY-host cleanup as upgrading.
This commit is contained in:
l0ng-ai
2026-08-08 10:08:01 +08:00
parent ebec6096d6
commit bbf92935b6
3 changed files with 41 additions and 30 deletions
+10 -6
View File
@@ -130,12 +130,16 @@ Filename: "{app}\tty7-app.exe"; Description: "{cm:LaunchProgram,tty7}"; Flags: n
[UninstallRun]
; Stop the daemon before the uninstaller deletes tty7-app.exe — the running daemon
; is the locked image of that file, so removing it fails otherwise. This runs at
; the start of uninstallation, before any files are removed. The installed binary
; is this version, which understands the flag; runhidden suppresses any flash and
; the call returns without opening a window. RunOnceId keys the entry so a repeated
; uninstall doesn't run it twice.
Filename: "{app}\tty7-app.exe"; Parameters: "--stop-daemon"; Flags: runhidden waituntilterminated; RunOnceId: "StopDaemon"
; is the locked image of that file, so removing it fails otherwise. Naming {app}
; widens the stop the same way PrepareToInstall's does: ConPTY hosts orphaned by
; a daemon that never got to shut down keep the installed images open, and their
; DeleteFile fails an uninstall exactly as it fails an upgrade. (The stop excludes
; the calling process itself, so the binary running this step is safe.) This runs
; at the start of uninstallation, before any files are removed. The installed
; binary is this version, which understands the flags; runhidden suppresses any
; flash and the call returns without opening a window. RunOnceId keys the entry
; so a repeated uninstall doesn't run it twice.
Filename: "{app}\tty7-app.exe"; Parameters: "--stop-daemon --update-install-dir ""{app}"""; Flags: runhidden waituntilterminated; RunOnceId: "StopDaemon"
; Unconditional, and deliberately not gated on the task: an install that had the
; menu registered and was later upgraded without the box ticked still holds the
; keys, and verbs pointing at a deleted exe are worse than a no-op. Removing keys
+12 -13
View File
@@ -419,12 +419,12 @@ fn reap_recorded_daemon() {
.is_some_and(|entry| is_reapable_daemon_name(&entry.name));
if matches {
log::warn!("reaping unreachable daemon (pid {pid}); its sessions will be hung up");
for descendant in winproc::descendants(&procs, pid) {
winproc::terminate(descendant);
winproc::wait_for_exit(descendant, REAP_WAIT_TIMEOUT);
}
winproc::terminate(pid);
winproc::wait_for_exit(pid, REAP_WAIT_TIMEOUT);
// One deadline across the whole tree: this runs synchronously before
// the first window exists, and a crash that left many hosts behind
// must not multiply the wait by their count.
let mut targets = winproc::descendants(&procs, pid);
targets.push(pid);
winproc::terminate_and_wait_all(&targets, Instant::now() + REAP_WAIT_TIMEOUT);
}
pidfile::remove();
}
@@ -455,12 +455,8 @@ pub fn stop_for_update(install_dir: &Path) -> Result<(), String> {
"terminating pid {pid} still running from {}",
install_dir.display()
);
winproc::terminate(pid);
}
for pid in holdouts {
let left = deadline.saturating_duration_since(Instant::now());
winproc::wait_for_exit(pid, left);
}
winproc::terminate_and_wait_all(&holdouts, deadline);
wait_until_images_unlocked(install_dir, deadline)
}
@@ -491,8 +487,11 @@ fn wait_until_images_unlocked(dir: &Path, deadline: Instant) -> Result<(), Strin
})
})
.filter(|path| {
own.as_deref()
.is_none_or(|own| std::fs::canonicalize(path).is_ok_and(|path| path != own))
// Excluding takes a positive identification: a candidate that
// cannot be canonicalized (delete-pending, held by a scanner) is
// a lock to wait out, not ours to skip.
!own.as_deref()
.is_some_and(|own| std::fs::canonicalize(path).is_ok_and(|path| path == own))
})
.collect();
+19 -11
View File
@@ -135,21 +135,29 @@ pub(crate) fn wait_for_exit(pid: u32, timeout: std::time::Duration) -> bool {
}
}
/// Terminates every pid in order and waits for each to release its image,
/// all bounded by the one `deadline` — never a fresh timeout per process.
/// Callers pass parents after children when a parent could respawn one.
pub(crate) fn terminate_and_wait_all(pids: &[u32], deadline: std::time::Instant) {
for &pid in pids {
terminate(pid);
}
for &pid in pids {
let left = deadline.saturating_duration_since(std::time::Instant::now());
if !wait_for_exit(pid, left) {
log::warn!("process {pid} did not exit in time");
}
}
}
/// Terminates every descendant of `root` and waits for them to release their
/// images, bounded by `timeout` overall. Deepest-first, like the per-pane
/// kill, so a parent never respawns a child we already visited.
pub(crate) fn reap_descendants_of(root: u32, timeout: std::time::Duration) {
let deadline = std::time::Instant::now() + timeout;
let targets = descendants(&snapshot(), root);
for &pid in &targets {
terminate(pid);
}
for pid in targets {
let left = deadline.saturating_duration_since(std::time::Instant::now());
if !wait_for_exit(pid, left) {
log::warn!("descendant process {pid} did not exit in time");
}
}
terminate_and_wait_all(
&descendants(&snapshot(), root),
std::time::Instant::now() + timeout,
);
}
/// Pids (other than the caller's own) whose executable image lives under