daemonize

package module
v0.1.7 Latest Latest
Warning

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

Go to latest
Published: May 31, 2026 License: MIT Imports: 12 Imported by: 0

README

daemonize

Go Reference Go Report Card CI Latest release

daemonize wraps any cobra command with Unix daemon lifecycle controls — start, stop, status, reload — 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 (
 	...
+	"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()
 			...
 		},
 	}
 	...
-	if err := cmd.Execute(); err != nil {
+	if err := daemonize.FromCobra(cmd).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 (
	"context"
	"fmt"
	"os"
	"os/signal"
	"syscall"

	"github.com/spf13/cobra"
)

func main() {
	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
	defer cancel()

	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")
	cmd.SetContext(ctx)

	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

Add .WithReload(syscall.SIGHUP) before DetachOn to register a reload subcommand that signals the running process.

Features

  • In-place enrichment: FromCobra(cmd).DetachOn(ready) returns the same *cobra.Command, now with start/stop/status (and optionally reload) 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 cancels (kills the child) if interrupted mid-startup; stop escalates to SIGKILL on interrupt.
  • 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

    WithReload(sig syscall.Signal) Daemon[T] // enables the "reload" subcommand
    WithName(name string) Daemon[T]          // override state-file base name
    WithGroup(name *string) Daemon[T]        // help-group title (nil = ungroup)
    WithStopTimeout(d time.Duration) Daemon[T] // SIGKILL fallback (0 = wait forever)

    // Runtime accessors / actions (usable without building the cobra tree)
    Stop() error
    Status() error
    Reload() 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).
reload WithReload(SIGHUP) and a worker that handles it.
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.
shutdown-timeout WithStopTimeout(...) escalates to SIGKILL on slow drain.
pid-cleanup Worker exits early without signaling ready; pid file gone.
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, status, and reload — 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.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Daemon

type Daemon[T any] interface {
	// FromCobra wraps a cobra command, retyping the builder to *cobra.Command so
	// Into returns the assembled root command.
	FromCobra(inner *cobra.Command) Daemon[*cobra.Command]
	// DetachOn assembles the lifecycle wrapper and returns it (as T). detachSig is
	// closed by the wrapped command once it is ready; the daemon relays that so
	// "start" stops streaming and detaches. Pass nil for no readiness relay.
	// After FromCobra, T is *cobra.Command (the root command to Execute).
	DetachOn(detachSig <-chan struct{}) T
	// WithReload enables the "reload" subcommand and sets the signal it sends to
	// the running process. It must match the signal the wrapped command listens
	// on. Without it, no reload subcommand is registered.
	WithReload(sig syscall.Signal) Daemon[T]
	// WithName overrides the state-file base name. By default it is derived from
	// the wrapped command's path (e.g. "server-serve"); WithName("foo") yields
	// ".foo.pid"/".foo.log" instead.
	WithName(name string) Daemon[T]
	// WithGroup sets the lifecycle help group's title (a trailing ":" is added);
	// pass nil to ungroup (list them under Additional Commands). Unset, they are
	// grouped under "Daemon Commands:".
	WithGroup(name *string) Daemon[T]
	// WithStopTimeout bounds how long Stop waits for the child to exit gracefully
	// before escalating to SIGKILL. A zero (or unset) timeout disables the
	// deadline — Stop waits indefinitely until the child exits or the user
	// interrupts (Ctrl+C), which always force-kills.
	WithStopTimeout(timeout time.Duration) Daemon[T]

	// Stop signals the running process (SIGTERM, escalating to SIGKILL on
	// timeout) and waits for it to exit. Usable without building the cobra tree.
	Stop() error
	// Status reports whether the process is running and clears a stale pid file.
	Status() error
	// Reload sends the configured reload signal (WithReload, default SIGHUP) to
	// the running process.
	Reload() error
	// PID reads the process ID from the pid file.
	PID() (int, error)
	// IsAlive reports whether the process named by the pid file is running.
	IsAlive() bool
	// PIDFile returns the path to the pid file, or an error if it is not yet
	// resolved (DetachOn has not run).
	PIDFile() (string, error)
	// LogFile returns the path to the log file, or an error if it is not yet
	// resolved (DetachOn has not run).
	LogFile() (string, error)
	// Name returns the effective state-file base name (WithName override or the
	// derived command path), or an error if it is not yet resolved.
	Name() (string, error)
}

Daemon is the builder for a background-lifecycle wrapper around a foreground command. Configure it with the With* methods, then call DetachOn. Obtain one from FromCobra.

func FromCobra

func FromCobra(command *cobra.Command) 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).WithReload(syscall.SIGHUP).DetachOn(ready)
Example

FromCobra wraps a cobra command with start/stop/status lifecycle subcommands. The wrapped command closes a readiness channel when it is up; the daemon translates that into a signal so "start" stops streaming the child's log and detaches.

package main

import (
	"github.com/cnuss/daemonize"
	"github.com/spf13/cobra"
)

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

	serve := &cobra.Command{
		Use: "serve",
		RunE: func(cmd *cobra.Command, args []string) error {
			// ... slow startup work (bind a port, warm caches, etc.) ...
			close(ready) // tell the daemon: I'm ready, you may detach
			select {}    // block in the foreground (real code would wait on signals)
		},
	}

	root := daemonize.FromCobra(serve).DetachOn(ready)
	_ = root.Execute()
}

func NewDaemon

func NewDaemon() Daemon[any]

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

Example

NewDaemon returns the untyped Daemon[any] bootstrap. Prefer FromCobra when you already know you are wrapping a cobra command; NewDaemon is for code that picks the backend at runtime.

package main

import (
	"github.com/cnuss/daemonize"
	"github.com/spf13/cobra"
)

func main() {
	ready := make(chan struct{})
	serve := &cobra.Command{Use: "serve"}

	root := daemonize.NewDaemon().FromCobra(serve).DetachOn(ready)
	_ = root.Execute()
}

type DaemonImpl

type DaemonImpl[T any] struct {
	// contains filtered or unexported fields
}

DaemonImpl is the default Daemon implementation. T is the wrapped value's type (and what Into returns).

func (*DaemonImpl[T]) DetachOn

func (d *DaemonImpl[T]) DetachOn(detachSig <-chan struct{}) T

DetachOn records the readiness channel, then assembles the lifecycle wrapper for the wrapped value and returns it (as T), dispatching on inner's type.

func (*DaemonImpl[T]) FromCobra

func (d *DaemonImpl[T]) FromCobra(command *cobra.Command) Daemon[*cobra.Command]

FromCobra wraps command, retyping the builder to *cobra.Command so Into returns the assembled root command. Configure with the With* methods after this call; reload is disabled until WithReload sets a signal.

func (*DaemonImpl[T]) IsAlive

func (d *DaemonImpl[T]) IsAlive() bool

func (*DaemonImpl[T]) LogFile

func (d *DaemonImpl[T]) LogFile() (string, error)

func (*DaemonImpl[T]) Name

func (d *DaemonImpl[T]) Name() (string, error)

func (*DaemonImpl[T]) PID

func (d *DaemonImpl[T]) PID() (int, error)

func (*DaemonImpl[T]) PIDFile

func (d *DaemonImpl[T]) PIDFile() (string, error)

func (*DaemonImpl[T]) Reload

func (d *DaemonImpl[T]) Reload() error

func (*DaemonImpl[T]) Status

func (d *DaemonImpl[T]) Status() error

func (*DaemonImpl[T]) Stop

func (d *DaemonImpl[T]) Stop() error

func (*DaemonImpl[T]) WithGroup

func (d *DaemonImpl[T]) WithGroup(name *string) Daemon[T]

func (*DaemonImpl[T]) WithName

func (d *DaemonImpl[T]) WithName(name string) Daemon[T]

func (*DaemonImpl[T]) WithReload

func (d *DaemonImpl[T]) WithReload(sig syscall.Signal) Daemon[T]

func (*DaemonImpl[T]) WithStopTimeout added in v0.1.5

func (d *DaemonImpl[T]) WithStopTimeout(timeout time.Duration) Daemon[T]

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.
reload command
Command reload adds the "reload" subcommand, wired to SIGHUP.
Command reload adds the "reload" subcommand, wired to SIGHUP.
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.
shutdown-timeout command
Command shutdown-timeout demonstrates WithStopTimeout: the worker stalls inside its shutdown handler past the configured timeout, so stop escalates to SIGKILL instead of waiting for a graceful exit that never comes.
Command shutdown-timeout demonstrates WithStopTimeout: the worker stalls inside its shutdown handler past the configured timeout, so stop escalates to SIGKILL instead of waiting for a graceful exit that never comes.
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).
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