Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb596c8aa9 | ||
|
|
f22058355b | ||
|
|
13fd2367bd | ||
|
|
f8d47cd3b5 |
+31
-8
@@ -38,15 +38,28 @@ sudo pacman -S base-devel pkgconf gtk3 webkit2gtk-4.1 nodejs npm
|
||||
```
|
||||
|
||||
**Go and node do not come from the package manager.** No current distribution
|
||||
ships a Go new enough for `go.mod` (Ubuntu 24.04 / Mint 22 have 1.22, Ubuntu
|
||||
22.04 / Mint 21 have 1.18), and Ubuntu 22.04 / Mint 21 ship node 12 where Vite
|
||||
needs 18. Both are the usual reason a first build fails with an error that
|
||||
points somewhere else entirely:
|
||||
ships a Go new enough for `go.mod` (Debian 12 has 1.19, Debian 13 has 1.24,
|
||||
Ubuntu 24.04 / Mint 22 have 1.22), and Ubuntu 22.04 / Mint 21 ship node 12 where
|
||||
Vite needs 18. Both are the usual reason a first build fails with an error that
|
||||
points somewhere else entirely.
|
||||
|
||||
**Take 1.26, not the latest.** Go can also be too NEW. The Wails CLI parses this
|
||||
package with the `golang.org/x/tools` its own `go.mod` pins, and that release
|
||||
cannot read the export data a newer compiler writes — so a fresh `go1.27` fails
|
||||
the build with something that names neither Go nor Wails:
|
||||
|
||||
```
|
||||
internal error: package "math" without types was imported from "hamlog/internal/geo"
|
||||
```
|
||||
|
||||
Met on Debian 13, where go.dev simply offers 1.27 as the current release. 1.25
|
||||
and 1.26 are what this repository is built with. If you already installed a
|
||||
newer one, replace it and reinstall the CLI so it is rebuilt:
|
||||
|
||||
```bash
|
||||
# Go, from go.dev
|
||||
wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz
|
||||
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz
|
||||
wget https://go.dev/dl/go1.26.3.linux-amd64.tar.gz
|
||||
sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.26.3.linux-amd64.tar.gz
|
||||
echo 'export PATH=/usr/local/go/bin:$HOME/go/bin:$PATH' >> ~/.profile # log out and back in
|
||||
|
||||
# node 20, only if `node -v` is below 18
|
||||
@@ -60,8 +73,18 @@ go install github.com/wailsapp/wails/v2/cmd/[email protected]
|
||||
wails doctor # says what is still missing
|
||||
```
|
||||
|
||||
`libwebkit2gtk-4.0` also works; pass `-tags webkit2_40` to `wails build` if your
|
||||
distribution only has the older one.
|
||||
**The webkit version decides a build tag.** Wails v2.11 defaults to
|
||||
webkit2gtk-**4.0**; to build against **4.1** — which is all Debian 13 ships —
|
||||
it has to be told:
|
||||
|
||||
```bash
|
||||
wails build -tags webkit2_41 # webkit 4.1 (Debian 13, Fedora, Arch)
|
||||
wails build # webkit 4.0 (Debian 12 and older)
|
||||
```
|
||||
|
||||
Without the tag on a 4.1-only machine the frontend builds, npm runs, and then
|
||||
cgo stops with `Package webkit2gtk-4.0 was not found in the pkg-config search
|
||||
path`. `linux-setup.sh` picks the tag for you.
|
||||
|
||||
## Build
|
||||
|
||||
|
||||
+35
-2
@@ -2,5 +2,38 @@
|
||||
|
||||
package main
|
||||
|
||||
// fatalBox is Windows-only; elsewhere the terminal carries the message.
|
||||
func fatalBox(title, text string) { println(title + ": " + text) }
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// fatalBox says why OpsLog is not starting, as visibly as the desktop allows.
|
||||
//
|
||||
// It used to be one println. That goes to stderr, and a binary started from a
|
||||
// file manager or a .desktop launcher has no stderr anybody will ever read — so
|
||||
// the refusal that was carefully worded arrived as "I click it and nothing
|
||||
// happens", which is the least useful thing a program can say.
|
||||
//
|
||||
// So: a real dialog when the desktop has one of the two tools that every
|
||||
// distribution ships with its desktop task, and stderr regardless. Neither is
|
||||
// required — the startup log has the same text, and nothing here is allowed to
|
||||
// stop OpsLog from exiting.
|
||||
func fatalBox(title, text string) {
|
||||
os.Stderr.WriteString(title + ": " + text + "\n")
|
||||
for _, c := range [][]string{
|
||||
{"zenity", "--error", "--no-wrap", "--title", title, "--text", text},
|
||||
{"kdialog", "--title", title, "--error", text},
|
||||
// Neither is a hard requirement, and on a headless box there is nothing
|
||||
// to show a dialog on anyway.
|
||||
} {
|
||||
if _, err := exec.LookPath(c[0]); err != nil {
|
||||
continue
|
||||
}
|
||||
cmd := exec.Command(c[0], c[1:]...)
|
||||
if err := cmd.Start(); err != nil {
|
||||
continue
|
||||
}
|
||||
_ = cmd.Wait()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -31,28 +29,3 @@ func TestRelaunchCmdPassesItsArgumentsAndFolder(t *testing.T) {
|
||||
t.Errorf("Dir = %q, want the executable's folder", cmd.Dir)
|
||||
}
|
||||
}
|
||||
|
||||
// The update relaunch goes through a helper that OUTLIVES this process.
|
||||
//
|
||||
// Two rewrites started the new exe from here instead, and both left operators
|
||||
// with no window after an update: the launch then happens while this process is
|
||||
// still alive and still holds the mutex. The helper waits for our pid first.
|
||||
// Restored from before 0430aab and pinned here so a third rewrite has to argue
|
||||
// with the two reports rather than rediscover them.
|
||||
func TestUpdateRelaunchWaitsForUsFromOutside(t *testing.T) {
|
||||
src, err := os.ReadFile("update.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read update.go: %v", err)
|
||||
}
|
||||
got := string(src)
|
||||
for _, want := range []string{"Wait-Process -Id", "Start-Process -FilePath"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("update.go no longer contains %q — the relaunch must wait for this process from outside it", want)
|
||||
}
|
||||
}
|
||||
// Start-Process shows the new window normally. A direct exec.Command(exe…)
|
||||
// here is the shape that broke it, twice.
|
||||
if strings.Contains(got, "exec.Command(exe") {
|
||||
t.Error("update.go starts the new exe directly again")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -48,3 +50,28 @@ func TestRelaunchIsDetached(t *testing.T) {
|
||||
t.Error("CREATE_NEW_PROCESS_GROUP is missing: a cleanup aimed at the old instance can reach the new one")
|
||||
}
|
||||
}
|
||||
|
||||
// The update relaunch goes through a helper that OUTLIVES this process.
|
||||
//
|
||||
// Two rewrites started the new exe from inside the dying one instead, and both
|
||||
// left operators with no window after an update: the launch then happens while
|
||||
// this process is still alive and still holds the mutex. The helper waits for
|
||||
// our pid first. Restored from before 0430aab and pinned here so a third
|
||||
// rewrite has to argue with the two reports rather than rediscover them.
|
||||
func TestWindowsUpdateRelaunchWaitsForUsFromOutside(t *testing.T) {
|
||||
src, err := os.ReadFile("updateswap_windows.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read updateswap_windows.go: %v", err)
|
||||
}
|
||||
got := string(src)
|
||||
for _, want := range []string{"Wait-Process -Id", "Start-Process -FilePath"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("the relaunch no longer contains %q — it must wait for this process from outside it", want)
|
||||
}
|
||||
}
|
||||
// Start-Process shows the new window normally. A direct exec.Command(exe…)
|
||||
// is the shape that broke it, twice.
|
||||
if strings.Contains(got, "exec.Command(exe") {
|
||||
t.Error("the Windows relaunch starts the new exe directly again")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestEveryRelaunchPassesItsPid(t *testing.T) {
|
||||
// quietly matched nothing at all — a guard that passes because it looks
|
||||
// nowhere.
|
||||
spawn := regexp.MustCompile(`(exec\.Command|relaunchCmd)\(exe, "--(post-update|relaunch)"[^)]*\)`)
|
||||
for _, file := range []string{"update.go", "app.go"} {
|
||||
for _, file := range []string{"update.go", "app.go", "updateswap_linux.go"} {
|
||||
src, err := os.ReadFile(file)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", file, err)
|
||||
|
||||
+58
-14
@@ -37,17 +37,26 @@ have gcc || have cc || note "a C compiler (Wails links against the system WebKit
|
||||
have pkg-config || note "pkg-config"
|
||||
pkg gtk+-3.0 || note "GTK 3 development headers"
|
||||
|
||||
# Wails 2.10+ builds against webkit2gtk-4.1; Debian 12 and older still ship 4.0,
|
||||
# which works with an extra build tag. Detect which one is present rather than
|
||||
# telling the operator to guess.
|
||||
# Which webkit, and therefore which build tag.
|
||||
#
|
||||
# Wails v2.11 defaults to webkit2gtk-4.0 and takes 4.1 only when told:
|
||||
# `#cgo !webkit2_41 pkg-config: webkit2gtk-4.0` against
|
||||
# `#cgo webkit2_41 pkg-config: webkit2gtk-4.1`. This script had it the other
|
||||
# way round and passed no tag on a Debian 13 box that has only 4.1, so the
|
||||
# frontend built and then cgo stopped with "Package webkit2gtk-4.0 was not
|
||||
# found in the pkg-config search path" — after several minutes of npm.
|
||||
#
|
||||
# libsoup travels with it: 4.0 pairs with libsoup-2.4, 4.1 with libsoup-3.0.
|
||||
webkit_tags=""
|
||||
if pkg webkit2gtk-4.1; then
|
||||
ok "webkit2gtk-4.1"
|
||||
ok "webkit2gtk-4.1 (building with -tags webkit2_41)"
|
||||
webkit_tags="-tags webkit2_41"
|
||||
pkg libsoup-3.0 || note "libsoup 3 development headers (webkit 4.1 links against them)"
|
||||
elif pkg webkit2gtk-4.0; then
|
||||
ok "webkit2gtk-4.0 (older — will build with -tags webkit2_40)"
|
||||
webkit_tags="-tags webkit2_40"
|
||||
ok "webkit2gtk-4.0 (the default — no tag needed)"
|
||||
pkg libsoup-2.4 || note "libsoup 2.4 development headers (webkit 4.0 links against them)"
|
||||
else
|
||||
note "webkit2gtk development headers (4.1 preferred, 4.0 accepted)"
|
||||
note "webkit2gtk development headers (4.1 or 4.0)"
|
||||
fi
|
||||
|
||||
# Node, and its VERSION — this is the trap on an LTS base. Ubuntu 22.04 (so
|
||||
@@ -77,10 +86,28 @@ if have go; then
|
||||
# golang-go` gets you a Go that cannot build this repository at all.
|
||||
if ! go version | grep -Eq 'go1\.(2[5-9]|[3-9][0-9])'; then
|
||||
note "go 1.25+ — $gov is too old for go.mod, and no apt package is new enough"
|
||||
ylw " wget https://go.dev/dl/go1.25.1.linux-amd64.tar.gz"
|
||||
ylw " sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.25.1.linux-amd64.tar.gz"
|
||||
ylw " wget https://go.dev/dl/go1.26.3.linux-amd64.tar.gz"
|
||||
ylw " sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.26.3.linux-amd64.tar.gz"
|
||||
ylw " echo 'export PATH=/usr/local/go/bin:\$HOME/go/bin:\$PATH' >> ~/.profile # then log out and back in"
|
||||
fi
|
||||
# And TOO NEW breaks too, which is the harder one to meet cold. The Wails
|
||||
# CLI parses this package with the golang.org/x/tools its own go.mod pins,
|
||||
# and that release cannot read the export data a newer compiler writes. The
|
||||
# failure names neither Go nor Wails:
|
||||
#
|
||||
# internal error: package "math" without types was imported from
|
||||
# "hamlog/internal/geo"
|
||||
#
|
||||
# Met on Debian 13 with go1.27.1, which is simply what go.dev offers as
|
||||
# latest. 1.25 and 1.26 are the versions this repository is built with.
|
||||
if go version | grep -Eq 'go1\.(2[7-9]|[3-9][0-9])'; then
|
||||
ylw " $gov is NEWER than the Wails CLI can parse this package with."
|
||||
ylw " The build fails with: internal error: package \"math\" without types."
|
||||
ylw " Install 1.26 (or 1.25) and rebuild the CLI with it:"
|
||||
ylw " wget https://go.dev/dl/go1.26.3.linux-amd64.tar.gz"
|
||||
ylw " sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.26.3.linux-amd64.tar.gz"
|
||||
ylw " go install github.com/wailsapp/wails/v2/cmd/[email protected]"
|
||||
fi
|
||||
else
|
||||
note "go 1.25+ — from https://go.dev/dl/, NOT from apt (no distribution ships 1.25 yet)"
|
||||
fi
|
||||
@@ -129,12 +156,29 @@ else
|
||||
fi
|
||||
|
||||
head_ "Sound server"
|
||||
if have pactl && pactl info >/dev/null 2>&1; then
|
||||
ok "$(pactl info | sed -n 's/^Server Name: //p')"
|
||||
# OpsLog speaks the PulseAudio protocol itself (jfreymuth/pulse) and connects to
|
||||
# the NATIVE SOCKET. That socket is what decides whether audio works — not
|
||||
# whether pactl is installed, which is a separate package (pulseaudio-utils).
|
||||
# This check used to ask for pactl and told a Debian 13 desktop running PipeWire
|
||||
# perfectly well that it had no sound server at all.
|
||||
sock="${PULSE_SERVER:-}"
|
||||
sock="${sock#unix:}"
|
||||
if [ -z "$sock" ] && [ -n "${XDG_RUNTIME_DIR:-}" ]; then
|
||||
sock="$XDG_RUNTIME_DIR/pulse/native"
|
||||
fi
|
||||
if [ -n "$sock" ] && [ -S "$sock" ]; then
|
||||
if have pactl && pactl info >/dev/null 2>&1; then
|
||||
ok "$(pactl info | sed -n 's/^Server Name: //p')"
|
||||
else
|
||||
ok "socket at $sock (install pulseaudio-utils if you want its name)"
|
||||
fi
|
||||
else
|
||||
ylw " No PulseAudio/PipeWire server answered. The voice keyer, the QSO"
|
||||
ylw " recorder and the CW decoder will report they cannot reach it."
|
||||
ylw " Everything else works without one."
|
||||
ylw " No PulseAudio/PipeWire socket found."
|
||||
ylw " The voice keyer, the QSO recorder and the CW decoder will report they"
|
||||
ylw " cannot reach it. Everything else works without one."
|
||||
ylw " sudo apt install pipewire-pulse wireplumber # Debian/Ubuntu"
|
||||
ylw " And run this from the DESKTOP session: the sound server belongs to the"
|
||||
ylw " logged-in session, so a remote shell may not see it."
|
||||
fi
|
||||
|
||||
head_ "Building"
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -214,51 +213,14 @@ func (a *App) DownloadAndApplyUpdate(url string) error {
|
||||
}
|
||||
applog.Printf("update: installed new build, scheduling relaunch")
|
||||
|
||||
// A DETACHED, HIDDEN POWERSHELL waits for this process to exit and THEN
|
||||
// starts the new exe. Restored, verbatim, from before 0430aab.
|
||||
//
|
||||
// Two rewrites tried to do without it and both failed on real stations.
|
||||
// Starting the new exe from here means launching it while this process is
|
||||
// still alive — and the comment on the original said exactly what that
|
||||
// costs: "Launching the new exe directly while we're still alive raced the
|
||||
// mutex and often left nothing running". Telling the new instance our pid so
|
||||
// it could wait on the other side looked equivalent and was not; operators
|
||||
// kept reporting no window after an update, on 0.27.23 and again after.
|
||||
// What the helper has that neither rewrite did is that it OUTLIVES us: the
|
||||
// launch happens after this process is completely gone, from a process that
|
||||
// was never our child.
|
||||
//
|
||||
// The cost is known and accepted. Windows Defender removed 0.27.14 from a
|
||||
// station as Trojan:Script/Wacatac.H!ml: an unsigned binary that replaces
|
||||
// itself, clears the mark-of-the-web and spawns a windowless script to start
|
||||
// another executable has the shape of a dropper, and the model reads shapes,
|
||||
// not intentions. The operator's answer is to allow OpsLog in Defender. An
|
||||
// updater that works and occasionally needs whitelisting beats one that
|
||||
// leaves people with no running program.
|
||||
//
|
||||
// HideWindow here is right and is NOT the bug that made the updated OpsLog
|
||||
// invisible: it hides POWERSHELL's console, which is the whole point. The
|
||||
// new OpsLog is started by Start-Process, with a normal show.
|
||||
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.Dir = dir
|
||||
hideConsole(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
// How the new build gets started differs by platform — see
|
||||
// scheduleRelaunch in updateswap_windows.go and updateswap_linux.go. On
|
||||
// Windows it is a helper that outlives us; on Linux it is simply the new
|
||||
// binary, because nothing there holds an image open.
|
||||
if err := a.scheduleRelaunch(exe, dir); err != nil {
|
||||
applog.Printf("update: the relaunch could not be started: %v", err)
|
||||
return fmt.Errorf("schedule relaunch: %w", err)
|
||||
return err
|
||||
}
|
||||
// The HELPER's pid, so the log says the launcher was started and not just
|
||||
// that we meant to. The new OpsLog logs its own arrival in startup.log; the
|
||||
// two together tell "the helper never ran" from "it ran and the exe did not
|
||||
// start", which have different causes.
|
||||
applog.Printf("update: relaunch helper started as pid %d — it waits for this process (pid %d) to exit, then starts %s",
|
||||
cmd.Process.Pid, os.Getpid(), filepath.Base(exe))
|
||||
// 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 handle.
|
||||
_ = cmd.Process.Release()
|
||||
if a.ctx != nil {
|
||||
wruntime.Quit(a.ctx)
|
||||
} else {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"hamlog/internal/applog"
|
||||
)
|
||||
@@ -40,3 +41,23 @@ func (a *App) scheduleDeferredSwap(exe, pending string) error {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -22,6 +22,49 @@ func clearDownloadMark(path string) { _ = os.Remove(path + ":Zone.Identifier") }
|
||||
// makeExecutable is a no-op on Windows, where the extension decides.
|
||||
func makeExecutable(path string) error { return nil }
|
||||
|
||||
// scheduleRelaunch starts the new build once THIS process is gone.
|
||||
//
|
||||
// A detached, hidden PowerShell waits for our pid and then launches the exe.
|
||||
// Restored verbatim from before 0430aab, after two rewrites that started the
|
||||
// new exe from inside the dying process both failed on real stations — the
|
||||
// original's own comment had already said why: "Launching the new exe
|
||||
// directly while we're still alive raced the mutex and often left nothing
|
||||
// running." Telling the new instance our pid so it could wait on the other
|
||||
// side looked equivalent and was not. What the helper has that neither
|
||||
// rewrite did is that it OUTLIVES us.
|
||||
//
|
||||
// The cost is known and accepted: Windows Defender removed 0.27.14 from a
|
||||
// station as Trojan:Script/Wacatac.H!ml, because an unsigned binary that
|
||||
// replaces itself, clears the mark-of-the-web and spawns a windowless script
|
||||
// to start another executable has the shape of a dropper. Allowing OpsLog in
|
||||
// Defender beats an updater that leaves people with no running program.
|
||||
//
|
||||
// HideWindow is right here and is NOT the bug that made the updated OpsLog
|
||||
// invisible: it hides POWERSHELL's console, which is the point, while
|
||||
// Start-Process shows the new window normally.
|
||||
func (a *App) scheduleRelaunch(exe, dir string) error {
|
||||
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.Dir = dir
|
||||
hideConsole(cmd)
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("schedule relaunch: %w", err)
|
||||
}
|
||||
// The HELPER's pid, so the log says the launcher was started and not just
|
||||
// that we meant to. The new OpsLog logs its own arrival in startup.log; the
|
||||
// two together tell "the helper never ran" from "it ran and the exe did not
|
||||
// start", which have different causes.
|
||||
applog.Printf("update: relaunch helper started as pid %d — it waits for this process (pid %d) to exit, then starts %s",
|
||||
cmd.Process.Pid, os.Getpid(), filepath.Base(exe))
|
||||
// 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 handle.
|
||||
_ = cmd.Process.Release()
|
||||
return nil
|
||||
}
|
||||
|
||||
// scheduleDeferredSwap hands the exe swap to a detached helper that runs AFTER
|
||||
// this process is gone.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user