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:
Kyle Evans 2019-09-25 02:37:40 +00:00
parent e44ed9d3d4
commit 5b80de237b
5 changed files with 171 additions and 59 deletions

View File

@ -191,6 +191,8 @@ typedef struct _entry {
#define NOT_UNTIL 0x10
#define SEC_RES 0x20
#define INTERVAL 0x40
#define DONT_LOG 0x80
#define MAIL_WHEN_ERR 0x100
time_t lastrun;
} entry;
@ -257,7 +259,7 @@ user *load_user(int, struct passwd *, char *),
entry *load_entry(FILE *, void (*)(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

View File

@ -41,6 +41,7 @@ static const char rcsid[] =
static void child_process(entry *, user *),
do_univ(user *);
static WAIT_T wait_on_child(PID_T, const char *);
void
do_command(e, u)
@ -94,7 +95,10 @@ child_process(e, u)
int stdin_pipe[2], stdout_pipe[2];
register char *input_data;
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)
struct passwd *pwd;
login_cap_t *lc;
@ -216,7 +220,7 @@ child_process(e, u)
/* fork again, this time so we can exec the user's command.
*/
switch (vfork()) {
switch (jobpid = vfork()) {
case -1:
log_it("CRON",getpid(),"error","can't vfork");
exit(ERROR_EXIT);
@ -237,7 +241,7 @@ child_process(e, u)
* the actual user command shell was going to get and the
* PID is part of the log message.
*/
/*local*/{
if ((e->flags & DONT_LOG) == 0) {
char *x = mkprints((u_char *)e->cmd, strlen(e->cmd));
log_it(usernm, getpid(), "CMD", x);
@ -359,8 +363,6 @@ child_process(e, u)
break;
}
children++;
/* middle process, child of original cron, parent of process running
* the user's command.
*/
@ -384,7 +386,7 @@ child_process(e, u)
* 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 int need_newline = FALSE;
register int escaped = FALSE;
@ -440,8 +442,6 @@ child_process(e, u)
*/
close(stdin_pipe[WRITE_PIPE]);
children++;
/*
* 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
@ -462,10 +462,6 @@ child_process(e, u)
ch = getc(in);
if (ch != EOF) {
register FILE *mail;
register int bytes = 1;
int status = 0;
Debug(DPROC|DEXT,
("[%d] got data (%x:%c) from grandchild\n",
getpid(), ch, ch))
@ -500,7 +496,7 @@ child_process(e, u)
hostname[sizeof(hostname) - 1] = '\0';
(void) snprintf(mailcmd, sizeof(mailcmd),
MAILARGS, MAILCMD);
if (!(mail = cron_popen(mailcmd, "w", e))) {
if (!(mail = cron_popen(mailcmd, "w", e, &mailpid))) {
warn("%s", MAILCMD);
(void) _exit(ERROR_EXIT);
}
@ -538,28 +534,56 @@ child_process(e, u)
if (mailto)
putc(ch, mail);
}
}
/*if data from grandchild*/
/* only close pipe if we opened it -- i.e., we're
* mailing...
Debug(DPROC, ("[%d] got EOF from grandchild\n", getpid()))
/* 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.
*/
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);
}
status = cron_pclose(mail);
/* if there was output and we could not mail it,
* log the facts so the poor user can figure out
* what's going on.
*/
if (mailto && status) {
if (status) {
char buf[MAX_TEMPSTR];
snprintf(buf, sizeof(buf),
@ -568,35 +592,38 @@ child_process(e, u)
status);
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;
}

View File

@ -55,9 +55,10 @@ static PID_T *pids;
static int fds;
FILE *
cron_popen(program, type, e)
cron_popen(program, type, e, pidptr)
char *program, *type;
entry *e;
PID_T *pidptr;
{
register char *cp;
FILE *iop;
@ -218,6 +219,9 @@ cron_popen(program, type, e)
free((char *)argv[argc]);
}
#endif
*pidptr = pid;
return(iop);
}

View File

@ -17,7 +17,7 @@
.\"
.\" $FreeBSD$
.\"
.Dd April 19, 2019
.Dd September 24, 2019
.Dt CRONTAB 5
.Os
.Sh NAME
@ -199,6 +199,8 @@ lists of names are not allowed.
.Pp
The ``sixth'' field (the rest of the line) specifies the command to be
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 %
character, will be executed by
.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
input.
.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
fields \(em day of month, and day of week.
If both fields are
@ -271,6 +289,10 @@ MAILTO=paul
5 4 * * sun echo "run at 5 after 4 every sunday"
# run at 5 minutes intervals, no matter how long it takes
@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
.Sh SEE ALSO
.Xr crontab 1 ,
@ -314,6 +336,14 @@ All of the
.Sq @
directives that can appear in place of the first five fields
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
.An Paul Vixie Aq Mt paul@vix.com
.Sh BUGS

View File

@ -35,7 +35,8 @@ static const char rcsid[] =
typedef enum ecode {
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
, e_class
#endif
@ -58,6 +59,7 @@ static char *ecodes[] =
"bad time specifier",
"bad username",
"bad group name",
"bad option",
"out of memory",
#ifdef LOGIN_CAP
"bad class name",
@ -429,6 +431,53 @@ load_entry(file, error_func, pw, envp)
}
#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"))
/* Everything up to the next \n or EOF is part of the command...