Simple and Oneshot services

A service's type answers one question: does the process keep running, or does it run once and exit? peinit supports exactly two answers — Simple and Oneshot — set by the Type field (default Simple). The type is independent of when the service starts; a Oneshot can be boot-triggered, timer-triggered, or demand-only, exactly like a Simple service.

Simple services #

A Simple service is a long-running daemon. peinit forks, installs the token, and execs the binary. The process is the service: while it runs, the service is running; when it exits, the service has stopped. This is the default and covers the large majority of services — registryd, authd, sshd, application daemons.

The interesting question for a Simple service is when does it count as started — the moment its dependents are allowed to begin. That is the readiness model, set by the Readiness field.

Readiness: Notify vs Alive #

ReadinessReady when…Use for
Notify (default)The service sends READY=1 via sd_notify.Any service that has meaningful startup work — opening a socket, loading state, connecting to a backend. The dependent genuinely should wait.
AliveThe process exists — readiness is immediate.Services with no startup handshake, where "the process is up" is as good a signal as you will get.

The distinction matters because it is a promise to dependents. With Notify, a service that declares itself a Requires target is telling peinit "do not start anything that needs me until I say READY=1." With Alive, peinit can only promise that the process was forked — not that it is functional.

Once ready, a Simple service transitions to Active and stays there until its process exits or it is stopped. What happens on exit — clean exit, crash, restart — is the subject of The service lifecycle and Keeping services running.

Oneshot services #

A Oneshot service is a run-to-completion task. peinit forks, installs the token, execs the binary, and waits for it to exit. It is the right type for setup work: creating directories, initialising a database, running a schema migration, applying a one-time fixup at boot.

For a Oneshot, readiness is exit, not a signal. The Readiness field is ignored entirely — READY=1 is meaningless for a process whose whole job is to finish. Success and failure are decided by the exit code:

  • Exit 0 (or any code listed in SuccessExitCodes) → the service succeeded.
  • Any other exit, or death by signal → the service failed, and the failure policy applies.

SuccessExitCodes is for binaries that signal a meaningful outcome with a non-zero code — for example, a tool that exits 2 to mean "nothing to do." Each entry is a decimal 0255; code 0 is always success and need not be listed.

Completed, and RemainAfterExit #

A successful Oneshot transitions to Completed, and what happens next depends on RemainAfterExit:

RemainAfterExitAfter a successful exit
0 (default)The service passes through Completed — releasing its dependents — then transitions to Inactive.
1The service stays in Completed.

Both forms satisfy dependents; the only difference is what a later status query shows. Use RemainAfterExit=1 when "this task is done" is a state worth seeing — a boot-time migration you want to confirm ran, say — rather than a service that quietly returns to Inactive.

Oneshot timing and hooks #

Two details distinguish a Oneshot's start sequence from a Simple one:

  • StartTimeout covers the entire run. For a Simple service the timeout covers startup until readiness; for a Oneshot it covers everything from the first pre-hook to process exit. A long-running Oneshot must raise StartTimeout accordingly (or send EXTEND_TIMEOUT_USEC as it works — see Keeping services running).
  • ExecStartPost runs only on success. Post-hooks fire after the successful exit. If the Oneshot fails, ExecStartPost does not run.

Restarting a Oneshot #

A Oneshot is not restarted on success, regardless of RestartPolicy — even Always. A successful exit is the goal, not a failure to retry. RestartPolicy governs only the response to failure (a non-zero exit). To re-run a Oneshot on a schedule, give it a timer trigger; that is the intended mechanism.

Simple vs Oneshot at a glance #

SimpleOneshot
Process lifetimeLong-running; is the serviceRuns once, exits
ReadinessNotify (READY=1) or AliveSuccessful exit — Readiness ignored
Satisfies dependents whenActiveCompleted (with or without RemainAfterExit)
Successful end stateActive (until stopped)Completed, then Inactive (unless RemainAfterExit=1)
ExecStartPost firesAfter readinessAfter successful exit only
StartTimeout coversStartup until readinessThe whole execution
Watchdog & health checksSupervised while ActiveNone — WatchdogTimeout/HealthCheck are inert
Restart on successn/aNever (use a timer to re-run)

The sd_notify contract #

A Notify-readiness service signals readiness by sending READY=1 over the sd_notify protocol — a datagram to the socket named in the NOTIFY_SOCKET environment variable, which peinit sets for every service. READY=1 is only the most visible message; the same channel carries watchdog keepalives (WATCHDOG=1), graceful-stop notice (STOPPING=1), timeout extensions (EXTEND_TIMEOUT_USEC), status strings (STATUS=), and the fd store. These are covered where they belong — readiness here, the rest in Keeping services running and Controlling services.

Readiness is always per start generation. A READY=1 left over from a previous incarnation of the process is never valid for the current start — peinit ties every notify message to the specific process it is currently supervising, verified by pidfd.

Who is allowed to send these messages is governed by NotifyAccess, which currently has a single value: Main (0, the default). Under Main, only the tracked main PID — the process peinit forked and supervises — is authorised to send sd_notify readiness and status messages; a message arriving from any other PID (a child, a helper, an unrelated process) is ignored. So if a worker your service spawns sends READY=1, peinit will not act on it — the readiness signal must come from the main process itself.

No forking daemons #

peinit does not support services that double-fork to background themselves. The whole reason a traditional daemon double-forks — to detach from the launcher — is moot when the launcher tracks the process it spawned. peinit obtains a pidfd for every process at fork time, which is a kernel handle to that exact process, immune to PID-reuse races. There is no MAINPID= mechanism for a service to redirect supervision onto a different process; peinit supervises what it forked.

If a legacy binary insists on daemonising, the fix lives at the packaging layer — wrap it so it stays in the foreground (commonly a --no-daemon/--foreground flag) — not at the init layer.

Where to start #

To control when either type of service starts, read Triggers and timers.

To follow a service from definition to running process and read its states, read The service lifecycle.

To configure restart, health checks, and watchdogs for a Simple service, read Keeping services running.

Edit this page