daemonize

package module
v0.1.23 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 4, 2026 License: MIT Imports: 3 Imported by: 0

README

daemonize

Go Reference Go Report Card CI CodeQL OpenSSF Scorecard Latest release

daemonize wraps any cobra command with Unix daemon lifecycle controls — start, stop, status — by re-execing the binary as a detached background process. The wrapped command runs in the foreground; the daemon manages backgrounding, a pid file, log streaming during startup and shutdown, and signal-based readiness, all without mutating the command.

Quick Start

Add the dependency, the daemonize import, and wrap cmd.Execute():

go get github.com/cnuss/daemonize
 import (
 	"fmt"
 	"os"
+	"syscall"

+	"github.com/cnuss/daemonize"
 	"github.com/spf13/cobra"
 )

 func main() {
+	ready := make(chan struct{})

 	cmd := &cobra.Command{
 		...
 		RunE: func(cmd *cobra.Command, args []string) error {
 			fmt.Printf("hello %s\n", message)
+			close(ready)
 			<-cmd.Context().Done()
 			...
 		},
 	}
 	cmd.Flags().StringVarP(&message, "message", "m", "world", "who to greet")

-	if err := cmd.Execute(); err != nil {
+	if err := daemonize.FromCobra(cmd).
+		WithShutdownSignal(os.Interrupt, syscall.SIGTERM).
+		DetachOn(ready).
+		Execute(); err != nil {
 		fmt.Fprintln(os.Stderr, "error:", err)
 		os.Exit(1)
 	}
 }

(Full source: examples/hello/main.go.)

Before

A basic cobra command:

package main

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
)

func main() {
	var message string
	cmd := &cobra.Command{
		Use:   "hello",
		Short: "Say hello",
		RunE: func(cmd *cobra.Command, args []string) error {
			fmt.Printf("hello %s\n", message)
			<-cmd.Context().Done()
			fmt.Println("stopping")
			return nil
		},
	}
	cmd.Flags().StringVarP(&message, "message", "m", "world", "who to greet")

	if err := cmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}
$ ./hello --help
Say hello

Usage:
  hello [flags]

Flags:
  -h, --help             help for hello
  -m, --message string   who to greet (default "world")
After
$ ./hello --help
Say hello

Usage:
  hello [flags]
  hello [command]

Daemon Commands:
  start       Start `hello` in the background
  status      Report whether `hello` is running
  stop        Stop the running `hello`

Additional Commands:
  completion  Generate the autocompletion script for the specified shell
  help        Help about any command

Flags:
  -h, --help             help for hello
  -m, --message string   who to greet (default "world")

Use "hello [command] --help" for more information about a command.

The wrapped flags (-m) carry through to start, so hello start -m there forwards exactly what the foreground worker would have seen:

$ ./hello help start
Start `hello` in the background

Usage:
  hello start [flags]

Flags:
  -h, --help             help for start
  -m, --message string   who to greet (default "world")

The command's own Short and flags stay as written; daemonize attaches the lifecycle subcommands and wraps RunE to own the pid file. Then:

./hello start         # daemonize (streams startup output, detaches when ready)
./hello status        # running (pid N)
./hello stop          # SIGTERM (Ctrl+C escalates to SIGKILL)
./hello               # run the wrapped command in the foreground
./hello -m there      # forward -m to the foreground run
./hello start -m there  # forward -m to the daemonized run

Platforms

  • Linux, macOS, and other Unix-likes — start/stop/status work through standard POSIX signals.
  • Windows — same surface, different primitives: CreateProcess(DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP) for detach, a <base>.ready sentinel file for the readiness handshake, and a per-daemon named pipe (\\.\pipe\daemonize-<base>) for graceful shutdown. Workers that want cross-platform graceful shutdown must use WithShutdownSignal + <-cmd.Context().Done() (raw signal.Notify(stop, SIGTERM) works on Unix but won't pick up the Windows pipe signal).

Side-by-side flow + the wire format for the named-pipe shutdown live in CLAUDE.md → Platform layer.

Exit codes

Lifecycle subcommands and the foreground run obey a fixed contract, verified by the TestExit* suite under ./e2e. Pinned for both Unix and Windows.

Exit 0 — clean
Path
start succeeded (child is detached and ready)
stop graceful — worker exited via cmd.Context().Done() teardown
stop when nothing was running (no pid file)
stop with a stale pid file — daemon cleared it
stop where the worker errored mid-shutdown but still exited
stop where Ctrl+C escalated to a forced kill — parent reaped cleanly
status running / not running / stale
Foreground worker returned without a shutdown signal arriving
Exit 1 — caller- or state-level failure
Path
start while a daemon is already running
start where the wrapped command exited during startup
start cancelled by Ctrl+C (startup cancelled)
status --output=<not text|json> (pflag rejection)
Any subcommand invoked with an unknown flag (cobra rejection)

Note: unknown POSITIONAL args are NOT a flag error — buildCobra defaults command.Args to cobra.ArbitraryArgs so positionals forward to the wrapped worker (see examples/with-args). Set a stricter Args validator on the wrapped command if you want positionals rejected.

Exit 128 + signum — foreground interrupted by signal

When the wrapped command is run directly (no start), a shutdown signal observed by WithShutdownSignal's signal.NotifyContext re-exits the process with the conventional 128 + signum code after the worker drains. Bash, init systems, and process supervisors expect this — a clean 0 would hide the fact that the run was interrupted.

Signal Exit code
SIGINT (2) 130
SIGTERM (15) 143
Other registered signals 128 + signum

Only fires for foreground runs of the wrapped command — daemon children spawned by start skip the re-raise so the parent's stop remains the authoritative exit reporter for that lifetime.

On Windows, the Go runtime maps both CTRL_C_EVENT and CTRL_BREAK_EVENT to syscall.SIGINT before delivery, so Ctrl+C of a foreground worker exits 130 on that target too.

Features

  • In-place enrichment: FromCobra(cmd).DetachOn(ready) returns the same *cobra.Command, now with start/stop/status attached as subcommands. Running the command directly still invokes its original RunE (the foreground worker); the wrapped RunE owns the pid file and relays readiness, so stop/status work against foreground runs too.
  • Channel-based readiness relay: the wrapped command closes a chan struct{} when bound/ready; the daemon translates that to SIGUSR1 internally so the parent can stop streaming and detach. Opaque to the wrapped command — it never sees a signal.
  • Streaming: start tails the child's log so the user sees real startup output until ready; stop tails it during graceful shutdown.
  • Ctrl+C handling: start sends SIGTERM to the child on the first Ctrl+C and waits for it to exit; a second Ctrl+C escalates to SIGKILL. stop follows the same pattern: first interrupt is the implicit SIGTERM, second escalates to SIGKILL.
  • Per-daemon state files: pid/log live under <UserCacheDir>/.<command-name>/<base>.{pid,log}. Override with WithName.
  • Help grouping: lifecycle subcommands are grouped (Daemon Commands: by default). Customize or disable with WithGroup.
  • Nestable: the enriched command can be mounted under a larger cobra tree — start re-execs along the full command path (foo run …).
  • Generic builder: Daemon[T] is parameterized; today T == *cobra.Command via FromCobra. Future backends can plug in.

Install

go get github.com/cnuss/daemonize

Module floor is go 1.21 / cobra v1.6.0.

API at a glance

type Daemon[T any] interface {
    FromCobra(inner *cobra.Command) Daemon[*cobra.Command]
    DetachOn(detachSig <-chan struct{}) T  // terminal: builds and returns T

    WithName(name string) Daemon[T]          // override state-file base name
    WithGroup(name *string) Daemon[T]        // help-group title (nil = ungroup)
    WithContext(parent context.Context) Daemon[T]    // parent ctx for the wrapped cmd; nil = opt out
    WithShutdownSignal(sigs ...os.Signal) Daemon[T]   // signal.NotifyContext around the parent ctx

    // Runtime accessors / actions (usable without building the cobra tree)
    Stop() error
    Status() error
    PID() (int, error)
    IsAlive() bool
    PIDFile() (string, error)
    LogFile() (string, error)
    Name() (string, error)
}

func NewDaemon() Daemon[any]                                       // untyped bootstrap
func FromCobra(command *cobra.Command) Daemon[*cobra.Command]      // shorthand

Examples

Self-contained programs in ./examples:

Example Demonstrates
hello Smallest wiring (FromCobra + DetachOn).
named WithName("widget") for custom pid/log file names.
grouped WithGroup(&"Lifecycle") for a custom help-group title.
ungrouped WithGroup(nil) to put lifecycle under Additional Commands.
with-args Flag + positional forwarding through start to the child.
slow-start Streaming a multi-second startup until ready.
slow-shutdown Streaming a multi-second graceful shutdown.
start-error Daemon detects a child that fails before signaling ready.
shutdown-error Daemon streams a failure during shutdown; still stops.
pid-cleanup Worker exits early without signaling ready; pid file gone.
stubborn Worker ignores SIGTERM; demonstrates Ctrl+C → SIGKILL.
subcommand Daemon mounted under a larger cobra tree (e.g. app run).

Run one locally:

make run hello start
make run hello status
make run hello stop

Testing

make test   # library unit tests (fast, in-package)
make e2e    # end-to-end harness: builds + drives every example binary

make e2e runs go test -count=1 -v ./e2e, with an isolated cache (HOME/XDG_CACHE_HOME) per test so pid/log files never collide.

Contributing

PRs welcome. See CONTRIBUTING.md for the local dev loop, release process, and what makes a good example.

License

MIT

Documentation

Overview

Package daemonize wraps a cobra command with Unix daemon lifecycle controls — start, stop, and status — by re-execing the binary as a detached background process. The wrapped command runs in the foreground; the daemon manages backgrounding, a pid file, log streaming during startup and shutdown, and signal-based readiness, all without mutating the command.

The package is split into three pieces:

  • daemonize (this package) — thin façade exposing NewDaemon and FromCobra. Stable surface for application code.
  • github.com/cnuss/daemonize/v1 — the stable Daemon[T] interface and StatusResult type. Application code that wants to declare types against the interface imports this.
  • github.com/cnuss/daemonize/v1alpha1 — the current implementation. Internals (DaemonImpl, helpers, cobra wiring) may change between alpha revisions; pin only if you need direct access to the struct.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func FromCobra

func FromCobra(command *cobra.Command) v1.Daemon[*cobra.Command]

FromCobra is a shorthand for NewDaemon().FromCobra(command). Use it when you already know you are wrapping a cobra command and don't need the untyped Daemon[any] bootstrap:

cmd := daemonize.FromCobra(serve).WithShutdownSignal(os.Interrupt, syscall.SIGTERM).DetachOn(ready)

func NewDaemon

func NewDaemon() v1.Daemon[any]

NewDaemon returns an unconfigured builder. Call FromCobra to wrap a command.

Types

This section is empty.

Directories

Path Synopsis
Package e2e builds each example binary and drives its lifecycle end-to-end, asserting the behavior that example demonstrates.
Package e2e builds each example binary and drives its lifecycle end-to-end, asserting the behavior that example demonstrates.
examples
grouped command
Command grouped sets a custom help group title for the lifecycle subcommands via WithGroup.
Command grouped sets a custom help group title for the lifecycle subcommands via WithGroup.
hello command
Command hello is the smallest daemonize example: wrap a worker, stream its startup via a readiness channel, and get start/stop/status.
Command hello is the smallest daemonize example: wrap a worker, stream its startup via a readiness channel, and get start/stop/status.
named command
Command named overrides the state-file base name with WithName, so the pid and log files are ".../<cache>/.serve/widget.{pid,log}" instead of being derived from the command path.
Command named overrides the state-file base name with WithName, so the pid and log files are ".../<cache>/.serve/widget.{pid,log}" instead of being derived from the command path.
pid-cleanup command
Command pid-cleanup is a degenerate worker: it never closes the readiness channel, sleeps for 5s, prints "hello world", and returns.
Command pid-cleanup is a degenerate worker: it never closes the readiness channel, sleeps for 5s, prints "hello world", and returns.
shutdown-error command
Command shutdown-error demonstrates a wrapped command that starts fine but errors during shutdown.
Command shutdown-error demonstrates a wrapped command that starts fine but errors during shutdown.
slow-shutdown command
Command slow-shutdown demonstrates a wrapped command with a slow, graceful shutdown.
Command slow-shutdown demonstrates a wrapped command with a slow, graceful shutdown.
slow-start command
Command slow-start demonstrates a wrapped command with a slow startup.
Command slow-start demonstrates a wrapped command with a slow startup.
start-error command
Command start-error demonstrates a wrapped command that fails during startup (before signaling readiness).
Command start-error demonstrates a wrapped command that fails during startup (before signaling readiness).
stubborn command
Command stubborn is a misbehaving worker that catches the graceful-shutdown signal and ignores it — only a hard kill stops it (SIGKILL on Unix, TerminateProcess on Windows).
Command stubborn is a misbehaving worker that catches the graceful-shutdown signal and ignores it — only a hard kill stops it (SIGKILL on Unix, TerminateProcess on Windows).
subcommand command
Command subcommand shows daemonize mounted under a larger cobra tree: the app has its own init/create/delete subcommands, and "subcommand run" is the daemonized entry point (so "subcommand run start", "subcommand run stop", "subcommand run serve", ...
Command subcommand shows daemonize mounted under a larger cobra tree: the app has its own init/create/delete subcommands, and "subcommand run" is the daemonized entry point (so "subcommand run start", "subcommand run stop", "subcommand run serve", ...
ungrouped command
Command ungrouped passes WithGroup(nil) so the lifecycle subcommands are not grouped — they appear under cobra's default "Additional Commands:".
Command ungrouped passes WithGroup(nil) so the lifecycle subcommands are not grouped — they appear under cobra's default "Additional Commands:".
with-args command
Command with-args shows that flags and positional args are forwarded verbatim to the wrapped command.
Command with-args shows that flags and positional args are forwarded verbatim to the wrapped command.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL