From owner-svn-src-all@freebsd.org Sun Oct 6 03:56:04 2019 Return-Path: Delivered-To: svn-src-all@mailman.nyi.freebsd.org Received: from mx1.freebsd.org (mx1.freebsd.org [IPv6:2610:1c1:1:606c::19:1]) by mailman.nyi.freebsd.org (Postfix) with ESMTP id A4E25F98D0; Sun, 6 Oct 2019 03:56:04 +0000 (UTC) (envelope-from kevans@FreeBSD.org) Received: from mxrelay.nyi.freebsd.org (mxrelay.nyi.freebsd.org [IPv6:2610:1c1:1:606c::19:3]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) server-signature RSA-PSS (4096 bits) client-signature RSA-PSS (4096 bits) client-digest SHA256) (Client CN "mxrelay.nyi.freebsd.org", Issuer "Let's Encrypt Authority X3" (verified OK)) by mx1.freebsd.org (Postfix) with ESMTPS id 46m8sw3RLJz4WPV; Sun, 6 Oct 2019 03:56:04 +0000 (UTC) (envelope-from kevans@FreeBSD.org) Received: from repo.freebsd.org (repo.freebsd.org [IPv6:2610:1c1:1:6068::e6a:0]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not present a certificate) by mxrelay.nyi.freebsd.org (Postfix) with ESMTPS id 3EEC51F6CA; Sun, 6 Oct 2019 03:56:04 +0000 (UTC) (envelope-from kevans@FreeBSD.org) Received: from repo.freebsd.org ([127.0.1.37]) by repo.freebsd.org (8.15.2/8.15.2) with ESMTP id x963u3Yh035218; Sun, 6 Oct 2019 03:56:03 GMT (envelope-from kevans@FreeBSD.org) Received: (from kevans@localhost) by repo.freebsd.org (8.15.2/8.15.2/Submit) id x963u3gR035213; Sun, 6 Oct 2019 03:56:03 GMT (envelope-from kevans@FreeBSD.org) Message-Id: <201910060356.x963u3gR035213@repo.freebsd.org> X-Authentication-Warning: repo.freebsd.org: kevans set sender to kevans@FreeBSD.org using -f From: Kyle Evans Date: Sun, 6 Oct 2019 03:56:03 +0000 (UTC) To: src-committers@freebsd.org, svn-src-all@freebsd.org, svn-src-stable@freebsd.org, svn-src-stable-11@freebsd.org Subject: svn commit: r353134 - in stable: 11/usr.sbin/cron/cron 11/usr.sbin/cron/crontab 11/usr.sbin/cron/lib 12/usr.sbin/cron/cron 12/usr.sbin/cron/crontab 12/usr.sbin/cron/lib X-SVN-Group: stable-11 X-SVN-Commit-Author: kevans X-SVN-Commit-Paths: in stable: 11/usr.sbin/cron/cron 11/usr.sbin/cron/crontab 11/usr.sbin/cron/lib 12/usr.sbin/cron/cron 12/usr.sbin/cron/crontab 12/usr.sbin/cron/lib X-SVN-Commit-Revision: 353134 X-SVN-Commit-Repository: base MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit X-BeenThere: svn-src-all@freebsd.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: "SVN commit messages for the entire src tree \(except for " user" and " projects" \)" List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Sun, 06 Oct 2019 03:56:04 -0000 Author: kevans Date: Sun Oct 6 03:56:02 2019 New Revision: 353134 URL: https://svnweb.freebsd.org/changeset/base/353134 Log: MFC r352668: cron: 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 Relnotes: yes Modified: stable/11/usr.sbin/cron/cron/cron.h stable/11/usr.sbin/cron/cron/do_command.c stable/11/usr.sbin/cron/cron/popen.c stable/11/usr.sbin/cron/crontab/crontab.5 stable/11/usr.sbin/cron/lib/entry.c Directory Properties: stable/11/ (props changed) Changes in other areas also in this revision: Modified: stable/12/usr.sbin/cron/cron/cron.h stable/12/usr.sbin/cron/cron/do_command.c stable/12/usr.sbin/cron/cron/popen.c stable/12/usr.sbin/cron/crontab/crontab.5 stable/12/usr.sbin/cron/lib/entry.c Directory Properties: stable/12/ (props changed) Modified: stable/11/usr.sbin/cron/cron/cron.h ============================================================================== --- stable/11/usr.sbin/cron/cron/cron.h Sun Oct 6 03:33:28 2019 (r353133) +++ stable/11/usr.sbin/cron/cron/cron.h Sun Oct 6 03:56:02 2019 (r353134) @@ -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 Modified: stable/11/usr.sbin/cron/cron/do_command.c ============================================================================== --- stable/11/usr.sbin/cron/cron/do_command.c Sun Oct 6 03:33:28 2019 (r353133) +++ stable/11/usr.sbin/cron/cron/do_command.c Sun Oct 6 03:56:02 2019 (r353134) @@ -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())) - 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); - } + /* 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 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*/ + if (*input_data && stdinjob > 0) + wait_on_child(stdinjob, "grandchild stdinjob"); +} - Debug(DPROC, ("[%d] got EOF from grandchild\n", getpid())) +static WAIT_T +wait_on_child(PID_T childpid, const char *name) { + WAIT_T waiter; + PID_T pid; - fclose(in); /* also closes stdout_pipe[READ_PIPE] */ - } + Debug(DPROC, ("[%d] waiting for %s (%d) to finish\n", + getpid(), name, childpid)) - /* wait for children to die. - */ - for (; children > 0; children--) - { - WAIT_T waiter; - PID_T pid; +#ifdef POSIX + while ((pid = waitpid(childpid, &waiter, 0)) < 0 && errno == EINTR) +#else + while ((pid = wait4(childpid, &waiter, 0, NULL)) < 0 && errno == EINTR) +#endif + ; - 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 (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; } Modified: stable/11/usr.sbin/cron/cron/popen.c ============================================================================== --- stable/11/usr.sbin/cron/cron/popen.c Sun Oct 6 03:33:28 2019 (r353133) +++ stable/11/usr.sbin/cron/cron/popen.c Sun Oct 6 03:56:02 2019 (r353134) @@ -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 @@ pfree: free((char *)argv[argc]); } #endif + + *pidptr = pid; + return(iop); } Modified: stable/11/usr.sbin/cron/crontab/crontab.5 ============================================================================== --- stable/11/usr.sbin/cron/crontab/crontab.5 Sun Oct 6 03:33:28 2019 (r353133) +++ stable/11/usr.sbin/cron/crontab/crontab.5 Sun Oct 6 03:56:02 2019 (r353134) @@ -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 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 Modified: stable/11/usr.sbin/cron/lib/entry.c ============================================================================== --- stable/11/usr.sbin/cron/lib/entry.c Sun Oct 6 03:33:28 2019 (r353133) +++ stable/11/usr.sbin/cron/lib/entry.c Sun Oct 6 03:56:02 2019 (r353134) @@ -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", @@ -428,6 +430,53 @@ load_entry(file, error_func, pw, envp) goto eof; } #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"))