diff --git a/packages/daemon-go/internal/detect/process_windows.go b/packages/daemon-go/internal/detect/process_windows.go index 1f75ad8..302f83b 100644 --- a/packages/daemon-go/internal/detect/process_windows.go +++ b/packages/daemon-go/internal/detect/process_windows.go @@ -11,6 +11,8 @@ import ( "encoding/json" "os/exec" "time" + + "github.com/dbisina/relay/internal/process" ) type procInfo struct { @@ -29,6 +31,7 @@ func listProcesses() ([]procInfo, error) { // Force array output so a single-process result still parses as JSON array. const ps = `@(Get-CimInstance Win32_Process | Select-Object ProcessId,Name,CommandLine) | ConvertTo-Json -Compress -Depth 3` cmd := exec.CommandContext(ctx, "powershell.exe", "-NoProfile", "-NonInteractive", "-Command", ps) + process.HideWindow(cmd) out, err := cmd.Output() if err != nil { return nil, err diff --git a/packages/daemon-go/internal/fsm/durability.go b/packages/daemon-go/internal/fsm/durability.go index d698a41..f680ce6 100644 --- a/packages/daemon-go/internal/fsm/durability.go +++ b/packages/daemon-go/internal/fsm/durability.go @@ -20,6 +20,8 @@ import ( "strconv" "strings" "time" + + "github.com/dbisina/relay/internal/process" ) const ( @@ -258,6 +260,7 @@ func (d *DurabilityManager) checkDrift(files []string) error { func (d *DurabilityManager) git(args ...string) error { cmd := exec.Command("git", args...) cmd.Dir = d.workDir + process.HideWindow(cmd) out, err := cmd.CombinedOutput() if err != nil { return fmt.Errorf("%w: %s", err, bytes.TrimSpace(out)) @@ -272,6 +275,7 @@ func (d *DurabilityManager) gitOutput(args ...string) (string, error) { func (d *DurabilityManager) gitOutputRaw(args ...string) (string, error) { cmd := exec.Command("git", args...) cmd.Dir = d.workDir + process.HideWindow(cmd) out, err := cmd.Output() if err != nil { return "", err diff --git a/packages/daemon-go/internal/process/job_unix.go b/packages/daemon-go/internal/process/job_unix.go index 81422c8..f8bb63d 100644 --- a/packages/daemon-go/internal/process/job_unix.go +++ b/packages/daemon-go/internal/process/job_unix.go @@ -22,6 +22,12 @@ func SetupChildProcess(cmd *exec.Cmd) error { return nil } +// HideWindow is a no-op on Unix: only Windows auto-allocates a console +// window for a spawned console-subsystem process with no console to inherit. +func HideWindow(cmd *exec.Cmd) { + _ = cmd +} + // AssignToJobObject is a no-op on Unix. func AssignToJobObject(cmd *exec.Cmd) error { return nil diff --git a/packages/daemon-go/internal/process/job_windows.go b/packages/daemon-go/internal/process/job_windows.go index f3ffab9..c8b2fe4 100644 --- a/packages/daemon-go/internal/process/job_windows.go +++ b/packages/daemon-go/internal/process/job_windows.go @@ -49,17 +49,36 @@ func initGlobalJob() { } // SetupChildProcess configures SysProcAttr so the child starts in a new -// process group (prevents Ctrl-C propagation) and can be assigned to the -// Job Object after Start(). +// process group (prevents Ctrl-C propagation), never gets a console window +// of its own, and can be assigned to the Job Object after Start(). +// +// HideWindow matters beyond cosmetics: since the desktop app's bundled +// daemon is itself linked with no console (a Windows GUI-subsystem binary, +// so Electron spawning it headlessly can't flash one either), any +// console-subsystem child it starts, the provider CLI this configures, +// would otherwise get a fresh console window auto-allocated by the loader +// because there is none to inherit. CREATE_NO_WINDOW stops that. func SetupChildProcess(cmd *exec.Cmd) error { if cmd.SysProcAttr == nil { cmd.SysProcAttr = &windows.SysProcAttr{} } // New process group: Ctrl-C in the terminal does not kill the agent. cmd.SysProcAttr.CreationFlags |= windows.CREATE_NEW_PROCESS_GROUP + cmd.SysProcAttr.HideWindow = true return nil } +// HideWindow prevents a background one-shot command (a git snapshot, a +// detection-scan probe, a verification step) from getting a console window, +// for callers that only need that and not the rest of SetupChildProcess's +// process-group / Job Object machinery meant for a long-lived agent. +func HideWindow(cmd *exec.Cmd) { + if cmd.SysProcAttr == nil { + cmd.SysProcAttr = &windows.SysProcAttr{} + } + cmd.SysProcAttr.HideWindow = true +} + // AssignToJobObject must be called immediately after cmd.Start(). // It attaches the new process to the global kill-on-close Job Object. func AssignToJobObject(cmd *exec.Cmd) error { diff --git a/packages/daemon-go/internal/verify/verify.go b/packages/daemon-go/internal/verify/verify.go index af4dde6..cab1276 100644 --- a/packages/daemon-go/internal/verify/verify.go +++ b/packages/daemon-go/internal/verify/verify.go @@ -15,6 +15,8 @@ import ( "runtime" "strings" "time" + + "github.com/dbisina/relay/internal/process" ) // Check is the result of running one acceptance command. @@ -78,6 +80,7 @@ func Run(ctx context.Context, workDir string, cmds []string) Result { c = exec.CommandContext(cctx, "sh", "-c", cmd) } c.Dir = workDir + process.HideWindow(c) var buf bytes.Buffer c.Stdout = &buf c.Stderr = &buf diff --git a/packages/daemon-go/internal/worktree/worktree.go b/packages/daemon-go/internal/worktree/worktree.go index 9908829..c6bcb83 100644 --- a/packages/daemon-go/internal/worktree/worktree.go +++ b/packages/daemon-go/internal/worktree/worktree.go @@ -16,6 +16,8 @@ import ( "os/exec" "path/filepath" "strings" + + "github.com/dbisina/relay/internal/process" ) // Manager — owns one worktree per session. @@ -176,6 +178,7 @@ func (m *Manager) ApplyDirtySnapshot(worktreePath string, snap *DirtySnapshot) e cmd := exec.Command("git", "-c", "core.autocrlf=false", "apply", "--whitespace=nowarn", "-") cmd.Dir = worktreePath cmd.Stdin = strings.NewReader(snap.Patch) + process.HideWindow(cmd) var out bytes.Buffer cmd.Stdout, cmd.Stderr = &out, &out if err := cmd.Run(); err != nil { @@ -198,6 +201,7 @@ func (m *Manager) ApplyDirtySnapshot(worktreePath string, snap *DirtySnapshot) e func (m *Manager) git(args ...string) (string, error) { cmd := exec.Command("git", args...) cmd.Dir = m.RepoDir + process.HideWindow(cmd) var out bytes.Buffer cmd.Stdout = &out cmd.Stderr = &out