chore: release v0.26.2
This commit is contained in:
@@ -6,7 +6,9 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"hamlog/internal/applog"
|
||||
@@ -26,6 +28,12 @@ type AutostartProgram struct {
|
||||
Path string `json:"path"`
|
||||
Args string `json:"args"`
|
||||
Enabled bool `json:"enabled"`
|
||||
// CloseOnExit asks for this program to be closed when OpsLog closes.
|
||||
//
|
||||
// Per program and not one switch for the list, because the answer differs
|
||||
// inside one station: an operator wants WSJT-X gone with the logger and the
|
||||
// rotator controller left running.
|
||||
CloseOnExit bool `json:"close_on_exit,omitempty"`
|
||||
}
|
||||
|
||||
// AutostartLaunchResult reports what happened for one program when launching.
|
||||
@@ -106,6 +114,52 @@ func (a *App) LaunchAutostartProgram(id string) (AutostartLaunchResult, error) {
|
||||
return AutostartLaunchResult{}, fmt.Errorf("program %q not found", id)
|
||||
}
|
||||
|
||||
// launched remembers the process id of every program OPSLOG started, keyed by
|
||||
// program id.
|
||||
//
|
||||
// Only what we started is ever closed. A copy of WSJT-X the operator opened
|
||||
// themselves — before OpsLog, for something else entirely — is theirs, and
|
||||
// closing it because a logger happened to quit would be taking a decision that
|
||||
// was never asked for. That is also why "already running" stores nothing.
|
||||
var launched = struct {
|
||||
sync.Mutex
|
||||
pid map[string]int
|
||||
}{pid: map[string]int{}}
|
||||
|
||||
// CloseAutostartPrograms closes the programs marked "close with OpsLog" — the
|
||||
// ones OpsLog itself launched this session.
|
||||
//
|
||||
// A polite close, never a kill: taskkill without /F posts WM_CLOSE, so WSJT-X
|
||||
// writes its settings and its log the way it would if the operator had clicked
|
||||
// the cross. Forcing it would lose exactly the state an operator cares about,
|
||||
// and a program that ignores a close request is entitled to.
|
||||
func (a *App) CloseAutostartPrograms() {
|
||||
progs, _ := a.GetAutostartPrograms()
|
||||
for _, p := range progs {
|
||||
if !p.CloseOnExit {
|
||||
continue
|
||||
}
|
||||
launched.Lock()
|
||||
pid, ok := launched.pid[p.ID]
|
||||
delete(launched.pid, p.ID)
|
||||
launched.Unlock()
|
||||
if !ok || pid <= 0 {
|
||||
continue // not started by us this session — not ours to close
|
||||
}
|
||||
name := strings.TrimSpace(p.Name)
|
||||
if name == "" {
|
||||
name = filepath.Base(p.Path)
|
||||
}
|
||||
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid))
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000}
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
applog.Printf("autostart: could not close %s (pid %d): %v — %s", name, pid, err, strings.TrimSpace(string(out)))
|
||||
continue
|
||||
}
|
||||
applog.Printf("autostart: asked %s (pid %d) to close", name, pid)
|
||||
}
|
||||
}
|
||||
|
||||
// launchProgram starts one program unless its executable is already running.
|
||||
func launchProgram(p AutostartProgram, running map[string]bool) AutostartLaunchResult {
|
||||
res := AutostartLaunchResult{ID: p.ID, Name: p.Name}
|
||||
@@ -131,6 +185,13 @@ func launchProgram(p AutostartProgram, running map[string]bool) AutostartLaunchR
|
||||
res.Status, res.Message = "error", err.Error()
|
||||
return res
|
||||
}
|
||||
// Remembered so it can be closed again on exit, if asked. Recorded BEFORE
|
||||
// the wait goroutine, which releases the handle.
|
||||
if cmd.Process != nil {
|
||||
launched.Lock()
|
||||
launched.pid[p.ID] = cmd.Process.Pid
|
||||
launched.Unlock()
|
||||
}
|
||||
// Don't wait on the child — it runs independently of OpsLog. Release the
|
||||
// handle so we don't accumulate zombies.
|
||||
go func() { _ = cmd.Wait() }()
|
||||
|
||||
Reference in New Issue
Block a user