Safely Check and Run SysV Services with invoke-rc.d

invoke-rc.d decides whether starting, stopping or restarting a service actually happens, and misreading its exit code can bite you. This guide walks through a safe workflow for checking a System V service action, reading the result, and running an approved action only when you understand its policy boundary. The examples use invoke-rc.d from Debian's init-system-helpers package, version 1.66ubuntu1 on this machine.

Allow about fifteen minutes. You need a shell and an installed init script under /etc/init.d/. Read-only checks normally need no elevated privilege. Starting, stopping or restarting a system service usually does, so use sudo only for the final state-changing command. This guide does not edit init scripts, runlevel links or policy-rc.d.

1. Confirm the command and service name

invoke-rc.d takes an init-script basename, an action, and optional parameters passed to the script. Check the installed command before copying an example:

$ command -v invoke-rc.d
/usr/sbin/invoke-rc.d
$ dpkg-query -W -f='${Package} ${Version}\n' init-system-helpers
init-system-helpers 1.66ubuntu1
$ invoke-rc.d --help
invoke-rc.d, Debian/SysVinit (/etc/rc?.d) initscript subsystem.

Use the basename, not a unit name with a .service suffix. For example, the local script is /etc/init.d/cron, so its basename is cron:

$ test -x /etc/init.d/cron && echo 'cron init script is executable'
cron init script is executable

Checkpoint: replace cron with the exact basename from your host. Do not guess a name from a package name, and do not use a made-up service as a test.

2. Ask what would happen without running the script

Use --query for a dry run. It does not execute the init script, and it automatically enables --disclose-deny and --no-fallback:

$ invoke-rc.d --query --disclose-deny cron start
$ printf 'invoke-rc.d status: %s\n' "$?"
invoke-rc.d status: 104

Status 104 means the action is allowed, but was not run because query mode is active. This is the useful pre-flight result: you have checked the policy decision without starting or restarting anything.

Other query results are deliberately not ordinary success or failure messages:

Do not treat every non-zero query status as permission to try the real command. Investigate the particular result first.

3. Inspect a running service with the status action

The status action is normally read-only from the service's point of view, but it still passes through policy and the init-script subsystem. Run it without sudo first:

$ invoke-rc.d cron status
... cron.service - Regular background program processing daemon
... Active: active (running) ...
$ printf 'invoke-rc.d status: %s\n' "$?"
invoke-rc.d status: 0

The exact service-manager output changes with the host and current uptime. The useful checks are that the command returned 0 and that the service reports the state you expected. A service's status output is not a promise that every action is available; individual init scripts may omit actions listed in the general interface.

For a missing or unsuitable script, capture the result rather than immediately retrying with elevated privileges:

$ invoke-rc.d definitely-not-an-init-script status
Unit definitely-not-an-init-script.service could not be found.
invoke-rc.d: initscript definitely-not-an-init-script, action "status" failed.
$ printf 'invoke-rc.d status: %s\n' "$?"
invoke-rc.d status: 4

That is an error from the attempted status action, not evidence that sudo would create the service. Check the basename and installed package instead.

4. Run a state-changing action only after the dry run

Starting, stopping and restarting can disrupt users or dependent processes. Before using one, check the exact action in query mode and arrange a recovery path. For a restart, that usually means knowing the service's configuration file, logs and normal start command, and having a maintenance window.

$ invoke-rc.d --query cron restart
$ printf 'query status: %s\n' "$?"
query status: 104

Only if that result matches your intention should you run the real action, normally with elevated privileges:

$ sudo invoke-rc.d cron restart
$ printf 'restart status: %s\n' "$?"
restart status: 0

Status 0 means the init script returned success. It can also mean a policy decision prevented the action when --disclose-deny was not used, or that a fallback action succeeded. If the exact policy outcome matters, include --disclose-deny on a non-query invocation and record the output.

There is no universal undo for a restart. To recover from an unsuccessful or unwanted change, inspect the service status and logs, then run the service's documented opposite action or restore the known-good configuration. Do not use --force as a recovery shortcut.

5. Understand policy, fallbacks and force

The policy layer is usually implemented by /usr/sbin/policy-rc.d, if that helper exists. It can allow an action, deny it, or request a fallback such as stopping when a restart is not allowed. invoke-rc.d also applies runlevel constraints itself. This is why a package maintainer script should use this interface rather than calling /etc/init.d/name directly.

--no-fallback suppresses fallback requests. The manual warns that this is usually a bad idea for actions other than start; leave it out unless you have a specific reason and have checked the policy contract.

--force tries to run the init script despite policy and non-fatal subsystem errors. It can bypass a deliberate protection, including a chroot or package-upgrade safeguard. Treat it as a security-sensitive and service-disrupting override, not as the normal fix for a denied action. The manual severely discourages it in package maintainer scripts.

On a system using native systemd units, --skip-systemd-native can make the command exit before doing anything when the requested service is native. This is intended for package scripts that defer native systemd work to deb-systemd-invoke; it is not a general replacement for understanding which service manager owns the unit.

6. Make scripts handle results explicitly

If a script needs a decision without changing service state, keep the query and its status together:

if invoke-rc.d --query --disclose-deny "$SERVICE_NAME" "$ACTION"; then
    printf '%s\n' 'unexpected ordinary success from query mode' >&2
    exit 1
fi
status=$?
case "$status" in
    104) printf '%s\n' 'action would be allowed' ;;
    101) printf '%s\n' 'action denied by policy or runlevel' ;;
    100) printf '%s\n' 'unknown init-script ID' >&2; exit 1 ;;
    *) printf 'invoke-rc.d query returned %s\n' "$status" >&2; exit 1 ;;
esac

In real shell code, capture the status immediately after the command. The compact example above illustrates the statuses, but its first branch intentionally rejects a zero result because query mode should return one of the documented 100 to 106 values. Keep SERVICE_NAME and ACTION controlled values, not unchecked user input.

Done means