2 * FCRON - periodic command scheduler
4 * Copyright 2000-2014 Thibault Godouet <fcron@free.fr>
6 * This program is free software; you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation; either version 2 of the License, or
9 * (at your option) any later version.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, write to the Free Software
18 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
20 * The GNU General Public License can also be found in the file
21 * `LICENSE' that comes with the fcron source distribution.
25 /* fcrondyn : interact dynamically with running fcron process :
26 * - list jobs, with their status, next time of execution, etc
28 * - send a signal to a job
34 #include "read_string.h"
37 #ifdef HAVE_LIBREADLINE
38 char **rl_cmpl_fcrondyn(const char *text, int start, int end);
39 char *rl_cmpl_command_generator(const char *text, int state);
40 #if defined(HAVE_READLINE_READLINE_H)
41 #include <readline/readline.h>
42 #elif defined(HAVE_READLINE_H)
44 #endif /* !defined(HAVE_READLINE_H) */
45 #if defined(HAVE_READLINE_HISTORY_H)
46 #include <readline/history.h>
47 #elif defined(HAVE_HISTORY_H)
49 #endif /* defined(HAVE_READLINE_HISTORY_H) */
50 #endif /* HAVE_LIBREADLINE */
54 void xexit(int exit_val);
55 RETSIGTYPE sigpipe_handler(int x);
56 int interactive_mode(int fd);
57 /* returned by parse_cmd() and/or talk_fcron() */
61 #define CMD_NOT_FOUND 4
63 int talk_fcron(char *cmd_str, int fd);
64 int parse_cmd(char *cmd_str, long int **cmd, int *cmd_len);
65 int connect_fcron(void);
66 int authenticate_user_password(int fd);
68 /* command line options */
71 /* needed by log part : */
72 char *prog_name = NULL;
76 /* uid/gid of user/group root
77 * (we don't use the static UID or GID as we ask for user and group names
78 * in the configure script) */
83 char *user_str = NULL;
87 struct cmd_list_ent cmd_list[] = {
88 /* name, desc, num opt, cmd code, cmd opts, cmd defaults */
89 {"ls", {NULL}, "List all jobs of user", 1, CMD_LIST_JOBS,
91 {"ls_lavgq", {}, "List jobs of user which are in lavg queue", 1,
92 CMD_LIST_LAVGQ, {USER}, {CUR_USER}},
93 {"ls_serialq", {}, "List jobs of user which are in serial queue",
94 1, CMD_LIST_SERIALQ, {USER}, {CUR_USER}},
95 {"ls_exeq", {}, "List running jobs of user", 1, CMD_LIST_EXEQ,
97 {"detail", {}, "Print details on job", 1, CMD_DETAILS,
98 {JOBID}, {ARG_REQUIRED}},
99 /* {"reschedule", {NULL}, "Reschedule next execution of job", 2,
100 CMD_RESCHEDULE, {TIME_AND_DATE, JOBID}, {ARG_REQUIRED, ARG_REQUIRED}}, */
101 {"runnow", {}, "Advance next execution of job to now", 1, CMD_RUNNOW,
102 {JOBID}, {ARG_REQUIRED}},
103 {"run", {}, "Run job now (without changing its current schedule)", 1,
104 CMD_RUN, {JOBID}, {ARG_REQUIRED}},
105 {"kill", {}, "Send signal to running job", 2, CMD_SEND_SIGNAL,
106 {SIGNAL, JOBID}, {ARG_REQUIRED, ARG_REQUIRED}},
107 {"renice", {}, "Renice running job", 2, CMD_RENICE,
108 {NICE_VALUE, JOBID}, {ARG_REQUIRED, ARG_REQUIRED}},
109 {"quit", {"q", "exit"}, "Quit fcrondyn", 0, QUIT_CMD, {}, {}},
110 {"help", {"h"}, "Display a help message", 0, HELP_CMD, {}, {}},
113 const int cmd_list_len = sizeof(cmd_list) / sizeof(cmd_list_ent);
117 /* print some informations about this program :
118 * version, license */
121 "fcrondyn " VERSION_QUOTED
122 " - interact dynamically with daemon fcron\n" "Copyright "
123 COPYRIGHT_QUOTED " Thibault Godouet <fcron@free.fr>\n"
124 "This program is free software distributed WITHOUT ANY WARRANTY.\n"
125 "See the GNU General Public License for more details.\n");
134 /* print a help message about command line options and exit */
138 "fcrondyn -x {command}\n"
140 " -i run fcrontab in interactive mode.\n"
141 " -x execute one command (in batch mode)\n"
142 " -c f make fcrontab use config file f.\n"
143 " -d set up debug mode.\n"
144 " -h display this help message.\n"
145 " -V display version & infos about fcrondyn.\n" "\n"
146 "To list the available commands, run:\n" " fcrondyn -x help\n");
152 sigpipe_handler(int x)
153 /* handle broken pipes ... */
156 "Broken pipe : fcron may have closed the connection\nThe connection "
157 "has been idle for more than %ds, or fcron may not be running anymore.\n",
159 fprintf(stderr, "Exiting ...\n");
174 /* used in parse_cmd : */
175 #define Write_cmd(DATA) \
176 memcpy(buf + *cmd_len, &DATA, sizeof(long int)); \
179 #define Strncmp(STR1, STR2, STR1_SIZE) \
180 strncmp(STR1, STR2, (STR1_SIZE < strlen(STR2)) ? strlen(STR2) : STR1_SIZE)
183 parse_cmd(char *cmd_str, long int **cmd, int *cmd_len)
184 /* read a command string, check if it is valid and translate it */
186 long int buf[SOCKET_MSG_LEN];
188 int i = 0, j = 0, rank = -1;
189 long int int_buf = 0;
190 struct passwd *pass = NULL;
192 long int sysfcrontab_uid = SYSFCRONTAB_UID;
195 bzero(buf, sizeof(buf));
197 remove_blanks(cmd_str); /* at the end of the string */
199 if ((word_size = get_word(&cmd_str)) == 0) {
200 fprintf(stderr, "Warning : Zero-length command name : line ignored.\n");
204 for (i = 0; i < cmd_list_len; i++) {
206 if (Strncmp(cmd_str, cmd_list[i].cmd_name, word_size) == 0) {
210 for (j = 0; j < MAX_NUM_ALIAS && cmd_list[i].cmd_alias[j] != NULL; j++) {
211 if (Strncmp(cmd_str, cmd_list[i].cmd_alias[j], word_size) == 0) {
218 fprintf(stderr, "Error : Unknown command.\n");
219 return CMD_NOT_FOUND;
221 else if (cmd_list[rank].cmd_code == QUIT_CMD) {
223 fprintf(stderr, "quit command\n");
226 else if (cmd_list[rank].cmd_code == HELP_CMD) {
228 fprintf(stderr, "Help command\n");
232 Write_cmd(cmd_list[rank].cmd_code);
235 fprintf(stderr, "command : %s\n", cmd_list[i].cmd_name);
237 cmd_str += word_size;
238 for (i = 0; i < cmd_list[rank].cmd_numopt; i++) {
240 if ((word_size = get_word(&cmd_str)) == 0) {
242 if (cmd_list[rank].cmd_default[i] == ARG_REQUIRED) {
243 fprintf(stderr, "Error : arg required !\n");
247 /* use default value : currently, works only with CUR_USER */
248 if (user_uid == rootuid) {
249 /* default for root = all */
253 fprintf(stderr, " uid = ALL\n");
258 fprintf(stderr, " uid = %d\n", (int)user_uid);
265 /* get value from line ... */
266 switch (cmd_list[rank].cmd_opt[i]) {
269 int_buf = (long int)*(cmd_str + word_size);
270 *(cmd_str + word_size) = '\0';
272 if (strcmp(cmd_str, SYSFCRONTAB) == 0) {
273 Write_cmd(sysfcrontab_uid);
277 if ((pass = getpwnam(cmd_str)) == NULL) {
279 "Error : '%s' isn't a valid username.\n",
283 Write_cmd(pass->pw_uid);
287 *(cmd_str + word_size) = (char)int_buf;
288 cmd_str += word_size;
290 fprintf(stderr, " uid = %d\n",
292 (pass) ? (int)pass->pw_uid : (int)SYSFCRONTAB_UID
300 /* after strtol(), cmd_str will be updated (first non-number char) */
301 if ((int_buf = strtol(cmd_str, &cmd_str, 10)) < 0
302 || int_buf >= LONG_MAX || (!isspace((int)*cmd_str)
303 && *cmd_str != '\0')) {
304 fprintf(stderr, "Error : invalid jobid.\n");
309 fprintf(stderr, " jobid = %ld\n", int_buf);
313 /* argghh !!! no standard function ! */
317 /* after strtol(), cmd_str will be updated (first non-number char) */
318 if ((int_buf = strtol(cmd_str, &cmd_str, 10)) > 20
319 || (int_buf < 0 && getuid() != rootuid) || int_buf < -20
320 || (!isspace((int)*cmd_str) && *cmd_str != '\0')) {
321 fprintf(stderr, "Error : invalid nice value.\n");
326 fprintf(stderr, " nicevalue = %ld\n", int_buf);
330 if (isalpha((int)*cmd_str)) {
331 for (j = 0; j < word_size; j++)
332 *(cmd_str + j) = tolower(*(cmd_str + j));
333 if (Strncmp(cmd_str, "hup", word_size) == 0)
335 else if (Strncmp(cmd_str, "int", word_size) == 0)
337 else if (Strncmp(cmd_str, "quit", word_size) == 0)
339 else if (Strncmp(cmd_str, "kill", word_size) == 0)
341 else if (Strncmp(cmd_str, "alrm", word_size) == 0)
343 else if (Strncmp(cmd_str, "term", word_size) == 0)
345 else if (Strncmp(cmd_str, "usr1", word_size) == 0)
347 else if (Strncmp(cmd_str, "usr2", word_size) == 0)
349 else if (Strncmp(cmd_str, "cont", word_size) == 0)
351 else if (Strncmp(cmd_str, "stop", word_size) == 0)
353 else if (Strncmp(cmd_str, "tstp", word_size) == 0)
357 "Error : unknow signal (try integer value)\n");
360 cmd_str += word_size;
362 /* after strtol(), cmd_str will be updated (first non-number char) */
363 else if ((int_buf = strtol(cmd_str, &cmd_str, 10)) <= 0
364 || int_buf >= LONG_MAX || (!isspace((int)*cmd_str)
365 && *cmd_str != '\0')) {
366 fprintf(stderr, "Error : invalid signal value.\n");
371 fprintf(stderr, " signal = %ld\n", int_buf);
375 fprintf(stderr, "Error : Unknown arg !");
381 Skip_blanks(cmd_str);
382 if (*cmd_str != '\0')
383 fprintf(stderr, "Warning : too much arguments : '%s' ignored.\n",
386 /* This is a valid command ... */
387 *cmd = alloc_safe(*cmd_len * sizeof(long int), "command string");
388 memcpy(*cmd, buf, *cmd_len * sizeof(long int));
394 authenticate_user_password(int fd)
395 /* authenticate user */
397 char *password = NULL;
398 char buf[USER_NAME_LEN + 16];
400 fd_set read_set; /* needed to use select to check if some data is waiting */
403 snprintf(buf, sizeof(buf), "password for %s :", user_str);
404 if ((password = read_string(CONV_ECHO_OFF, buf)) == NULL)
407 len = snprintf(buf, sizeof(buf), "%s", user_str) + 1;
408 len += snprintf(buf + len, sizeof(buf) - len, "%s", password) + 1;
409 send(fd, buf, len, 0);
414 tv.tv_sec = MAX_WAIT_TIME;
417 FD_SET(fd, &read_set);
418 if (select(fd + 1, &read_set, NULL, NULL, &tv) <= 0) {
419 error_e("Couldn't get data from socket during %d seconds.",
423 while (recv(fd, buf, sizeof(buf), 0) < 0 && errno == EINTR)
424 if (errno == EINTR && debug_opt)
425 fprintf(stderr, "Got EINTR ...");
426 if (strncmp(buf, "1", sizeof("1")) != 0)
435 /* connect to fcron through a socket, and identify user */
438 struct sockaddr_un addr;
442 if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
443 die_e("could not create socket");
445 addr.sun_family = AF_UNIX;
446 len = strlen(fifofile);
447 if (len > sizeof(addr.sun_path) - 1)
448 die("Error : fifo file path too long (max is %d)",
449 sizeof(addr.sun_path) - 1);
450 /* length(fifofile) < sizeof(add.sun_path), so strncpy will terminate
451 * the string with at least one \0 (not necessarily required by the OS,
452 * but still a good idea) */
453 strncpy(addr.sun_path, fifofile, sizeof(addr.sun_path));
454 addr.sun_path[sizeof(addr.sun_path) - 1] = '\0';
455 sun_len = (addr.sun_path - (char *)&addr) + len;
457 addr.sun_len = sun_len;
460 if (connect(fd, (struct sockaddr *)&addr, sun_len) < 0)
461 die_e("Cannot connect() to fcron (check if fcron is running)");
463 /* Nothing to do on the client side if we use SO_PASSCRED */
464 #if !defined(SO_PASSCRED) && !defined(HAVE_GETPEERUCRED) && !defined(HAVE_GETPEEREID)
465 if (authenticate_user_password(fd) == ERR) {
466 fprintf(stderr, "Invalid password or too many authentication failures"
467 " (try to connect later).\n(In the later case, fcron rejects all"
468 " new authentication during %d secs)\n", AUTH_WAIT);
469 die("Unable to authenticate user");
471 #endif /* SO_PASSCRED HAVE_GETPEERUCRED HAVE_GETPEEREID */
478 talk_fcron(char *cmd_str, int fd)
479 /* read a string command, check if it is valid and translate it,
480 * send it to fcron, and print its answer */
482 long int *cmd = NULL;
486 char existing_connection = (fd < 0) ? 0 : 1;
487 fd_set read_set; /* needed to use select to check if some data is waiting */
490 switch (parse_cmd(cmd_str, &cmd, &cmd_len)) {
496 printf("Command recognized by fcrondyn :\n");
497 printf("------------------------------\n");
498 for (i = 0; i < cmd_list_len; i++) {
499 len = printf("%s ", cmd_list[i].cmd_name);
502 for (j = 0; j < cmd_list[i].cmd_numopt; j++) {
503 if (cmd_list[i].cmd_default[j] != ARG_REQUIRED)
505 switch (cmd_list[i].cmd_opt[j]) {
507 len += printf("user");
510 len += printf("jobid");
513 len += printf("time");
516 len += printf("niceval");
519 len += printf("sig");
522 len += printf("bool");
525 len += printf("unknown_arg!");
527 if (cmd_list[i].cmd_default[j] != ARG_REQUIRED)
531 /* Align correctly the descriptions : */
532 printf("%*s%s", 24 - len, "", cmd_list[i].cmd_desc);
534 /* print alias list (if any) */
535 if (cmd_list[i].cmd_alias[0] != NULL) {
536 printf(" (aliases:");
538 j < MAX_NUM_ALIAS && cmd_list[i].cmd_alias[j] != NULL;
540 printf(" %s", cmd_list[i].cmd_alias[j]);
552 return CMD_NOT_FOUND;
561 /* This is a valid command (so we'll have to free() it) ... */
563 if (!existing_connection && (fd = connect_fcron()) == ERR)
566 send(fd, cmd, cmd_len * sizeof(long int), 0);
570 tv.tv_sec = MAX_WAIT_TIME;
573 FD_SET(fd, &read_set);
574 if (select(fd + 1, &read_set, NULL, NULL, &tv) <= 0) {
575 error_e("Couldn't get data from socket during %d seconds.",
581 while ((read_len = (size_t) recv(fd, buf, sizeof(buf) - 1, 0)) >= 0
584 if (errno == EINTR && debug_opt)
585 fprintf(stderr, "got EINTR ...\n");
586 else if (read_len > sizeof(buf)) {
587 /* weird ... no data yet ? */
589 fprintf(stderr, "no data yet ?");
591 else if (read_len <= 0) {
593 fprintf(stderr, "read_len = %d\n", (int)read_len);
594 fprintf(stderr, "connection closed by fcron\n");
595 shutdown(fd, SHUT_RDWR);
599 /* ensure the string is terminated by a '\0' for when we'll printf() it */
600 buf[read_len] = '\0';
603 /* check for the end of command output marker */
604 if (read_len >= sizeof(END_STR) &&
605 strncmp(&buf[read_len - sizeof(END_STR)], END_STR,
606 sizeof(END_STR)) == 0)
611 error_e("error in recv()");
614 if (!existing_connection)
615 xclose_check(&fd, "unix socket");
620 #ifdef HAVE_LIBREADLINE
621 /* Attempt to complete on the contents of TEXT. START and END bound the
622 region of rl_line_buffer that contains the word to complete. TEXT is
623 the word to complete. We can use the entire contents of rl_line_buffer
624 in case we want to do some simple parsing. Return the array of matches,
625 or NULL if there aren't any. */
627 rl_cmpl_fcrondyn(const char *text, int start, int end)
631 matches = (char **)NULL;
633 /* If this word is at the start of the line, then it is a command
634 * to complete. Otherwise it is an argument which we ignore for now */
636 matches = rl_completion_matches(text, rl_cmpl_command_generator);
642 /* Generator function for command completion. STATE lets us know whether
643 to start from scratch; without any state (i.e. STATE == 0), then we
644 start at the top of the list. */
646 rl_cmpl_command_generator(const char *text, int state)
648 static int list_index, len;
651 /* If this is a new word to complete, initialize now. This includes
652 * saving the length of TEXT for efficiency, and initializing the index
659 /* Return the next name which partially matches from the command list. */
660 while (list_index < cmd_list_len) {
661 name = cmd_list[list_index].cmd_name;
663 if (strncmp(name, text, len) == 0) {
664 return (strdup2(name));
668 /* If no names matched, then return NULL. */
669 return ((char *)NULL);
671 #endif /* HAVE_LIBREADLINE */
674 interactive_mode(int fd)
675 /* provide user a prompt, read command, send it to fcron, print its answer,
676 * then give another prompt, etc, until user type an exit command */
678 char existing_connection = (fd < 0) ? 0 : 1;
680 #ifdef HAVE_LIBREADLINE
681 char *line_read = NULL;
682 #else /* HAVE_LIBREADLINE */
684 #endif /* HAVE_LIBREADLINE */
686 if (!existing_connection && (fd = connect_fcron()) == ERR)
689 #ifdef HAVE_LIBREADLINE
690 /* Allow conditional parsing of the ~/.inputrc file. */
691 rl_readline_name = "fcrondyn";
693 /* Tell the completer that we want a crack first. */
694 rl_attempted_completion_function = rl_cmpl_fcrondyn;
697 line_read = readline("fcrondyn> ");
699 if (line_read == NULL) {
700 /* Handle EOF gracefully and move past the prompt line */
705 return_code = talk_fcron(line_read, fd);
707 #ifdef HAVE_READLINE_HISTORY
708 if (line_read && *line_read) {
709 add_history(line_read);
711 #endif /* HAVE_READLINE_HISTORY */
714 if (return_code == QUIT_CMD || return_code == ERR) {
718 #else /* HAVE_LIBREADLINE */
719 while (fprintf(stderr, "fcrondyn> ")
720 && fgets(buf, sizeof(buf), stdin) != NULL
721 && (return_code = talk_fcron(buf, fd)) != QUIT_CMD
722 && return_code != ERR) ;
723 #endif /* HAVE_LIBREADLINE */
725 if (!existing_connection)
726 xclose_check(&fd, "unix socket");
733 parseopt(int argc, char *argv[])
739 extern int optind, opterr, optopt;
741 /* constants and variables defined by command line */
744 c = getopt(argc, argv, "hVdc:ix:");
762 Set(fcronconf, optarg);
770 Set(cmd_str, optarg);
774 fprintf(stderr, "(setopt) Missing parameter.\n");
781 fprintf(stderr, "(setopt) Warning: getopt returned %c.\n", c);
787 for (i = optind; i <= argc; i++)
788 fprintf(stderr, "Unknown argument \"%s\"", argv[i]);
795 main(int argc, char **argv)
798 int fd = (-1); /* fd == -1 means connection to fcron is not currently open */
799 struct passwd *pass = NULL;
801 rootuid = get_user_uid_safe(ROOTNAME);
802 rootgid = get_group_gid_safe(ROOTGROUP);
804 if (strrchr(argv[0], '/') == NULL)
807 prog_name = strrchr(argv[0], '/') + 1;
811 if ((pass = getpwuid(user_uid)) == NULL)
812 die("user \"%s\" is not in passwd file. Aborting.", USERNAME);
813 user_str = strdup2(pass->pw_name);
815 /* drop suid rights that we don't need, but keep the sgid rights
816 * for now as we will need them for read_conf() and is_allowed() */
818 seteuid_safe(user_uid);
820 if (setuid(user_uid) < 0)
821 die_e("could not setuid() to %d", user_uid);
823 /* interpret command line options */
824 parseopt(argc, argv);
826 /* read fcron.conf and update global parameters */
829 if (!is_allowed(user_str)) {
830 die("User \"%s\" is not allowed to use %s. Aborting.", user_str,
834 /* we don't need anymore special rights : drop remaining ones */
836 setegid_safe(user_gid);
838 if (setgid(user_gid) < 0)
839 die_e("could not setgid() to %d", user_gid);
841 /* check for broken pipes ... */
842 signal(SIGPIPE, sigpipe_handler);
845 return_code = interactive_mode(fd);
847 return_code = talk_fcron(cmd_str, fd);
849 xexit((return_code == OK) ? EXIT_OK : EXIT_ERR);