improved settings window

This commit is contained in:
onysd 2026-08-26 02:44:59 +03:00
parent 80bc8d352c
commit 1c9a192a96
13 changed files with 271 additions and 89 deletions

View file

@ -0,0 +1,9 @@
//go:build !windows
package procctl
import "os/exec"
// hideWindow is a no-op on non-Windows -- there is no console window to
// suppress. See hidewindow_windows.go for why this exists at all.
func hideWindow(cmd *exec.Cmd) {}

View file

@ -0,0 +1,25 @@
//go:build windows
package procctl
import (
"os/exec"
"syscall"
)
// hideWindow stops the spawned process from popping a console window on
// Windows. Every exec.Command in this package (tasklist, taskkill, docker,
// git, go build) is a console-subsystem binary -- when the caller (this
// admin panel process) has no console of its own to inherit (the normal
// case once it's running detached, per procctl's own launch()), Windows
// implicitly creates a brand new one for each child. With the live
// Services-tab polling calling tasklist + docker compose ps every few
// seconds, that showed up as a terminal window flashing on screen
// repeatedly. CREATE_NO_WINDOW suppresses it without changing anything
// about how the child runs or what output it produces.
func hideWindow(cmd *exec.Cmd) {
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.HideWindow = true
}

View file

@ -133,13 +133,17 @@ func pidAlive(pid int) bool {
return false
}
if runtime.GOOS == "windows" {
out, err := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid)).Output()
cmd := exec.Command("tasklist", "/FI", fmt.Sprintf("PID eq %d", pid))
hideWindow(cmd)
out, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(out), strconv.Itoa(pid))
}
return exec.Command("kill", "-0", strconv.Itoa(pid)).Run() == nil
cmd := exec.Command("kill", "-0", strconv.Itoa(pid))
hideWindow(cmd)
return cmd.Run() == nil
}
// killPID mirrors kill_pid() in server-panel.py, MINUS its "/T" tree-kill on
@ -162,12 +166,18 @@ func killPID(pid int) {
return
}
if runtime.GOOS == "windows" {
_ = exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F").Run()
cmd := exec.Command("taskkill", "/PID", strconv.Itoa(pid), "/F")
hideWindow(cmd)
_ = cmd.Run()
return
}
_ = exec.Command("kill", "-TERM", strconv.Itoa(pid)).Run()
term := exec.Command("kill", "-TERM", strconv.Itoa(pid))
hideWindow(term)
_ = term.Run()
time.Sleep(time.Second)
_ = exec.Command("kill", "-KILL", strconv.Itoa(pid)).Run()
kill := exec.Command("kill", "-KILL", strconv.Itoa(pid))
hideWindow(kill)
_ = kill.Run()
}
// launch starts exePath detached, cwd=Root, stdout/stderr appended to
@ -189,6 +199,7 @@ func (m *Manager) launch(exePath, logPath string) (int, error) {
cmd.Stdout = logf
cmd.Stderr = logf
cmd.Stdin = nil
hideWindow(cmd)
if err := cmd.Start(); err != nil {
return 0, fmt.Errorf("start %s: %w", exePath, err)
}
@ -224,6 +235,7 @@ func (m *Manager) ensureDocker(ctx context.Context, st State) (string, error) {
"TELESRV_DOCKER_PROJECT="+st.DockerProject,
"TELESRV_DOCKER_PREFIX="+st.DockerPrefix,
)
hideWindow(cmd)
out, err := cmd.CombinedOutput()
log := "$ docker compose up -d\n" + string(out)
if err != nil {
@ -233,6 +245,7 @@ func (m *Manager) ensureDocker(ctx context.Context, st State) (string, error) {
deadline := time.Now().Add(postgresWaitTimeout)
for {
pgCmd := exec.CommandContext(ctx, "docker", "exec", st.DockerPrefix+"-postgres", "pg_isready", "-U", "telesrv", "-d", "telesrv")
hideWindow(pgCmd)
if pgCmd.Run() == nil {
return log + "\nPostgreSQL ready\n", nil
}
@ -284,6 +297,7 @@ func (m *Manager) DockerStatus(ctx context.Context) ([]DockerService, error) {
"TELESRV_DOCKER_PROJECT="+st.DockerProject,
"TELESRV_DOCKER_PREFIX="+st.DockerPrefix,
)
hideWindow(cmd)
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("docker compose ps failed: %w", err)
@ -303,6 +317,32 @@ func (m *Manager) DockerStatus(ctx context.Context) ([]DockerService, error) {
return services, nil
}
// CheckUpdates fetches from the remote and reports how many commits the
// local branch is behind its upstream, WITHOUT pulling or building anything
// -- the Services tab's "Check updates" button uses this to decide whether
// to offer a real Update (GitPull + rebuild + restart) or tell the operator
// they're already current.
func (m *Manager) CheckUpdates(ctx context.Context) (int, error) {
fetchCmd := exec.CommandContext(ctx, "git", "fetch")
fetchCmd.Dir = m.Root
hideWindow(fetchCmd)
if out, err := fetchCmd.CombinedOutput(); err != nil {
return 0, fmt.Errorf("git fetch failed: %w: %s", err, strings.TrimSpace(string(out)))
}
countCmd := exec.CommandContext(ctx, "git", "rev-list", "--count", "HEAD..@{upstream}")
countCmd.Dir = m.Root
hideWindow(countCmd)
out, err := countCmd.Output()
if err != nil {
return 0, fmt.Errorf("git rev-list failed: %w", err)
}
n, err := strconv.Atoi(strings.TrimSpace(string(out)))
if err != nil {
return 0, fmt.Errorf("parse commit count: %w", err)
}
return n, nil
}
// --- build steps ---------------------------------------------------------
// GitPull runs `git pull --ff-only`, deliberately never a real merge -- see
@ -310,6 +350,7 @@ func (m *Manager) DockerStatus(ctx context.Context) ([]DockerService, error) {
func (m *Manager) GitPull(ctx context.Context) (string, error) {
cmd := exec.CommandContext(ctx, "git", "pull", "--ff-only")
cmd.Dir = m.Root
hideWindow(cmd)
out, err := cmd.CombinedOutput()
log := "$ git pull --ff-only\n" + string(out)
return log, err
@ -339,6 +380,7 @@ func (m *Manager) goBuild(ctx context.Context, outPath, pkg string) (string, err
}
cmd := exec.CommandContext(ctx, "go", "build", "-o", outPath, pkg)
cmd.Dir = m.Root
hideWindow(cmd)
out, err := cmd.CombinedOutput()
return fmt.Sprintf("$ go build -o %s %s\n%s", filepath.Base(outPath), pkg, string(out)), err
}