Add remain:yes option for run/task oneshot commands

Similar to systemd's RemainAfterExit=yes.  Prevents the task from
re-running on runlevel re-entry and ensures the post: script runs
when explicitly stopped or when leaving valid runlevels.

Useful for tasks that set up persistent state like firewall rules:

    task [2345] remain:yes \
         post:/usr/sbin/teardown-firewall \
         /usr/sbin/setup-firewall -- Firewall setup

Not supported for bootstrap-only tasks (runlevel S only) since these
are deleted immediately after completion.
This commit is contained in:
Aaron Andersen
2026-02-05 22:08:22 -05:00
parent a215747355
commit 5f7e8457af
7 changed files with 121 additions and 4 deletions
+5
View File
@@ -69,6 +69,11 @@ confines of that the following options are available:
* `respawn` -- bypasses the `restart` mechanism completely, allows
endless restarts. Useful in many use-cases, but not what `service`
was originally designed for so not the default behavior
* `remain:yes` -- for `run` and `task` only. Prevents the task from
re-running on runlevel re-entry and ensures the `post:` script runs
when the task is explicitly stopped or leaves its valid runlevels.
Similar to systemd's `RemainAfterExit=yes`. See [Task and Run](task-and-run.md)
for more details
* `oncrash:reboot` -- when all retries have failed, and the service
has *crashed*, if this option is set the system is rebooted
* `oncrash:script` -- similarly, but instead of rebooting, call the
+41
View File
@@ -44,3 +44,44 @@ redirects can be used:
Please note, `;`, `&&`, `||`, and similar are *not supported*. Any
non-trivial constructs are better placed in a separate shell script.
remain:yes
----------
By default, a `run` or `task` will re-run each time its runlevel is
entered, and its `post:` script does not run on completion.
With `remain:yes`, the task runs once and does not re-run on runlevel
re-entry:
task [2345] remain:yes /usr/sbin/setup-firewall -- Firewall setup
This has the following effects:
* The task does not re-run on runlevel re-entry
* The `post:` script runs when:
- The task is explicitly stopped (`initctl stop NAME`)
- The task leaves its valid runlevels (e.g., runlevel change)
This is useful for tasks that set up persistent state where:
* Cleanup should only happen on explicit stop or when leaving valid runlevels
* The setup should not be re-run on every runlevel entry
**Example:** Setting up firewall rules with cleanup on shutdown:
```
task [2345] remain:yes \
post:/usr/sbin/teardown-firewall \
/usr/sbin/setup-firewall -- Firewall setup
```
The firewall rules are created once. The `post:` script runs when
entering runlevel 0 (halt) or 6 (reboot), or on explicit stop.
> [!NOTE]
> The `remain:yes` option is not supported for bootstrap-only tasks
> (tasks with only runlevel S). Bootstrap tasks are deleted immediately
> after completion, and their `post:` scripts never run. A warning is
> logged if `remain:yes` is used on such tasks.
+14
View File
@@ -398,6 +398,20 @@ mechanism completely, allows endless restarts. Useful in many
use-cases, but not what
.Cm service
was originally designed for so not the default behavior.
.It Cm remain:yes
for
.Cm run
and
.Cm task
only. Prevents the task from re-running on runlevel re-entry and
ensures the
.Cm post:
script runs when the task is explicitly stopped or leaves its valid
runlevels. Similar to systemd's
.Cm RemainAfterExit=yes .
.Pp
.Sy Note:
not supported for bootstrap-only tasks (runlevels [S] only).
.It Cm oncrash:reboot
when all retries have failed, and the service
has
+52 -4
View File
@@ -1798,7 +1798,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
char *dev = NULL;
int respawn = 0;
int levels = 0;
int forking = 0, manual = 0, nowarn = 0;
int forking = 0, manual = 0, remain = 0, nowarn = 0;
int restart_max = SVC_RESPAWN_MAX;
int restart_tmo = 0;
unsigned oncrash_action = SVC_ONCRASH_IGNORE;
@@ -1866,6 +1866,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
forking = 1;
else if (MATCH_CMD(cmd, "manual:yes", arg))
manual = 1;
else if (MATCH_CMD(cmd, "remain:yes", arg))
remain = 1;
else if (MATCH_CMD(cmd, "restart:", arg)) {
if (MATCH_CMD(arg, "always", arg))
restart_max = -1;
@@ -2171,6 +2173,18 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
memset(svc->ifstmt, 0, sizeof(svc->ifstmt));
svc->manual = manual;
svc->nowarn = nowarn;
/*
* remain:yes is not supported for bootstrap-only tasks. These
* tasks are deleted immediately after completion and their post:
* scripts never run. This is by design.
*/
if (remain && svc_is_runtask(svc) && !ISOTHER(levels, INIT_LEVEL)) {
logit(LOG_WARNING, "%s: remain:yes ignored for bootstrap-only tasks",
svc_ident(svc, NULL, 0));
remain = 0;
}
svc->remain = remain;
svc->respawn = respawn;
svc->forking = forking;
svc->restart_max = restart_max;
@@ -2859,8 +2873,12 @@ restart:
if (svc_is_removed(svc) && svc_has_cleanup(svc)) {
svc_set_state(svc, SVC_CLEANUP_STATE);
service_cleanup_script(svc);
} else
} else {
/* Unblock remain tasks after post script so they can restart */
if (svc_is_remain(svc))
svc_unblock(svc);
svc_set_state(svc, SVC_HALTED_STATE);
}
}
break;
@@ -2876,9 +2894,16 @@ restart:
break;
case SVC_DONE_STATE:
if (svc_is_changed(svc))
/* Remain tasks: always run post script when stopped, even if config changed */
if (svc_is_runtask(svc) && svc_is_remain(svc) && svc_is_stopped(svc)) {
if (svc_has_post(svc)) {
svc_set_state(svc, SVC_TEARDOWN_STATE);
service_post_script(svc);
} else
svc_set_state(svc, SVC_HALTED_STATE);
} else if (svc_is_changed(svc))
svc_set_state(svc, SVC_HALTED_STATE);
if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
else if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
svc_set_state(svc, SVC_WAITING_STATE);
break;
@@ -3180,6 +3205,29 @@ void service_runtask_clean(void)
if (!svc_is_runtask(svc))
continue;
/* Remain tasks stay in DONE state if still valid in new runlevel */
if (svc_is_remain(svc) && svc->state == SVC_DONE_STATE) {
if (svc_in_runlevel(svc, runlevel) && !svc_is_changed(svc))
continue; /* Keep once flag, stay in DONE */
/* Config changed or leaving runlevel: stop, run post, restart */
svc->once = 0;
svc_stop(svc);
service_step(svc);
continue;
}
/*
* On reload (SIGHUP), only remain tasks (handled above) should
* have their once flag cleared and be restarted. Regular run/task
* that already ran in this runlevel must not run again.
*
* On runlevel change, continue below to reset once flag so tasks
* can run again in the new runlevel.
*/
if (sm_in_reload())
continue;
/* run/task declared with <!> */
if (svc->sighup)
svc->once = 1;
+3
View File
@@ -461,6 +461,9 @@ restart:
/* First reload all *.conf in /etc/finit.d/ */
conf_reload();
/* Handle remain tasks that need to run post script before restart */
service_runtask_clean();
/*
* Then, mark all affected service conditions as in-flux and
* let all affected services move to WAITING/HALTED
+4
View File
@@ -344,6 +344,10 @@ svc_t *svc_stop_completed(void)
for (svc = svc_iterator(&iter, 1); svc; svc = svc_iterator(&iter, 0)) {
if (svc->state == SVC_STOPPING_STATE && svc->pid > 1)
return svc;
/* Also wait for remain tasks running their post script */
if (svc_is_remain(svc) && svc->state == SVC_TEARDOWN_STATE && svc->pid > 1)
return svc;
}
return NULL;
+2
View File
@@ -139,6 +139,7 @@ typedef struct svc {
svc_type_t type; /* Service, run, task, ... */
char protect; /* Services like dbus-daemon & udev by Finit */
char manual; /* run/task that require `initctl start foo` */
char remain; /* run/task: stay in DONE state, run post: on stop */
char nowarn; /* Skip or log warning if cmd missing or conflicts */
const int dirty; /* 0: unmodified, 1: modified */
const int removed;
@@ -282,6 +283,7 @@ static inline int svc_is_tty (svc_t *svc) { return svc && SVC_TYPE_TTY
static inline int svc_is_runtask (svc_t *svc) { return svc && (SVC_TYPE_RUNTASK & svc->type);}
static inline int svc_is_forking (svc_t *svc) { return svc && svc->forking; }
static inline int svc_is_manual (svc_t *svc) { return svc && svc->manual; }
static inline int svc_is_remain (svc_t *svc) { return svc && svc->remain; }
static inline int svc_is_noreload (svc_t *svc) { return svc && (0 == svc->sighup && 0 == svc->reload_script[0]); }
static inline int svc_in_runlevel (svc_t *svc, int runlevel) { return svc && ISSET(svc->runlevels, runlevel); }