Skip to content

Commit ff930bb

Browse files
committed
conmon: add --timer-command to run command every N seconds
Signed-off-by: Giuseppe Scrivano <gscrivan@redhat.com>
1 parent e25af44 commit ff930bb

File tree

7 files changed

+440
-32
lines changed

7 files changed

+440
-32
lines changed

docs/conmon.8.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,20 @@ Specify the Container UUID to use.
142142
**--version**
143143
Print the version and exit.
144144

145+
**--timer-command**
146+
Execute COMMAND every SECONDS. Format: ID:SECONDS:COMMAND. Can be specified multiple times.
147+
148+
This option allows conmon to periodically execute commands at specified intervals while
149+
the container is running. Commands must be specified with full paths. IDs must be
150+
incremental starting from 0. This is useful for periodic health checks, log rotation,
151+
or other maintenance tasks.
152+
153+
**--timer-command-argument**
154+
Add an argument to timer-command with ID. Format: ID:ARGUMENT. Can be specified multiple times.
155+
156+
This option allows adding command-line arguments to a specific timer-command by ID.
157+
Arguments are added in the order they are specified.
158+
145159
## SEE ALSO
146160
podman(1), buildah(1), cri-o(1), crun(8), runc(8)
147161

src/cli.c

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
#include <glib-unix.h>
99
#include <stdio.h>
1010
#include <stdlib.h>
11+
#include <string.h>
12+
#include <errno.h>
13+
#include <limits.h>
1114
#ifdef __linux__
1215
#include <linux/limits.h>
1316
#endif
@@ -57,6 +60,9 @@ char *opt_sdnotify_socket = NULL;
5760
gboolean opt_full_attach_path = FALSE;
5861
char *opt_seccomp_notify_socket = NULL;
5962
char *opt_seccomp_notify_plugins = NULL;
63+
gchar **opt_timer_command = NULL;
64+
gchar **opt_timer_command_argument = NULL;
65+
GPtrArray *timer_command_entries = NULL;
6066
GOptionEntry opt_entries[] = {
6167
{"api-version", 0, 0, G_OPTION_ARG_NONE, &opt_api_version, "Conmon API version to use", NULL},
6268
{"bundle", 'b', 0, G_OPTION_ARG_STRING, &opt_bundle_path, "Location of the OCI Bundle path", NULL},
@@ -117,6 +123,10 @@ GOptionEntry opt_entries[] = {
117123
"Path to the socket where the seccomp notification fd is received", NULL},
118124
{"seccomp-notify-plugins", 0, 0, G_OPTION_ARG_STRING, &opt_seccomp_notify_plugins,
119125
"Plugins to use for managing the seccomp notifications", NULL},
126+
{"timer-command", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_timer_command,
127+
"Execute COMMAND every SECONDS. Format: ID:SECONDS:COMMAND. Can be specified multiple times", NULL},
128+
{"timer-command-argument", 0, 0, G_OPTION_ARG_STRING_ARRAY, &opt_timer_command_argument,
129+
"Add an argument to timer-command with ID. Format: ID:ARGUMENT. Can be specified multiple times", NULL},
120130
{NULL, 0, 0, 0, NULL, NULL, NULL}};
121131

122132

@@ -205,4 +215,111 @@ void process_cli()
205215
if (opt_no_container_partial_message && !logging_is_journald_enabled()) {
206216
nwarnf("--no-container-partial-message has no effect without journald log driver");
207217
}
218+
219+
/* Parse timer-command entries with ID-based arguments */
220+
if (opt_timer_command || opt_timer_command_argument) {
221+
timer_command_entries = g_ptr_array_new_with_free_func(g_free);
222+
223+
/* Parse timer-command commands first */
224+
if (opt_timer_command) {
225+
for (int i = 0; opt_timer_command[i] != NULL; i++) {
226+
char *entry = g_strdup(opt_timer_command[i]);
227+
char *first_colon = strchr(entry, ':');
228+
if (!first_colon) {
229+
nexitf("Invalid timer-command format '%s'. Expected ID:SECONDS:COMMAND", opt_timer_command[i]);
230+
}
231+
232+
*first_colon = '\0';
233+
char *second_colon = strchr(first_colon + 1, ':');
234+
if (!second_colon) {
235+
nexitf("Invalid timer-command format '%s'. Expected ID:SECONDS:COMMAND", opt_timer_command[i]);
236+
}
237+
238+
*second_colon = '\0';
239+
240+
char *id_str = entry;
241+
char *interval_str = first_colon + 1;
242+
char *command = second_colon + 1;
243+
244+
/* Parse ID */
245+
char *endptr;
246+
errno = 0;
247+
long id_long = strtol(id_str, &endptr, 10);
248+
if (errno != 0 || endptr == id_str || *endptr != '\0' || id_long < 0 || id_long > INT_MAX) {
249+
nexitf("Invalid timer-command ID '%s' in '%s'", id_str, opt_timer_command[i]);
250+
}
251+
int id = (int)id_long;
252+
253+
/* Require incremental IDs starting from 0 */
254+
int expected_id = i;
255+
if (id != expected_id) {
256+
nexitf("Timer-command IDs must be incremental starting from 0. Expected %d, got %d", expected_id, id);
257+
}
258+
259+
/* Parse interval */
260+
errno = 0;
261+
long interval_long = strtol(interval_str, &endptr, 10);
262+
if (errno != 0 || endptr == interval_str || *endptr != '\0' || interval_long <= 0
263+
|| interval_long > INT_MAX) {
264+
nexitf("Invalid timer-command interval '%s' in '%s'", interval_str, opt_timer_command[i]);
265+
}
266+
int interval = (int)interval_long;
267+
268+
if (*command == '\0') {
269+
nexitf("Empty timer-command command in '%s'", opt_timer_command[i]);
270+
}
271+
272+
timer_command_entry_t *cc_entry = g_malloc(sizeof(timer_command_entry_t));
273+
cc_entry->id = id;
274+
cc_entry->interval = interval;
275+
cc_entry->args = g_malloc(sizeof(gchar *) * 2);
276+
cc_entry->args[0] = g_strdup(command);
277+
cc_entry->args[1] = NULL;
278+
279+
g_ptr_array_add(timer_command_entries, cc_entry);
280+
281+
ndebugf("Added timer-command: ID=%d, %d seconds, command: %s", id, interval, command);
282+
g_free(entry);
283+
}
284+
}
285+
286+
if (opt_timer_command_argument) {
287+
for (int i = 0; opt_timer_command_argument[i] != NULL; i++) {
288+
char *entry = g_strdup(opt_timer_command_argument[i]);
289+
char *colon = strchr(entry, ':');
290+
if (!colon) {
291+
nexitf("Invalid timer-command-argument format '%s'. Expected ID:ARGUMENT", opt_timer_command_argument[i]);
292+
}
293+
294+
*colon = '\0';
295+
char *id_str = entry;
296+
char *argument = colon + 1;
297+
298+
/* Parse ID */
299+
char *endptr;
300+
errno = 0;
301+
long id_long = strtol(id_str, &endptr, 10);
302+
if (errno != 0 || endptr == id_str || *endptr != '\0' || id_long < 0 || id_long > INT_MAX) {
303+
nexitf("Invalid timer-command ID '%s' in argument '%s'", id_str, opt_timer_command_argument[i]);
304+
}
305+
int id = (int)id_long;
306+
307+
if (id < 0 || id >= (int)timer_command_entries->len) {
308+
nexitf("Timer-command ID %d not found for argument '%s'", id, argument);
309+
}
310+
timer_command_entry_t *target_entry = g_ptr_array_index(timer_command_entries, id);
311+
312+
int arg_count = 0;
313+
while (target_entry->args[arg_count])
314+
arg_count++;
315+
316+
target_entry->args = g_realloc(target_entry->args, sizeof(gchar *) * (arg_count + 2));
317+
target_entry->args[arg_count] = g_strdup(argument);
318+
target_entry->args[arg_count + 1] = NULL;
319+
320+
ndebugf("Added argument '%s' to timer-command ID %d", argument, id);
321+
g_free(entry);
322+
}
323+
}
324+
}
208325
}

src/cli.h

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
#include <glib.h> /* gboolean and GOptionEntry */
55
#include <stdint.h> /* int64_t */
6+
#include <time.h> /* time_t */
67

78
extern gboolean opt_version;
89
extern gboolean opt_terminal;
@@ -49,6 +50,16 @@ extern char *opt_seccomp_notify_socket;
4950
extern char *opt_seccomp_notify_plugins;
5051
extern GOptionEntry opt_entries[];
5152
extern gboolean opt_full_attach_path;
53+
extern gchar **opt_timer_command;
54+
extern gchar **opt_timer_command_argument;
55+
56+
typedef struct {
57+
int id;
58+
int interval;
59+
gchar **args;
60+
} timer_command_entry_t;
61+
62+
extern GPtrArray *timer_command_entries;
5263

5364
int initialize_cli(int argc, char *argv[]);
5465
void process_cli();

src/conmon.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,17 @@ static void disconnect_std_streams(int dev_null_r, int dev_null_w)
3434
pexit("Failed to dup over stderr");
3535
}
3636

37+
38+
static gboolean timer_command_timer_cb(gpointer user_data)
39+
{
40+
timer_command_entry_t *entry = (timer_command_entry_t *)user_data;
41+
42+
ndebugf("Executing timer-command: %s", entry->args[0]);
43+
execute_command_detached(entry->args, 0);
44+
45+
return G_SOURCE_CONTINUE; // Keep repeating
46+
}
47+
3748
#define DEFAULT_UMASK 0022
3849

3950
int main(int argc, char *argv[])
@@ -423,6 +434,15 @@ int main(int argc, char *argv[])
423434
g_timeout_add_seconds(opt_timeout, timeout_cb, NULL);
424435
}
425436

437+
/* Add individual timers for each timer-command entry */
438+
if (timer_command_entries && timer_command_entries->len > 0) {
439+
for (guint i = 0; i < timer_command_entries->len; i++) {
440+
timer_command_entry_t *entry = g_ptr_array_index(timer_command_entries, i);
441+
g_timeout_add_seconds(entry->interval, timer_command_timer_cb, entry);
442+
ndebugf("Timer-command timer set up: %d seconds, command: %s", entry->interval, entry->args[0]);
443+
}
444+
}
445+
426446
if (data.exit_status_cache) {
427447
GHashTableIter iter;
428448
gpointer key, value;

src/ctr_exit.c

Lines changed: 77 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
#include "oom.h"
1111

1212
#include <errno.h>
13+
#include <fcntl.h>
1314
#include <glib.h>
1415
#include <glib-unix.h>
1516
#include <signal.h>
1617
#include <stdlib.h>
18+
#include <sys/wait.h>
1719
#include <unistd.h>
1820

1921
volatile sig_atomic_t container_pid = -1;
@@ -152,53 +154,105 @@ void container_exit_cb(G_GNUC_UNUSED GPid pid, int status, G_GNUC_UNUSED gpointe
152154
g_main_loop_quit(main_loop);
153155
}
154156

155-
void do_exit_command()
157+
/*
158+
* Execute a command with double fork to completely detach the process.
159+
* Used for timer-command commands that should run independently of conmon.
160+
* args[0] must be the command path and args must be NULL-terminated.
161+
*/
162+
void execute_command_detached(gchar **args, int delay)
156163
{
157-
if (signal(SIGCHLD, SIG_DFL) == SIG_ERR) {
158-
_pexit("Failed to reset signal for SIGCHLD");
164+
pid_t first_fork = fork();
165+
if (first_fork < 0) {
166+
nwarnf("Failed to fork for command: %s", args[0]);
167+
return;
168+
}
169+
170+
if (first_fork) {
171+
int status;
172+
waitpid(first_fork, &status, 0);
173+
return;
174+
}
175+
176+
pid_t second_fork = fork();
177+
if (second_fork < 0) {
178+
_exit(1);
179+
} else if (second_fork == 0) {
180+
close_all_fds_ge_than(3);
181+
182+
int null_fd = open("/dev/null", O_RDWR);
183+
if (null_fd >= 0) {
184+
dup2(null_fd, STDIN_FILENO);
185+
dup2(null_fd, STDOUT_FILENO);
186+
dup2(null_fd, STDERR_FILENO);
187+
if (null_fd > 2) {
188+
close(null_fd);
189+
}
190+
}
191+
192+
setsid();
193+
194+
execute_command(args, delay, false);
195+
_exit(EXIT_FAILURE);
196+
} else {
197+
_exit(0);
159198
}
199+
}
160200

161-
/*
162-
* Close everything except stdin, stdout and stderr.
163-
*/
201+
/*
202+
* Execute a command with single fork.
203+
* If do_wait is true, wait for completion (used by exit commands).
204+
* If do_wait is false, don't wait (used by detached commands after double fork).
205+
* args[0] must be the command path and args must be NULL-terminated.
206+
*/
207+
void execute_command(gchar **args, int delay, gboolean do_wait)
208+
{
164209
close_all_fds_ge_than(3);
165210

166-
/*
167-
* We don't want the exit command to be reaped by the parent conmon
168-
* as that would prevent double-fork from doing its job.
169-
* Unfortunately, that also means that any new subchildren from
170-
* still running processes could also get lost
171-
*/
172211
if (set_subreaper(false) != 0) {
173212
nwarn("Failed to disable self subreaper attribute - might wait for indirect children a long time");
174213
}
175214

176-
pid_t exit_pid = fork();
177-
if (exit_pid < 0) {
215+
pid_t child_pid = fork();
216+
if (child_pid < 0) {
178217
_pexit("Failed to fork");
179218
}
180219

181-
if (exit_pid) {
220+
if (child_pid) {
221+
if (!do_wait) {
222+
return;
223+
}
182224
int ret, exit_status = 0;
183225

184-
/*
185-
* Make sure to cleanup any zombie process that the container runtime
186-
* could have left around.
187-
*/
188226
do {
189227
int tmp;
190228

191229
exit_status = 0;
192230
ret = waitpid(-1, &tmp, 0);
193-
if (ret == exit_pid)
231+
if (ret == child_pid)
194232
exit_status = get_exit_status(tmp);
195233
} while ((ret < 0 && errno == EINTR) || ret > 0);
196234

197235
if (exit_status)
198236
_exit(exit_status);
199-
200237
return;
201238
}
239+
if (delay > 0) {
240+
ndebugf("Sleeping for %d seconds before executing command", delay);
241+
sleep(delay);
242+
}
243+
244+
reset_oom_adjust();
245+
246+
execv(args[0], args);
247+
248+
_exit(EXIT_FAILURE);
249+
}
250+
251+
void do_exit_command()
252+
{
253+
if (signal(SIGCHLD, SIG_DFL) == SIG_ERR) {
254+
_pexit("Failed to reset signal for SIGCHLD");
255+
}
202256

203257
/* Count the additional args, if any. */
204258
size_t n_args = 0;
@@ -214,19 +268,10 @@ void do_exit_command()
214268
args[n_args + 1] = opt_exit_args[n_args];
215269
args[n_args + 1] = NULL;
216270

217-
if (opt_exit_delay) {
218-
ndebugf("Sleeping for %d seconds before executing exit command", opt_exit_delay);
219-
sleep(opt_exit_delay);
220-
}
221-
222-
reset_oom_adjust();
223-
224-
execv(opt_exit_command, args);
225-
226-
/* Should not happen, but better be safe. */
227-
_exit(EXIT_FAILURE);
271+
execute_command(args, opt_exit_delay, true);
228272
}
229273

274+
230275
void reap_children()
231276
{
232277
/* We need to reap any zombies (from an OCI runtime that errored) before

src/ctr_exit.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ gboolean timeout_cb(G_GNUC_UNUSED gpointer user_data);
2222
int get_exit_status(int status);
2323
void runtime_exit_cb(G_GNUC_UNUSED GPid pid, int status, G_GNUC_UNUSED gpointer user_data);
2424
void container_exit_cb(G_GNUC_UNUSED GPid pid, int status, G_GNUC_UNUSED gpointer user_data);
25+
void execute_command_detached(gchar **args, int delay);
26+
void execute_command(gchar **args, int delay, gboolean do_wait);
2527
void do_exit_command();
2628
void reap_children();
2729
void cleanup_socket_dir_symlink();

0 commit comments

Comments
 (0)