cron: add log suppression and mail suppression for successful runs
This commit adds two new extensions to crontab, ported from OpenBSD: - -n: suppress mail on succesful run - -q: suppress logging of command execution The -q option appears decades old, but -n is relatively new. The original proposal by Job Snijder can be found here [1], and gives very convincing reasons for inclusion in base. This patch is a nearly identical port of OpenBSD cron for -q and -n features. It is written to follow existing conventions and style of the existing codebase. Example usage: # should only send email, but won't show up in log * * * * * -q date # should not send email * * * * * -n date # should not send email or log * * * * * -n -q date # should send email because of ping failure * * * * * -n -q ping -c 1 5.5.5.5 [1]: https://marc.info/?l=openbsd-tech&m=152874866117948&w=2 PR: 237538 Submitted by: Naveen Nathan <freebsd_t.lastninja.net> Reviewed by: bcr (manpages) MFC after: 1 week Differential Revision: https://reviews.freebsd.org/D20046
This commit is contained in:
parent
e44ed9d3d4
commit
5b80de237b
@ -191,6 +191,8 @@ typedef struct _entry {
|
|||||||
#define NOT_UNTIL 0x10
|
#define NOT_UNTIL 0x10
|
||||||
#define SEC_RES 0x20
|
#define SEC_RES 0x20
|
||||||
#define INTERVAL 0x40
|
#define INTERVAL 0x40
|
||||||
|
#define DONT_LOG 0x80
|
||||||
|
#define MAIL_WHEN_ERR 0x100
|
||||||
time_t lastrun;
|
time_t lastrun;
|
||||||
} entry;
|
} entry;
|
||||||
|
|
||||||
@ -257,7 +259,7 @@ user *load_user(int, struct passwd *, char *),
|
|||||||
entry *load_entry(FILE *, void (*)(char *),
|
entry *load_entry(FILE *, void (*)(char *),
|
||||||
struct passwd *, char **);
|
struct passwd *, char **);
|
||||||
|
|
||||||
FILE *cron_popen(char *, char *, entry *);
|
FILE *cron_popen(char *, char *, entry *, PID_T *);
|
||||||
|
|
||||||
|
|
||||||
/* in the C tradition, we only create
|
/* in the C tradition, we only create
|
||||||
|
@ -41,6 +41,7 @@ static const char rcsid[] =
|
|||||||
static void child_process(entry *, user *),
|
static void child_process(entry *, user *),
|
||||||
do_univ(user *);
|
do_univ(user *);
|
||||||
|
|
||||||
|
static WAIT_T wait_on_child(PID_T, const char *);
|
||||||
|
|
||||||
void
|
void
|
||||||
do_command(e, u)
|
do_command(e, u)
|
||||||
@ -94,7 +95,10 @@ child_process(e, u)
|
|||||||
int stdin_pipe[2], stdout_pipe[2];
|
int stdin_pipe[2], stdout_pipe[2];
|
||||||
register char *input_data;
|
register char *input_data;
|
||||||
char *usernm, *mailto, *mailfrom;
|
char *usernm, *mailto, *mailfrom;
|
||||||
int children = 0;
|
PID_T jobpid, stdinjob, mailpid;
|
||||||
|
register FILE *mail;
|
||||||
|
register int bytes = 1;
|
||||||
|
int status = 0;
|
||||||
# if defined(LOGIN_CAP)
|
# if defined(LOGIN_CAP)
|
||||||
struct passwd *pwd;
|
struct passwd *pwd;
|
||||||
login_cap_t *lc;
|
login_cap_t *lc;
|
||||||
@ -216,7 +220,7 @@ child_process(e, u)
|
|||||||
|
|
||||||
/* fork again, this time so we can exec the user's command.
|
/* fork again, this time so we can exec the user's command.
|
||||||
*/
|
*/
|
||||||
switch (vfork()) {
|
switch (jobpid = vfork()) {
|
||||||
case -1:
|
case -1:
|
||||||
log_it("CRON",getpid(),"error","can't vfork");
|
log_it("CRON",getpid(),"error","can't vfork");
|
||||||
exit(ERROR_EXIT);
|
exit(ERROR_EXIT);
|
||||||
@ -237,7 +241,7 @@ child_process(e, u)
|
|||||||
* the actual user command shell was going to get and the
|
* the actual user command shell was going to get and the
|
||||||
* PID is part of the log message.
|
* PID is part of the log message.
|
||||||
*/
|
*/
|
||||||
/*local*/{
|
if ((e->flags & DONT_LOG) == 0) {
|
||||||
char *x = mkprints((u_char *)e->cmd, strlen(e->cmd));
|
char *x = mkprints((u_char *)e->cmd, strlen(e->cmd));
|
||||||
|
|
||||||
log_it(usernm, getpid(), "CMD", x);
|
log_it(usernm, getpid(), "CMD", x);
|
||||||
@ -359,8 +363,6 @@ child_process(e, u)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
children++;
|
|
||||||
|
|
||||||
/* middle process, child of original cron, parent of process running
|
/* middle process, child of original cron, parent of process running
|
||||||
* the user's command.
|
* the user's command.
|
||||||
*/
|
*/
|
||||||
@ -384,7 +386,7 @@ child_process(e, u)
|
|||||||
* we would block here. thus we must fork again.
|
* we would block here. thus we must fork again.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
if (*input_data && fork() == 0) {
|
if (*input_data && (stdinjob = fork()) == 0) {
|
||||||
register FILE *out = fdopen(stdin_pipe[WRITE_PIPE], "w");
|
register FILE *out = fdopen(stdin_pipe[WRITE_PIPE], "w");
|
||||||
register int need_newline = FALSE;
|
register int need_newline = FALSE;
|
||||||
register int escaped = FALSE;
|
register int escaped = FALSE;
|
||||||
@ -440,8 +442,6 @@ child_process(e, u)
|
|||||||
*/
|
*/
|
||||||
close(stdin_pipe[WRITE_PIPE]);
|
close(stdin_pipe[WRITE_PIPE]);
|
||||||
|
|
||||||
children++;
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* read output from the grandchild. it's stderr has been redirected to
|
* read output from the grandchild. it's stderr has been redirected to
|
||||||
* it's stdout, which has been redirected to our pipe. if there is any
|
* it's stdout, which has been redirected to our pipe. if there is any
|
||||||
@ -462,10 +462,6 @@ child_process(e, u)
|
|||||||
|
|
||||||
ch = getc(in);
|
ch = getc(in);
|
||||||
if (ch != EOF) {
|
if (ch != EOF) {
|
||||||
register FILE *mail;
|
|
||||||
register int bytes = 1;
|
|
||||||
int status = 0;
|
|
||||||
|
|
||||||
Debug(DPROC|DEXT,
|
Debug(DPROC|DEXT,
|
||||||
("[%d] got data (%x:%c) from grandchild\n",
|
("[%d] got data (%x:%c) from grandchild\n",
|
||||||
getpid(), ch, ch))
|
getpid(), ch, ch))
|
||||||
@ -500,7 +496,7 @@ child_process(e, u)
|
|||||||
hostname[sizeof(hostname) - 1] = '\0';
|
hostname[sizeof(hostname) - 1] = '\0';
|
||||||
(void) snprintf(mailcmd, sizeof(mailcmd),
|
(void) snprintf(mailcmd, sizeof(mailcmd),
|
||||||
MAILARGS, MAILCMD);
|
MAILARGS, MAILCMD);
|
||||||
if (!(mail = cron_popen(mailcmd, "w", e))) {
|
if (!(mail = cron_popen(mailcmd, "w", e, &mailpid))) {
|
||||||
warn("%s", MAILCMD);
|
warn("%s", MAILCMD);
|
||||||
(void) _exit(ERROR_EXIT);
|
(void) _exit(ERROR_EXIT);
|
||||||
}
|
}
|
||||||
@ -538,28 +534,56 @@ child_process(e, u)
|
|||||||
if (mailto)
|
if (mailto)
|
||||||
putc(ch, mail);
|
putc(ch, mail);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/*if data from grandchild*/
|
||||||
|
|
||||||
/* only close pipe if we opened it -- i.e., we're
|
Debug(DPROC, ("[%d] got EOF from grandchild\n", getpid()))
|
||||||
* mailing...
|
|
||||||
|
/* also closes stdout_pipe[READ_PIPE] */
|
||||||
|
fclose(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* wait for children to die.
|
||||||
|
*/
|
||||||
|
if (jobpid > 0) {
|
||||||
|
WAIT_T waiter;
|
||||||
|
|
||||||
|
waiter = wait_on_child(jobpid, "grandchild command job");
|
||||||
|
|
||||||
|
/* If everything went well, and -n was set, _and_ we have mail,
|
||||||
|
* we won't be mailing... so shoot the messenger!
|
||||||
|
*/
|
||||||
|
if (WIFEXITED(waiter) && WEXITSTATUS(waiter) == 0
|
||||||
|
&& (e->flags & MAIL_WHEN_ERR) == MAIL_WHEN_ERR
|
||||||
|
&& mailto) {
|
||||||
|
Debug(DPROC, ("[%d] %s executed successfully, mail suppressed\n",
|
||||||
|
getpid(), "grandchild command job"))
|
||||||
|
kill(mailpid, SIGKILL);
|
||||||
|
(void)fclose(mail);
|
||||||
|
mailto = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* only close pipe if we opened it -- i.e., we're
|
||||||
|
* mailing...
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (mailto) {
|
||||||
|
Debug(DPROC, ("[%d] closing pipe to mail\n",
|
||||||
|
getpid()))
|
||||||
|
/* Note: the pclose will probably see
|
||||||
|
* the termination of the grandchild
|
||||||
|
* in addition to the mail process, since
|
||||||
|
* it (the grandchild) is likely to exit
|
||||||
|
* after closing its stdout.
|
||||||
*/
|
*/
|
||||||
|
status = cron_pclose(mail);
|
||||||
if (mailto) {
|
|
||||||
Debug(DPROC, ("[%d] closing pipe to mail\n",
|
|
||||||
getpid()))
|
|
||||||
/* Note: the pclose will probably see
|
|
||||||
* the termination of the grandchild
|
|
||||||
* in addition to the mail process, since
|
|
||||||
* it (the grandchild) is likely to exit
|
|
||||||
* after closing its stdout.
|
|
||||||
*/
|
|
||||||
status = cron_pclose(mail);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* if there was output and we could not mail it,
|
/* if there was output and we could not mail it,
|
||||||
* log the facts so the poor user can figure out
|
* log the facts so the poor user can figure out
|
||||||
* what's going on.
|
* what's going on.
|
||||||
*/
|
*/
|
||||||
if (mailto && status) {
|
if (status) {
|
||||||
char buf[MAX_TEMPSTR];
|
char buf[MAX_TEMPSTR];
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf),
|
snprintf(buf, sizeof(buf),
|
||||||
@ -568,35 +592,38 @@ child_process(e, u)
|
|||||||
status);
|
status);
|
||||||
log_it(usernm, getpid(), "MAIL", buf);
|
log_it(usernm, getpid(), "MAIL", buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
} /*if data from grandchild*/
|
|
||||||
|
|
||||||
Debug(DPROC, ("[%d] got EOF from grandchild\n", getpid()))
|
|
||||||
|
|
||||||
fclose(in); /* also closes stdout_pipe[READ_PIPE] */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* wait for children to die.
|
|
||||||
*/
|
|
||||||
for (; children > 0; children--)
|
|
||||||
{
|
|
||||||
WAIT_T waiter;
|
|
||||||
PID_T pid;
|
|
||||||
|
|
||||||
Debug(DPROC, ("[%d] waiting for grandchild #%d to finish\n",
|
|
||||||
getpid(), children))
|
|
||||||
pid = wait(&waiter);
|
|
||||||
if (pid < OK) {
|
|
||||||
Debug(DPROC, ("[%d] no more grandchildren--mail written?\n",
|
|
||||||
getpid()))
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
Debug(DPROC, ("[%d] grandchild #%d finished, status=%04x",
|
|
||||||
getpid(), pid, WEXITSTATUS(waiter)))
|
|
||||||
if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
|
|
||||||
Debug(DPROC, (", dumped core"))
|
|
||||||
Debug(DPROC, ("\n"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (*input_data && stdinjob > 0)
|
||||||
|
wait_on_child(stdinjob, "grandchild stdinjob");
|
||||||
|
}
|
||||||
|
|
||||||
|
static WAIT_T
|
||||||
|
wait_on_child(PID_T childpid, const char *name) {
|
||||||
|
WAIT_T waiter;
|
||||||
|
PID_T pid;
|
||||||
|
|
||||||
|
Debug(DPROC, ("[%d] waiting for %s (%d) to finish\n",
|
||||||
|
getpid(), name, childpid))
|
||||||
|
|
||||||
|
#ifdef POSIX
|
||||||
|
while ((pid = waitpid(childpid, &waiter, 0)) < 0 && errno == EINTR)
|
||||||
|
#else
|
||||||
|
while ((pid = wait4(childpid, &waiter, 0, NULL)) < 0 && errno == EINTR)
|
||||||
|
#endif
|
||||||
|
;
|
||||||
|
|
||||||
|
if (pid < OK)
|
||||||
|
return waiter;
|
||||||
|
|
||||||
|
Debug(DPROC, ("[%d] %s (%d) finished, status=%04x",
|
||||||
|
getpid(), name, pid, WEXITSTATUS(waiter)))
|
||||||
|
if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
|
||||||
|
Debug(DPROC, (", dumped core"))
|
||||||
|
Debug(DPROC, ("\n"))
|
||||||
|
|
||||||
|
return waiter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -55,9 +55,10 @@ static PID_T *pids;
|
|||||||
static int fds;
|
static int fds;
|
||||||
|
|
||||||
FILE *
|
FILE *
|
||||||
cron_popen(program, type, e)
|
cron_popen(program, type, e, pidptr)
|
||||||
char *program, *type;
|
char *program, *type;
|
||||||
entry *e;
|
entry *e;
|
||||||
|
PID_T *pidptr;
|
||||||
{
|
{
|
||||||
register char *cp;
|
register char *cp;
|
||||||
FILE *iop;
|
FILE *iop;
|
||||||
@ -218,6 +219,9 @@ pfree:
|
|||||||
free((char *)argv[argc]);
|
free((char *)argv[argc]);
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
*pidptr = pid;
|
||||||
|
|
||||||
return(iop);
|
return(iop);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
.\"
|
.\"
|
||||||
.\" $FreeBSD$
|
.\" $FreeBSD$
|
||||||
.\"
|
.\"
|
||||||
.Dd April 19, 2019
|
.Dd September 24, 2019
|
||||||
.Dt CRONTAB 5
|
.Dt CRONTAB 5
|
||||||
.Os
|
.Os
|
||||||
.Sh NAME
|
.Sh NAME
|
||||||
@ -199,6 +199,8 @@ lists of names are not allowed.
|
|||||||
.Pp
|
.Pp
|
||||||
The ``sixth'' field (the rest of the line) specifies the command to be
|
The ``sixth'' field (the rest of the line) specifies the command to be
|
||||||
run.
|
run.
|
||||||
|
One or more command options may precede the command to modify processing
|
||||||
|
behavior.
|
||||||
The entire command portion of the line, up to a newline or %
|
The entire command portion of the line, up to a newline or %
|
||||||
character, will be executed by
|
character, will be executed by
|
||||||
.Pa /bin/sh
|
.Pa /bin/sh
|
||||||
@ -211,6 +213,22 @@ Percent-signs (%) in the command, unless escaped with backslash
|
|||||||
after the first % will be sent to the command as standard
|
after the first % will be sent to the command as standard
|
||||||
input.
|
input.
|
||||||
.Pp
|
.Pp
|
||||||
|
The following command options can be supplied:
|
||||||
|
.Bl -tag -width Ds
|
||||||
|
.It Fl n
|
||||||
|
No mail is sent after a successful run.
|
||||||
|
The execution output will only be mailed if the command exits with a non-zero
|
||||||
|
exit code.
|
||||||
|
The
|
||||||
|
.Fl n
|
||||||
|
option is an attempt to cure potentially copious volumes of mail coming from
|
||||||
|
.Xr cron 8 .
|
||||||
|
.It Fl q
|
||||||
|
Execution will not be logged.
|
||||||
|
.El
|
||||||
|
.sp
|
||||||
|
Duplicate options are not allowed.
|
||||||
|
.Pp
|
||||||
Note: The day of a command's execution can be specified by two
|
Note: The day of a command's execution can be specified by two
|
||||||
fields \(em day of month, and day of week.
|
fields \(em day of month, and day of week.
|
||||||
If both fields are
|
If both fields are
|
||||||
@ -271,6 +289,10 @@ MAILTO=paul
|
|||||||
5 4 * * sun echo "run at 5 after 4 every sunday"
|
5 4 * * sun echo "run at 5 after 4 every sunday"
|
||||||
# run at 5 minutes intervals, no matter how long it takes
|
# run at 5 minutes intervals, no matter how long it takes
|
||||||
@300 svnlite up /usr/src
|
@300 svnlite up /usr/src
|
||||||
|
# run every minute, suppress logging
|
||||||
|
* * * * * -q date
|
||||||
|
# run every minute, only send mail if ping fails
|
||||||
|
* * * * * -n ping -c 1 freebsd.org
|
||||||
.Ed
|
.Ed
|
||||||
.Sh SEE ALSO
|
.Sh SEE ALSO
|
||||||
.Xr crontab 1 ,
|
.Xr crontab 1 ,
|
||||||
@ -314,6 +336,14 @@ All of the
|
|||||||
.Sq @
|
.Sq @
|
||||||
directives that can appear in place of the first five fields
|
directives that can appear in place of the first five fields
|
||||||
are extensions.
|
are extensions.
|
||||||
|
.Pp
|
||||||
|
Command processing can be modified using command options.
|
||||||
|
The
|
||||||
|
.Sq -q
|
||||||
|
option suppresses logging.
|
||||||
|
The
|
||||||
|
.Sq -n
|
||||||
|
option does not mail on successful run.
|
||||||
.Sh AUTHORS
|
.Sh AUTHORS
|
||||||
.An Paul Vixie Aq Mt paul@vix.com
|
.An Paul Vixie Aq Mt paul@vix.com
|
||||||
.Sh BUGS
|
.Sh BUGS
|
||||||
|
@ -35,7 +35,8 @@ static const char rcsid[] =
|
|||||||
|
|
||||||
typedef enum ecode {
|
typedef enum ecode {
|
||||||
e_none, e_minute, e_hour, e_dom, e_month, e_dow,
|
e_none, e_minute, e_hour, e_dom, e_month, e_dow,
|
||||||
e_cmd, e_timespec, e_username, e_group, e_mem
|
e_cmd, e_timespec, e_username, e_group, e_option,
|
||||||
|
e_mem
|
||||||
#ifdef LOGIN_CAP
|
#ifdef LOGIN_CAP
|
||||||
, e_class
|
, e_class
|
||||||
#endif
|
#endif
|
||||||
@ -58,6 +59,7 @@ static char *ecodes[] =
|
|||||||
"bad time specifier",
|
"bad time specifier",
|
||||||
"bad username",
|
"bad username",
|
||||||
"bad group name",
|
"bad group name",
|
||||||
|
"bad option",
|
||||||
"out of memory",
|
"out of memory",
|
||||||
#ifdef LOGIN_CAP
|
#ifdef LOGIN_CAP
|
||||||
"bad class name",
|
"bad class name",
|
||||||
@ -429,6 +431,53 @@ load_entry(file, error_func, pw, envp)
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
Debug(DPARS, ("load_entry()...checking for command options\n"))
|
||||||
|
|
||||||
|
ch = get_char(file);
|
||||||
|
|
||||||
|
while (ch == '-') {
|
||||||
|
Debug(DPARS|DEXT, ("load_entry()...expecting option\n"))
|
||||||
|
switch (ch = get_char(file)) {
|
||||||
|
case 'n':
|
||||||
|
Debug(DPARS|DEXT, ("load_entry()...got MAIL_WHEN_ERR ('n') option\n"))
|
||||||
|
/* only allow the user to set the option once */
|
||||||
|
if ((e->flags & MAIL_WHEN_ERR) == MAIL_WHEN_ERR) {
|
||||||
|
Debug(DPARS|DEXT, ("load_entry()...duplicate MAIL_WHEN_ERR ('n') option\n"))
|
||||||
|
ecode = e_option;
|
||||||
|
goto eof;
|
||||||
|
}
|
||||||
|
e->flags |= MAIL_WHEN_ERR;
|
||||||
|
break;
|
||||||
|
case 'q':
|
||||||
|
Debug(DPARS|DEXT, ("load_entry()...got DONT_LOG ('q') option\n"))
|
||||||
|
/* only allow the user to set the option once */
|
||||||
|
if ((e->flags & DONT_LOG) == DONT_LOG) {
|
||||||
|
Debug(DPARS|DEXT, ("load_entry()...duplicate DONT_LOG ('q') option\n"))
|
||||||
|
ecode = e_option;
|
||||||
|
goto eof;
|
||||||
|
}
|
||||||
|
e->flags |= DONT_LOG;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Debug(DPARS|DEXT, ("load_entry()...invalid option '%c'\n", ch))
|
||||||
|
ecode = e_option;
|
||||||
|
goto eof;
|
||||||
|
}
|
||||||
|
ch = get_char(file);
|
||||||
|
if (ch!='\t' && ch!=' ') {
|
||||||
|
ecode = e_option;
|
||||||
|
goto eof;
|
||||||
|
}
|
||||||
|
|
||||||
|
Skip_Blanks(ch, file)
|
||||||
|
if (ch == EOF || ch == '\n') {
|
||||||
|
ecode = e_cmd;
|
||||||
|
goto eof;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unget_char(ch, file);
|
||||||
|
|
||||||
Debug(DPARS, ("load_entry()...about to parse command\n"))
|
Debug(DPARS, ("load_entry()...about to parse command\n"))
|
||||||
|
|
||||||
/* Everything up to the next \n or EOF is part of the command...
|
/* Everything up to the next \n or EOF is part of the command...
|
||||||
|
Loading…
x
Reference in New Issue
Block a user