├── .gitignore ├── CHANGELOG ├── COPYING ├── Makefile ├── README ├── chuser.c ├── concat.c ├── crond.8 ├── crond.markdown ├── crontab.1 ├── crontab.c ├── crontab.markdown ├── database.c ├── defs.h ├── extra ├── crond.conf ├── crond.logrotate ├── crond.rc ├── crond.service ├── crontab.vim ├── prune-cronstamps ├── root.crontab └── run-cron ├── job.c ├── main.c └── subs.c /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | protos.h 3 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | todo 2 | * add timestamps to -d output 3 | 4 | * manual cron.update prodding doesn't affect cl_NotUntil 5 | 6 | * Use hash when ID=... is supplied. FS#18292. 7 | 8 | * FS#18352: Another thing: when moving the original file to the backup name, and the edited version is written in it's place, the file is written without preserving the same permissions as the original, so if you have a umask that prevents others from reading your stuff, crontab won't be able to load the new file. 9 | 10 | git 11 | * Numeric loglevels specified by 'crond -l ' weren't being validated. 12 | Now we no longer accept numeric loglevels; they must be specified 13 | symbolically. Thanks to Rogutės Sparnuotos. 14 | 15 | * Continued portability improvements. Makefile now uses -lbsd-compat. 16 | Factored allocation and string calls to utils.c. 17 | 18 | * Added extra/crond.service for systemd. Thanks to Miklos Vajna. 19 | 20 | * Many internal changes and annotations to pass splint review. 21 | 22 | * Documentation and error message updates. 23 | 24 | v4.5 1-May-2011 25 | * Some cron jobs were running multiple times. Now we make sure not to 26 | ArmJobs that are already running; and not to resynchronize while jobs are 27 | running; and to poll the DST setting. (Fixes Arch FS#18681; thanks to Vincent 28 | Cappe and Paul Gideon Dann for identifying the second issue; and Tilman 29 | Sauerbeck for identifying the third.) 30 | 31 | * @monthly was wrongly being parsed the same as @yearly (fixes Arch 32 | FS#19123). Thanks to Peter Johnson, Paul Gideon Dann, and Tilman Sauerbeck. 33 | 34 | * extra/crond.rc: now uses $CROND_ARGS from /etc/conf.d/crond; sample included 35 | as extra/crond.conf. Suggested by Eric Bélanger. 36 | 37 | * Running `/etc/rc.d/crond start` after startup could leak unwanted 38 | environment into cronjobs; now we force crond to start in empty env 39 | (fixes Arch FS#22085). Thanks to Mantas. 40 | 41 | * Also set LOGNAME environment variable in cronjobs. Requested by Michael 42 | Trunner; fixes Arch FS#18338. 43 | 44 | * extra/crond.logrotate now correctly gets pid from /var/run/crond.pid 45 | (fixes Arch FS#18039). Thanks to Kay Abendroth, revel, and Chlump Chatkupt. 46 | 47 | * extra/prune-cronstamps now only deletes files, and is formatted as a 48 | @weekly crontab. Thanks to Alec Moskvin . 49 | 50 | * extra/crontab.vim works around an issue where vim's writebackup would 51 | interfere with crontab's security model (addresses Arch FS#18352). 52 | Thanks to Armadillo and Simon Williams. 53 | 54 | * Makefile uses $LDFLAGS (fixes Arch FS#23784). Thanks to Kristoffer Tidemann 55 | and Mike Frysinger. 56 | 57 | * defs.h sets default locations for CRONTABS and CRONSTAMPS beneath /var/spool/cron/, 58 | as in earlier versions of dcron. 59 | 60 | * Documentation updates. 61 | 62 | * Thanks for testing and feedback: Feifei Jia, Spider.007, Ray Kohler, 63 | Igor Zakharoff, Edward Hades, and Joe Lightning. 64 | 65 | v4.4 17-Jan-2010 66 | * Finished mailjobs were being left as zombie processes. Fixed. 67 | 68 | * When using crond with logging-to-file, user jobs could only log some 69 | events if they had write access to the log. Fixed this by having crond 70 | keep a file descriptor open to the log; also added a SIGHUP handler 71 | to make crond re-open the logfile. The sample logrotate script now 72 | sends that signal. 73 | 74 | * More sensible command-line parsing by crontab. 75 | 76 | * Add prune-cronstamps to extra; document extra/*; general improvement 77 | of README and manpages. 78 | 79 | * Portability improvements, and defs.h now has fuller comments about 80 | requirements. 81 | 82 | * Makefile improvements: `make` now caches variables for `make install`; 83 | don't stomp CFLAGS environment variable, and added BINDIR,SBINDIR,MANDIR. 84 | 85 | * Thanks to Juergen Daubert for more testing and suggestions. 86 | 87 | v4.3 11-Jan-2010 88 | * Internal refactoring to make buffer overflow checks 89 | clearer and portability issues more explicit. 90 | 91 | * Made file argument to -L mandatory; optional args to 92 | getopt needs GNU extensions. 93 | 94 | * Makefile tweaks. Added CRONTAB_GROUP for `make install`. 95 | Renamed TIMESTAMPS -> CRONSTAMPS. 96 | 97 | * Thanks to Juergen Daubert for testing and suggestions. 98 | 99 | v4.2 11-Jan-2010 100 | * Makefile tweaks; moved more constants to #defines. 101 | 102 | v4.1 10-Jan-2010 103 | * Fixed bug in parsing some numeric fields in crontabs. (Terminus of range 104 | wasn't being modded.) 105 | 106 | * Updated Makefile to make it easier to customize timestamps at configure 107 | time. Also, if LC_TIME is defined when crond runs, we use that instead of 108 | compiled-in default (for logging to files, to customize syslog output use 109 | syslog-ng's 'template' command). 110 | 111 | * Fixed Makefile permissions on crond and crontab binaries. 112 | 113 | v4.0 6-Jan-2010 114 | * Jim Pryor took over development; folded in changes from his fork "yacron" 115 | 116 | * Applied "Daniel's patch" from dcron 3.x tarballs to enable logging to syslog or 117 | files. Added further logging improvements. 118 | 119 | * Added -m user@host and -M mailer options 120 | 121 | * Various crontab syntax extensions, including "2nd Monday of every month", 122 | @reboot, @daily, and finer-grained frequency specifiers. 123 | 124 | * Jobs can wait until AFTER other jobs have finished. 125 | 126 | * Enhanced parsing of cron.update file, to make it possible for scripts to 127 | interact with a running crond in limited ways. 128 | 129 | * Various internal changes 130 | 131 | * Updated Makefile, manpage buildchain, and docs 132 | 133 | v3.2 134 | Fixed a minor bug, remove the newline terminating a line only if there 135 | is a newline to remove. 136 | 137 | v3.1 138 | Add support for root-run crontab files in /etc/cron.d and rewrite a 139 | good chunk of the crontab file management code. By VMiklos and Matt 140 | Dillon. 141 | 142 | v3.0 143 | Fix /tmp race and misc cleanups from Emiel Kollof 144 | 145 | v2.9 146 | Modernize the code, remove strcpy() and sprintf() in favor of snprintf(). 147 | (Supplied by Christine Jamison ) 148 | 149 | v2.8 150 | Fixed bug found by Christian HOFFMANN. newline removal was broken 151 | for lines that began with whitespace, causing crontab lines to be 152 | chopped off. 153 | 154 | v2.7 155 | Committed changes suggested by 156 | Ragnar Hojland Espinosa 157 | 158 | Fixed a few printfs, removed strdup() function ( strdup() is now standard 159 | in all major clib's ) 160 | 161 | v2.4-2.6 162 | ( changes lost ) 163 | 164 | v2.3 165 | dillon: Fixed bug in job.c -- if ChangeUser() fails, would return from child fork rather 166 | then exit! Oops. 167 | 168 | v2.2 169 | dillon: Initial release 170 | 171 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Dillon's crond and crontab 2 | VERSION = 4.5 3 | 4 | # these variables can be configured by e.g. `make SCRONTABS=/different/path` 5 | PREFIX = /usr/local 6 | CRONTAB_GROUP = wheel 7 | SCRONTABS = /etc/cron.d 8 | CRONTABS = /var/spool/cron/crontabs 9 | CRONSTAMPS = /var/spool/cron/cronstamps 10 | # used for syslog 11 | LOG_IDENT = crond 12 | # used for logging to file (syslog manages its own timestamps) 13 | # if LC_TIME is set, it will override any compiled-in timestamp format 14 | TIMESTAMP_FMT = %b %e %H:%M:%S 15 | SBINDIR = $(PREFIX)/sbin 16 | BINDIR = $(PREFIX)/bin 17 | MANDIR = $(PREFIX)/share/man 18 | 19 | -include config 20 | 21 | 22 | SHELL = /bin/sh 23 | INSTALL = install -o root 24 | INSTALL_PROGRAM = $(INSTALL) -D 25 | INSTALL_DATA = $(INSTALL) -D -m0644 -g root 26 | INSTALL_DIR = $(INSTALL) -d -m0755 -g root 27 | CFLAGS ?= -O2 28 | CFLAGS += -Wall -Wstrict-prototypes -Wno-missing-field-initializers 29 | SRCS = main.c subs.c database.c job.c concat.c chuser.c 30 | OBJS = main.o subs.o database.o job.o concat.o chuser.o 31 | TABSRCS = crontab.c chuser.c 32 | TABOBJS = crontab.o chuser.o 33 | PROTOS = protos.h 34 | LIBS = 35 | LDFLAGS = 36 | DEFS = -DVERSION='"$(VERSION)"' \ 37 | -DSCRONTABS='"$(SCRONTABS)"' -DCRONTABS='"$(CRONTABS)"' \ 38 | -DCRONSTAMPS='"$(CRONSTAMPS)"' -DLOG_IDENT='"$(LOG_IDENT)"' \ 39 | -DTIMESTAMP_FMT='"$(TIMESTAMP_FMT)"' 40 | 41 | # save variables needed for `make install` in config 42 | all: $(PROTOS) crond crontab ; 43 | rm -f config 44 | echo "PREFIX = $(PREFIX)" >> config 45 | echo "SBINDIR = $(SBINDIR)" >> config 46 | echo "BINDIR = $(BINDIR)" >> config 47 | echo "MANDIR = $(MANDIR)" >> config 48 | echo "CRONTAB_GROUP = $(CRONTAB_GROUP)" >> config 49 | echo "SCRONTABS = $(SCRONTABS)" >> config 50 | echo "CRONTABS = $(CRONTABS)" >> config 51 | echo "CRONSTAMPS = $(CRONSTAMPS)" >> config 52 | 53 | protos.h: $(SRCS) $(TABSRCS) 54 | fgrep -h Prototype $(SRCS) $(TABSRCS) > protos.h 55 | 56 | crond: $(OBJS) 57 | $(CC) $(LDFLAGS) $^ $(LIBS) -o crond 58 | 59 | crontab: $(TABOBJS) 60 | $(CC) $(LDFLAGS) $^ -o crontab 61 | 62 | %.o: %.c defs.h $(PROTOS) 63 | $(CC) $(CFLAGS) $(CPPFLAGS) -c $(DEFS) $< -o $@ 64 | 65 | install: 66 | $(INSTALL_PROGRAM) -m0700 -g root crond $(DESTDIR)$(SBINDIR)/crond 67 | $(INSTALL_PROGRAM) -m4750 -g $(CRONTAB_GROUP) crontab $(DESTDIR)$(BINDIR)/crontab 68 | $(INSTALL_DATA) crontab.1 $(DESTDIR)$(MANDIR)/man1/crontab.1 69 | $(INSTALL_DATA) crond.8 $(DESTDIR)$(MANDIR)/man8/crond.8 70 | $(INSTALL_DIR) $(DESTDIR)$(SCRONTABS) 71 | $(INSTALL_DIR) $(DESTDIR)$(CRONTABS) 72 | $(INSTALL_DIR) $(DESTDIR)$(CRONSTAMPS) 73 | 74 | clean: force 75 | rm -f *.o $(PROTOS) 76 | rm -f crond crontab config 77 | 78 | force: ; 79 | 80 | man: force 81 | -pandoc -t man -f markdown -s crontab.markdown -o crontab.1 82 | -pandoc -t man -f markdown -s crond.markdown -o crond.8 83 | 84 | # for maintainer's use only 85 | TARNAME = /home/abs/_dcron/dcron-$(VERSION).tar.gz 86 | dist: clean man 87 | bsdtar -cz --exclude repo/.git -f $(TARNAME).new -s'=^repo=dcron-$(VERSION)=' -C .. repo 88 | mv -f $(TARNAME).new $(TARNAME) 89 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | DCRON - DILLON'S LIGHTWEIGHT CRON DAEMON 2 | ======================================== 3 | 4 | **I (dubiousjim) haven't been able to maintain this project for a while and am not actively using the code myself. Here is [a fork](https://github.com/ptchinster/dcron) 5 | with active development. I recommend users to follow there instead, and submit issues and pull requests over there.** 6 | 7 | This lightweight cron daemon aims to be simple and secure, with just enough 8 | features to stay useful. It was written from scratch by Matt Dillon in 1994. 9 | It's now developed and maintained by James Pryor. 10 | 11 | In the author's opinion, having to combine a cron daemon with another daemon 12 | like anacron makes for too much complexity. So the goal is a simple cron daemon 13 | that can also take over the central functions of anacron. 14 | 15 | Unlike other fatter cron daemons, though, this cron doesn't even try to manage 16 | environment variables or act as a shell. All jobs are run with `/bin/sh` for 17 | conformity and portability. We don't try to use the user's preferred shell: 18 | that breaks down for special users and even makes some of us normal users 19 | unhappy (for example, /bin/csh does not use a true O_APPEND mode and has 20 | difficulty redirecting stdout and stderr both to different places!). You can, 21 | of course, run shell scripts in whatever language you like by making them 22 | executable with #!/bin/csh or whatever as the first line. If you don't like 23 | the extra processes, just `exec` them. 24 | 25 | If you need to set special environment variables, pass them as arguments to a 26 | script. 27 | 28 | The programs were written with an eye towards security, hopefully we haven't 29 | forgotton anything. The programs were also written with an eye towards nice, 30 | clean, algorithmically sound code. It's small, and the only fancy code is that 31 | which deals with child processes. We do not try to optimize with vfork() since 32 | it causes headaches and is rather pointless considering we're execing a shell 33 | most of the time, and we pay close attention to leaving descriptors open in the 34 | crond and close attention to preventing crond from running away. 35 | 36 | 37 | DOWNLOADING 38 | ----------- 39 | 40 | The project is hosted at: . 41 | 42 | The latest version is 4.5, which can be downloaded here: 43 | . 44 | 45 | A public git repo is available at: . 46 | 47 | 48 | COMPILING 49 | --------- 50 | 51 | You must use a compiler that understands prototypes, such as GCC. 52 | 53 | (1) The following compile-time defaults are configurable via 54 | command-line assignments on the `make` line (they're shown here with 55 | their default values): 56 | 57 | PREFIX=/usr/local # where files will ultimately be installed 58 | SBINDIR = $(PREFIX)/sbin # where crond will be installed 59 | BINDIR = $(PREFIX)/bin # where crontab will be installed 60 | MANDIR = $(PREFIX)/share/man # where manpages will be installed 61 | CRONTABS = /var/spool/cron/crontabs # default dir for per-user crontabs 62 | CRONSTAMPS = /var/spool/cron/cronstamps # default dir 63 | SCRONTABS = /etc/cron.d # default dir for system crontabs 64 | 65 | CRONTAB_GROUP = wheel # who's allowed to edit their own crontabs? 66 | LOG_IDENT = crond # syslog uses facility LOG_CRON and this identity 67 | TIMESTAMP_FMT = %b %e %H:%M:%S # used if LC_TIME unset and logging to file 68 | 69 | A few additional compile-time settings are defined in defs.h. If you find yourself 70 | wanting to edit defs.h directly, try editing the DEFS line in the Makefile instead. 71 | 72 | (2) Run make with your desired settings. For example: 73 | 74 | make PREFIX=/usr CRONTAB_GROUP=users 75 | 76 | (3) If you're using the git version, you might also want to `make man`, 77 | to be sure the manpages are updated. This requires 78 | [pandoc](http://johnmacfarlane.net/pandoc/). 79 | 80 | 81 | INSTALLING 82 | ---------- 83 | 84 | (4) `make install` installs the files underneath PREFIX (by default, /usr/local). 85 | If you're packaging, you can supply a DESTDIR argument here: 86 | 87 | make DESTDIR=/path/to/your/package/root install 88 | 89 | Permissions will be as follows: 90 | 91 | -rwx------ 0 root root 32232 Jan 6 18:58 /usr/local/sbin/crond 92 | -rwsr-x--- 0 root wheel 15288 Jan 6 18:58 /usr/local/bin/crontab 93 | 94 | Only users belonging to crontab's group (here "wheel") will be able to use it. 95 | You may want to create a special "cron" group and assign crontab to it: 96 | 97 | groupadd cron 98 | chgrp cron /usr/local/bin/crontab 99 | chmod 4750 /usr/local/bin/crontab 100 | 101 | (If the group already exists, you can specify it by supplying CRONTAB_GROUP 102 | to the `make` or `make install` commands.) 103 | 104 | Then add users to group "cron" when you want them to be able to install 105 | or edit their own crontabs. The superuser is able to install crontabs for users 106 | who don't have the privileges to edit their own. 107 | 108 | You should schedule crond to run automatically from system startup, using 109 | /etc/rc.local or a similar mechanism. crond automatically detaches. By default 110 | it logs all events <= loglevel NOTICE to syslog. 111 | 112 | The crontab files are normally located in /var/spool/cron/crontabs, and timestamps 113 | are normally in /var/spool/cron/cronstamps. These directories normally have permissions: 114 | 115 | drwxr-xr-x 2 root root 4096 Jan 6 18:50 /var/spool/cron 116 | drwxr-xr-x 1 root root 0 Jan 6 18:58 /var/spool/cron/crontabs 117 | drwxr-xr-x 1 root root 0 Jan 6 18:58 /var/spool/cron/cronstamps/ 118 | 119 | Here is the superuser's crontab, created using `sudo crontab -e`: 120 | 121 | -rw------- 0 root root 513 Jan 6 18:58 /var/spool/cron/crontabs/root 122 | 123 | TESTING 124 | ------- 125 | 126 | Use the crontab program to create a personal crontab with the following 127 | two lines: 128 | 129 | * * * * * date >> /tmp/test 130 | * * * * * date 131 | 132 | Check the log output of crond to ensure the cron entries are being 133 | run once a minute, check /tmp/test to ensure the date is being 134 | appended to it once a minute, and check your mail to ensure that crond 135 | is mailing you the date from the other entry once a minute. 136 | 137 | After you are through testing cron, delete the entries with `crontab -e` 138 | or `crontab -d`. 139 | 140 | EXTRAS 141 | ------ 142 | 143 | The following are included in the "extra" folder. None of them are installed 144 | by `make install`: 145 | 146 | crond.rc 147 | : This is an example rc script to start and stop crond. It could be placed in 148 | /etc/rc.d or /etc/init.d in suitable systems. 149 | 150 | crond.conf 151 | : This contains user-modifiable settings for crond.rc. The sample crond.rc 152 | expects to source this file from /etc/conf.d/crond. 153 | 154 | run-cron 155 | : This simple shell script is a bare-bones alternative to Debian's run-parts. 156 | 157 | crond.service 158 | : This is an example sysvinit service to start and stop crond. It 159 | could be placed in /lib/systemd/system in suitable systems. 160 | 161 | root.crontab 162 | : This is an example crontab to install for the root user, or to install 163 | in /etc/cron.d. It runs any executable scripts located in the directories /etc/cron.hourly, 164 | /etc/cron.daily, /etc/cron.weekly, and /etc/cron.monthly at the appropriate times. 165 | This example uses the run-cron script mentioned above, and relies on you to 166 | create the /etc/cron.* directories. 167 | 168 | prune-cronstamps 169 | : crond never removes any files from your cronstamps directory. If usernames 170 | are abandoned, or cron job names are abandoned, unused files will accumulate 171 | there. This simple cronjob will prune any cronstamp files older than three months. 172 | It will run weekly if placed in /etc/cron.d. 173 | 174 | crond.logrotate 175 | : This is an example to place in /etc/logrotate.d. This config file assumes you 176 | run crond using -L /var/log/crond.log. If you run crond using syslog instead (the default), 177 | you may prefer to configure the rotation of all your syslog-generated logs in a 178 | single config file. 179 | 180 | crontab.vim 181 | : This makes vim handle backup files in way that doesn't interfere with crontab's security 182 | model. 183 | 184 | 185 | BUG REPORTS, SUBMISSIONS 186 | ------------------------ 187 | 188 | Send any bug reports and source code changes to James Pryor: 189 | . 190 | 191 | We aim to keep this program simple, secure, and bug-free, in preference to 192 | adding features. Those advanced features we have added recently (such as 193 | @noauto, FREQ= and AFTER= tags, advanced cron.update parsing) fit naturally 194 | into the existing codebase. 195 | 196 | Our goal is also to make this program compilable in as near to a C89-strict a 197 | manner as possible. Less-portable features we're aware of are described in the 198 | comments to defs.h. We'll reduce these dependencies as feasible. Do let us know 199 | if any of them are an obstacle to using crond on your platform. 200 | 201 | Changes to defs.h, whether to override defaults or to accommodate your platform, 202 | should be made by a combination of a -D option in the Makefile 203 | and an #ifdef for that option in defs.h. Don't rely on pre-definitions made 204 | by the C compiler. 205 | 206 | Prototypes for system functions should come from external include 207 | files and NOT from defs.h or any source file. If no prototype exists for a 208 | particular function, contact your vendor to get an update for your includes. 209 | 210 | Note that the source code, especially in regard to changing the 211 | effective user, is Linux specific (SysVish). We welcome any changes 212 | in regard to making the mechanism work with other platforms. 213 | 214 | 215 | CREDITS 216 | ------- 217 | 218 | We use `concat`, a lightweight replacement for `asprintf`, in order to be more 219 | portable. This was written by Solar Designer and is in the public domain. 220 | 221 | -------------------------------------------------------------------------------- /chuser.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * CHUSER.C 4 | * 5 | * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com) 6 | * Copyright 2009-2019 James Pryor 7 | * May be distributed under the GNU General Public License version 2 or any later version. 8 | */ 9 | 10 | #include "defs.h" 11 | 12 | Prototype int ChangeUser(const char *user, char *dochdir); 13 | 14 | int 15 | ChangeUser(const char *user, char *dochdir) 16 | { 17 | struct passwd *pas; 18 | 19 | /* 20 | * Obtain password entry and change privilages 21 | */ 22 | 23 | if ((pas = getpwnam(user)) == 0) { 24 | printlogf(LOG_ERR, "failed to get uid for %s\n", user); 25 | return(-1); 26 | } 27 | setenv("USER", pas->pw_name, 1); 28 | setenv("LOGNAME", pas->pw_name, 1); 29 | setenv("HOME", pas->pw_dir, 1); 30 | setenv("SHELL", "/bin/sh", 1); 31 | 32 | /* 33 | * Change running state to the user in question 34 | */ 35 | 36 | if (initgroups(user, pas->pw_gid) < 0) { 37 | printlogf(LOG_ERR, "initgroups failed: %s %s\n", user, strerror(errno)); 38 | return(-1); 39 | } 40 | if (setregid(pas->pw_gid, pas->pw_gid) < 0) { 41 | printlogf(LOG_ERR, "setregid failed: %s %d\n", user, pas->pw_gid); 42 | return(-1); 43 | } 44 | if (setreuid(pas->pw_uid, pas->pw_uid) < 0) { 45 | printlogf(LOG_ERR, "setreuid failed: %s %d\n", user, pas->pw_uid); 46 | return(-1); 47 | } 48 | if (dochdir) { 49 | /* try to change to $HOME */ 50 | if (chdir(pas->pw_dir) < 0) { 51 | printlogf(LOG_ERR, "chdir failed: %s %s\n", user, pas->pw_dir); 52 | /* dochdir is a backup directory, usually /tmp */ 53 | if (chdir(dochdir) < 0) { 54 | printlogf(LOG_ERR, "chdir failed: %s %s\n", user, dochdir); 55 | return(-1); 56 | } 57 | } 58 | } 59 | return(pas->pw_uid); 60 | } 61 | 62 | -------------------------------------------------------------------------------- /concat.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * CONCAT.C 4 | * 5 | * Concatenates a variable number of strings. The argument list must be 6 | * terminated with a NULL. Returns a pointer to malloc(3)'ed memory with 7 | * the concatenated string, or NULL on error. 8 | * 9 | * This code deals gracefully with potential integer overflows (perhaps when 10 | * input strings are maliciously long), as well as with input strings changing 11 | * from under it (perhaps because of misbehavior of another thread). It does 12 | * not depend on non-portable functions such as snprintf() and asprintf(). 13 | * 14 | * Written by Solar Designer and placed in the 15 | * public domain. 16 | * 17 | * Usage: result = concat(str1, "separator", str2, "separator", str3, NULL); 18 | * Retrieved from https://openwall.info/wiki/people/solar/software/public-domain-source-code/concat 19 | * See also http://seclists.org/bugtraq/2006/Nov/594 20 | * 21 | */ 22 | 23 | #include "defs.h" 24 | 25 | Prototype char *concat(const char *s1, ...); 26 | 27 | char * 28 | concat(const char *s1, ...) 29 | { 30 | va_list args; 31 | const char *s; 32 | char *p, *result; 33 | size_t l, m, n; 34 | 35 | m = n = strlen(s1); 36 | va_start(args, s1); 37 | while ((s = va_arg(args, char *))) { 38 | l = strlen(s); 39 | if ((m += l) < l) break; 40 | } 41 | va_end(args); 42 | if (s || m >= INT_MAX) return NULL; 43 | 44 | result = malloc(m + 1); 45 | if (!result) return NULL; 46 | 47 | memcpy(p = result, s1, n); 48 | p += n; 49 | va_start(args, s1); 50 | while ((s = va_arg(args, char *))) { 51 | l = strlen(s); 52 | if ((n += l) < l || n > m) break; 53 | memcpy(p, s, l); 54 | p += l; 55 | } 56 | va_end(args); 57 | if (s || m != n || p - result != n) { 58 | free(result); 59 | return NULL; 60 | } 61 | 62 | *p = 0; 63 | return result; 64 | } 65 | -------------------------------------------------------------------------------- /crond.8: -------------------------------------------------------------------------------- 1 | .TH CROND 8 "1 May 2011" 2 | .SH NAME 3 | .PP 4 | crond - dillon's lightweight cron daemon 5 | .SH SYNOPSIS 6 | .PP 7 | \f[B]crond [-s dir] [-c dir] [-t dir] [-m user\@host] [-M mailhandler] [-S|-L file] [-l loglevel] [-b|-f|-d]\f[] 8 | .SH OPTIONS 9 | .PP 10 | \f[B]crond\f[] is a background daemon that parses individual 11 | crontab files and executes commands on behalf of the users in 12 | question. 13 | .TP 14 | .B -s dir 15 | directory of system crontabs (defaults to /etc/cron.d) 16 | .RS 17 | .RE 18 | .TP 19 | .B -c dir 20 | directory of per-user crontabs (defaults to 21 | /var/spool/cron/crontabs) 22 | .RS 23 | .RE 24 | .TP 25 | .B -t dir 26 | directory of timestamps for \@freq and FREQ=\&... jobs (defaults to 27 | /var/spool/cron/cronstamps) 28 | .RS 29 | .RE 30 | .TP 31 | .B -m user\@host 32 | where should the output of cronjobs be directed? (defaults to local 33 | user) Some mail handlers (like msmtp) can't route mail to local 34 | users. 35 | If that's what you're using, then you should supply a remote 36 | address using this switch. 37 | Cron output for all users will be directed to that address. 38 | Alternatively, you could supply a different mail handler using the 39 | -M switch, to log or otherwise process the messages instead of 40 | mailing them. 41 | Alternatively, you could just direct the stdout and stderr of your 42 | cron jobs to /dev/null. 43 | .RS 44 | .RE 45 | .TP 46 | .B -M mailhandler 47 | Any output that cronjobs print to stdout or stderr gets formatted 48 | as an email and piped to \f[C]/usr/sbin/sendmail\ -t\ -oem\ -i\f[]. 49 | Attempts to mail this are also logged. 50 | This switch permits the user to substitute a different mailhandler, 51 | or a script, for sendmail. 52 | That custom mailhandler is called with no arguments, and with the 53 | mail headers and cronjob output supplied to stdin. 54 | When a custom mailhandler is used, mailing is no longer logged 55 | (have your mailhandler do that if you want it). 56 | When cron jobs generate no stdout or stderr, nothing is sent to 57 | either sendmail or a custom mailhandler. 58 | .RS 59 | .RE 60 | .TP 61 | .B -S 62 | log events to syslog, using syslog facility LOG_CRON and identity 63 | `crond' (this is the default behavior). 64 | .RS 65 | .RE 66 | .TP 67 | .B -L file 68 | log to specified file instead of syslog. 69 | .RS 70 | .RE 71 | .TP 72 | .B -l loglevel 73 | log events at the specified, or more important, loglevels. 74 | The default is `notice'. 75 | Valid level names are as described in logger(1) and syslog(3): 76 | alert, crit, debug, emerg, err, error (deprecated synonym for err), 77 | info, notice, panic (deprecated synonym for emerg), warning, warn 78 | (deprecated synonym for warning). 79 | .RS 80 | .RE 81 | .TP 82 | .B -b 83 | run \f[B]crond\f[] in the background (default unless -d or -f is 84 | specified) 85 | .RS 86 | .RE 87 | .TP 88 | .B -f 89 | run \f[B]crond\f[] in the foreground. 90 | All log messages are sent to stderr instead of syslog or a -L file. 91 | .RS 92 | .RE 93 | .TP 94 | .B -d 95 | turn on debugging. 96 | This option sets the logging level to `debug' and causes 97 | \f[B]crond\f[] to run in the foreground. 98 | .RS 99 | .RE 100 | .SH DESCRIPTION 101 | .PP 102 | \f[B]crond\f[] is responsible for scanning the crontab files and 103 | running their commands at the appropriate time. 104 | It always synchronizes to the top of the minute, matching the 105 | current time against its internal list of parsed crontabs. 106 | That list is stored so that it can be scanned very quickly, and 107 | \f[B]crond\f[] can deal with several hundred crontabs with several 108 | thousand entries without using noticeable CPU. 109 | .PP 110 | Cron jobs are not re-executed if a previous instance of them is 111 | still running. 112 | For example, if you have a crontab command \f[C]sleep\ 70\f[], that 113 | you request to be run every minute, \f[B]crond\f[] will skip this 114 | job when it sees it is still running. 115 | So the job won't be run more frequently than once every two 116 | minutes. 117 | If you do not like this feature, you can run your commands in the 118 | background with an \f[C]&\f[]. 119 | .PP 120 | \f[B]crond\f[] automatically detects when the clock has been 121 | changed, during its per-minute scans. 122 | Backwards time-changes of an hour or less won't re-run cron jobs 123 | from the intervening period. 124 | \f[B]crond\f[] will effectively sleep until it catches back up to 125 | the original time. 126 | Forwards time-changes of an hour or less (or if the computer is 127 | suspended and resumed again within an hour) will run any missed 128 | jobs exactly once. 129 | Changes greater than an hour in either direction cause 130 | \f[B]crond\f[] to re-calculate when jobs should be run, and not 131 | attempt to execute any missed commands. 132 | This is effectively the same as if \f[B]crond\f[] had been stopped 133 | and re-started. 134 | .PP 135 | For example, suppose it's 10 am, and a job is scheduled to run 136 | every day at 10:30 am. 137 | If you set the system's clock forward to 11 am, crond will 138 | immediately run the 10:30 job. 139 | If on the other hand you set the system's clock forward to noon, 140 | the 10:30 am job will be skipped until the next day. 141 | Jobs scheduled using \@daily and the like work differently; see 142 | crontab(1) for details. 143 | .PP 144 | \f[B]crond\f[] has a number of built in limitations to reduce the 145 | chance of it being ill-used. 146 | Potentially infinite loops during parsing are dealt with via a 147 | failsafe counter, and non-root crontabs are limited to 256 crontab 148 | entries. 149 | Crontab lines may not be longer than 1024 characters, including the 150 | newline. 151 | .PP 152 | Whenever \f[B]crond\f[] must run a job, it first creates a 153 | daemon-owned temporary file O_EXCL and O_APPEND to store any 154 | output, then fork()s and changes its user and group permissions to 155 | match that of the user the job is being run for, then 156 | \f[B]exec\f[]s \f[B]/bin/sh -c \f[] to run the job. 157 | The temporary file remains under the ownership of the daemon to 158 | prevent the user from tampering with it. 159 | Upon job completion, \f[B]crond\f[] verifies the secureness of the 160 | mail file and, if it has been appended to, mails the file to the 161 | specified address. 162 | The \f[B]sendmail\f[] program (or custom mail handler, if supplied) 163 | is run under the user's uid to prevent mail related security holes. 164 | .PP 165 | When a user edits their crontab, \f[B]crontab\f[] first copies the 166 | crontab to a user owned file before running the user's preferred 167 | editor. 168 | The suid \f[B]crontab\f[] keeps an open descriptor to the file 169 | which it later uses to copy the file back, thereby ensuring the 170 | user has not tampered with the file type. 171 | .PP 172 | \f[B]crontab\f[] notifies \f[B]crond\f[] that a user's crontab file 173 | has been modified (or created or deleted) through the 174 | \[lq]cron.update\[rq] file, which resides in the per-user crontabs 175 | directory (usually /var/spool/cron/crontabs). 176 | \f[B]crontab\f[] appends the filename of the modified crontab file 177 | to \[lq]cron.update\[rq]; and \f[B]crond\f[] inspects this file to 178 | determine when to reparse or otherwise update its internal list of 179 | parsed crontabs. 180 | .PP 181 | Whenever a \[lq]cron.update\[rq] file is seen, \f[B]crond\f[] also 182 | re-reads timestamp files from its timestamp directory (usually 183 | /var/spool/cron/cronstamps). 184 | Normally these will just mirror \f[B]crond\f[]'s own internal 185 | representations, but this mechanism could be used to manually 186 | notify \f[B]crond\f[] that you've externally updated the 187 | timestamps. 188 | .PP 189 | The \[lq]cron.update\[rq] file can also be used to ask 190 | \f[B]crond\f[] to schedule a \[lq]named\[rq] cron job. 191 | To do this, append a line of the form: 192 | .IP 193 | .nf 194 | \f[C] 195 | clio\ job1\ !job2 196 | \f[] 197 | .fi 198 | .PP 199 | to \[lq]cron.update\[rq]. 200 | This request that user clio's job1 should be scheduled (waiting 201 | first for the successful completion of any jobs named in job1's 202 | AFTER= tag), and job2 should also be scheduled (without waiting for 203 | other jobs). 204 | See crontab(1) for more about tags and named jobs. 205 | .PP 206 | The directory of per-user crontabs is re-parsed once every hour in 207 | any case. 208 | Any crontabs in the system directory (usually /etc/cron.d) are 209 | parsed at the same time. 210 | This directory can be used by packaging systems. 211 | When you install a package foo, it might write its own foo-specific 212 | crontab to /etc/cron.d/foo. 213 | .PP 214 | The superuser has a per-user crontab along with other users. 215 | It usually resides at /var/spool/cron/crontabs/root. 216 | .PP 217 | Users can only have a crontab if they have an entry in /etc/passwd; 218 | however they do not need to have login shell privileges. 219 | Cron jobs are always run under /bin/sh; see crontab(1) for more 220 | details. 221 | .PP 222 | Unlike \f[B]crontab\f[], the \f[B]crond\f[] program does not keep 223 | open descriptors to crontab files while running their jobs, as this 224 | could cause \f[B]crond\f[] to run out of descriptors. 225 | .SH SEE ALSO 226 | .PP 227 | \f[B]crontab\f[](1) 228 | .SH AUTHORS 229 | .PP 230 | Matthew Dillon (dillon\@apollo.backplane.com): original 231 | developer 232 | .PD 0 233 | .P 234 | .PD 235 | Jim Pryor (profjim\@jimpryor.net): current 236 | developer 237 | -------------------------------------------------------------------------------- /crond.markdown: -------------------------------------------------------------------------------- 1 | % CROND(8) 2 | % 3 | % 20 Nov 2019 4 | 5 | NAME 6 | ==== 7 | crond - dillon's lightweight cron daemon 8 | 9 | SYNOPSIS 10 | ======== 11 | **crond [-s dir] [-c dir] [-t dir] [-m user@host] [-M mailhandler] 12 | [-S|-L file] [-l loglevel] [-b|-f|-d]** 13 | 14 | OPTIONS 15 | ======= 16 | **crond** is a background daemon that parses individual crontab files and 17 | executes commands on behalf of the users in question. 18 | 19 | -s dir 20 | : directory of system crontabs (defaults to /etc/cron.d) 21 | 22 | -c dir 23 | : directory of per-user crontabs (defaults to /var/spool/cron/crontabs) 24 | 25 | -t dir 26 | : directory of timestamps for @freq and FREQ=... jobs 27 | (defaults to /var/spool/cron/cronstamps) 28 | 29 | -m user@host 30 | : where should the output of cronjobs be directed? (defaults to local user) 31 | Some mail handlers (like msmtp) can't route mail to local users. If that's 32 | what you're using, then you should supply a remote address using this switch. 33 | Cron output for all users will be directed to that address. Alternatively, you 34 | could supply a different mail handler using the -M switch, to log or otherwise 35 | process the messages instead of mailing them. Alternatively, you could just 36 | direct the stdout and stderr of your cron jobs to /dev/null. 37 | 38 | -M mailhandler 39 | : Any output that cronjobs print to stdout or stderr gets formatted as an email 40 | and piped to `/usr/sbin/sendmail -t -oem -i`. Attempts to mail this are also 41 | logged. This switch permits the user to substitute a different mailhandler, 42 | or a script, for sendmail. That custom mailhandler is called with no 43 | arguments, and with the mail headers and cronjob output supplied to 44 | stdin. When a custom mailhandler is used, mailing is no longer logged 45 | (have your mailhandler do that if you want it). When cron jobs generate no 46 | stdout or stderr, nothing is sent to either sendmail or a custom mailhandler. 47 | 48 | -S 49 | : log events to syslog, using syslog facility LOG_CRON and identity 'crond' (this is the default behavior). 50 | 51 | -L file 52 | : log to specified file instead of syslog. 53 | 54 | -l loglevel 55 | : log events at the specified, or more important, loglevels. The default is 56 | 'notice'. Valid level names are as described in logger(1) and syslog(3): 57 | alert, crit, debug, emerg, err, error (deprecated synonym for err), info, 58 | notice, panic (deprecated synonym for emerg), warning, warn (deprecated 59 | synonym for warning). 60 | 61 | -b 62 | : run **crond** in the background (default unless -d or -f is specified) 63 | 64 | -f 65 | : run **crond** in the foreground. All log messages are sent to stderr instead 66 | of syslog or a -L file. 67 | 68 | -d 69 | : turn on debugging. This option sets the logging level to 'debug' and causes 70 | **crond** to run in the foreground. 71 | 72 | DESCRIPTION 73 | =========== 74 | 75 | **crond** is responsible for scanning the crontab files and running their 76 | commands at the appropriate time. It always synchronizes to the top of the 77 | minute, matching the current time against its internal list of parsed crontabs. 78 | That list is stored so that it can be scanned very quickly, and **crond** can deal 79 | with several hundred crontabs with several thousand entries without using noticeable CPU. 80 | 81 | 82 | Cron jobs are not re-executed if a previous instance of them is still running. 83 | For example, if you have a crontab command `sleep 70`, that you request to be 84 | run every minute, **crond** will skip this job when it sees it is still 85 | running. So the job won't be run more frequently than once every two minutes. 86 | If you do not like this feature, you can run your commands in the background 87 | with an `&`. 88 | 89 | **crond** automatically detects when the clock has been changed, during its 90 | per-minute scans. Backwards time-changes of an hour or less won't re-run cron 91 | jobs from the intervening period. **crond** will effectively sleep until it 92 | catches back up to the original time. Forwards time-changes of an hour or less 93 | (or if the computer is suspended and resumed again within an hour) will run any 94 | missed jobs exactly once. Changes greater than an hour in either direction 95 | cause **crond** to re-calculate when jobs should be run, and not attempt to 96 | execute any missed commands. This is effectively the same as if **crond** had 97 | been stopped and re-started. 98 | 99 | 100 | 101 | For example, suppose it's 10 am, and a job is scheduled to run every day at 102 | 10:30 am. If you set the system's clock forward to 11 am, crond will immediately run 103 | the 10:30 job. If on the other hand you set the system's clock forward to noon, 104 | the 10:30 am job will be skipped until the next day. Jobs scheduled using 105 | @daily and the like work differently; see crontab(1) for details. 106 | 107 | 108 | 109 | **crond** has a number of built in limitations to reduce the chance of it being 110 | ill-used. Potentially infinite loops during parsing are dealt with via a 111 | failsafe counter, and non-root crontabs are limited to 256 crontab 112 | entries. Crontab lines may not be longer than 1024 characters, including the 113 | newline. 114 | 115 | Whenever **crond** must run a job, it first creates a daemon-owned temporary 116 | file O_EXCL and O_APPEND to store any output, then fork()s and changes its user 117 | and group permissions to match that of the user the job is being run for, then 118 | **exec**s **/bin/sh -c ** to run the job. The temporary file remains 119 | under the ownership of the daemon to prevent the user from tampering with it. 120 | Upon job completion, **crond** verifies the secureness of the mail file and, if 121 | it has been appended to, mails the file to the specified address. The **sendmail** program 122 | (or custom mail handler, if supplied) is run under the user's uid to prevent mail 123 | related security holes. 124 | 125 | When a user edits their crontab, **crontab** first copies the 126 | crontab to a user owned file before running the user's preferred editor. The 127 | suid **crontab** keeps an open descriptor to the file which it later uses to 128 | copy the file back, thereby ensuring the user has not tampered with the file 129 | type. 130 | 131 | 132 | 133 | 134 | **crontab** notifies **crond** that a user's crontab file has been 135 | modified (or created or deleted) through the "cron.update" file, which resides 136 | in the per-user crontabs directory (usually /var/spool/cron/crontabs). **crontab** 137 | appends the filename of the modified crontab file to "cron.update"; and 138 | **crond** inspects this file to determine when to reparse or otherwise update 139 | its internal list of parsed crontabs. 140 | 141 | Whenever a "cron.update" file is seen, **crond** also re-reads timestamp 142 | files from its timestamp directory (usually /var/spool/cron/cronstamps). Normally 143 | these will just mirror **crond**'s own internal representations, but this 144 | mechanism could be used to manually notify **crond** that you've externally 145 | updated the timestamps. 146 | 147 | The "cron.update" file can also be used to ask **crond** to schedule a "named" 148 | cron job. To do this, append a line of the form: 149 | 150 | clio job1 !job2 151 | 152 | to "cron.update". This request that user clio's job1 should be scheduled 153 | (waiting first for the successful completion of any jobs named in job1's AFTER= 154 | tag), and job2 should also be scheduled (without waiting for other jobs). See 155 | crontab(1) for more about tags and named jobs. 156 | 157 | 158 | 159 | The directory of per-user crontabs is re-parsed once every hour in any case. 160 | Any crontabs in the system directory (usually /etc/cron.d) are parsed at the 161 | same time. This directory can be used by packaging systems. When you install a 162 | package foo, it might write its own foo-specific crontab to /etc/cron.d/foo. 163 | 164 | The superuser has a per-user crontab along with other users. It usually resides 165 | at /var/spool/cron/crontabs/root. 166 | 167 | Users can only have a crontab if they have an entry in /etc/passwd; however 168 | they do not need to have login shell privileges. Cron jobs are always run under 169 | /bin/sh; see crontab(1) for more details. 170 | 171 | 172 | 173 | Unlike **crontab**, the **crond** program does not keep open descriptors to 174 | crontab files while running their jobs, as this could cause **crond** to run 175 | out of descriptors. 176 | 177 | 178 | SEE ALSO 179 | ======== 180 | **crontab**(1) 181 | 182 | AUTHORS 183 | ======= 184 | Matthew Dillon (dillon@apollo.backplane.com): original developer 185 | James Pryor (dubiousjim@gmail.com): current developer 186 | -------------------------------------------------------------------------------- /crontab.1: -------------------------------------------------------------------------------- 1 | .TH CRONTAB 1 "1 May 2011" 2 | .SH NAME 3 | .PP 4 | crontab - manipulate per-user crontabs (dillon's lightweight cron 5 | daemon) 6 | .SH SYNOPSIS 7 | .PP 8 | \f[B]crontab file [-u user]\f[] - replace crontab from file 9 | .PP 10 | \f[B]crontab - [-u user]\f[] - replace crontab from stdin 11 | .PP 12 | \f[B]crontab -l [-u user]\f[] - list crontab for user 13 | .PP 14 | \f[B]crontab -e [-u user]\f[] - edit crontab for user 15 | .PP 16 | \f[B]crontab -d [-u user]\f[] - delete crontab for user 17 | .PP 18 | \f[B]crontab -c dir\f[] - specify crontab directory 19 | .SH DESCRIPTION 20 | .PP 21 | \f[B]crontab\f[] manipulates the per-user crontabs. 22 | .PP 23 | Generally the -e option is used to edit your crontab. 24 | \f[B]crontab\f[] will use the editor specified by your EDITOR or 25 | VISUAL environment variable (or /usr/bin/vi) to edit the crontab. 26 | .PP 27 | \f[B]crontab\f[] doesn't provide the kinds of protections that 28 | programs like \f[B]visudo\f[] do against syntax errors and 29 | simultaneous edits. 30 | Errors won't be detected until \f[B]crond\f[] reads the crontab 31 | file. 32 | What \f[B]crontab\f[] does is provide a mechanism for users who may 33 | not themselves have write privileges to the crontab folder to 34 | nonetheless install or edit their crontabs. 35 | It also notifies a running crond daemon of any changes to these 36 | files. 37 | .PP 38 | Only users who belong to the same group as the \f[B]crontab\f[] 39 | binary will be able to install or edit crontabs. 40 | However it'll be possible for the superuser to install crontabs 41 | even for users who don't have the privileges to install them 42 | themselves. 43 | (Even for users who don't have a login shell.) 44 | Only the superuser may use the -u or -c switches to specify a 45 | different user and/or crontab directory. 46 | .PP 47 | The superuser also has his or her own per-user crontab, saved as 48 | /var/spool/cron/crontabs/root. 49 | .PP 50 | Unlike other cron daemons, this crond/crontab package doesn't try 51 | to do everything under the sun. 52 | It doesn't try to keep track of user's preferred shells; that would 53 | require special-casing users with no login shell. 54 | Instead, it just runs all commands using \f[C]/bin/sh\f[]. 55 | (Commands can of course be script files written in any shell you 56 | like.) 57 | .PP 58 | Nor does it do any special environment handling. 59 | A shell script is better-suited to doing that than a cron daemon. 60 | This cron daemon sets up only four environment variables: USER, 61 | LOGNAME, HOME, and SHELL. 62 | .PP 63 | Our crontab format is roughly similar to that used by vixiecron. 64 | Individual fields may contain a time, a time range, a time range 65 | with a skip factor, a symbolic range for the day of week and month 66 | in year, and additional subranges delimited with commas. 67 | Blank lines in the crontab or lines that begin with a hash (#) are 68 | ignored. 69 | If you specify both a day in the month and a day of week, it will 70 | be interpreted as the Nth such day in the month. 71 | .PP 72 | Some examples: 73 | .IP 74 | .nf 75 | \f[C] 76 | #\ MIN\ HOUR\ DAY\ MONTH\ DAYOFWEEK\ \ COMMAND 77 | #\ run\ `date`\ at\ 6:10\ am\ every\ day 78 | 10\ 6\ *\ *\ *\ date 79 | 80 | #\ run\ every\ two\ hours\ at\ the\ top\ of\ the\ hour 81 | 0\ */2\ *\ *\ *\ date 82 | 83 | #\ run\ every\ two\ hours\ between\ 11\ pm\ and\ 7\ am,\ and\ again\ at\ 8\ am 84 | 0\ 23-7/2,8\ *\ *\ *\ date 85 | 86 | #\ run\ at\ 4:00\ am\ on\ January\ 1st 87 | 0\ 4\ 1\ jan\ *\ date 88 | 89 | #\ run\ every\ day\ at\ 11\ am,\ appending\ all\ output\ to\ a\ file 90 | 0\ 11\ *\ *\ *\ date\ >>\ /var/log/date-output\ 2>&1 91 | \f[] 92 | .fi 93 | .PP 94 | To request the last Monday, etc. 95 | in a month, ask for the \[lq]6th\[rq] one. 96 | This will always match the last Monday, etc., even if there are 97 | only four Mondays in the month: 98 | .IP 99 | .nf 100 | \f[C] 101 | #\ run\ at\ 11\ am\ on\ the\ first\ and\ last\ Mon,\ Tue,\ Wed\ of\ each\ month 102 | 0\ 11\ 1,6\ *\ mon-wed\ date 103 | 104 | #\ run\ at\ noon\ on\ the\ fourth\ and\ last\ Friday\ of\ each\ month 105 | 0\ 12\ 4,6\ *\ fri\ date 106 | \f[] 107 | .fi 108 | .PP 109 | When the fourth Monday in a month is also the last, this will match against 110 | both the \[lq]4th\[rq] and the \[lq]6th\[rq] but the job is scheduled only 111 | once. 112 | .PP 113 | The following formats are also recognized: 114 | .IP 115 | .nf 116 | \f[C] 117 | #\ schedule\ this\ job\ only\ once,\ when\ crond\ starts\ up 118 | \@reboot\ date 119 | 120 | #\ schedule\ this\ job\ whenever\ crond\ is\ running,\ and\ sees\ that\ at\ least\ one 121 | #\ hour\ has\ elapsed\ since\ it\ last\ ran 122 | \@hourly\ ID=job1\ date 123 | \f[] 124 | .fi 125 | .PP 126 | The formats \@hourly, \@daily, \@weekly, \@monthly, and \@yearly 127 | need to update timestamp files when their jobs have been run. 128 | The timestamp files are saved as 129 | /var/spool/cron/cronstamps/user.jobname. 130 | So for all of these formats, the cron command needs a jobname, 131 | given by prefixing the command with \f[C]ID=jobname\f[]. 132 | (This syntax was chosen to maximize the chance that our crontab 133 | files will be readable by other cron daemons as well. 134 | They might just interpret the ID=jobname as a command-line 135 | environment variable assignment.) 136 | .PP 137 | There's also this esoteric option, whose usefulness will be 138 | explained later: 139 | .IP 140 | .nf 141 | \f[C] 142 | #\ don\[aq]t\ ever\ schedule\ this\ job\ on\ its\ own;\ only\ run\ it\ when\ it\[aq]s\ triggered 143 | #\ as\ a\ "dependency"\ of\ another\ job\ (see\ below),\ or\ when\ the\ user\ explicitly 144 | #\ requests\ it\ through\ the\ "cron.update"\ file\ (see\ crond(8)) 145 | \@noauto\ ID=namedjob\ date 146 | \f[] 147 | .fi 148 | .PP 149 | There's also a format available for finer-grained control of 150 | frequencies: 151 | .IP 152 | .nf 153 | \f[C] 154 | #\ run\ whenever\ it\[aq]s\ between\ 2-4\ am,\ and\ at\ least\ one\ day\ (1d) 155 | #\ has\ elapsed\ since\ this\ job\ ran 156 | *\ 2-4\ *\ *\ *\ ID=job2\ FREQ=1d\ date 157 | 158 | #\ as\ before,\ but\ re-try\ every\ 10\ minutes\ (10m)\ if\ my_command 159 | #\ exits\ with\ code\ 11\ (EAGAIN) 160 | *\ 2-4\ *\ *\ *\ ID=job3\ FREQ=1d/10m\ my_command 161 | \f[] 162 | .fi 163 | .PP 164 | These formats also update timestamp files, and so also require 165 | their jobs to be assigned IDs. 166 | .PP 167 | Notice the technique used in the second example: jobs can exit with 168 | code 11 to indicate they lacked the resources to run (for example, 169 | no network was available), and so should be tried again after a 170 | brief delay. 171 | This works for jobs using either \@freq or FREQ=\&... formats; but 172 | the FREQ=\&.../10m syntax is the only way to customize the length 173 | of the delay before re-trying. 174 | .PP 175 | Jobs can be made to \[lq]depend\[rq] on, or wait until AFTER other 176 | jobs have successfully completed. 177 | Consider the following crontab: 178 | .IP 179 | .nf 180 | \f[C] 181 | *\ *\ *\ *\ *\ ID=job4\ FREQ=1d\ first_command 182 | *\ *\ *\ *\ *\ ID=job5\ FREQ=1h\ AFTER=job4/30m\ second_command 183 | \f[] 184 | .fi 185 | .PP 186 | Here, whenever job5 is up to be run, if job4 is scheduled to run 187 | within the next 30 minutes (30m), job5 will first wait for it to 188 | successfully complete. 189 | .PP 190 | (What if job4 doesn't successfully complete? If job4 returns with 191 | exit code EAGAIN, job5 will continue to wait until job4 is 192 | retried\[em]even if that won't be within the hour. 193 | If job4 returns with any other non-zero exit code, job5 will be 194 | removed from the queue without running.) 195 | .PP 196 | Jobs can be told to wait for multiple other jobs, as follows: 197 | .IP 198 | .nf 199 | \f[C] 200 | 10\ *\ *\ *\ *\ ID=job6\ AFTER=job4/1h,job7\ third_command 201 | \f[] 202 | .fi 203 | .PP 204 | The waiting job6 doesn't care what order job4 and job7 complete in. 205 | If job6 comes up to be re-scheduled (an hour later) while an 206 | earlier instance is still waiting, only a single instance of job6 207 | will remain in the queue. 208 | It will have all of its \[lq]waiting flags\[rq] reset: so each of 209 | job7 and job4 (supposing again that job4 would run within the next 210 | 1h) will again have to complete before job6 will run. 211 | .PP 212 | If a job waits on a \@reboot or \@noauto job, the target job being 213 | waited on will also be scheduled to run. 214 | This technique can be used to have a common job scheduled as 215 | \@noauto that several other jobs depend on (and so call as a 216 | subroutine). 217 | .PP 218 | The command portion of a cron job is run with 219 | \f[C]/bin/sh\ -c\ ...\f[] and may therefore contain any valid 220 | Bourne shell command. 221 | A common practice is to prefix your command with \f[B]exec\f[] to 222 | keep the process table uncluttered. 223 | It is also common to redirect job output to a file or to /dev/null. 224 | If you do not, and the command generates output on stdout or 225 | stderr, that output will be mailed to the local user whose crontab 226 | the job comes from. 227 | If you have crontabs for special users, such as uucp, who can't 228 | receive local mail, you may want to create mail aliases for them or 229 | adjust this behavior. 230 | (See crond(8) for details how to adjust it.) 231 | .PP 232 | Whenever jobs return an exit code that's neither 0 nor 11 (EAGAIN), 233 | that event will be logged, regardless of whether any stdout or 234 | stderr is generated. 235 | The job's timestamp will also be updated, and it won't be run again 236 | until it would next be normally scheduled. 237 | Any jobs waiting on the failed job will be canceled; they won't be 238 | run until they're next scheduled. 239 | .SH TODO 240 | .PP 241 | Ought to be able to have several crontab files for any given user, 242 | as an organizational tool. 243 | .SH SEE ALSO 244 | .PP 245 | \f[B]crond\f[](8) 246 | .SH AUTHORS 247 | .PP 248 | Matthew Dillon (dillon\@apollo.backplane.com): original 249 | developer 250 | .PD 0 251 | .P 252 | .PD 253 | Jim Pryor (profjim\@jimpryor.net): current 254 | developer 255 | -------------------------------------------------------------------------------- /crontab.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * CRONTAB.C 4 | * 5 | * crontab [-u user] [-c dir] [-l|-e|-d|file|-] 6 | * usually run as setuid root 7 | * -u and -c options only work if getuid() == geteuid() 8 | * 9 | * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com) 10 | * Copyright 2009-2019 James Pryor 11 | * May be distributed under the GNU General Public License version 2 or any later version. 12 | */ 13 | 14 | #include "defs.h" 15 | 16 | Prototype void printlogf(int level, const char *ctl, ...); 17 | 18 | void Usage(void); 19 | int GetReplaceStream(const char *user, const char *file); 20 | void EditFile(const char *user, const char *file); 21 | 22 | const char *CDir = CRONTABS; 23 | int UserId; 24 | 25 | 26 | int 27 | main(int ac, char **av) 28 | { 29 | enum { NONE, EDIT, LIST, REPLACE, DELETE } option = NONE; 30 | struct passwd *pas; 31 | char *repFile = NULL; 32 | int repFd = 0; 33 | int i; 34 | char caller[SMALL_BUFFER]; /* user that ran program */ 35 | 36 | UserId = getuid(); 37 | if ((pas = getpwuid(UserId)) == NULL) { 38 | perror("getpwuid"); 39 | exit(1); 40 | } 41 | /* [v]snprintf write at most size including \0; they'll null-terminate, even when they truncate */ 42 | /* return value >= size means result was truncated */ 43 | if (snprintf(caller, sizeof(caller), "%s", pas->pw_name) >= sizeof(caller)) { 44 | printlogf(0, "username '%s' too long", caller); 45 | exit(1); 46 | } 47 | 48 | opterr = 0; 49 | while ((i=getopt(ac,av,"ledu:c:")) != -1) { 50 | switch(i) { 51 | case 'l': 52 | if (option != NONE) 53 | Usage(); 54 | else 55 | option = LIST; 56 | break; 57 | case 'e': 58 | if (option != NONE) 59 | Usage(); 60 | else 61 | option = EDIT; 62 | break; 63 | case 'd': 64 | if (option != NONE) 65 | Usage(); 66 | else 67 | option = DELETE; 68 | break; 69 | case 'u': 70 | /* getopt guarantees optarg != 0 here */ 71 | if (*optarg != 0 && getuid() == geteuid()) { 72 | pas = getpwnam(optarg); 73 | if (pas) { 74 | UserId = pas->pw_uid; 75 | /* paranoia */ 76 | if ((pas = getpwuid(UserId)) == NULL) { 77 | perror("getpwuid"); 78 | exit(1); 79 | } 80 | } else { 81 | printlogf(0, "user '%s' unknown", optarg); 82 | exit(1); 83 | } 84 | } else { 85 | printlogf(0, "-u option: superuser only"); 86 | exit(1); 87 | } 88 | break; 89 | case 'c': 90 | /* getopt guarantees optarg != 0 here */ 91 | if (*optarg != 0 && getuid() == geteuid()) { 92 | CDir = optarg; 93 | } else { 94 | printlogf(0, "-c option: superuser only"); 95 | exit(1); 96 | } 97 | break; 98 | default: 99 | /* unrecognized -X */ 100 | option = NONE; 101 | } 102 | } 103 | 104 | if (option == NONE && optind == ac - 1) { 105 | if (av[optind][0] != '-') { 106 | option = REPLACE; 107 | repFile = av[optind]; 108 | optind++; 109 | } else if (av[optind][1] == 0) { 110 | option = REPLACE; 111 | optind++; 112 | } 113 | } 114 | if (option == NONE || optind != ac) { 115 | Usage(); 116 | } 117 | 118 | /* 119 | * If there is a replacement file, obtain a secure descriptor to it. 120 | */ 121 | 122 | if (repFile) { 123 | repFd = GetReplaceStream(caller, repFile); 124 | if (repFd < 0) { 125 | printlogf(0, "unable to read replacement file %s", repFile); 126 | exit(1); 127 | } 128 | } 129 | 130 | /* 131 | * Change directory to our crontab directory 132 | */ 133 | 134 | if (chdir(CDir) < 0) { 135 | printlogf(0, "cannot change dir to %s: %s", CDir, strerror(errno)); 136 | exit(1); 137 | } 138 | 139 | /* 140 | * Handle options as appropriate 141 | */ 142 | 143 | switch(option) { 144 | case LIST: 145 | { 146 | FILE *fi; 147 | char buf[RW_BUFFER]; 148 | 149 | if ((fi = fopen(pas->pw_name, "r"))) { 150 | while (fgets(buf, sizeof(buf), fi) != NULL) 151 | fputs(buf, stdout); 152 | fclose(fi); 153 | } else { 154 | fprintf(stderr, "no crontab for %s\n", pas->pw_name); 155 | /* no error code */ 156 | } 157 | } 158 | break; 159 | case EDIT: 160 | { 161 | FILE *fi; 162 | int fd; 163 | int n; 164 | char tmp[] = TMPDIR "/crontab.XXXXXX"; 165 | char buf[RW_BUFFER]; 166 | 167 | /* 168 | * Create temp file with perm 0600 and O_EXCL flag, ensuring that this call creates the file 169 | * Read from fi for "$CDir/$USER", write to fd for temp file 170 | * EditFile changes user if necessary, and runs editor on temp file 171 | * Then we delete the temp file, keeping its fd as repFd 172 | */ 173 | if ((fd = mkstemp(tmp)) >= 0) { 174 | chown(tmp, getuid(), getgid()); 175 | if ((fi = fopen(pas->pw_name, "r"))) { 176 | while ((n = fread(buf, 1, sizeof(buf), fi)) > 0) 177 | write(fd, buf, n); 178 | } 179 | EditFile(caller, tmp); 180 | remove(tmp); 181 | lseek(fd, 0L, 0); 182 | repFd = fd; 183 | } else { 184 | printlogf(0, "unable to create %s: %s", tmp, strerror(errno)); 185 | exit(1); 186 | } 187 | 188 | } 189 | option = REPLACE; 190 | /* fall through */ 191 | case REPLACE: 192 | { 193 | char buf[RW_BUFFER]; 194 | char path[SMALL_BUFFER]; 195 | int fd; 196 | int n; 197 | 198 | /* 199 | * Read from repFd, write to fd for "$CDir/$USER.new" 200 | */ 201 | snprintf(path, sizeof(path), "%s.new", pas->pw_name); 202 | if ((fd = open(path, O_CREAT|O_TRUNC|O_EXCL|O_APPEND|O_WRONLY, 0600)) >= 0) { 203 | while ((n = read(repFd, buf, sizeof(buf))) > 0) { 204 | write(fd, buf, n); 205 | } 206 | close(fd); 207 | rename(path, pas->pw_name); 208 | } else { 209 | fprintf(stderr, "unable to create %s/%s: %s\n", 210 | CDir, 211 | path, 212 | strerror(errno) 213 | ); 214 | } 215 | close(repFd); 216 | } 217 | break; 218 | case DELETE: 219 | remove(pas->pw_name); 220 | break; 221 | case NONE: 222 | default: 223 | break; 224 | } 225 | 226 | /* 227 | * Bump notification file. Handle window where crond picks file up 228 | * before we can write our entry out. 229 | */ 230 | 231 | if (option == REPLACE || option == DELETE) { 232 | FILE *fo; 233 | struct stat st; 234 | 235 | while ((fo = fopen(CRONUPDATE, "a"))) { 236 | fprintf(fo, "%s\n", pas->pw_name); 237 | fflush(fo); 238 | if (fstat(fileno(fo), &st) != 0 || st.st_nlink != 0) { 239 | fclose(fo); 240 | break; 241 | } 242 | fclose(fo); 243 | /* loop */ 244 | } 245 | if (fo == NULL) { 246 | fprintf(stderr, "unable to append to %s/%s\n", CDir, CRONUPDATE); 247 | } 248 | } 249 | exit(0); 250 | /* not reached */ 251 | } 252 | 253 | void 254 | printlogf(int level, const char *ctl, ...) 255 | { 256 | va_list va; 257 | char buf[LOG_BUFFER]; 258 | 259 | va_start(va, ctl); 260 | vsnprintf(buf, sizeof(buf), ctl, va); 261 | write(2, buf, strlen(buf)); 262 | va_end(va); 263 | } 264 | 265 | void 266 | Usage(void) 267 | { 268 | /* 269 | * parse error 270 | */ 271 | printf("crontab " VERSION "\n"); 272 | printf("crontab file [-u user] replace crontab from file\n"); 273 | printf("crontab - [-u user] replace crontab from stdin\n"); 274 | printf("crontab -l [-u user] list crontab\n"); 275 | printf("crontab -e [-u user] edit crontab\n"); 276 | printf("crontab -d [-u user] delete crontab\n"); 277 | printf("crontab -c dir specify crontab directory\n"); 278 | exit(2); 279 | } 280 | 281 | int 282 | GetReplaceStream(const char *user, const char *file) 283 | { 284 | int filedes[2]; 285 | int pid; 286 | int fd; 287 | int n; 288 | char buf[RW_BUFFER]; 289 | 290 | if (pipe(filedes) < 0) { 291 | perror("pipe"); 292 | return(-1); 293 | } 294 | if ((pid = fork()) < 0) { 295 | perror("fork"); 296 | return(-1); 297 | } 298 | if (pid > 0) { 299 | /* 300 | * PARENT 301 | * Read from pipe[0], return it (or -1 if it's empty) 302 | */ 303 | 304 | close(filedes[1]); 305 | if (read(filedes[0], buf, 1) != 1) { 306 | close(filedes[0]); 307 | filedes[0] = -1; 308 | } 309 | return(filedes[0]); 310 | } 311 | 312 | /* 313 | * CHILD 314 | * Read from fd for "$file", write to pipe[1] 315 | */ 316 | 317 | close(filedes[0]); 318 | 319 | if (ChangeUser(user, NULL) < 0) 320 | exit(0); 321 | 322 | fd = open(file, O_RDONLY); 323 | if (fd < 0) { 324 | printlogf(0, "unable to open %s: %s", file, strerror(errno)); 325 | exit(1); 326 | } 327 | buf[0] = 0; 328 | write(filedes[1], buf, 1); 329 | while ((n = read(fd, buf, sizeof(buf))) > 0) { 330 | write(filedes[1], buf, n); 331 | } 332 | exit(0); 333 | } 334 | 335 | void 336 | EditFile(const char *user, const char *file) 337 | { 338 | int pid; 339 | 340 | if ((pid = fork()) == 0) { 341 | /* 342 | * CHILD - change user and run editor on "$file" 343 | */ 344 | const char *ptr; 345 | char visual[SMALL_BUFFER]; 346 | 347 | if (ChangeUser(user, TMPDIR) < 0) 348 | exit(0); 349 | if ((ptr = getenv("EDITOR")) == NULL || strlen(ptr) >= sizeof(visual)) 350 | if ((ptr = getenv("VISUAL")) == NULL || strlen(ptr) >= sizeof(visual)) 351 | ptr = PATH_VI; 352 | 353 | /* [v]snprintf write at most size including \0; they'll null-terminate, even when they truncate */ 354 | /* return value >= size means result was truncated */ 355 | if (snprintf(visual, sizeof(visual), "%s %s", ptr, file) < sizeof(visual)) 356 | execl("/bin/sh", "/bin/sh", "-c", visual, NULL); 357 | printlogf(0, "couldn't exec %s", visual); 358 | exit(1); 359 | } 360 | if (pid < 0) { 361 | /* 362 | * PARENT - failure 363 | */ 364 | perror("fork"); 365 | exit(1); 366 | } 367 | waitpid(pid, NULL, 0); 368 | } 369 | 370 | -------------------------------------------------------------------------------- /crontab.markdown: -------------------------------------------------------------------------------- 1 | % CRONTAB(1) 2 | % 3 | % 20 Nov 2019 4 | 5 | NAME 6 | ==== 7 | crontab - manipulate per-user crontabs (dillon's lightweight cron daemon) 8 | 9 | SYNOPSIS 10 | ======== 11 | **crontab file [-u user]** - replace crontab from file 12 | 13 | **crontab - [-u user]** - replace crontab from stdin 14 | 15 | **crontab -l [-u user]** - list crontab for user 16 | 17 | **crontab -e [-u user]** - edit crontab for user 18 | 19 | **crontab -d [-u user]** - delete crontab for user 20 | 21 | **crontab -c dir** - specify crontab directory 22 | 23 | DESCRIPTION 24 | =========== 25 | 26 | **crontab** manipulates the per-user crontabs. 27 | 28 | Generally the -e option is used to edit your crontab. **crontab** will use 29 | the editor specified by your EDITOR or VISUAL environment 30 | variable (or /usr/bin/vi) to edit the crontab. 31 | 32 | **crontab** doesn't provide the kinds of protections that programs like **visudo** do 33 | against syntax errors and simultaneous edits. Errors won't be detected until 34 | **crond** reads the crontab file. What **crontab** does is provide a mechanism for 35 | users who may not themselves have write privileges to the crontab folder 36 | to nonetheless install or edit their crontabs. It also notifies a running crond 37 | daemon of any changes to these files. 38 | 39 | Only users who belong to the same group as the **crontab** binary will be able 40 | to install or edit crontabs. However it'll be possible for the superuser to 41 | install crontabs even for users who don't have the privileges to install them 42 | themselves. (Even for users who don't have a login shell.) Only the superuser may use 43 | the -u or -c switches to specify a different user and/or crontab directory. 44 | 45 | The superuser also has his or her own per-user crontab, saved as 46 | /var/spool/cron/crontabs/root. 47 | 48 | 49 | Unlike other cron daemons, this crond/crontab package doesn't try to do 50 | everything under the sun. It doesn't try to keep track of user's preferred 51 | shells; that would require special-casing users with no login shell. Instead, 52 | it just runs all commands using `/bin/sh`. (Commands can of course be script 53 | files written in any shell you like.) 54 | 55 | Nor does it do any special environment handling. A shell script is 56 | better-suited to doing that than a cron daemon. This cron daemon sets up only 57 | four environment variables: USER, LOGNAME, HOME, and SHELL. 58 | 59 | 60 | Our crontab format is roughly similar to that used by vixiecron. Individual 61 | fields may contain a time, a time range, a time range with a skip factor, a 62 | symbolic range for the day of week and month in year, and additional subranges 63 | delimited with commas. Blank lines in the crontab or lines that begin with a 64 | hash (#) are ignored. If you specify both a day in the month and a day of week, 65 | it will be interpreted as the Nth such day in the month. 66 | 67 | Some examples: 68 | 69 | # MIN HOUR DAY MONTH DAYOFWEEK COMMAND 70 | # run `date` at 6:10 am every day 71 | 10 6 * * * date 72 | 73 | # run every two hours at the top of the hour 74 | 0 */2 * * * date 75 | 76 | # run every two hours between 11 pm and 7 am, and again at 8 am 77 | 0 23-7/2,8 * * * date 78 | 79 | # run at 4:00 am on January 1st 80 | 0 4 1 jan * date 81 | 82 | # run every day at 11 am, appending all output to a file 83 | 0 11 * * * date >> /var/log/date-output 2>&1 84 | 85 | To request the last Monday, etc. in a month, ask for the "5th" one. This will always match the last Monday, etc., even if there are only four Mondays in the month: 86 | 87 | # run at 11 am on the first and last Mon, Tue, Wed of each month 88 | 0 11 1,5 * mon-wed date 89 | 90 | When the fourth Monday in a month is the last, it will match against both the "4th" and the "5th" (it will only run once if both are specified). 91 | 92 | The following formats are also recognized: 93 | 94 | # schedule this job only once, when crond starts up 95 | @reboot date 96 | 97 | # schedule this job whenever crond is running, and sees that at least one 98 | # hour has elapsed since it last ran 99 | @hourly ID=job1 date 100 | 101 | The formats @hourly, @daily, @weekly, @monthly, and @yearly need to update 102 | timestamp files when their jobs have been run. The timestamp files are saved as 103 | /var/spool/cron/cronstamps/user.jobname. So for all of these formats, the cron 104 | command needs a jobname, given by prefixing the command with `ID=jobname`. 105 | (This syntax was chosen to maximize the chance that our crontab files will be 106 | readable by other cron daemons as well. They might just interpret the 107 | ID=jobname as a command-line environment variable assignment.) 108 | 109 | There's also this esoteric option, whose usefulness will be explained later: 110 | 111 | # don't ever schedule this job on its own; only run it when it's triggered 112 | # as a "dependency" of another job (see below), or when the user explicitly 113 | # requests it through the "cron.update" file (see crond(8)) 114 | @noauto ID=namedjob date 115 | 116 | There's also a format available for finer-grained control of frequencies: 117 | 118 | # run whenever it's between 2-4 am, and at least one day (1d) 119 | # has elapsed since this job ran 120 | * 2-4 * * * ID=job2 FREQ=1d date 121 | 122 | # as before, but re-try every 10 minutes (10m) if my_command 123 | # exits with code 11 (EAGAIN) 124 | * 2-4 * * * ID=job3 FREQ=1d/10m my_command 125 | 126 | These formats also update timestamp files, and so also require their jobs to be assigned 127 | IDs. 128 | 129 | Notice the technique used in the second example: jobs can exit with code 11 to 130 | indicate they lacked the resources to run (for example, no network was 131 | available), and so should be tried again after a brief delay. This works for 132 | jobs using either @freq or FREQ=... formats; but the FREQ=.../10m syntax is the 133 | only way to customize the length of the delay before re-trying. 134 | 135 | Jobs can be made to "depend" on, or wait until AFTER other jobs have 136 | successfully completed. Consider the following crontab: 137 | 138 | * * * * * ID=job4 FREQ=1d first_command 139 | * * * * * ID=job5 FREQ=1h AFTER=job4/30m second_command 140 | 141 | Here, whenever job5 is up to be run, if job4 is scheduled to run within the 142 | next 30 minutes (30m), job5 will first wait for it to successfully complete. 143 | 144 | (What if job4 doesn't successfully complete? If job4 returns with exit code 145 | EAGAIN, job5 will continue to wait until job4 is retried---even if that won't 146 | be within the hour. If job4 returns with any other non-zero exit code, job5 147 | will be removed from the queue without running.) 148 | 149 | Jobs can be told to wait for multiple other jobs, as follows: 150 | 151 | 10 * * * * ID=job6 AFTER=job4/1h,job7 third_command 152 | 153 | The waiting job6 doesn't care what order job4 and job7 complete in. If job6 comes 154 | up to be re-scheduled (an hour later) while an earlier instance is still waiting, only a 155 | single instance of job6 will remain in the queue. It will have all of its 156 | "waiting flags" reset: so each of job7 and job4 (supposing again that job4 would run within the 157 | next 1h) will again have to complete before job6 will run. 158 | 159 | If a job waits on a @reboot or @noauto job, the target job being waited on will 160 | also be scheduled to run. This technique can be used to have a common job scheduled as @noauto 161 | that several other jobs depend on (and so call as a subroutine). 162 | 163 | The command portion of a cron job is run with `/bin/sh -c ...` and may 164 | therefore contain any valid Bourne shell command. A common practice is to 165 | prefix your command with **exec** to keep the process table uncluttered. It is 166 | also common to redirect job output to a file or to /dev/null. If you do not, 167 | and the command generates output on stdout or stderr, that output will be 168 | mailed to the local user whose crontab the job comes from. If you have crontabs 169 | for special users, such as uucp, who can't receive local mail, you may want to 170 | create mail aliases for them or adjust this behavior. (See crond(8) for details 171 | how to adjust it.) 172 | 173 | Whenever jobs return an exit code that's neither 0 nor 11 (EAGAIN), that event 174 | will be logged, regardless of whether any stdout or stderr is generated. The job's 175 | timestamp will also be updated, and it won't be run again until it would next 176 | be normally scheduled. Any jobs waiting on the failed job will be canceled; they 177 | won't be run until they're next scheduled. 178 | 179 | 180 | TODO 181 | ==== 182 | Ought to be able to have several crontab files for any given user, as 183 | an organizational tool. 184 | 185 | 186 | SEE ALSO 187 | ======== 188 | **crond**(8) 189 | 190 | AUTHORS 191 | ======= 192 | Matthew Dillon (dillon@apollo.backplane.com): original developer 193 | James Pryor (dubiousjim@gmail.com): current developer 194 | -------------------------------------------------------------------------------- /database.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * DATABASE.C 4 | * 5 | * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com) 6 | * Copyright 2009-2019 James Pryor 7 | * May be distributed under the GNU General Public License version 2 or any later version. 8 | */ 9 | 10 | #include "defs.h" 11 | 12 | #define FIRST_DOW (1 << 0) 13 | #define SECOND_DOW (1 << 1) 14 | #define THIRD_DOW (1 << 2) 15 | #define FOURTH_DOW (1 << 3) 16 | #define FIFTH_DOW (1 << 4) 17 | #define LAST_DOW (1 << 5) 18 | #define ALL_DOW (FIRST_DOW|SECOND_DOW|THIRD_DOW|FOURTH_DOW|FIFTH_DOW|LAST_DOW) 19 | 20 | Prototype void CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2); 21 | Prototype void SynchronizeDir(const char *dpath, const char *user_override, int initial_scan); 22 | Prototype void ReadTimestamps(const char *user); 23 | Prototype int TestJobs(time_t t1, time_t t2); 24 | Prototype int TestStartupJobs(void); 25 | Prototype int ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2); 26 | Prototype void RunJobs(void); 27 | Prototype int CheckJobs(void); 28 | 29 | void SynchronizeFile(const char *dpath, const char *fname, const char *uname); 30 | void DeleteFile(CronFile **pfile); 31 | char *ParseInterval(int *interval, char *ptr); 32 | char *ParseField(char *userName, char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr); 33 | void FixDayDow(CronLine *line); 34 | void PrintLine(CronLine *line); 35 | void PrintFile(CronFile *file, char* loc, char* fname, int line); 36 | 37 | CronFile *FileBase = NULL; 38 | 39 | const char *DowAry[] = { 40 | "sun", 41 | "mon", 42 | "tue", 43 | "wed", 44 | "thu", 45 | "fri", 46 | "sat", 47 | 48 | "Sun", 49 | "Mon", 50 | "Tue", 51 | "Wed", 52 | "Thu", 53 | "Fri", 54 | "Sat", 55 | NULL 56 | }; 57 | 58 | const char *MonAry[] = { 59 | "jan", 60 | "feb", 61 | "mar", 62 | "apr", 63 | "may", 64 | "jun", 65 | "jul", 66 | "aug", 67 | "sep", 68 | "oct", 69 | "nov", 70 | "dec", 71 | 72 | "Jan", 73 | "Feb", 74 | "Mar", 75 | "Apr", 76 | "May", 77 | "Jun", 78 | "Jul", 79 | "Aug", 80 | "Sep", 81 | "Oct", 82 | "Nov", 83 | "Dec", 84 | NULL 85 | }; 86 | 87 | const char *FreqAry[] = { 88 | "noauto", 89 | "reboot", 90 | "hourly", 91 | "daily", 92 | "weekly", 93 | "monthly", 94 | "yearly", 95 | NULL 96 | }; 97 | 98 | /* 99 | * Check the cron.update file in the specified directory. If user_override 100 | * is NULL then the files in the directory belong to the user whose name is 101 | * the file, otherwise they belong to the user_override user. 102 | */ 103 | void 104 | CheckUpdates(const char *dpath, const char *user_override, time_t t1, time_t t2) 105 | { 106 | FILE *fi; 107 | char buf[SMALL_BUFFER]; 108 | char *fname, *ptok, *job; 109 | char *path; 110 | 111 | if (!(path = concat(dpath, "/", CRONUPDATE, NULL))) { 112 | errno = ENOMEM; 113 | perror("CheckUpdates"); 114 | exit(1); 115 | } 116 | if ((fi = fopen(path, "r")) != NULL) { 117 | remove(path); 118 | printlogf(LOG_INFO, "reading %s/%s\n", dpath, CRONUPDATE); 119 | while (fgets(buf, sizeof(buf), fi) != NULL) { 120 | /* 121 | * if buf has only sep chars, return NULL and point ptok at buf's terminating 0 122 | * else return pointer to first non-sep of buf and 123 | * if there's a following sep, overwrite it to 0 and point ptok to next char 124 | * else point ptok at buf's terminating 0 125 | */ 126 | fname = strtok_r(buf, " \t\n", &ptok); 127 | 128 | if (user_override) 129 | SynchronizeFile(dpath, fname, user_override); 130 | else if (!getpwnam(fname)) 131 | printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n", dpath, fname); 132 | else if (*ptok == 0 || *ptok == '\n') { 133 | SynchronizeFile(dpath, fname, fname); 134 | ReadTimestamps(fname); 135 | } else { 136 | /* if fname is followed by whitespace, we prod any following jobs */ 137 | CronFile *file = FileBase; 138 | while (file) { 139 | if (strcmp(file->cf_UserName, fname) == 0) 140 | break; 141 | file = file->cf_Next; 142 | } 143 | if (!file) 144 | printlogf(LOG_WARNING, "unable to prod for user %s: no crontab\n", fname); 145 | else { 146 | CronLine *line; 147 | /* calling strtok(ptok...) then strtok(NULL) is equiv to calling strtok_r(NULL,..&ptok) */ 148 | while ((job = strtok(ptok, " \t\n")) != NULL) { 149 | time_t force = t2; 150 | ptok = NULL; 151 | if (*job == '!') { 152 | force = (time_t)-1; 153 | ++job; 154 | } 155 | line = file->cf_LineBase; 156 | while (line) { 157 | if (line->cl_JobName && strcmp(line->cl_JobName, job) == 0) 158 | break; 159 | line = line->cl_Next; 160 | } 161 | if (line) 162 | ArmJob(file, line, t1, force); 163 | else { 164 | printlogf(LOG_WARNING, "unable to prod for user %s: unknown job %s\n", fname, job); 165 | /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */ 166 | } 167 | } 168 | } 169 | } 170 | } 171 | fclose(fi); 172 | } 173 | free(path); 174 | } 175 | 176 | void 177 | SynchronizeDir(const char *dpath, const char *user_override, int initial_scan) 178 | { 179 | CronFile **pfile; 180 | CronFile *file; 181 | struct dirent *den; 182 | DIR *dir; 183 | char *path; 184 | 185 | if (DebugOpt) 186 | printlogf(LOG_DEBUG, "Synchronizing %s\n", dpath); 187 | 188 | /* 189 | * Delete all database CronFiles for this directory. DeleteFile() will 190 | * free *pfile and relink the *pfile pointer, or in the alternative will 191 | * mark it as deleted. 192 | */ 193 | pfile = &FileBase; 194 | while ((file = *pfile) != NULL) { 195 | if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0) { 196 | DeleteFile(pfile); 197 | } else { 198 | pfile = &file->cf_Next; 199 | } 200 | } 201 | 202 | /* 203 | * Since we are resynchronizing the entire directory, remove the 204 | * the CRONUPDATE file. 205 | */ 206 | if (!(path = concat(dpath, "/", CRONUPDATE, NULL))) { 207 | errno = ENOMEM; 208 | perror("SynchronizeDir"); 209 | exit(1); 210 | } 211 | remove(path); 212 | free(path); 213 | 214 | /* 215 | * Scan the specified directory 216 | */ 217 | if ((dir = opendir(dpath)) != NULL) { 218 | while ((den = readdir(dir)) != NULL) { 219 | if (strchr(den->d_name, '.') != NULL) 220 | continue; 221 | if (strcmp(den->d_name, CRONUPDATE) == 0) 222 | continue; 223 | if (user_override) { 224 | SynchronizeFile(dpath, den->d_name, user_override); 225 | } else if (getpwnam(den->d_name)) { 226 | SynchronizeFile(dpath, den->d_name, den->d_name); 227 | } else { 228 | printlogf(LOG_WARNING, "ignoring %s/%s (non-existent user)\n", 229 | dpath, den->d_name); 230 | } 231 | } 232 | closedir(dir); 233 | } else { 234 | if (initial_scan) 235 | printlogf(LOG_ERR, "unable to scan directory %s\n", dpath); 236 | /* softerror, do not exit the program */ 237 | } 238 | } 239 | 240 | 241 | void 242 | ReadTimestamps(const char *user) 243 | { 244 | CronFile *file; 245 | CronLine *line; 246 | FILE *fi; 247 | char buf[SMALL_BUFFER]; 248 | char *ptr; 249 | struct tm tm = {0}; 250 | time_t sec, freq; 251 | 252 | file = FileBase; 253 | while (file != NULL) { 254 | if (file->cf_Deleted == 0 && (!user || strcmp(user, file->cf_UserName) == 0)) { 255 | line = file->cf_LineBase; 256 | while (line != NULL) { 257 | if (line->cl_Timestamp) { 258 | if ((fi = fopen(line->cl_Timestamp, "r")) != NULL) { 259 | if (fgets(buf, sizeof(buf), fi) != NULL) { 260 | int fake = 0; 261 | ptr = buf; 262 | if (strncmp(buf, "after ", 6) == 0) { 263 | fake = 1; 264 | ptr += 6; 265 | } 266 | sec = (time_t)-1; 267 | ptr = strptime(ptr, CRONSTAMP_FMT, &tm); 268 | if (ptr && (*ptr == 0 || *ptr == '\n')) { 269 | /* strptime uses current seconds when seconds not specified? anyway, we don't get round minutes */ 270 | tm.tm_sec = 0; 271 | tm.tm_isdst = -1; 272 | sec = mktime(&tm); 273 | } 274 | if (sec == (time_t)-1) { 275 | printlogf(LOG_ERR, "unable to parse timestamp (user %s job %s)\n", file->cf_UserName, line->cl_JobName); 276 | /* we continue checking other timestamps in this CronFile */ 277 | } else { 278 | /* sec -= sec % 60; */ 279 | if (fake) { 280 | line->cl_NotUntil = sec; 281 | } else { 282 | line->cl_LastRan = sec; 283 | freq = (line->cl_Freq > 0) ? line->cl_Freq : line->cl_Delay; 284 | /* if (line->cl_NotUntil < line->cl_LastRan + freq) */ 285 | line->cl_NotUntil = line->cl_LastRan + freq; 286 | } 287 | } 288 | } 289 | fclose(fi); 290 | } else { 291 | int succeeded = 0; 292 | printlogf(LOG_NOTICE, "no timestamp found (user %s job %s)\n", file->cf_UserName, line->cl_JobName); 293 | /* write a fake timestamp file so our initial NotUntil doesn't keep being reset every hour when crond does a SynchronizeDir */ 294 | if ((fi = fopen(line->cl_Timestamp, "w")) != NULL) { 295 | if (strftime(buf, sizeof(buf), CRONSTAMP_FMT, localtime(&line->cl_NotUntil))) 296 | if (fputs("after ", fi) >= 0) 297 | if (fputs(buf,fi) >= 0) 298 | succeeded = 1; 299 | fclose(fi); 300 | } 301 | if (!succeeded) 302 | printlogf(LOG_WARNING, "unable to write timestamp to %s (user %s %s)\n", line->cl_Timestamp, file->cf_UserName, line->cl_Description); 303 | } 304 | } 305 | line = line->cl_Next; 306 | } 307 | } 308 | file = file->cf_Next; 309 | } 310 | } 311 | 312 | void 313 | SynchronizeFile(const char *dpath, const char *fileName, const char *userName) 314 | { 315 | CronFile **pfile; 316 | CronFile *file; 317 | int maxEntries; 318 | int maxLines; 319 | char buf[RW_BUFFER]; /* max length for crontab lines */ 320 | char *path; 321 | FILE *fi; 322 | 323 | /* 324 | * Limit entries 325 | */ 326 | if (strcmp(userName, "root") == 0) 327 | maxEntries = 65535; 328 | else 329 | maxEntries = MAXLINES; 330 | maxLines = maxEntries * 10; 331 | 332 | /* 333 | * Delete any existing copy of this CronFile 334 | */ 335 | pfile = &FileBase; 336 | while ((file = *pfile) != NULL) { 337 | if (file->cf_Deleted == 0 && strcmp(file->cf_DPath, dpath) == 0 && 338 | strcmp(file->cf_FileName, fileName) == 0 339 | ) { 340 | DeleteFile(pfile); 341 | } else { 342 | pfile = &file->cf_Next; 343 | } 344 | } 345 | 346 | if (!(path = concat(dpath, "/", fileName, NULL))) { 347 | errno = ENOMEM; 348 | perror("SynchronizeFile"); 349 | exit(1); 350 | } 351 | if ((fi = fopen(path, "r")) != NULL) { 352 | struct stat sbuf; 353 | 354 | if (fstat(fileno(fi), &sbuf) == 0 && sbuf.st_uid == DaemonUid) { 355 | CronFile *file = calloc(1, sizeof(CronFile)); 356 | CronLine **pline; 357 | time_t tnow = time(NULL); 358 | tnow -= tnow % 60; 359 | 360 | file->cf_UserName = strdup(userName); 361 | file->cf_FileName = strdup(fileName); 362 | file->cf_DPath = strdup(dpath); 363 | pline = &file->cf_LineBase; 364 | 365 | /* fgets reads at most size-1 chars until \n or EOF, then adds a\0; \n if present is stored in buf */ 366 | while (fgets(buf, sizeof(buf), fi) != NULL && --maxLines) { 367 | CronLine line; 368 | char *ptr = buf; 369 | int len; 370 | 371 | while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n') 372 | ++ptr; 373 | 374 | len = strlen(ptr); 375 | if (len && ptr[len-1] == '\n') 376 | ptr[--len] = 0; 377 | 378 | if (*ptr == 0 || *ptr == '#') 379 | continue; 380 | 381 | if (--maxEntries == 0) 382 | break; 383 | 384 | memset(&line, 0, sizeof(line)); 385 | 386 | if (DebugOpt) 387 | printlogf(LOG_DEBUG, "User %s Entry %s\n", userName, buf); 388 | 389 | if (*ptr == '@') { 390 | /* 391 | * parse @hourly, etc 392 | */ 393 | int j; 394 | line.cl_Delay = -1; 395 | ptr += 1; 396 | for (j = 0; FreqAry[j]; ++j) { 397 | if (strncmp(ptr, FreqAry[j], strlen(FreqAry[j])) == 0) { 398 | break; 399 | } 400 | } 401 | if (FreqAry[j]) { 402 | ptr += strlen(FreqAry[j]); 403 | switch(j) { 404 | case 0: 405 | /* noauto */ 406 | line.cl_Freq = -2; 407 | line.cl_Delay = 0; 408 | break; 409 | case 1: 410 | /* reboot */ 411 | line.cl_Freq = -1; 412 | line.cl_Delay = 0; 413 | break; 414 | case 2: 415 | line.cl_Freq = HOURLY_FREQ; 416 | break; 417 | case 3: 418 | line.cl_Freq = DAILY_FREQ; 419 | break; 420 | case 4: 421 | line.cl_Freq = WEEKLY_FREQ; 422 | break; 423 | case 5: 424 | line.cl_Freq = MONTHLY_FREQ; 425 | break; 426 | case 6: 427 | line.cl_Freq = YEARLY_FREQ; 428 | break; 429 | /* else line.cl_Freq will remain 0 */ 430 | } 431 | } 432 | 433 | if (!line.cl_Freq || (*ptr != ' ' && *ptr != '\t')) { 434 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, buf); 435 | continue; 436 | } 437 | 438 | if (line.cl_Delay < 0) { 439 | /* 440 | * delays on @daily, @hourly, etc are 1/20 of the frequency 441 | * so they don't all start at once 442 | * this also affects how they behave when the job returns EAGAIN 443 | */ 444 | line.cl_Delay = line.cl_Freq / 20; 445 | line.cl_Delay -= line.cl_Delay % 60; 446 | if (line.cl_Delay == 0) 447 | line.cl_Delay = 60; 448 | /* all minutes are permitted */ 449 | for (j=0; j<60; ++j) 450 | line.cl_Mins[j] = 1; 451 | for (j=0; j<24; ++j) 452 | line.cl_Hrs[j] = 1; 453 | for (j=1; j<32; ++j) 454 | /* days are numbered 1..31 */ 455 | line.cl_Days[j] = 1; 456 | for (j=0; j<12; ++j) 457 | line.cl_Mons[j] = 1; 458 | } 459 | 460 | while (*ptr == ' ' || *ptr == '\t') 461 | ++ptr; 462 | 463 | } else { 464 | /* 465 | * parse date ranges 466 | */ 467 | 468 | ptr = ParseField(file->cf_UserName, line.cl_Mins, FIELD_MINUTES, 0, 1, 469 | NULL, ptr); 470 | ptr = ParseField(file->cf_UserName, line.cl_Hrs, FIELD_HOURS, 0, 1, 471 | NULL, ptr); 472 | ptr = ParseField(file->cf_UserName, line.cl_Days, FIELD_M_DAYS, 0, 1, 473 | NULL, ptr); 474 | ptr = ParseField(file->cf_UserName, line.cl_Mons, FIELD_MONTHS, -1, 1, 475 | MonAry, ptr); 476 | ptr = ParseField(file->cf_UserName, line.cl_Dow, FIELD_W_DAYS, 0, ALL_DOW, 477 | DowAry, ptr); 478 | /* 479 | * check failure 480 | */ 481 | 482 | if (ptr == NULL) 483 | continue; 484 | 485 | /* 486 | * fix days and dow - if one is not * and the other 487 | * is *, the other is set to 0, and vise-versa 488 | */ 489 | 490 | FixDayDow(&line); 491 | } 492 | 493 | /* check for ID=... and AFTER=... and FREQ=... */ 494 | do { 495 | if (strncmp(ptr, ID_TAG, strlen(ID_TAG)) == 0) { 496 | if (line.cl_JobName) { 497 | /* only assign ID_TAG once */ 498 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr); 499 | ptr = NULL; 500 | } else { 501 | ptr += strlen(ID_TAG); 502 | /* 503 | * name = strsep(&ptr, seps): 504 | * return name = ptr, and if ptr contains sep chars, overwrite first with 0 and point ptr to next char 505 | * else set ptr=NULL 506 | */ 507 | if (!(line.cl_Description = concat("job ", strsep(&ptr, " \t"), NULL))) { 508 | errno = ENOMEM; 509 | perror("SynchronizeFile"); 510 | exit(1); 511 | } 512 | line.cl_JobName = line.cl_Description + 4; 513 | if (!ptr) 514 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, ID_TAG, line.cl_JobName); 515 | } 516 | } else if (strncmp(ptr, FREQ_TAG, strlen(FREQ_TAG)) == 0) { 517 | if (line.cl_Freq) { 518 | /* only assign FREQ_TAG once */ 519 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr); 520 | ptr = NULL; 521 | } else { 522 | char *base = ptr; 523 | ptr += strlen(FREQ_TAG); 524 | ptr = ParseInterval(&line.cl_Freq, ptr); 525 | if (ptr && *ptr == '/') 526 | ptr = ParseInterval(&line.cl_Delay, ++ptr); 527 | else 528 | line.cl_Delay = line.cl_Freq; 529 | if (!ptr) { 530 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", userName, base); 531 | } else if (*ptr != ' ' && *ptr != '\t') { 532 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s\n", userName, base); 533 | ptr = NULL; 534 | } 535 | } 536 | } else if (strncmp(ptr, WAIT_TAG, strlen(WAIT_TAG)) == 0) { 537 | if (line.cl_Waiters) { 538 | /* only assign WAIT_TAG once */ 539 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: repeated %s\n", userName, ptr); 540 | ptr = NULL; 541 | } else { 542 | short more = 1; 543 | char *name; 544 | ptr += strlen(WAIT_TAG); 545 | do { 546 | CronLine *job, **pjob; 547 | if (strcspn(ptr,",") < strcspn(ptr," \t")) 548 | name = strsep(&ptr, ","); 549 | else { 550 | more = 0; 551 | name = strsep(&ptr, " \t"); 552 | } 553 | if (!ptr || *ptr == 0) { 554 | /* unexpectedly this was the last token in buf; so abort */ 555 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: no command after %s%s\n", userName, WAIT_TAG, name); 556 | ptr = NULL; 557 | } else { 558 | int waitfor = 0; 559 | char *w, *wsave; 560 | if ((w = strchr(name, '/')) != NULL) { 561 | wsave = w++; 562 | w = ParseInterval(&waitfor, w); 563 | if (!w || *w != 0) { 564 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s%s\n", userName, WAIT_TAG, name); 565 | ptr = NULL; 566 | } else 567 | /* truncate name */ 568 | *wsave = 0; 569 | } 570 | if (ptr) { 571 | /* look for a matching CronLine */ 572 | pjob = &file->cf_LineBase; 573 | while ((job = *pjob) != NULL) { 574 | if (job->cl_JobName && strcmp(job->cl_JobName, name) == 0) { 575 | CronWaiter *waiter = malloc(sizeof(CronWaiter)); 576 | CronNotifier *notif = malloc(sizeof(CronNotifier)); 577 | waiter->cw_Flag = -1; 578 | waiter->cw_MaxWait = waitfor; 579 | waiter->cw_NotifLine = job; 580 | waiter->cw_Notifier = notif; 581 | waiter->cw_Next = line.cl_Waiters; /* add to head of line.cl_Waiters */ 582 | line.cl_Waiters = waiter; 583 | notif->cn_Waiter = waiter; 584 | notif->cn_Next = job->cl_Notifs; /* add to head of job->cl_Notifs */ 585 | job->cl_Notifs = notif; 586 | break; 587 | } else 588 | pjob = &job->cl_Next; 589 | } 590 | if (!job) { 591 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: unknown job %s\n", userName, name); 592 | /* we can continue parsing this line, we just don't install any CronWaiter for the requested job */ 593 | } 594 | } 595 | } 596 | } while (ptr && more); 597 | } 598 | } else 599 | break; 600 | if (!ptr) 601 | break; 602 | while (*ptr == ' ' || *ptr == '\t') 603 | ++ptr; 604 | } while (!line.cl_JobName || !line.cl_Waiters || !line.cl_Freq); 605 | 606 | if (line.cl_JobName && (!ptr || *line.cl_JobName == 0)) { 607 | /* we're aborting, or ID= was empty */ 608 | free(line.cl_Description); 609 | line.cl_Description = NULL; 610 | line.cl_JobName = NULL; 611 | } 612 | if (ptr && line.cl_Delay > 0 && !line.cl_JobName) { 613 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: writing timestamp requires job %s to be named\n", userName, ptr); 614 | ptr = NULL; 615 | } 616 | if (!ptr) { 617 | /* couldn't parse so we abort; free any cl_Waiters */ 618 | if (line.cl_Waiters) { 619 | CronWaiter **pwaiters, *waiters; 620 | pwaiters = &line.cl_Waiters; 621 | while ((waiters = *pwaiters) != NULL) { 622 | *pwaiters = waiters->cw_Next; 623 | /* leave the Notifier allocated but disabled */ 624 | waiters->cw_Notifier->cn_Waiter = NULL; 625 | free(waiters); 626 | } 627 | } 628 | continue; 629 | } 630 | /* now we've added any ID=... or AFTER=... */ 631 | 632 | /* 633 | * copy command string 634 | */ 635 | line.cl_Shell = strdup(ptr); 636 | 637 | if (line.cl_Delay > 0) { 638 | if (!(line.cl_Timestamp = concat(TSDir, "/", userName, ".", line.cl_JobName, NULL))) { 639 | errno = ENOMEM; 640 | perror("SynchronizeFile"); 641 | exit(1); 642 | } 643 | line.cl_NotUntil = tnow + line.cl_Delay; 644 | } 645 | 646 | if (line.cl_JobName) { 647 | if (DebugOpt) 648 | printlogf(LOG_DEBUG, " Command %s Job %s\n\n", line.cl_Shell, line.cl_JobName); 649 | } else { 650 | /* when cl_JobName is NULL, we point cl_Description to cl_Shell */ 651 | line.cl_Description = line.cl_Shell; 652 | if (DebugOpt) 653 | printlogf(LOG_DEBUG, " Command %s\n\n", line.cl_Shell); 654 | } 655 | 656 | *pline = calloc(1, sizeof(CronLine)); 657 | /* copy working CronLine to newly allocated one */ 658 | **pline = line; 659 | 660 | pline = &((*pline)->cl_Next); 661 | } 662 | 663 | *pline = NULL; 664 | 665 | file->cf_Next = FileBase; 666 | FileBase = file; 667 | 668 | if (maxLines == 0 || maxEntries == 0) 669 | printlogf(LOG_WARNING, "maximum number of lines reached for user %s\n", userName); 670 | } 671 | fclose(fi); 672 | } 673 | free(path); 674 | } 675 | 676 | char * 677 | ParseInterval(int *interval, char *ptr) 678 | { 679 | int n = 0; 680 | if (ptr && *ptr >= '0' && *ptr <= '9' && (n = strtol(ptr, &ptr, 10)) > 0) 681 | switch (*ptr) { 682 | case 'm': 683 | n *= 60; 684 | break; 685 | case 'h': 686 | n *= HOURLY_FREQ; 687 | break; 688 | case 'd': 689 | n *= DAILY_FREQ; 690 | break; 691 | case 'w': 692 | n *= WEEKLY_FREQ; 693 | break; 694 | default: 695 | n = 0; 696 | } 697 | if (n > 0) { 698 | *interval = n; 699 | return (ptr+1); 700 | } else 701 | return (NULL); 702 | } 703 | 704 | char * 705 | ParseField(char *user, char *ary, int modvalue, int offset, int onvalue, const char **names, char *ptr) 706 | { 707 | char *base = ptr; 708 | int n1 = -1; 709 | int n2 = -1; 710 | 711 | if (base == NULL) 712 | return (NULL); 713 | 714 | while (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') { 715 | int skip = 0; 716 | 717 | /* 718 | * Handle numeric digit or symbol or '*' 719 | */ 720 | 721 | if (*ptr == '*') { 722 | n1 = 0; /* everything will be filled */ 723 | n2 = modvalue - 1; 724 | skip = 1; 725 | ++ptr; 726 | } else if (*ptr >= '0' && *ptr <= '9') { 727 | if (n1 < 0) 728 | n1 = strtol(ptr, &ptr, 10) + offset; 729 | else 730 | n2 = strtol(ptr, &ptr, 10) + offset; 731 | skip = 1; 732 | } else if (names) { 733 | int i; 734 | 735 | for (i = 0; names[i]; ++i) { 736 | if (strncmp(ptr, names[i], strlen(names[i])) == 0) { 737 | break; 738 | } 739 | } 740 | if (names[i]) { 741 | ptr += strlen(names[i]); 742 | if (n1 < 0) 743 | n1 = i; 744 | else 745 | n2 = i; 746 | skip = 1; 747 | } 748 | } 749 | 750 | /* 751 | * handle optional range '-' 752 | */ 753 | 754 | if (skip == 0) { 755 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base); 756 | return(NULL); 757 | } 758 | if (*ptr == '-' && n2 < 0) { 759 | ++ptr; 760 | continue; 761 | } 762 | 763 | /* 764 | * collapse single-value ranges, handle skipmark, and fill 765 | * in the character array appropriately. 766 | */ 767 | 768 | if (n2 < 0) 769 | n2 = n1; 770 | 771 | n2 = n2 % modvalue; 772 | 773 | if (*ptr == '/') 774 | skip = strtol(ptr + 1, &ptr, 10); 775 | 776 | /* 777 | * fill array, using a failsafe is the easiest way to prevent 778 | * an endless loop 779 | */ 780 | 781 | { 782 | int s0 = 1; 783 | int failsafe = 1024; 784 | 785 | --n1; 786 | do { 787 | n1 = (n1 + 1) % modvalue; 788 | 789 | if (--s0 == 0) { 790 | ary[n1] = onvalue; 791 | s0 = skip; 792 | } 793 | } while (n1 != n2 && --failsafe); 794 | 795 | if (failsafe == 0) { 796 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base); 797 | return(NULL); 798 | } 799 | } 800 | if (*ptr != ',') 801 | break; 802 | ++ptr; 803 | n1 = -1; 804 | n2 = -1; 805 | } 806 | 807 | if (*ptr != ' ' && *ptr != '\t' && *ptr != '\n') { 808 | printlogf(LOG_WARNING, "failed parsing crontab for user %s: %s\n", user, base); 809 | return(NULL); 810 | } 811 | 812 | while (*ptr == ' ' || *ptr == '\t' || *ptr == '\n') 813 | ++ptr; 814 | 815 | if (DebugOpt) { 816 | int i; 817 | 818 | for (i = 0; i < modvalue; ++i) 819 | if (modvalue == FIELD_W_DAYS) 820 | printlogf(LOG_DEBUG, "%2x ", ary[i]); 821 | else 822 | printlogf(LOG_DEBUG, "%d", ary[i]); 823 | printlogf(LOG_DEBUG, "\n"); 824 | } 825 | 826 | return(ptr); 827 | } 828 | 829 | /* Reconcile Days of Month with Days of Week. 830 | * There are four cases to cover: 831 | * 1) DoM and DoW are both specified as *; the task may run on any day 832 | * 2) DoM is * and DoW is specific; the task runs weekly on the specified DoW(s) 833 | * 3) DoM is specific and DoW is *; the task runs on the specified DoM, regardless 834 | * of which day of the week they fall 835 | * 4) DoM is in the range [1..5] and DoW is specific; the task runs on the Nth 836 | * specified DoW. DoM > 5 means the last such DoW in that month 837 | */ 838 | void 839 | FixDayDow(CronLine *line) 840 | { 841 | unsigned short i; 842 | short DowStar = 1; 843 | short DomStar = 1; 844 | char mask = 0; 845 | 846 | for (i = 0; i < arysize(line->cl_Dow); ++i) { 847 | if (line->cl_Dow[i] == 0) { 848 | /* '*' was NOT specified in the DoW field on this CronLine */ 849 | DowStar = 0; 850 | break; 851 | } 852 | } 853 | 854 | for (i = 0; i < arysize(line->cl_Days); ++i) { 855 | if (line->cl_Days[i] == 0) { 856 | /* '*' was NOT specified in the Date field on this CronLine */ 857 | DomStar = 0; 858 | break; 859 | } 860 | } 861 | 862 | /* When cases 1, 2 or 3 there is nothing left to do */ 863 | if (DowStar || DomStar) 864 | return; 865 | 866 | /* Set individual bits within the DoW mask... */ 867 | for (i = 0; i < arysize(line->cl_Days); ++i) { 868 | if (line->cl_Days[i]) { 869 | if (i < 6) 870 | mask |= 1 << (i - 1); 871 | else 872 | mask |= LAST_DOW; 873 | } 874 | } 875 | 876 | /* and apply the mask to each DoW element */ 877 | for (i = 0; i < arysize(line->cl_Dow); ++i) { 878 | if (line->cl_Dow[i]) 879 | line->cl_Dow[i] = mask; 880 | else 881 | line->cl_Dow[i] = 0; 882 | } 883 | 884 | /* case 4 relies on the DoW value to guard the date instead of using the 885 | * cl_Days field for this purpose; so we must set each element of cl_Days 886 | * to 1 to allow the DoW bitmask test to be made 887 | */ 888 | memset(line->cl_Days, 1, sizeof(line->cl_Days)); 889 | } 890 | 891 | /* 892 | * DeleteFile() - destroy a CronFile. 893 | * 894 | * The CronFile (*pfile) is destroyed if possible, and marked cf_Deleted 895 | * if there are still active processes running on it. *pfile is relinked 896 | * on success. 897 | */ 898 | void 899 | DeleteFile(CronFile **pfile) 900 | { 901 | CronFile *file = *pfile; 902 | CronLine **pline = &file->cf_LineBase; 903 | CronLine *line; 904 | CronWaiter **pwaiters, *waiters; 905 | CronNotifier **pnotifs, *notifs; 906 | 907 | file->cf_Running = 0; 908 | file->cf_Deleted = 1; 909 | 910 | while ((line = *pline) != NULL) { 911 | if (line->cl_Pid > JOB_NONE) { 912 | file->cf_Running = 1; 913 | pline = &line->cl_Next; 914 | } else { 915 | *pline = line->cl_Next; 916 | free(line->cl_Shell); 917 | 918 | if (line->cl_JobName) 919 | /* this frees both cl_Description and cl_JobName 920 | * if cl_JobName is NULL, Description pointed to ch_Shell, which was already freed 921 | */ 922 | free(line->cl_Description); 923 | if (line->cl_Timestamp) 924 | free(line->cl_Timestamp); 925 | 926 | pnotifs = &line->cl_Notifs; 927 | while ((notifs = *pnotifs) != NULL) { 928 | *pnotifs = notifs->cn_Next; 929 | if (notifs->cn_Waiter) { 930 | notifs->cn_Waiter->cw_NotifLine = NULL; 931 | notifs->cn_Waiter->cw_Notifier = NULL; 932 | } 933 | free(notifs); 934 | } 935 | pwaiters = &line->cl_Waiters; 936 | while ((waiters = *pwaiters) != NULL) { 937 | *pwaiters = waiters->cw_Next; 938 | if (waiters->cw_Notifier) 939 | waiters->cw_Notifier->cn_Waiter = NULL; 940 | free(waiters); 941 | } 942 | 943 | free(line); 944 | } 945 | } 946 | if (file->cf_Running == 0) { 947 | *pfile = file->cf_Next; 948 | free(file->cf_DPath); 949 | free(file->cf_FileName); 950 | free(file->cf_UserName); 951 | free(file); 952 | } 953 | } 954 | 955 | 956 | /* 957 | * TestJobs() 958 | * 959 | * determine which jobs need to be run. Under normal conditions, the 960 | * period is about a minute (one scan). Worst case it will be one 961 | * hour (60 scans). 962 | */ 963 | 964 | int 965 | TestJobs(time_t t1, time_t t2) 966 | { 967 | short nJobs = 0; 968 | time_t t; 969 | CronFile *file; 970 | CronLine *line; 971 | 972 | PrintFile(FileBase, "TestJobs()", __FILE__, __LINE__); 973 | for (file = FileBase; file; file = file->cf_Next) { 974 | if (file->cf_Deleted) 975 | continue; 976 | for (line = file->cf_LineBase; line; line = line->cl_Next) { 977 | struct CronWaiter *waiter; 978 | 979 | if (line->cl_Pid == JOB_WAITING) { 980 | /* can job stop waiting? */ 981 | int ready = 1; 982 | waiter = line->cl_Waiters; 983 | while (waiter != NULL) { 984 | if (waiter->cw_Flag > 0) { 985 | /* notifier exited unsuccessfully */ 986 | ready = 2; 987 | break; 988 | } else if (waiter->cw_Flag < 0) 989 | /* still waiting, notifier hasn't run to completion */ 990 | ready = 0; 991 | waiter = waiter->cw_Next; 992 | } 993 | if (ready == 2) { 994 | if (DebugOpt) 995 | printlogf(LOG_DEBUG, "cancelled waiting: user %s %s\n", file->cf_UserName, line->cl_Description); 996 | line->cl_Pid = JOB_NONE; 997 | } else if (ready) { 998 | if (DebugOpt) 999 | printlogf(LOG_DEBUG, "finished waiting: user %s %s\n", file->cf_UserName, line->cl_Description); 1000 | nJobs += ArmJob(file, line, 0, -1); 1001 | /* 1002 | if (line->cl_NotUntil) 1003 | line->cl_NotUntil = t2; 1004 | */ 1005 | } 1006 | } 1007 | } 1008 | } 1009 | 1010 | /* 1011 | * Find jobs > t1 and <= t2 1012 | */ 1013 | 1014 | for (t = t1 - t1 % 60; t <= t2; t += 60) { 1015 | if (t > t1) { 1016 | struct tm *tp = localtime(&t); 1017 | 1018 | char n_wday = 1 << ((tp->tm_mday - 1) / 7); 1019 | if (n_wday >= FOURTH_DOW) { 1020 | struct tm tnext = *tp; 1021 | tnext.tm_mday += 7; 1022 | if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon) 1023 | n_wday |= LAST_DOW; /* last dow in month is always recognized as 6th bit */ 1024 | } 1025 | 1026 | for (file = FileBase; file; file = file->cf_Next) { 1027 | if (file->cf_Deleted) 1028 | continue; 1029 | for (line = file->cf_LineBase; line; line = line->cl_Next) { 1030 | if ((line->cl_Pid == JOB_WAITING || line->cl_Pid == JOB_NONE) && (line->cl_Freq == 0 || (line->cl_Freq > 0 && t2 >= line->cl_NotUntil))) { 1031 | /* (re)schedule job? */ 1032 | if (line->cl_Mins[tp->tm_min] && 1033 | line->cl_Hrs[tp->tm_hour] && 1034 | (line->cl_Days[tp->tm_mday] && n_wday & line->cl_Dow[tp->tm_wday]) 1035 | ) { 1036 | if (line->cl_NotUntil) 1037 | line->cl_NotUntil = t2 - t2 % 60 + line->cl_Delay; /* save what minute this job was scheduled/started waiting, plus cl_Delay */ 1038 | nJobs += ArmJob(file, line, t1, t2); 1039 | } 1040 | } 1041 | } 1042 | } 1043 | } 1044 | } 1045 | return(nJobs); 1046 | } 1047 | 1048 | /* 1049 | * ArmJob: if t2 is (time_t)-1, we force-schedule the job without any waiting 1050 | * else it will wait on any of its declared notifiers who will run <= t2 + cw_MaxWait 1051 | */ 1052 | 1053 | int 1054 | ArmJob(CronFile *file, CronLine *line, time_t t1, time_t t2) 1055 | { 1056 | struct CronWaiter *waiter; 1057 | if (line->cl_Pid > JOB_NONE) { 1058 | printlogf(LOG_NOTICE, "process already running (%d): user %s %s\n", 1059 | line->cl_Pid, 1060 | file->cf_UserName, 1061 | line->cl_Description 1062 | ); 1063 | } else if (t2 == -1 && line->cl_Pid != JOB_ARMED) { 1064 | line->cl_Pid = JOB_ARMED; 1065 | file->cf_Ready = 1; 1066 | return 1; 1067 | } else if (line->cl_Pid == JOB_NONE) { 1068 | /* arming a waiting job (cl_Pid == -2) without forcing has no effect */ 1069 | line->cl_Pid = JOB_ARMED; 1070 | /* if we have any waiters, zero them and arm cl_Pid=-2 */ 1071 | waiter = line->cl_Waiters; 1072 | while (waiter != NULL) { 1073 | /* check if notifier will run <= t2 + cw_Max_Wait? */ 1074 | if (!waiter->cw_NotifLine) 1075 | /* notifier deleted */ 1076 | waiter->cw_Flag = 0; 1077 | else if (waiter->cw_NotifLine->cl_Pid != JOB_NONE) { 1078 | /* if notifier is armed, or waiting, or running, we wait for it */ 1079 | waiter->cw_Flag = -1; 1080 | line->cl_Pid = JOB_WAITING; 1081 | } else if (waiter->cw_NotifLine->cl_Freq < 0) { 1082 | /* arm any @noauto or @reboot jobs we're waiting on */ 1083 | ArmJob(file, waiter->cw_NotifLine, t1, t2); 1084 | waiter->cw_Flag = -1; 1085 | line->cl_Pid = JOB_WAITING; 1086 | } else { 1087 | time_t t; 1088 | if (waiter->cw_MaxWait == 0) 1089 | /* when no MaxWait interval specified, we always wait */ 1090 | waiter->cw_Flag = -1; 1091 | else if (waiter->cw_NotifLine->cl_Freq == 0 || (waiter->cw_NotifLine->cl_Freq > 0 && t2 + waiter->cw_MaxWait >= waiter->cw_NotifLine->cl_NotUntil)) { 1092 | /* default is don't wait */ 1093 | waiter->cw_Flag = 0; 1094 | for (t = t1 - t1 % 60; t <= t2; t += 60) { 1095 | if (t > t1) { 1096 | struct tm *tp = localtime(&t); 1097 | 1098 | char n_wday = 1 << ((tp->tm_mday - 1) / 7); 1099 | if (n_wday >= FOURTH_DOW) { 1100 | struct tm tnext = *tp; 1101 | tnext.tm_mday += 7; 1102 | if (mktime(&tnext) != (time_t)-1 && tnext.tm_mon != tp->tm_mon) 1103 | n_wday |= LAST_DOW; /* last dow in month is always recognized as 6th */ 1104 | } 1105 | if (line->cl_Mins[tp->tm_min] && 1106 | line->cl_Hrs[tp->tm_hour] && 1107 | (line->cl_Days[tp->tm_mday] && n_wday & line->cl_Dow[tp->tm_wday]) 1108 | ) { 1109 | /* notifier will run soon enough, we wait for it */ 1110 | waiter->cw_Flag = -1; 1111 | line->cl_Pid = JOB_WAITING; 1112 | break; 1113 | } 1114 | } 1115 | } 1116 | } 1117 | } 1118 | waiter = waiter->cw_Next; 1119 | } 1120 | if (line->cl_Pid == JOB_ARMED) { 1121 | /* job is ready to run */ 1122 | file->cf_Ready = 1; 1123 | if (DebugOpt) 1124 | printlogf(LOG_DEBUG, "scheduled: user %s %s\n", 1125 | file->cf_UserName, 1126 | line->cl_Description 1127 | ); 1128 | return 1; 1129 | } else if (DebugOpt) 1130 | printlogf(LOG_DEBUG, "waiting: user %s %s\n", 1131 | file->cf_UserName, 1132 | line->cl_Description 1133 | ); 1134 | } 1135 | return 0; 1136 | } 1137 | 1138 | int 1139 | TestStartupJobs(void) 1140 | { 1141 | short nJobs = 0; 1142 | time_t t1 = time(NULL); 1143 | CronFile *file; 1144 | CronLine *line; 1145 | 1146 | t1 = t1 - t1 % 60 + 60; 1147 | 1148 | for (file = FileBase; file; file = file->cf_Next) { 1149 | if (DebugOpt) 1150 | printlogf(LOG_DEBUG, "TestStartup for FILE %s/%s USER %s:\n", 1151 | file->cf_DPath, file->cf_FileName, file->cf_UserName); 1152 | for (line = file->cf_LineBase; line; line = line->cl_Next) { 1153 | struct CronWaiter *waiter; 1154 | if (DebugOpt) { 1155 | if (line->cl_JobName) 1156 | printlogf(LOG_DEBUG, " LINE %s JOB %s\n", line->cl_Shell, line->cl_JobName); 1157 | else 1158 | printlogf(LOG_DEBUG, " LINE %s\n", line->cl_Shell); 1159 | } 1160 | 1161 | if (line->cl_Freq == -1) { 1162 | /* freq is @reboot */ 1163 | 1164 | line->cl_Pid = JOB_ARMED; 1165 | /* if we have any waiters, reset them and arm Pid = -2 */ 1166 | waiter = line->cl_Waiters; 1167 | while (waiter != NULL) { 1168 | waiter->cw_Flag = -1; 1169 | line->cl_Pid = JOB_WAITING; 1170 | /* we only arm @noauto jobs we're waiting on, not other @reboot jobs */ 1171 | if (waiter->cw_NotifLine && waiter->cw_NotifLine->cl_Freq == -2) 1172 | ArmJob(file, waiter->cw_NotifLine, t1, t1+60); 1173 | waiter = waiter->cw_Next; 1174 | } 1175 | if (line->cl_Pid == JOB_ARMED) { 1176 | /* job is ready to run */ 1177 | file->cf_Ready = 1; 1178 | ++nJobs; 1179 | if (DebugOpt) 1180 | printlogf(LOG_DEBUG, " scheduled: %s\n", line->cl_Description); 1181 | } else if (DebugOpt) 1182 | printlogf(LOG_DEBUG, " waiting: %s\n", line->cl_Description); 1183 | 1184 | } 1185 | 1186 | } /* for line */ 1187 | } 1188 | return(nJobs); 1189 | } 1190 | 1191 | void 1192 | RunJobs(void) 1193 | { 1194 | CronFile *file; 1195 | CronLine *line; 1196 | 1197 | for (file = FileBase; file; file = file->cf_Next) { 1198 | if (file->cf_Ready) { 1199 | file->cf_Ready = 0; 1200 | 1201 | for (line = file->cf_LineBase; line; line = line->cl_Next) { 1202 | if (line->cl_Pid == JOB_ARMED) { 1203 | 1204 | RunJob(file, line); 1205 | 1206 | printlogf(LOG_INFO, "FILE %s/%s USER %s PID %3d %s\n", 1207 | file->cf_DPath, 1208 | file->cf_FileName, 1209 | file->cf_UserName, 1210 | line->cl_Pid, 1211 | line->cl_Description 1212 | ); 1213 | if (line->cl_Pid < JOB_NONE) 1214 | /* QUESTION how could this happen? RunJob will leave cl_Pid set to 0 or the actual pid */ 1215 | file->cf_Ready = 1; 1216 | else if (line->cl_Pid > JOB_NONE) 1217 | file->cf_Running = 1; 1218 | } 1219 | } 1220 | } 1221 | } 1222 | } 1223 | 1224 | /* 1225 | * CheckJobs() - check for job completion 1226 | * 1227 | * Check for job completion, return number of CronFiles still running after 1228 | * all done. 1229 | */ 1230 | 1231 | int 1232 | CheckJobs(void) 1233 | { 1234 | CronFile *file; 1235 | CronLine *line; 1236 | int nStillRunning = 0; 1237 | 1238 | for (file = FileBase; file; file = file->cf_Next) { 1239 | if (file->cf_Running) { 1240 | file->cf_Running = 0; 1241 | 1242 | for (line = file->cf_LineBase; line; line = line->cl_Next) { 1243 | if (line->cl_Pid > JOB_NONE) { 1244 | int status; 1245 | int r = waitpid(line->cl_Pid, &status, WNOHANG); 1246 | 1247 | /* waitpid returns -1 for error, 0 if cl_Pid still running, cl_Pid if it's dead */ 1248 | 1249 | if (r < 0 || r == line->cl_Pid) { 1250 | if (r > 0 && WIFEXITED(status)) 1251 | status = WEXITSTATUS(status); 1252 | else 1253 | status = 1; 1254 | EndJob(file, line, status); 1255 | 1256 | } else if (r == 0) { 1257 | file->cf_Running = 1; 1258 | } 1259 | } 1260 | } 1261 | nStillRunning += file->cf_Running; 1262 | } 1263 | /* For the purposes of this check, increase the "still running" counter if a file has lines that are waiting */ 1264 | if (file->cf_Running == 0) { 1265 | for (line = file->cf_LineBase; line; line = line->cl_Next) { 1266 | if (line->cl_Pid == JOB_WAITING) { 1267 | nStillRunning += 1; 1268 | break; 1269 | } 1270 | } 1271 | } 1272 | } 1273 | return(nStillRunning); 1274 | } 1275 | 1276 | void 1277 | PrintLine(CronLine *line) 1278 | { 1279 | int i; 1280 | if (!line) 1281 | return; 1282 | 1283 | printlogf(LOG_DEBUG, "CronLine:\n------------\n"); 1284 | printlogf(LOG_DEBUG, " Command: %s\n", line->cl_Shell); 1285 | //printlogf(LOG_DEBUG, " Desc: %s\n", line->cl_Description); 1286 | printlogf(LOG_DEBUG, " Freq: %s\n", (line->cl_Freq ? 1287 | (line->cl_Freq == -1 ? "(noauto)" : "(startup") : "(use arrays)")); 1288 | printlogf(LOG_DEBUG, " PID: %d\n", line->cl_Pid); 1289 | 1290 | printlogf(LOG_DEBUG, " Mins: "); 1291 | for (i = 0; i < 60; ++i) 1292 | printlogf(LOG_DEBUG, "%d", line->cl_Mins[i]); 1293 | 1294 | printlogf(LOG_DEBUG, "\n Hrs: "); 1295 | for (i = 0; i < 24; ++i) 1296 | printlogf(LOG_DEBUG, "%d", line->cl_Hrs[i]); 1297 | 1298 | printlogf(LOG_DEBUG, "\n Days: "); 1299 | for (i = 0; i < 32; ++i) 1300 | printlogf(LOG_DEBUG, "%d", line->cl_Days[i]); 1301 | 1302 | printlogf(LOG_DEBUG, "\n Mons: "); 1303 | for (i = 0; i < 12; ++i) 1304 | printlogf(LOG_DEBUG, "%d", line->cl_Mons[i]); 1305 | 1306 | printlogf(LOG_DEBUG, "\n Dow: "); 1307 | for (i = 0; i < 7; ++i) 1308 | printlogf(LOG_DEBUG, "%02x ", line->cl_Dow[i]); 1309 | printlogf(LOG_DEBUG, "\n\n"); 1310 | } 1311 | 1312 | void 1313 | PrintFile(CronFile *file, char* loc, char* fname, int line) 1314 | { 1315 | CronFile *f; 1316 | CronLine *l; 1317 | 1318 | printlogf(LOG_DEBUG, "%s %s:%d\n", loc, fname, line); 1319 | 1320 | if (!file) 1321 | return; 1322 | 1323 | f = file; 1324 | while (f) { 1325 | 1326 | if (strncmp(file->cf_UserName, "root", 4)) { 1327 | printlogf(LOG_DEBUG, "FILE %s/%s USER %s\n=============================\n", 1328 | file->cf_DPath, 1329 | file->cf_FileName, 1330 | file->cf_UserName); 1331 | l = f->cf_LineBase; 1332 | 1333 | while (l) { 1334 | PrintLine(l); 1335 | l = l->cl_Next; 1336 | } 1337 | } 1338 | f = f->cf_Next; 1339 | } 1340 | 1341 | } 1342 | -------------------------------------------------------------------------------- /defs.h: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * DEFS.H 4 | * 5 | * Copyright 1994-1998 Matthew Dillon (dillon@backplane.com) 6 | * Copyright 2009-2019 James Pryor 7 | * May be distributed under the GNU General Public License version 2 or any later version. 8 | */ 9 | 10 | /* 11 | * portability issues 12 | * 0. gcc defaults to _BSD_SOURCE and _POSIX_SOURCE 13 | * 1. need _POSIX_SOURCE or _XOPEN_SOURCE for getopt, fileno, sigaction 14 | * 2. need _XOPEN_SOURCE for strptime 15 | * 3. need _BSD_SOURCE for setenv, mk{d,s}temp, [v]snprintf, initgroups, strsep, strdup, setre{u,g}id, gethostname, perror 16 | * 4. use concat.c instead of requiring asprintf / _GNU_SOURCE 17 | */ 18 | 19 | #define _XOPEN_SOURCE 1 20 | #define _DEFAULT_SOURCE 1 21 | #define _BSD_SOURCE 1 22 | 23 | #include 24 | #include 25 | #include 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #ifndef S_SPLINT_S 35 | #include 36 | #endif 37 | #include 38 | #include 39 | #include 40 | #include 41 | #include 42 | #include 43 | 44 | #include 45 | #include 46 | #include 47 | 48 | #define Prototype extern 49 | #define arysize(ary) (sizeof(ary)/sizeof((ary)[0])) 50 | 51 | #ifndef SCRONTABS 52 | #define SCRONTABS "/etc/cron.d" 53 | #endif 54 | #ifndef CRONTABS 55 | #define CRONTABS "/var/spool/cron/crontabs" 56 | #endif 57 | #ifndef CRONSTAMPS 58 | #define CRONSTAMPS "/var/spool/cron/cronstamps" 59 | #endif 60 | #ifndef LOG_IDENT 61 | #define LOG_IDENT "crond" 62 | #endif 63 | #ifndef TIMESTAMP_FMT 64 | #define TIMESTAMP_FMT "%b %e %H:%M:%S" 65 | #endif 66 | 67 | #ifndef LOG_LEVEL 68 | #define LOG_LEVEL LOG_NOTICE 69 | #endif 70 | #ifndef CRONSTAMP_FMT 71 | #define CRONSTAMP_FMT "%Y-%m-%d %H:%M" 72 | #endif 73 | #ifndef CRONUPDATE 74 | #define CRONUPDATE "cron.update" 75 | #endif 76 | #ifndef TMPDIR 77 | #define TMPDIR "/tmp" 78 | #endif 79 | 80 | #ifndef SENDMAIL 81 | #define SENDMAIL "/usr/sbin/sendmail" 82 | #endif 83 | #ifndef SENDMAIL_ARGS 84 | #define SENDMAIL_ARGS "-t", "-oem", "-i" 85 | #endif 86 | #ifndef PATH_VI 87 | #define PATH_VI "/usr/bin/vi" /* location of vi */ 88 | #endif 89 | 90 | #ifndef ID_TAG 91 | #define ID_TAG "ID=" 92 | #endif 93 | #ifndef WAIT_TAG 94 | #define WAIT_TAG "AFTER=" 95 | #endif 96 | #ifndef FREQ_TAG 97 | #define FREQ_TAG "FREQ=" 98 | #endif 99 | 100 | #define HOURLY_FREQ 60 * 60 101 | #define DAILY_FREQ 24 * HOURLY_FREQ 102 | #define WEEKLY_FREQ 7 * DAILY_FREQ 103 | #define MONTHLY_FREQ 30 * DAILY_FREQ 104 | #define YEARLY_FREQ 365 * DAILY_FREQ 105 | 106 | #define FIELD_MINUTES 60 107 | #define FIELD_HOURS 24 108 | #define FIELD_M_DAYS 32 109 | #define FIELD_MONTHS 12 110 | #define FIELD_W_DAYS 7 111 | 112 | #define JOB_NONE 0 113 | #define JOB_ARMED -1 114 | #define JOB_WAITING -2 115 | 116 | #define LOGHEADER TIMESTAMP_FMT " %%s " LOG_IDENT ": " 117 | #define LOCALE_LOGHEADER "%c %%s " LOG_IDENT ": " 118 | 119 | /* Limits */ 120 | #define MAXOPEN 256 /* close fds < this limit */ 121 | #define MAXLINES 256 /* max lines in non-root crontabs */ 122 | #define SMALL_BUFFER 256 123 | #define RW_BUFFER 1024 124 | #define LOG_BUFFER 2048 /* max size of log line */ 125 | 126 | typedef struct CronFile { 127 | struct CronFile *cf_Next; 128 | struct CronLine *cf_LineBase; 129 | char *cf_DPath; /* Directory path to cronfile */ 130 | char *cf_FileName; /* Name of cronfile */ 131 | char *cf_UserName; /* username to execute jobs as */ 132 | int cf_Ready; /* bool: one or more jobs ready */ 133 | int cf_Running; /* bool: one or more jobs running */ 134 | int cf_Deleted; /* marked for deletion, ignore */ 135 | } CronFile; 136 | 137 | typedef struct CronLine { 138 | struct CronLine *cl_Next; 139 | char *cl_Shell; /* shell command */ 140 | char *cl_Description; /* either "" or "job " */ 141 | char *cl_JobName; /* job name, if any */ 142 | char *cl_Timestamp; /* path to timestamp file, if cl_Freq defined */ 143 | struct CronWaiter *cl_Waiters; 144 | struct CronNotifier *cl_Notifs; 145 | int cl_Freq; /* 0 (use arrays), minutes, -1 (noauto), -2 (startup) */ 146 | int cl_Delay; /* defaults to cl_Freq or hourly */ 147 | time_t cl_LastRan; 148 | time_t cl_NotUntil; 149 | int cl_Pid; /* running pid, 0, or armed (-1), or waiting (-2) */ 150 | int cl_MailFlag; /* running pid is for mail */ 151 | int cl_MailPos; /* 'empty file' size */ 152 | char cl_Mins[FIELD_MINUTES]; /* 0-59 */ 153 | char cl_Hrs[FIELD_HOURS]; /* 0-23 */ 154 | char cl_Days[FIELD_M_DAYS]; /* 1-31 */ 155 | char cl_Mons[FIELD_MONTHS]; /* 0-11 */ 156 | char cl_Dow[FIELD_W_DAYS]; /* 0-6, beginning sunday */ 157 | } CronLine; 158 | 159 | typedef struct CronWaiter { 160 | struct CronWaiter *cw_Next; 161 | struct CronNotifier *cw_Notifier; 162 | struct CronLine *cw_NotifLine; 163 | short cw_Flag; 164 | int cw_MaxWait; 165 | } CronWaiter; 166 | 167 | typedef struct CronNotifier { 168 | struct CronNotifier *cn_Next; 169 | struct CronWaiter *cn_Waiter; 170 | } CronNotifier; 171 | 172 | #include "protos.h" 173 | 174 | -------------------------------------------------------------------------------- /extra/crond.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Parameters to be passed to crond 3 | # 4 | CROND_ARGS="-S -l info" 5 | -------------------------------------------------------------------------------- /extra/crond.logrotate: -------------------------------------------------------------------------------- 1 | /var/log/crond.log { 2 | sharedscripts 3 | copytruncate 4 | missingok 5 | postrotate 6 | kill -HUP `cat /var/run/crond.pid` 7 | endscript 8 | } 9 | -------------------------------------------------------------------------------- /extra/crond.rc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | . /etc/rc.conf 4 | . /etc/rc.d/functions 5 | . /etc/conf.d/crond 6 | 7 | PID=$(pidof -o %PPID /usr/sbin/crond) 8 | case $1 in 9 | start) 10 | stat_busy "Starting Cron Daemon" 11 | 12 | # defaults to using syslog, and sendmail-ing cron output to local user 13 | # to mail output to remote address instead, add "-m user@host" 14 | # to CROND_ARGS in /etc/conf.d/crond 15 | if [[ -z $PID ]] && env -i PATH="/sbin:/usr/sbin:/bin:/usr/bin" /usr/sbin/crond $CROND_ARGS; then 16 | 17 | PID=$(pidof -o %PPID /usr/sbin/crond) 18 | echo "$PID" > /var/run/crond.pid 19 | add_daemon crond 20 | stat_done 21 | else 22 | stat_fail 23 | exit 1 24 | fi 25 | ;; 26 | 27 | stop) 28 | stat_busy "Stopping Cron Daemon" 29 | if [[ ! -z $PID ]] && kill "$PID" &>/dev/null; then 30 | rm_daemon crond 31 | stat_done 32 | else 33 | stat_fail 34 | exit 1 35 | fi 36 | ;; 37 | 38 | restart) 39 | $0 stop 40 | $0 start 41 | ;; 42 | 43 | *) 44 | echo "Usage: $0 {start|stop|restart}" >&2 45 | exit 1 46 | 47 | esac 48 | -------------------------------------------------------------------------------- /extra/crond.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Cron Daemon 3 | After=syslog.target 4 | 5 | [Service] 6 | ExecStart=/usr/sbin/crond -S -l info 7 | Type=forking 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | 12 | -------------------------------------------------------------------------------- /extra/crontab.vim: -------------------------------------------------------------------------------- 1 | au BufEnter crontab.* setl backupcopy=yes 2 | -------------------------------------------------------------------------------- /extra/prune-cronstamps: -------------------------------------------------------------------------------- 1 | # /etc/cron.d/prune-cronstamps 2 | 3 | # Prunes any files in /var/spool/cron/cronstamps that haven't been used in ninety 4 | # days. We check that both mtime and atime are greater than this: 5 | # 6 | # atime because the cronstamp may be in use (crond is reading it) but the 7 | # job keeps failing. So the cronstamp hasn't yet been updated. 8 | # 9 | # mtime because the volume the cronstamp is located on may be mounted noatime 10 | # and so its atime won't be updated. At least its mtime will be updated when 11 | # it's modified. 12 | 13 | @weekly ID=prune-cronstamps find /var/spool/cron/cronstamps -type f -mtime +90 -atime +90 -delete 14 | 15 | -------------------------------------------------------------------------------- /extra/root.crontab: -------------------------------------------------------------------------------- 1 | # root crontab 2 | # DO NOT EDIT THIS FILE MANUALLY! USE crontab -e INSTEAD 3 | 4 | # man 1 crontab for acceptable formats: 5 | # 6 | # <@freq> 7 | 8 | # SYSTEM DAILY/WEEKLY/... FOLDERS 9 | @hourly ID=sys-hourly /usr/sbin/run-cron /etc/cron.hourly 10 | @daily ID=sys-daily /usr/sbin/run-cron /etc/cron.daily 11 | @weekly ID=sys-weekly /usr/sbin/run-cron /etc/cron.weekly 12 | @monthly ID=sys-monthly /usr/sbin/run-cron /etc/cron.monthly 13 | 14 | -------------------------------------------------------------------------------- /extra/run-cron: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # this is a bare-bones alternative to Debian's run-parts 3 | 4 | if ! [ -d "$1" ]; then 5 | echo "Usage: $0 crondir" >&2 6 | exit 1 7 | fi 8 | 9 | for cronfile in "$1"/* ; do 10 | if [ -x "$cronfile" ]; then 11 | "$cronfile" 12 | fi 13 | done 14 | unset cronfile 15 | -------------------------------------------------------------------------------- /job.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * JOB.C 4 | * 5 | * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com) 6 | * Copyright 2009-2019 James Pryor 7 | * May be distributed under the GNU General Public License version 2 or any later version. 8 | */ 9 | 10 | #include "defs.h" 11 | 12 | Prototype void RunJob(CronFile *file, CronLine *line); 13 | Prototype void EndJob(CronFile *file, CronLine *line, int exit_status); 14 | 15 | Prototype const char *SendMail; 16 | 17 | void 18 | RunJob(CronFile *file, CronLine *line) 19 | { 20 | char mailFile[SMALL_BUFFER]; 21 | int mailFd; 22 | const char *value = Mailto; 23 | 24 | line->cl_Pid = 0; 25 | line->cl_MailFlag = 0; 26 | 27 | /* 28 | * try to open mail output file - owner root so nobody can screw with it. 29 | */ 30 | 31 | snprintf(mailFile, sizeof(mailFile), TempFileFmt, 32 | file->cf_UserName, (int)getpid()); 33 | 34 | if ((mailFd = open(mailFile, O_CREAT|O_TRUNC|O_WRONLY|O_EXCL|O_APPEND, 0600)) >= 0) { 35 | /* success: write headers to mailFile */ 36 | line->cl_MailFlag = 1; 37 | /* if we didn't specify a -m Mailto, use the local user */ 38 | if (!value) 39 | value = file->cf_UserName; 40 | fdprintf(mailFd, "To: %s\nSubject: cron for user %s %s\n\n", 41 | value, 42 | file->cf_UserName, 43 | line->cl_Description 44 | ); 45 | /* remember mailFile's size */ 46 | line->cl_MailPos = lseek(mailFd, 0, 1); 47 | } 48 | /* 49 | * else no mailFd, we complain later and don't check job output 50 | * but we still run the job if we can 51 | */ 52 | 53 | 54 | /* 55 | * Fork as the user in question and run program 56 | */ 57 | 58 | if ((line->cl_Pid = fork()) == 0) { 59 | /* 60 | * CHILD, FORK OK, PRE-EXEC 61 | * 62 | * Change running state to the user in question 63 | */ 64 | 65 | if (ChangeUser(file->cf_UserName, TempDir) < 0) { 66 | printlogf(LOG_ERR, "unable to ChangeUser (user %s %s)\n", 67 | file->cf_UserName, 68 | line->cl_Description 69 | ); 70 | exit(0); 71 | } 72 | 73 | /* from this point we are unpriviledged */ 74 | 75 | if (DebugOpt) 76 | printlogf(LOG_DEBUG, "child running: %s\n", line->cl_Description); 77 | 78 | /* 79 | * Inside child, we copy our fd 2 (which may be /dev/null) into 80 | * an open-until-exec fd 8 81 | */ 82 | dup2(2, 8); 83 | fcntl(8, F_SETFD, FD_CLOEXEC); 84 | fclose(stderr); 85 | 86 | if (mailFd >= 0) { 87 | /* stdin is already /dev/null, setup stdout and stderr > mailFile */ 88 | dup2(mailFd, 1); 89 | dup2(mailFd, 2); 90 | close(mailFd); 91 | } else { 92 | /* complain about no mailFd to log (now associated with fd 8) */ 93 | fdprintlogf(LOG_WARNING, 8, "unable to create mail file %s: cron output for user %s %s to /dev/null\n", 94 | mailFile, 95 | file->cf_UserName, 96 | line->cl_Description 97 | ); 98 | /* stderr > /dev/null */ 99 | dup2(1, 2); 100 | } 101 | 102 | /* 103 | * Start a new process group, so that children still in the crond's process group 104 | * are all mailjobs. 105 | */ 106 | setpgid(0, 0); 107 | 108 | execl("/bin/sh", "/bin/sh", "-c", line->cl_Shell, NULL); 109 | /* 110 | * CHILD FAILED TO EXEC CRONJOB 111 | * 112 | * Complain to our log (now associated with fd 8) 113 | */ 114 | fdprintlogf(LOG_ERR, 8, "unable to exec (user %s cmd /bin/sh -c %s)\n", 115 | file->cf_UserName, 116 | line->cl_Shell 117 | ); 118 | /* 119 | * Also complain to stdout, which will be either the mailFile or /dev/null 120 | */ 121 | fdprintf(1, "unable to exec: /bin/sh -c %s\n", line->cl_Shell); 122 | exit(0); 123 | 124 | } else if (line->cl_Pid < 0) { 125 | /* 126 | * PARENT, FORK FAILED 127 | * 128 | * Complain to log (with regular fd 2) 129 | */ 130 | printlogf(LOG_ERR, "unable to fork (user %s %s)\n", 131 | file->cf_UserName, 132 | line->cl_Description 133 | ); 134 | line->cl_Pid = 0; 135 | remove(mailFile); 136 | 137 | } else { 138 | /* 139 | * PARENT, FORK SUCCESS 140 | * 141 | * rename mail-file based on pid of child process 142 | */ 143 | char mailFile2[SMALL_BUFFER]; 144 | 145 | snprintf(mailFile2, sizeof(mailFile2), TempFileFmt, 146 | file->cf_UserName, line->cl_Pid); 147 | rename(mailFile, mailFile2); 148 | } 149 | 150 | /* 151 | * Close the mail file descriptor.. we can't just leave it open in 152 | * a structure, closing it later, because we might run out of descriptors 153 | */ 154 | 155 | if (mailFd >= 0) 156 | close(mailFd); 157 | } 158 | 159 | /* 160 | * EndJob - called when main job terminates 161 | */ 162 | 163 | void 164 | EndJob(CronFile *file, CronLine *line, int exit_status) 165 | { 166 | int mailFd; 167 | char mailFile[SMALL_BUFFER]; 168 | struct stat sbuf; 169 | struct CronNotifier *notif; 170 | 171 | if (line->cl_Pid <= 0) { 172 | /* 173 | * No job. This should never happen. 174 | */ 175 | line->cl_Pid = 0; 176 | return; 177 | } 178 | 179 | 180 | /* 181 | * check return status 182 | */ 183 | if (line->cl_Delay > 0) { 184 | if (exit_status == EAGAIN) { 185 | /* 186 | * returned EAGAIN, wait cl_Delay then retry 187 | * we base off the time the job was scheduled/started waiting, not the time it finished 188 | * 189 | * line->cl_NotUntil = time(NULL) + line->cl_Delay; // use this to base off time finished 190 | * line->cl_NotUntil += line->cl_Delay; // already applied 191 | */ 192 | } else { 193 | /* 194 | * process finished without returning EAGAIN (it may have returned some other error) 195 | * mark as having run and update timestamp 196 | */ 197 | FILE *fi; 198 | char buf[SMALL_BUFFER]; 199 | int succeeded = 0; 200 | /* 201 | * we base off the time the job was scheduled/started waiting, not the time it finished 202 | * 203 | * line->cl_LastRan = time(NULL); // use this to base off time finished 204 | */ 205 | line->cl_LastRan = line->cl_NotUntil - line->cl_Delay; 206 | if ((fi = fopen(line->cl_Timestamp, "w")) != NULL) { 207 | if (strftime(buf, sizeof(buf), CRONSTAMP_FMT, localtime(&line->cl_LastRan))) 208 | if (fputs(buf, fi) >= 0) 209 | succeeded = 1; 210 | fclose(fi); 211 | } 212 | if (!succeeded) 213 | printlogf(LOG_WARNING, "unable to write timestamp to %s (user %s %s)\n", line->cl_Timestamp, file->cf_UserName, line->cl_Description); 214 | line->cl_NotUntil = line->cl_LastRan; 215 | line->cl_NotUntil += (line->cl_Freq > 0) ? line->cl_Freq : line->cl_Delay; 216 | } 217 | } 218 | 219 | if (exit_status != EAGAIN) { 220 | /* 221 | * notify any waiters 222 | */ 223 | notif = line->cl_Notifs; 224 | while (notif) { 225 | if (notif->cn_Waiter) { 226 | notif->cn_Waiter->cw_Flag = exit_status; 227 | } 228 | notif = notif->cn_Next; 229 | } 230 | 231 | if (exit_status) { 232 | /* 233 | * log non-zero exit_status 234 | */ 235 | printlogf(LOG_NOTICE, "exit status %d from user %s %s\n", 236 | exit_status, 237 | file->cf_UserName, 238 | line->cl_Description 239 | ); 240 | 241 | } 242 | } 243 | if (!exit_status || exit_status == EAGAIN) 244 | if (DebugOpt) 245 | printlogf(LOG_DEBUG, "exit status %d from user %s %s\n", 246 | exit_status, 247 | file->cf_UserName, 248 | line->cl_Description 249 | ); 250 | 251 | 252 | if (line->cl_MailFlag != 1) { 253 | /* End of job and no mail file */ 254 | line->cl_Pid = 0; 255 | return; 256 | } 257 | 258 | /* 259 | * Calculate mailFile's name before clearing cl_Pid 260 | */ 261 | snprintf(mailFile, sizeof(mailFile), TempFileFmt, 262 | file->cf_UserName, line->cl_Pid); 263 | line->cl_Pid = 0; 264 | 265 | line->cl_MailFlag = 0; 266 | 267 | /* 268 | * Check mail file. If size has increased and 269 | * the file is still valid, we sendmail it. 270 | */ 271 | 272 | mailFd = open(mailFile, O_RDONLY); 273 | remove(mailFile); 274 | if (mailFd < 0) { 275 | return; 276 | } 277 | 278 | /* Was mailFile tampered with, or didn't grow? */ 279 | 280 | if (fstat(mailFd, &sbuf) < 0 || 281 | sbuf.st_uid != DaemonUid || 282 | sbuf.st_nlink != 0 || 283 | sbuf.st_size == line->cl_MailPos || 284 | !S_ISREG(sbuf.st_mode) 285 | ) { 286 | close(mailFd); 287 | return; 288 | } 289 | 290 | if ((line->cl_Pid = fork()) == 0) { 291 | /* 292 | * CHILD, FORK OK, PRE-EXEC 293 | * 294 | * Change user id - no way in hell security can be compromised 295 | * by the mailing and we already verified the mail file. 296 | */ 297 | 298 | if (ChangeUser(file->cf_UserName, TempDir) < 0) { 299 | printlogf(LOG_ERR, "unable to ChangeUser to send mail (user %s %s)\n", 300 | file->cf_UserName, 301 | line->cl_Description 302 | ); 303 | exit(0); 304 | } 305 | 306 | /* from this point we are unpriviledged */ 307 | 308 | /* 309 | * Inside child, we copy our fd 2 (which may be /dev/null) into 310 | * an open-until-exec fd 8 311 | */ 312 | 313 | dup2(2, 8); 314 | fcntl(8, F_SETFD, FD_CLOEXEC); 315 | fclose(stderr); 316 | 317 | /* 318 | * Run sendmail with stdin < mailFile and stderr > /dev/null 319 | */ 320 | 321 | dup2(mailFd, 0); 322 | dup2(1, 2); 323 | close(mailFd); 324 | 325 | if (!SendMail) { 326 | /* 327 | * If using standard sendmail, note in our log (now associated with fd 8) 328 | * that we're trying to mail output 329 | */ 330 | fdprintlogf(LOG_INFO, 8, "mailing cron output for user %s %s\n", 331 | file->cf_UserName, 332 | line->cl_Description 333 | ); 334 | execl(SENDMAIL, SENDMAIL, SENDMAIL_ARGS, NULL); 335 | 336 | /* exec failed: pass through and log the error */ 337 | SendMail = SENDMAIL; 338 | 339 | } else { 340 | /* 341 | * If using custom mailer script, just try to exec it 342 | */ 343 | execl(SendMail, SendMail, NULL); 344 | } 345 | 346 | /* 347 | * CHILD FAILED TO EXEC SENDMAIL 348 | * 349 | * Complain to our log (now associated with fd 8) 350 | */ 351 | 352 | fdprintlogf(LOG_WARNING, 8, "unable to exec %s: cron output for user %s %s to /dev/null\n", 353 | SendMail, 354 | file->cf_UserName, 355 | line->cl_Description 356 | ); 357 | exit(0); 358 | 359 | } else if (line->cl_Pid < 0) { 360 | /* 361 | * PARENT, FORK FAILED 362 | * 363 | * Complain to our log (with regular fd 2) 364 | */ 365 | printlogf(LOG_WARNING, "unable to fork: cron output for user %s %s to /dev/null\n", 366 | file->cf_UserName, 367 | line->cl_Description 368 | ); 369 | line->cl_Pid = 0; 370 | } else { 371 | /* 372 | * PARENT, FORK OK 373 | * 374 | * We clear cl_Pid even when mailjob successfully forked 375 | * and catch the dead mailjobs with our SIGCHLD handler. 376 | */ 377 | line->cl_Pid = 0; 378 | } 379 | 380 | close(mailFd); 381 | 382 | } 383 | -------------------------------------------------------------------------------- /main.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * MAIN.C 4 | * 5 | * crond [-s dir] [-c dir] [-t dir] [-m user@host] [-M mailer] [-S|-L [file]] [-l level] [-b|-f|-d] 6 | * run as root, but NOT setuid root 7 | * 8 | * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com) 9 | * Copyright 2009-2019 James Pryor 10 | * May be distributed under the GNU General Public License version 2 or any later version. 11 | */ 12 | 13 | #include "defs.h" 14 | 15 | Prototype short DebugOpt; 16 | Prototype short LogLevel; 17 | Prototype short ForegroundOpt; 18 | Prototype short SyslogOpt; 19 | Prototype const char *CDir; 20 | Prototype const char *SCDir; 21 | Prototype const char *TSDir; 22 | Prototype const char *LogFile; 23 | Prototype const char *LogHeader; 24 | Prototype uid_t DaemonUid; 25 | Prototype pid_t DaemonPid; 26 | Prototype const char *SendMail; 27 | Prototype const char *Mailto; 28 | Prototype char *TempDir; 29 | Prototype char *TempFileFmt; 30 | 31 | short DebugOpt = 0; 32 | short LogLevel = LOG_LEVEL; 33 | short ForegroundOpt = 0; 34 | short SyslogOpt = 1; 35 | const char *CDir = CRONTABS; 36 | const char *SCDir = SCRONTABS; 37 | const char *TSDir = CRONSTAMPS; 38 | const char *LogFile = NULL; /* opened with mode 0600 */ 39 | const char *LogHeader = LOGHEADER; 40 | const char *SendMail = NULL; 41 | const char *Mailto = NULL; 42 | char *TempDir; 43 | char *TempFileFmt; 44 | 45 | uid_t DaemonUid; 46 | pid_t DaemonPid; 47 | 48 | int 49 | main(int ac, char **av) 50 | { 51 | const char *LevelAry[] = { 52 | "emerg", 53 | "alert", 54 | "crit", 55 | "err", 56 | "warning", 57 | "notice", 58 | "info", 59 | "debug", 60 | "panic", 61 | "error", 62 | "warn", 63 | NULL 64 | }; 65 | int i; 66 | 67 | /* 68 | * parse options 69 | */ 70 | 71 | DaemonUid = getuid(); 72 | 73 | opterr = 0; 74 | 75 | while ((i = getopt(ac,av,"dl:L:fbSc:s:m:M:t:")) != -1) { 76 | switch (i) { 77 | case 'l': 78 | { 79 | char *ptr; 80 | int j; 81 | ptr = optarg; 82 | for (j = 0; LevelAry[j]; ++j) { 83 | if (strncmp(ptr, LevelAry[j], strlen(LevelAry[j])) == 0) { 84 | break; 85 | } 86 | } 87 | switch(j) { 88 | case 0: 89 | case 8: 90 | /* #define LOG_EMERG 0 [* system is unusable *] */ 91 | LogLevel = LOG_EMERG; 92 | break; 93 | case 1: 94 | /* #define LOG_ALERT 1 [* action must be taken immediately *] */ 95 | LogLevel = LOG_ALERT; 96 | break; 97 | case 2: 98 | /* #define LOG_CRIT 2 [* critical conditions *] */ 99 | LogLevel = LOG_CRIT; 100 | break; 101 | case 3: 102 | case 9: 103 | /* #define LOG_ERR 3 [* error conditions *] */ 104 | LogLevel = LOG_ERR; 105 | break; 106 | case 4: 107 | case 10: 108 | /* #define LOG_WARNING 4 [* warning conditions *] */ 109 | LogLevel = LOG_WARNING; 110 | break; 111 | case 5: 112 | /* #define LOG_NOTICE 5 [* normal but significant condition *] */ 113 | LogLevel = LOG_NOTICE; 114 | break; 115 | case 6: 116 | /* #define LOG_INFO 6 [* informational *] */ 117 | LogLevel = LOG_INFO; 118 | break; 119 | case 7: 120 | /* #define LOG_DEBUG 7 [* debug-level messages *] */ 121 | LogLevel = LOG_DEBUG; 122 | break; 123 | default: 124 | LogLevel = atoi(optarg); 125 | } 126 | } 127 | break; 128 | case 'd': 129 | DebugOpt = 1; 130 | LogLevel = LOG_DEBUG; 131 | /* fall through to include f too */ 132 | case 'f': 133 | ForegroundOpt = 1; 134 | break; 135 | case 'b': 136 | ForegroundOpt = 0; 137 | break; 138 | case 'S': /* log through syslog */ 139 | SyslogOpt = 1; 140 | break; 141 | case 'L': /* use internal log formatter */ 142 | SyslogOpt = 0; 143 | LogFile = optarg; 144 | /* if LC_TIME is defined, we use it for logging to file instead of compiled-in TIMESTAMP_FMT */ 145 | if (getenv("LC_TIME") != NULL) { 146 | LogHeader = LOCALE_LOGHEADER; 147 | } 148 | break; 149 | case 'c': 150 | if (*optarg != 0) CDir = optarg; 151 | break; 152 | case 's': 153 | if (*optarg != 0) SCDir = optarg; 154 | break; 155 | case 't': 156 | if (*optarg != 0) TSDir = optarg; 157 | break; 158 | case 'M': 159 | if (*optarg != 0) SendMail = optarg; 160 | break; 161 | case 'm': 162 | if (*optarg != 0) Mailto = optarg; 163 | break; 164 | default: 165 | /* 166 | * check for parse error 167 | */ 168 | printf("dillon's cron daemon " VERSION "\n"); 169 | printf("crond [-s dir] [-c dir] [-t dir] [-m user@host] [-M mailer] [-S|-L [file]] [-l level] [-b|-f|-d]\n"); 170 | printf("-s directory of system crontabs (defaults to %s)\n", SCRONTABS); 171 | printf("-c directory of per-user crontabs (defaults to %s)\n", CRONTABS); 172 | printf("-t directory of timestamps (defaults to %s)\n", CRONSTAMPS); 173 | printf("-m user@host where should cron output be directed? (defaults to local user)\n"); 174 | printf("-M mailer (defaults to %s)\n", SENDMAIL); 175 | printf("-S log to syslog using identity '%s' (default)\n", LOG_IDENT); 176 | printf("-L file log to specified file instead of syslog\n"); 177 | printf("-l loglevel log events <= this level (defaults to %s (level %d))\n", LevelAry[LOG_LEVEL], LOG_LEVEL); 178 | printf("-b run in background (default)\n"); 179 | printf("-f run in foreground\n"); 180 | printf("-d run in debugging mode\n"); 181 | exit(2); 182 | } 183 | } 184 | 185 | /* 186 | * close stdin and stdout. 187 | * close unused descriptors - don't need. 188 | * optional detach from controlling terminal 189 | */ 190 | 191 | fclose(stdin); 192 | fclose(stdout); 193 | 194 | i = open("/dev/null", O_RDWR); 195 | if (i < 0) { 196 | perror("open: /dev/null"); 197 | exit(1); 198 | } 199 | dup2(i, 0); 200 | dup2(i, 1); 201 | 202 | /* create tempdir with permissions 0755 for cron output */ 203 | TempDir = strdup(TMPDIR "/cron.XXXXXX"); 204 | if (mkdtemp(TempDir) == NULL) { 205 | perror("mkdtemp"); 206 | exit(1); 207 | } 208 | if (chmod(TempDir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH)) { 209 | perror("chmod"); 210 | exit(1); 211 | } 212 | if (!(TempFileFmt = concat(TempDir, "/cron.%s.%d", NULL))) { 213 | errno = ENOMEM; 214 | perror("main"); 215 | exit(1); 216 | } 217 | 218 | if (ForegroundOpt == 0) { 219 | 220 | int fd; 221 | int pid; 222 | 223 | if ((pid = fork()) < 0) { 224 | /* fork failed */ 225 | perror("fork"); 226 | exit(1); 227 | } else if (pid > 0) { 228 | /* parent */ 229 | exit(0); 230 | } 231 | /* child continues */ 232 | 233 | /* become session leader, detach from terminal */ 234 | 235 | if (setsid() < 0) 236 | perror("setsid"); 237 | if ((fd = open("/dev/tty", O_RDWR)) >= 0) { 238 | ioctl(fd, TIOCNOTTY, 0); 239 | close(fd); 240 | } 241 | 242 | /* setup logging for backgrounded daemons */ 243 | 244 | if (SyslogOpt) { 245 | /* start SIGHUP and SIGCHLD handling while stderr still open */ 246 | initsignals(); 247 | /* 2> /dev/null */ 248 | fclose(stderr); 249 | dup2(1, 2); 250 | 251 | /* open syslog */ 252 | openlog(LOG_IDENT, LOG_CONS|LOG_PID, LOG_CRON); 253 | 254 | } else { 255 | /* open logfile */ 256 | if ((fd = open(LogFile, O_WRONLY|O_CREAT|O_APPEND, 0600)) >= 0) { 257 | /* start SIGHUP ignoring, SIGCHLD handling while stderr still open */ 258 | initsignals(); 259 | /* 2> LogFile */ 260 | fclose(stderr); 261 | dup2(fd, 2); 262 | } else { 263 | int n = errno; 264 | fdprintf(2, "failed to open logfile '%s', reason: %s", LogFile, strerror(n)); 265 | exit(n); 266 | } 267 | } 268 | } else { 269 | /* daemon in foreground */ 270 | 271 | /* stay in existing session, but start a new process group */ 272 | if (setpgid(0,0)) { 273 | perror("setpgid"); 274 | exit(1); 275 | } 276 | 277 | /* stderr stays open, start SIGHUP ignoring, SIGCHLD handling */ 278 | initsignals(); 279 | } 280 | 281 | /* close all other fds, including the ones we opened as /dev/null and LogFile */ 282 | for (i = 3; i < MAXOPEN; ++i) { 283 | close(i); 284 | } 285 | 286 | 287 | /* 288 | * main loop - synchronize to 1 second after the minute, minimum sleep 289 | * of 1 second. 290 | */ 291 | 292 | printlogf(LOG_NOTICE,"%s " VERSION " dillon's cron daemon, started with loglevel %s\n", av[0], LevelAry[LogLevel]); 293 | SynchronizeDir(CDir, NULL, 1); 294 | SynchronizeDir(SCDir, "root", 1); 295 | ReadTimestamps(NULL); 296 | TestStartupJobs(); /* @startup jobs only run when crond is started, not when their crontab is loaded */ 297 | 298 | { 299 | time_t t1 = time(NULL); 300 | time_t t2; 301 | long dt; 302 | short rescan = 60; 303 | short stime = 60; 304 | 305 | for (;;) { 306 | sleep((stime + 1) - (short)(time(NULL) % stime)); 307 | 308 | t2 = time(NULL); 309 | dt = t2 - t1; 310 | 311 | /* 312 | * The file 'cron.update' is checked to determine new cron 313 | * jobs. The directory is rescanned once an hour to deal 314 | * with any screwups. 315 | * 316 | * check for disparity. Disparities over an hour either way 317 | * result in resynchronization. A reverse-indexed disparity 318 | * less then an hour causes us to effectively sleep until we 319 | * match the original time (i.e. no re-execution of jobs that 320 | * have just been run). A forward-indexed disparity less then 321 | * an hour causes intermediate jobs to be run, but only once 322 | * in the worst case. 323 | * 324 | * when running jobs, the inequality used is greater but not 325 | * equal to t1, and less then or equal to t2. 326 | */ 327 | 328 | if (--rescan == 0) { 329 | /* 330 | * If we resynchronize while jobs are running, we'll clobber 331 | * the job pids, so we won't know what's already running. 332 | */ 333 | if (CheckJobs() > 0) { 334 | rescan = 1; 335 | } else { 336 | rescan = 60; 337 | SynchronizeDir(CDir, NULL, 0); 338 | SynchronizeDir(SCDir, "root", 0); 339 | ReadTimestamps(NULL); 340 | } 341 | } 342 | if (rescan < 60) { 343 | CheckUpdates(CDir, NULL, t1, t2); 344 | CheckUpdates(SCDir, "root", t1, t2); 345 | } 346 | if (DebugOpt) 347 | printlogf(LOG_DEBUG, "Wakeup dt=%d\n", dt); 348 | if (dt < -60*60 || dt > 60*60) { 349 | t1 = t2; 350 | printlogf(LOG_NOTICE,"time disparity of %d minutes detected\n", dt / 60); 351 | } else if (dt > 0) { 352 | TestJobs(t1, t2); 353 | RunJobs(); 354 | sleep(5); 355 | if (CheckJobs() > 0) 356 | stime = 10; 357 | else 358 | stime = 60; 359 | t1 = t2; 360 | } 361 | } 362 | } 363 | /* not reached */ 364 | } 365 | 366 | -------------------------------------------------------------------------------- /subs.c: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * SUBS.C 4 | * 5 | * Copyright 1994 Matthew Dillon (dillon@apollo.backplane.com) 6 | * Copyright 2009-2019 James Pryor 7 | * May be distributed under the GNU General Public License version 2 or any later version. 8 | */ 9 | 10 | #include "defs.h" 11 | 12 | Prototype void printlogf(int level, const char *ctl, ...); 13 | Prototype void fdprintlogf(int level, int fd, const char *ctl, ...); 14 | Prototype void fdprintf(int fd, const char *ctl, ...); 15 | Prototype void initsignals(void); 16 | Prototype char Hostname[SMALL_BUFFER]; 17 | 18 | void vlog(int level, int fd, const char *ctl, va_list va); 19 | 20 | char Hostname[SMALL_BUFFER]; 21 | 22 | 23 | void 24 | printlogf(int level, const char *ctl, ...) 25 | { 26 | va_list va; 27 | 28 | va_start(va, ctl); 29 | vlog(level, 2, ctl, va); 30 | va_end(va); 31 | } 32 | 33 | void 34 | fdprintlogf(int level, int fd, const char *ctl, ...) 35 | { 36 | va_list va; 37 | 38 | va_start(va, ctl); 39 | vlog(level, fd, ctl, va); 40 | va_end(va); 41 | } 42 | 43 | void 44 | fdprintf(int fd, const char *ctl, ...) 45 | { 46 | va_list va; 47 | char buf[LOG_BUFFER]; 48 | 49 | va_start(va, ctl); 50 | vsnprintf(buf, sizeof(buf), ctl, va); 51 | write(fd, buf, strlen(buf)); 52 | va_end(va); 53 | } 54 | 55 | void 56 | vlog(int level, int fd, const char *ctl, va_list va) 57 | { 58 | char buf[LOG_BUFFER]; 59 | static short suppressHeader = 0; 60 | 61 | if (level <= LogLevel) { 62 | if (ForegroundOpt) { 63 | /* 64 | * when -d or -f, we always (and only) log to stderr 65 | * fd will be 2 except when 2 is bound to a execing subprocess, then it will be 8 66 | * [v]snprintf write at most size including \0; they'll null-terminate, even when they truncate 67 | * we don't care here whether it truncates 68 | */ 69 | vsnprintf(buf, sizeof(buf), ctl, va); 70 | write(fd, buf, strlen(buf)); 71 | } else if (SyslogOpt) { 72 | /* log to syslog */ 73 | vsnprintf(buf, sizeof(buf), ctl, va); 74 | syslog(level, "%s", buf); 75 | 76 | } else { 77 | /* log to file */ 78 | 79 | time_t t = time(NULL); 80 | struct tm *tp = localtime(&t); 81 | int buflen, hdrlen = 0; 82 | buf[0] = 0; /* in case suppressHeader or strftime fails */ 83 | if (!suppressHeader) { 84 | /* 85 | * run LogHeader through strftime --> [yields hdr] plug in Hostname --> [yields buf] 86 | */ 87 | char hdr[SMALL_BUFFER]; 88 | /* strftime returns strlen of result, provided that result plus a \0 fit into buf of size */ 89 | if (strftime(hdr, sizeof(hdr), LogHeader, tp)) { 90 | if (gethostname(Hostname, sizeof(Hostname))==0) 91 | /* gethostname successful */ 92 | /* result will be \0-terminated except gethostname doesn't promise to do so if it has to truncate */ 93 | Hostname[sizeof(Hostname)-1] = 0; 94 | else 95 | Hostname[0] = 0; /* gethostname() call failed */ 96 | /* [v]snprintf write at most size including \0; they'll null-terminate, even when they truncate */ 97 | /* return value >= size means result was truncated */ 98 | if ((hdrlen = snprintf(buf, sizeof(hdr), hdr, Hostname)) >= sizeof(hdr)) 99 | hdrlen = sizeof(hdr) - 1; 100 | } 101 | } 102 | if ((buflen = vsnprintf(buf + hdrlen, sizeof(buf) - hdrlen, ctl, va) + hdrlen) >= sizeof(buf)) 103 | buflen = sizeof(buf) - 1; 104 | 105 | write(fd, buf, buflen); 106 | /* if previous write wasn't \n-terminated, we suppress header on next write */ 107 | suppressHeader = (buf[buflen-1] != '\n'); 108 | 109 | } 110 | } 111 | } 112 | 113 | void reopenlogger(int sig) { 114 | int fd; 115 | if (getpid() == DaemonPid) { 116 | /* only daemon handles, children should ignore */ 117 | if ((fd = open(LogFile, O_WRONLY|O_CREAT|O_APPEND, 0600)) < 0) { 118 | /* can't reopen log file, exit */ 119 | exit(errno); 120 | } 121 | dup2(fd, 2); 122 | close(fd); 123 | } 124 | } 125 | 126 | void waitmailjob(int sig) { 127 | /* 128 | * Wait for any children in our process group. 129 | * These will all be mailjobs. 130 | */ 131 | pid_t child; 132 | do { 133 | child = waitpid(-DaemonPid, NULL, WNOHANG); 134 | /* call was interrupted, try again: won't happen because we use SA_RESTART */ 135 | /* if (child == (pid_t)-1 && errno == EINTR) continue; */ 136 | } while (child > (pid_t) 0); 137 | /* if no pending children, child,errno == -1,ECHILD */ 138 | /* if all children still running, child == 0 */ 139 | } 140 | 141 | void 142 | initsignals (void) { 143 | struct sigaction sa; 144 | int n; 145 | 146 | /* save daemon's pid globally */ 147 | DaemonPid = getpid(); 148 | 149 | /* restart any system calls that were interrupted by signal */ 150 | sa.sa_flags = SA_RESTART; 151 | if (!ForegroundOpt && !SyslogOpt) 152 | sa.sa_handler = reopenlogger; 153 | else 154 | sa.sa_handler = SIG_IGN; 155 | if (sigaction (SIGHUP, &sa, NULL) != 0) { 156 | n = errno; 157 | fdprintf(2, "failed to start SIGHUP handling, reason: %s", strerror(errno)); 158 | exit(n); 159 | } 160 | sa.sa_flags = SA_RESTART; 161 | sa.sa_handler = waitmailjob; 162 | if (sigaction (SIGCHLD, &sa, NULL) != 0) { 163 | n = errno; 164 | fdprintf(2, "failed to start SIGCHLD handling, reason: %s", strerror(errno)); 165 | exit(n); 166 | } 167 | 168 | } 169 | 170 | --------------------------------------------------------------------------------