Skip to content

Commit 56c8d3d

Browse files
committed
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.
1 parent a215747 commit 56c8d3d

5 files changed

Lines changed: 98 additions & 3 deletions

File tree

doc/config/service-opts.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ confines of that the following options are available:
6969
* `respawn` -- bypasses the `restart` mechanism completely, allows
7070
endless restarts. Useful in many use-cases, but not what `service`
7171
was originally designed for so not the default behavior
72+
* `remain:yes` -- for `run` and `task` only. Prevents the task from
73+
re-running on runlevel re-entry and ensures the `post:` script runs
74+
when the task is explicitly stopped or leaves its valid runlevels.
75+
Similar to systemd's `RemainAfterExit=yes`. See [Task and Run](task-and-run.md)
76+
for more details
7277
* `oncrash:reboot` -- when all retries have failed, and the service
7378
has *crashed*, if this option is set the system is rebooted
7479
* `oncrash:script` -- similarly, but instead of rebooting, call the

doc/config/task-and-run.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,44 @@ redirects can be used:
4444

4545
Please note, `;`, `&&`, `||`, and similar are *not supported*. Any
4646
non-trivial constructs are better placed in a separate shell script.
47+
48+
49+
remain:yes
50+
----------
51+
52+
By default, a `run` or `task` will re-run each time its runlevel is
53+
entered, and its `post:` script does not run on completion.
54+
55+
With `remain:yes`, the task runs once and does not re-run on runlevel
56+
re-entry:
57+
58+
task [2345] remain:yes /usr/sbin/setup-firewall -- Firewall setup
59+
60+
This has the following effects:
61+
62+
* The task does not re-run on runlevel re-entry
63+
* The `post:` script runs when:
64+
- The task is explicitly stopped (`initctl stop NAME`)
65+
- The task leaves its valid runlevels (e.g., runlevel change)
66+
67+
This is useful for tasks that set up persistent state where:
68+
69+
* Cleanup should only happen on explicit stop or when leaving valid runlevels
70+
* The setup should not be re-run on every runlevel entry
71+
72+
**Example:** Setting up firewall rules with cleanup on shutdown:
73+
74+
```
75+
task [2345] remain:yes \
76+
post:/usr/sbin/teardown-firewall \
77+
/usr/sbin/setup-firewall -- Firewall setup
78+
```
79+
80+
The firewall rules are created once. The `post:` script runs when
81+
entering runlevel 0 (halt) or 6 (reboot), or on explicit stop.
82+
83+
> [!NOTE]
84+
> The `remain:yes` option is not supported for bootstrap-only tasks
85+
> (tasks with only runlevel S). Bootstrap tasks are deleted immediately
86+
> after completion, and their `post:` scripts never run. A warning is
87+
> logged if `remain:yes` is used on such tasks.

man/finit.conf.5

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,20 @@ mechanism completely, allows endless restarts. Useful in many
398398
use-cases, but not what
399399
.Cm service
400400
was originally designed for so not the default behavior.
401+
.It Cm remain:yes
402+
for
403+
.Cm run
404+
and
405+
.Cm task
406+
only. Prevents the task from re-running on runlevel re-entry and
407+
ensures the
408+
.Cm post:
409+
script runs when the task is explicitly stopped or leaves its valid
410+
runlevels. Similar to systemd's
411+
.Cm RemainAfterExit=yes .
412+
.Pp
413+
.Sy Note:
414+
not supported for bootstrap-only tasks (runlevels [S] only).
401415
.It Cm oncrash:reboot
402416
when all retries have failed, and the service
403417
has

src/service.c

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1798,7 +1798,7 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
17981798
char *dev = NULL;
17991799
int respawn = 0;
18001800
int levels = 0;
1801-
int forking = 0, manual = 0, nowarn = 0;
1801+
int forking = 0, manual = 0, remain = 0, nowarn = 0;
18021802
int restart_max = SVC_RESPAWN_MAX;
18031803
int restart_tmo = 0;
18041804
unsigned oncrash_action = SVC_ONCRASH_IGNORE;
@@ -1866,6 +1866,8 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
18661866
forking = 1;
18671867
else if (MATCH_CMD(cmd, "manual:yes", arg))
18681868
manual = 1;
1869+
else if (MATCH_CMD(cmd, "remain:yes", arg))
1870+
remain = 1;
18691871
else if (MATCH_CMD(cmd, "restart:", arg)) {
18701872
if (MATCH_CMD(arg, "always", arg))
18711873
restart_max = -1;
@@ -2171,6 +2173,18 @@ int service_register(int type, char *cfg, struct rlimit rlimit[], char *file)
21712173
memset(svc->ifstmt, 0, sizeof(svc->ifstmt));
21722174
svc->manual = manual;
21732175
svc->nowarn = nowarn;
2176+
2177+
/*
2178+
* remain:yes is not supported for bootstrap-only tasks. These
2179+
* tasks are deleted immediately after completion and their post:
2180+
* scripts never run. This is by design.
2181+
*/
2182+
if (remain && svc_is_runtask(svc) && !ISOTHER(levels, INIT_LEVEL)) {
2183+
logit(LOG_WARNING, "%s: remain:yes ignored for bootstrap-only tasks",
2184+
svc_ident(svc, NULL, 0));
2185+
remain = 0;
2186+
}
2187+
svc->remain = remain;
21742188
svc->respawn = respawn;
21752189
svc->forking = forking;
21762190
svc->restart_max = restart_max;
@@ -2876,9 +2890,16 @@ int service_step(svc_t *svc)
28762890
break;
28772891

28782892
case SVC_DONE_STATE:
2879-
if (svc_is_changed(svc))
2893+
/* Remain tasks: always run post script when stopped, even if config changed */
2894+
if (svc_is_runtask(svc) && svc_is_remain(svc) && svc_is_stopped(svc)) {
2895+
if (svc_has_post(svc)) {
2896+
svc_set_state(svc, SVC_TEARDOWN_STATE);
2897+
service_post_script(svc);
2898+
} else
2899+
svc_set_state(svc, SVC_HALTED_STATE);
2900+
} else if (svc_is_changed(svc))
28802901
svc_set_state(svc, SVC_HALTED_STATE);
2881-
if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
2902+
else if (svc_is_runtask(svc) && svc_is_manual(svc) && enabled)
28822903
svc_set_state(svc, SVC_WAITING_STATE);
28832904
break;
28842905

@@ -3180,6 +3201,18 @@ void service_runtask_clean(void)
31803201
if (!svc_is_runtask(svc))
31813202
continue;
31823203

3204+
/* Remain tasks stay in DONE state if still valid in new runlevel */
3205+
if (svc_is_remain(svc) && svc->state == SVC_DONE_STATE) {
3206+
if (svc_in_runlevel(svc, runlevel) && !svc_is_changed(svc))
3207+
continue; /* Keep once flag, stay in DONE */
3208+
3209+
/* Config changed or leaving runlevel: stop, run post, restart */
3210+
svc->once = 0;
3211+
svc_stop(svc);
3212+
service_step(svc);
3213+
continue;
3214+
}
3215+
31833216
/* run/task declared with <!> */
31843217
if (svc->sighup)
31853218
svc->once = 1;

src/svc.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ typedef struct svc {
139139
svc_type_t type; /* Service, run, task, ... */
140140
char protect; /* Services like dbus-daemon & udev by Finit */
141141
char manual; /* run/task that require `initctl start foo` */
142+
char remain; /* run/task: stay in DONE state, run post: on stop */
142143
char nowarn; /* Skip or log warning if cmd missing or conflicts */
143144
const int dirty; /* 0: unmodified, 1: modified */
144145
const int removed;
@@ -282,6 +283,7 @@ static inline int svc_is_tty (svc_t *svc) { return svc && SVC_TYPE_TTY
282283
static inline int svc_is_runtask (svc_t *svc) { return svc && (SVC_TYPE_RUNTASK & svc->type);}
283284
static inline int svc_is_forking (svc_t *svc) { return svc && svc->forking; }
284285
static inline int svc_is_manual (svc_t *svc) { return svc && svc->manual; }
286+
static inline int svc_is_remain (svc_t *svc) { return svc && svc->remain; }
285287
static inline int svc_is_noreload (svc_t *svc) { return svc && (0 == svc->sighup && 0 == svc->reload_script[0]); }
286288

287289
static inline int svc_in_runlevel (svc_t *svc, int runlevel) { return svc && ISSET(svc->runlevels, runlevel); }

0 commit comments

Comments
 (0)