fix(windows): make the install directory actually replaceable before updating

The updater stopped the daemon and started the Inno installer the moment
the daemon's endpoint disappeared — but the endpoint going away is not
the same event as the images being released. The ConPTY hosts
(OpenConsole.exe) are the daemon's children, not the shells', so the
per-pane kill never reached them, and the daemon's exit(0) skipped every
destructor that would have closed them; they kept the installed
OpenConsole.exe open for seconds after --stop-daemon returned. Silent
Setup then hit the lock, took the suppressed dialog's default (Abort),
and the updater's recovery relaunched the old build — "updated,
restarted, still the old version". A daemon that died without cleaning
up made it permanent: its orphaned hosts survive indefinitely, which is
the DeleteFile-code-5 users hit even after "closing everything".

Reproduced both shapes in isolation before fixing: with a pane open,
--stop-daemon returned ~1s in while OpenConsole.exe stayed locked for
another ~1.4s; after taskkill on the daemon, the orphaned host held the
lock forever.

The shutdown now finishes what it starts, at every layer that can be
the last one standing:

  * The daemon reaps its remaining descendants and waits for them
    before exiting, while the endpoint — the signal stop() watches —
    is still up.
  * stop() reads the pidfile before asking, and waits for that process
    to actually exit after the endpoint goes, not just stop listening.
  * The recorded-daemon reap waits for the images to be released
    instead of returning on the async TerminateProcess.
  * stop_for_update(dir) — reached via --stop-daemon
    --update-install-dir, which PrepareToInstall and the portable
    updater now pass — also terminates anything still running from the
    installation directory (the orphan case no pidfile can name) and
    only returns once the .exe/.dll images there open for writing,
    naming the holdouts in the error if they never do.
  * The updater runs that clearing itself before invoking Setup, so a
    directory that cannot be cleared fails with a cause in update.log
    and relaunches the previous build, instead of Inno's bare
    "DeleteFile failed; code 5".

The update dialog on Windows also told a macOS truth — "the background
service keeps running, so whatever is open in your panes survives".
Windows cannot replace a running daemon's image, so its install path
stops the service; the dialog now says so.
This commit is contained in:
l0ng-ai
2026-08-08 09:45:15 +08:00
parent 1f91719da9
commit ebec6096d6
11 changed files with 343 additions and 6 deletions
+9 -3
View File
@@ -148,14 +148,20 @@ Filename: "{app}\tty7-app.exe"; Parameters: "--unregister-explorer-menu"; Flags:
understand --stop-daemon and would launch the GUI instead — so we extract the
*new* tty7-app.exe to {tmp} and run that. It connects to the running daemon, hangs
up every shell, waits for it to exit (releasing the file lock), then returns
without opening a window. Best effort: any failure falls through to the Restart
Manager backstop, and a fresh install simply has no daemon to stop. *)
without opening a window. Naming {app} widens the stop into "make this directory
replaceable": ConPTY hosts (OpenConsole.exe) orphaned by a daemon that never got
to shut down keep the installed images open — invisible to the daemon stop, fatal
to the DeleteFile below — so anything still running from {app} is terminated and
the call waits until the images there actually open for writing. Best effort: any
failure falls through to the Restart Manager backstop, and a fresh install simply
has nothing to stop. *)
function PrepareToInstall(var NeedsRestart: Boolean): String;
var
ResultCode: Integer;
begin
ExtractTemporaryFile('tty7-app.exe');
Exec(ExpandConstant('{tmp}\tty7-app.exe'), '--stop-daemon', '',
Exec(ExpandConstant('{tmp}\tty7-app.exe'),
'--stop-daemon --update-install-dir "' + ExpandConstant('{app}') + '"', '',
SW_HIDE, ewWaitUntilTerminated, ResultCode);
Result := '';
end;