daemonize

package module
v0.1.0 Latest Latest
Warning

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

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

README

daemonize

Go Reference

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.

Features

  • Mutation-free: the caller's *cobra.Command is wrapped, never modified (RunE, flags, Use, etc. all untouched).
  • 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 assembled root command can be mounted under a larger cobra tree — start re-execs along the full command path (foo run serve …).
  • 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.

Quick start

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"

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

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

	serve := &cobra.Command{
		Use:   "serve",
		Short: "Run the server in the foreground",
		RunE: func(cmd *cobra.Command, args []string) error {
			// ... slow startup work ...
			close(ready) // signal the daemon: I'm up

			stop := make(chan os.Signal, 1)
			signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
			<-stop
			return nil
		},
	}

	cmd := daemonize.FromCobra(serve).
		WithReload(syscall.SIGHUP).
		DetachOn(ready)

	if err := cmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, "error:", err)
		os.Exit(1)
	}
}

Then:

./app start       # daemonize (streams startup output, detaches when ready)
./app status      # running (pid N)
./app reload      # SIGHUP to the running process
./app stop        # SIGTERM, escalating to SIGKILL on timeout
./app serve       # run the wrapped command in the foreground
./app             # bare alias for "start"

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)

    // 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
minimal 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.
subcommand Daemon mounted under a larger cobra tree (e.g. app run).

Run one locally:

make run minimal start
make run minimal status
make run minimal 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.

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

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]

	// 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 IntoCobra. 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)

func NewDaemon

func NewDaemon() Daemon[any]

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

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]

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.
minimal command
Command minimal is the smallest daemonize example: wrap a worker, stream its startup via a readiness channel, and get start/stop/status.
Command minimal 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.
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.
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