From d327db3f573ccda0e96841fac198329dd7bb1e5f Mon Sep 17 00:00:00 2001 From: rouggy Date: Sun, 19 Jul 2026 19:53:19 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20auto-update=20relaunch=20=E2=80=94=20cle?= =?UTF-8?q?ar=20Mark-of-the-Web=20+=20wait=20for=20exit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new build downloaded and OpsLog quit, but never came back. Two Windows causes: the freshly written exe carried the internet Zone.Identifier mark, so SmartScreen wanted to prompt "are you sure you want to open this?" — invisibly, since we launch it programmatically — and silently blocked the launch; and starting the new exe while the old one was still exiting raced the single- instance mutex. Now the swapped exe's Zone.Identifier stream is removed, and the relaunch is done by a detached, hidden PowerShell that Wait-Process's on our PID (so we're fully gone and the mutex is free) before Start-Process'ing the new exe. --- update.go | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/update.go b/update.go index 37b94b8..12b1c8b 100644 --- a/update.go +++ b/update.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strconv" "strings" + "syscall" "time" wruntime "github.com/wailsapp/wails/v2/pkg/runtime" @@ -159,14 +160,26 @@ func (a *App) DownloadAndApplyUpdate(url string) error { _ = os.Rename(oldExe, exe) // roll back return fmt.Errorf("install new exe: %w", err) } - applog.Printf("update: installed new exe, relaunching") + // Clear the "downloaded from the internet" mark (NTFS Zone.Identifier stream). + // Otherwise Windows SmartScreen wants to prompt "are you sure you want to open + // this?" — but since we launch the exe programmatically that prompt never shows, + // and the launch is silently blocked. This is exactly why the relaunch failed. + _ = os.Remove(exe + ":Zone.Identifier") + applog.Printf("update: installed new exe, scheduling relaunch") - // Relaunch with a flag so the fresh instance waits for THIS one to exit and - // free the single-instance mutex instead of bailing out immediately. - cmd := exec.Command(exe, "--post-update") - cmd.Dir = dir + // Relaunch via a detached, hidden PowerShell that WAITS for this process to exit + // (so the single-instance mutex is free) and THEN starts the new exe. Launching + // the new exe directly while we're still alive raced the mutex and often left + // nothing running; waiting for our own exit first makes the restart reliable, + // and the launcher outlives us. + quoted := strings.ReplaceAll(exe, "'", "''") + ps := fmt.Sprintf( + "Wait-Process -Id %d -ErrorAction SilentlyContinue; Start-Sleep -Milliseconds 400; Start-Process -FilePath '%s' -ArgumentList '--post-update'", + os.Getpid(), quoted) + cmd := exec.Command("powershell", "-NoProfile", "-WindowStyle", "Hidden", "-Command", ps) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000} // CREATE_NO_WINDOW if err := cmd.Start(); err != nil { - return fmt.Errorf("relaunch: %w", err) + return fmt.Errorf("schedule relaunch: %w", err) } if a.ctx != nil { wruntime.Quit(a.ctx)