Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions cmd/iptw/daemon_unix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//go:build !windows

package main

import (
"os"
"os/exec"
"syscall"
)

// maybeDaemonize re-executes the process detached from the controlling
// terminal so the user can close the launching shell once the tray icon
// appears. It is a no-op when --foreground is passed.
//
// How it works:
// 1. The original (parent) process re-execs itself with all the same
// arguments plus "--foreground", using Setsid=true so the child gets a
// new session and is no longer attached to the terminal.
// 2. The parent then exits with code 0, freeing the terminal.
// 3. The child (which has --foreground set) skips this function and runs
// the application normally.
func maybeDaemonize(foreground bool) {
if foreground {
return
}

exe, err := os.Executable()
if err != nil {
// Cannot determine the executable path; just run in the foreground.
return
}

// Forward every original argument and add --foreground so the child does
// not daemonize again.
args := make([]string, 0, len(os.Args)-1+1)
args = append(args, os.Args[1:]...)
args = append(args, "--foreground")

cmd := exec.Command(exe, args...)
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
// Disconnect all standard streams so the child has no reference to the
// parent terminal.
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil

if err := cmd.Start(); err != nil {
// If we cannot spawn the background child, fall through and run
// normally in the foreground rather than failing silently.
return
}

// Parent exits, releasing the terminal.
os.Exit(0)
}
7 changes: 7 additions & 0 deletions cmd/iptw/daemon_windows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build windows

package main

// maybeDaemonize is a no-op on Windows. Binaries built with -H windowsgui
// already have no console window, so there is nothing to detach from.
func maybeDaemonize(_ bool) {}
7 changes: 7 additions & 0 deletions cmd/iptw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ var (
func main() {
var forceStart bool
var showVersion bool
var foreground bool
flag.BoolVar(&forceStart, "force", false, "Force start even if another instance appears to be running")
flag.BoolVar(&showVersion, "version", false, "Show version information")
flag.BoolVar(&foreground, "foreground", false, "Run in the foreground (keep terminal attached)")
flag.Parse()

// Handle version request
Expand All @@ -35,6 +37,11 @@ func main() {
return
}

// On macOS/Linux: detach from the terminal so the user can close the
// launching shell. The process re-execs itself with --foreground and
// the parent exits immediately. This is a no-op on Windows.
maybeDaemonize(foreground)

// On Windows GUI builds, set up file logging immediately so that any
// startup failure is recorded even before the config is read.
if closer := logging.SetupWindowsFileLogger(slog.LevelDebug); closer != nil {
Expand Down