cli: split banger.go god file into focused files
Pure code motion — banger.go 3508→240 LOC, same-package decomposition keeps all identifiers visible without export changes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3a5f4cd40d
commit
3f6ecb4376
12 changed files with 3478 additions and 3268 deletions
138
internal/cli/daemon_lifecycle.go
Normal file
138
internal/cli/daemon_lifecycle.go
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"banger/internal/api"
|
||||
"banger/internal/config"
|
||||
"banger/internal/model"
|
||||
"banger/internal/paths"
|
||||
"banger/internal/rpc"
|
||||
)
|
||||
|
||||
// ensureDaemon pings the socket; on miss it auto-starts bangerd, on
|
||||
// version mismatch it restarts. Every CLI command that needs to talk
|
||||
// to the daemon routes through here.
|
||||
func ensureDaemon(ctx context.Context) (paths.Layout, model.DaemonConfig, error) {
|
||||
layout, err := paths.Resolve()
|
||||
if err != nil {
|
||||
return paths.Layout{}, model.DaemonConfig{}, err
|
||||
}
|
||||
cfg, err := config.Load(layout)
|
||||
if err != nil {
|
||||
return paths.Layout{}, model.DaemonConfig{}, err
|
||||
}
|
||||
if ping, err := daemonPingFunc(ctx, layout.SocketPath); err == nil {
|
||||
if daemonOutdated(ping.PID) {
|
||||
if err := restartDaemon(ctx, layout, ping.PID); err != nil {
|
||||
return paths.Layout{}, model.DaemonConfig{}, err
|
||||
}
|
||||
return layout, cfg, nil
|
||||
}
|
||||
return layout, cfg, nil
|
||||
}
|
||||
if err := startDaemon(ctx, layout); err != nil {
|
||||
return paths.Layout{}, model.DaemonConfig{}, err
|
||||
}
|
||||
return layout, cfg, nil
|
||||
}
|
||||
|
||||
// daemonOutdated reports whether the running daemon binary differs
|
||||
// from the one on disk — useful after `make install` when the user's
|
||||
// session still holds a handle to an old daemon. os.SameFile compares
|
||||
// inode + dev, so a fresh binary at the same path registers as
|
||||
// different.
|
||||
func daemonOutdated(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
daemonBin, err := bangerdPathFunc()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
currentInfo, err := os.Stat(daemonBin)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
runningInfo, err := os.Stat(daemonExePath(pid))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !os.SameFile(currentInfo, runningInfo)
|
||||
}
|
||||
|
||||
func restartDaemon(ctx context.Context, layout paths.Layout, pid int) error {
|
||||
stopCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, _ = rpc.Call[api.ShutdownResult](stopCtx, layout.SocketPath, "shutdown", api.Empty{})
|
||||
if waitForPIDExit(pid, 2*time.Second) {
|
||||
return startDaemon(ctx, layout)
|
||||
}
|
||||
if proc, err := os.FindProcess(pid); err == nil {
|
||||
_ = proc.Signal(syscall.SIGTERM)
|
||||
}
|
||||
if !waitForPIDExit(pid, 2*time.Second) {
|
||||
return fmt.Errorf("timed out restarting stale daemon pid %d", pid)
|
||||
}
|
||||
return startDaemon(ctx, layout)
|
||||
}
|
||||
|
||||
func waitForPIDExit(pid int, timeout time.Duration) bool {
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if !pidRunning(pid) {
|
||||
return true
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
return !pidRunning(pid)
|
||||
}
|
||||
|
||||
func pidRunning(pid int) bool {
|
||||
if pid <= 0 {
|
||||
return false
|
||||
}
|
||||
proc, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return proc.Signal(syscall.Signal(0)) == nil
|
||||
}
|
||||
|
||||
func startDaemon(ctx context.Context, layout paths.Layout) error {
|
||||
if err := paths.Ensure(layout); err != nil {
|
||||
return err
|
||||
}
|
||||
logFile, err := os.OpenFile(layout.DaemonLog, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer logFile.Close()
|
||||
|
||||
daemonBin, err := paths.BangerdPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := buildDaemonCommand(daemonBin)
|
||||
cmd.Stdout = logFile
|
||||
cmd.Stderr = logFile
|
||||
cmd.Stdin = nil
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := rpc.WaitForSocket(layout.SocketPath, 5*time.Second); err != nil {
|
||||
return fmt.Errorf("daemon failed to start; inspect %s: %w", layout.DaemonLog, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildDaemonCommand(daemonBin string) *exec.Cmd {
|
||||
return exec.Command(daemonBin)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue