//go:build linux package main import ( "fmt" "os" "path/filepath" "strconv" "hamlog/internal/applog" ) // clearDownloadMark has nothing to clear on Linux: there is no // mark-of-the-web, and no SmartScreen to refuse a programmatic launch. func clearDownloadMark(path string) {} // makeExecutable restores the executable bit. A binary downloaded over HTTP // arrives 0644 — on Windows the extension decides and this is a no-op, but here // a freshly installed OpsLog that nothing can exec is a dead station. func makeExecutable(path string) error { return os.Chmod(path, 0o755) } // scheduleDeferredSwap is the fallback for when the running binary could not be // renamed out of the way — the path Windows needs a detached PowerShell helper // for, because nothing there can move a file over a running image. // // On Linux it should never be reached. A rename only touches the directory // entry, and the running process holds the inode, so replacing the binary of a // live process is ordinary and the staging rename in DownloadAndApplyUpdate // succeeds. If it did fail, the cause was the filesystem (read-only mount, no // write permission on the directory, a full disk) and no helper would get past // it either — so do the honest thing: try the move once more now, and say // plainly what is wrong if it still refuses. func (a *App) scheduleDeferredSwap(exe, pending string) error { if err := os.Rename(pending, exe); err != nil { return fmt.Errorf("install the new build: %w (the folder %s must be writable)", err, filepath.Dir(exe)) } if err := makeExecutable(exe); err != nil { return fmt.Errorf("make the new build executable: %w", err) } applog.Printf("update: installed %s over %s after the staging rename failed", filepath.Base(pending), filepath.Base(exe)) return nil } // scheduleRelaunch starts the new build. On Linux that is simply the new binary. // // None of the Windows machinery applies: nothing holds an executable open while // it runs, so the swap above already succeeded, and there is no mutex to race — // the single-instance guard is an flock the dying process releases as it exits, // and the new one waits for our pid before trying it. No helper, no script, and // nothing that needs to outlive us. func (a *App) scheduleRelaunch(exe, dir string) error { cmd := relaunchCmd(exe, "--post-update", "--wait-pid", strconv.Itoa(os.Getpid())) if err := cmd.Start(); err != nil { return fmt.Errorf("schedule relaunch: %w", err) } applog.Printf("update: relaunch started as pid %d — it waits for this process (pid %d) to exit", cmd.Process.Pid, os.Getpid()) // Released rather than waited on: this process is about to exit, and a child // that outlives its parent must not be left as a zombie. _ = cmd.Process.Release() return nil }