up
This commit is contained in:
+198
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"hamlog/internal/applog"
|
||||
|
||||
wruntime "github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
// keyAutostartPrograms holds the per-profile list of external programs OpsLog
|
||||
// launches on startup (JSON array of AutostartProgram).
|
||||
const keyAutostartPrograms = "autostart.programs"
|
||||
|
||||
// AutostartProgram is one external application OpsLog can launch when it starts
|
||||
// — e.g. WSJT-X, JTAlert, a rotator controller. Stored per profile.
|
||||
type AutostartProgram struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
Args string `json:"args"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
// AutostartLaunchResult reports what happened for one program when launching.
|
||||
type AutostartLaunchResult struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"` // launched | already_running | missing | disabled | error
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// GetAutostartPrograms returns the active profile's autostart list.
|
||||
func (a *App) GetAutostartPrograms() ([]AutostartProgram, error) {
|
||||
out := []AutostartProgram{}
|
||||
if a.settings == nil {
|
||||
return out, nil
|
||||
}
|
||||
s, _ := a.settings.Get(a.ctx, keyAutostartPrograms)
|
||||
if strings.TrimSpace(s) == "" {
|
||||
return out, nil
|
||||
}
|
||||
if err := json.Unmarshal([]byte(s), &out); err != nil {
|
||||
return []AutostartProgram{}, nil
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SaveAutostartPrograms persists the autostart list for the active profile.
|
||||
func (a *App) SaveAutostartPrograms(progs []AutostartProgram) error {
|
||||
if a.settings == nil {
|
||||
return fmt.Errorf("db not initialized")
|
||||
}
|
||||
b, err := json.Marshal(progs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.settings.Set(a.ctx, keyAutostartPrograms, string(b))
|
||||
}
|
||||
|
||||
// BrowseExecutable opens a native file picker for choosing a program to launch.
|
||||
func (a *App) BrowseExecutable() (string, error) {
|
||||
return wruntime.OpenFileDialog(a.ctx, wruntime.OpenDialogOptions{
|
||||
Title: "Choose a program to launch on startup",
|
||||
Filters: []wruntime.FileFilter{
|
||||
{DisplayName: "Programs (*.exe;*.bat;*.cmd)", Pattern: "*.exe;*.bat;*.cmd"},
|
||||
{DisplayName: "All files (*.*)", Pattern: "*.*"},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// LaunchAutostartPrograms starts every enabled program that isn't already
|
||||
// running, returning a per-program result. Used at startup (best effort) and by
|
||||
// the "Launch now" button in settings.
|
||||
func (a *App) LaunchAutostartPrograms() []AutostartLaunchResult {
|
||||
progs, _ := a.GetAutostartPrograms()
|
||||
running := runningProcessNames()
|
||||
out := make([]AutostartLaunchResult, 0, len(progs))
|
||||
for _, p := range progs {
|
||||
if !p.Enabled {
|
||||
continue
|
||||
}
|
||||
out = append(out, launchProgram(p, running))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LaunchAutostartProgram launches a single program by id on demand (the per-row
|
||||
// "launch now" action), regardless of its enabled flag.
|
||||
func (a *App) LaunchAutostartProgram(id string) (AutostartLaunchResult, error) {
|
||||
progs, err := a.GetAutostartPrograms()
|
||||
if err != nil {
|
||||
return AutostartLaunchResult{}, err
|
||||
}
|
||||
for _, p := range progs {
|
||||
if p.ID == id {
|
||||
return launchProgram(p, runningProcessNames()), nil
|
||||
}
|
||||
}
|
||||
return AutostartLaunchResult{}, fmt.Errorf("program %q not found", id)
|
||||
}
|
||||
|
||||
// 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}
|
||||
path := strings.TrimSpace(p.Path)
|
||||
if path == "" {
|
||||
res.Status, res.Message = "error", "no path configured"
|
||||
return res
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
res.Status, res.Message = "missing", "executable not found: "+path
|
||||
return res
|
||||
}
|
||||
// Skip if a process with the same executable name is already running, so we
|
||||
// never spawn a second copy of WSJT-X / JTAlert / etc.
|
||||
base := strings.ToLower(filepath.Base(path))
|
||||
if running[base] {
|
||||
res.Status, res.Message = "already_running", base+" already running"
|
||||
return res
|
||||
}
|
||||
cmd := exec.Command(path, splitArgs(p.Args)...)
|
||||
cmd.Dir = filepath.Dir(path) // many ham apps expect their own folder as CWD
|
||||
if err := cmd.Start(); err != nil {
|
||||
res.Status, res.Message = "error", err.Error()
|
||||
return res
|
||||
}
|
||||
// Don't wait on the child — it runs independently of OpsLog. Release the
|
||||
// handle so we don't accumulate zombies.
|
||||
go func() { _ = cmd.Wait() }()
|
||||
res.Status, res.Message = "launched", "started "+base
|
||||
return res
|
||||
}
|
||||
|
||||
// splitArgs does a minimal shell-like split of an argument string, honouring
|
||||
// double quotes so a path with spaces stays one argument.
|
||||
func splitArgs(s string) []string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
var args []string
|
||||
var cur strings.Builder
|
||||
inQuote := false
|
||||
for _, r := range s {
|
||||
switch {
|
||||
case r == '"':
|
||||
inQuote = !inQuote
|
||||
case r == ' ' && !inQuote:
|
||||
if cur.Len() > 0 {
|
||||
args = append(args, cur.String())
|
||||
cur.Reset()
|
||||
}
|
||||
default:
|
||||
cur.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if cur.Len() > 0 {
|
||||
args = append(args, cur.String())
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
// runningProcessNames returns the set of lowercase executable names currently
|
||||
// running, via the Windows `tasklist`. Best effort — on failure the set is
|
||||
// empty (we then just attempt to launch, which is acceptable).
|
||||
func runningProcessNames() map[string]bool {
|
||||
out := map[string]bool{}
|
||||
cmd := exec.Command("tasklist", "/FO", "CSV", "/NH")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true, CreationFlags: 0x08000000} // CREATE_NO_WINDOW
|
||||
data, err := cmd.Output()
|
||||
if err != nil {
|
||||
applog.Printf("autostart: tasklist failed: %v", err)
|
||||
return out
|
||||
}
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
// CSV row: "image.exe","PID",... — take the first quoted field.
|
||||
field := line
|
||||
if i := strings.Index(line[1:], "\""); i >= 0 && strings.HasPrefix(line, "\"") {
|
||||
field = line[1 : i+1]
|
||||
}
|
||||
field = strings.Trim(field, "\"")
|
||||
if field != "" {
|
||||
out[strings.ToLower(field)] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user