pax_global_header00006660000000000000000000000064147551274460014531gustar00rootroot0000000000000052 comment=c8ddac9b8e0fb8f4b9c2000df3db7e61973a2d5a systemd-cron-2.5.1/000077500000000000000000000000001475512744600141655ustar00rootroot00000000000000systemd-cron-2.5.1/.clang-format000066400000000000000000000020771475512744600165460ustar00rootroot00000000000000--- Language : Cpp BasedOnStyle : LLVM AlignAfterOpenBracket : true AlignEscapedNewlinesLeft : true AlignConsecutiveAssignments : true AllowShortFunctionsOnASingleLine : Inline AlwaysBreakTemplateDeclarations : true ColumnLimit : 160 ConstructorInitializerIndentWidth : 6 IndentCaseLabels : true MaxEmptyLinesToKeep : 2 KeepEmptyLinesAtTheStartOfBlocks : false NamespaceIndentation : All PointerAlignment : Middle SpacesBeforeTrailingComments : 2 IndentWidth : 2 TabWidth : 2 UseTab : ForIndentation SpaceBeforeParens : Never FixNamespaceComments : false ... systemd-cron-2.5.1/.gitignore000066400000000000000000000000261475512744600161530ustar00rootroot00000000000000/out/ /Makefile *.swp systemd-cron-2.5.1/CHANGELOG000066400000000000000000000421431475512744600154030ustar00rootroot00000000000000v2.5.1 : 2025-02-18 [Josef Johansson] Update: contrib/systemd-cron.spec New feature: contrib/sendmail-ala-vixie that formats From: & Subject: like vixie cron did New feature: mail_for_job exports $user in environment to $sendmail [наб] Fix: Don't run systemd-analyze in crontab --translate on systemd <250 Change: crontab -l exits 1 instead of ENOENT if no crontab Fix: systemd.cron(7)'s .Nm said systemd.crom Fix: Replace OnSuccess= with ExecStopPost= in generated units when systemd <249 detected in ./configure Clarify: crontab(5) said MAILFROM defaulted to root@HOSTNAME, not root v2.5.0 : 2025-01-21 [Andrew Sayers] Clarify: "ignoring /etc/… because native timer is present" & "ignoring /etc/… because it is masked" -> "ignoring /etc/… because it is overridden by native .timer unit" & "ignoring /etc/… because it is overridden and masked by native .timer unit" [Josef Johansson] Fix: RANDOM_DELAY=1 being ignored as-if it were unset or =0 [наб] Clarify: PERSISTENT=auto is the default except in anacrontab(5) New feature: "CRON_INHERIT_VARIABLES=var1 var2 var3..." in /etc/crontab copies var1 var2 &c. to /etc/cron.d/* and /etc/cron.*/*; this allows global defaults for both environment variables and cron configuration like RANDOM_DELAY, SHELL, &c. New feature: ./configure --with-pamname=$pamconfigname to generate PAMName=$pamconfigname into .service units (+ KillMode=control-group by necessity) v2.4.1 : 2024-08-13 [наб] Fix: always set WorkingDir=~ to behave like other crons. Fix: if no-run-parts, don't generate units for /etc/cron.{period}/{job} if it isn't executable v2.4.0 : 2024-04-29 [наб] New feature: Support seeding the stdin of a job with the % operator alike other cron implementations. Fix: Unescape \% as expected for existing crontabs. Security Fix: Check single-word path commands as the user fsugid to avoid that user X use the generator to spy on user Y and see whether a file exist in it's $HOME. [adetiste] Fix: use proper CXXFLAGS instead of CFLAGS for C++ programs. v2.3.4 : 2024-03-25 [наб] Fix: /etc/crontab CRON_MAIL_{SUCCESS,FORMAT} & MAILTO not being honoured if no jobs were given therein Clarify: anacron's support for @monthly and @yearly vis-à-vis sd-cron's 10 @-periods (cf. Debian #1067752) v2.3.3 : 2024-02-24 [наб] Fix: fix OnFailure email handler when build with legacy run-parts mode v2.3.2 : 2024-02-11 [adetiste] Fix last weekday of month new feature. v2.3.1 : 2024-02-11 [наб] New feature: allow SENDMAIL to be overridden to use a sendmail-{matrix,appraise} connector. New feature: replace OpenSSL by lighter libmd. Fix: add a flag to disable PCH during build. [adetiste] New feature: support Last-day-of-{week,month} that maps with `~` operator introduced in systemd 233. Fix: document existing MAILFROM= in manpage. v2.3.0 : 2023-10-14 [наб] New feature: support random time ranges with the '~' operator. Fix: fix error handling in parse_period() that turned bad values in '*'. Fix: fix parsing of day of week ending on 'Sun'day. Fix: allow resetting the timezone to the system default using TZ= / CRON_TZ= . [adetiste] Fix: also accept timezones prefixed with ':' as documented in tzset(3) v2.2.0 : 2023-09-24 [наб] Fix: fix an assertion in the generator that happens on hardened toolchain systems. * helpfull error messages when an incorrect crontab is supplied, both in crontab and in the journal. Other improvements to the crontab tool: * syntax highlighting for `crontab --list`. * colour handling will now obey TERM= & NO_COLOR= environment variables. * `crontab --translate` will have it's output checked by `systemd-analyze verify` to ensure the generated units match running systemd expected syntax. v2.1.3 : 2023-09-13 [наб] Fix: tweak again the integration tests to equally work on distribution's various isolation environments. Fix: one-word lines depending unduly on system state: putting/removing executables in/from $PATH, reloading, then reverting the change, would leave the cron units broken. systemd-cron won't expand these paths anymore at boot/refresh time; the expansion will be done by the shell/kernel at runtime. Fix: never chomp '> /dev/null' a the end of crontabs. [adetiste] New: consider CRON_TZ= the same as TZ=. v2.1.2 : 2023-09-10 The 'run-parts' mode is now disabled by default; this new default can be reverted with a configure flag. [наб] Fix: add 'SyslogFacility=cron' to generated services Fix: use this facility to more precisely filter from the journal for CRON_MAIL_SUCCESS=nonempty and CRON_MAIL_FORMAT=nometadata [adetiste] Fix: add 'SyslogFacility=cron' to static services used in run-parts mode Fix: define 'User=root' in static services too, without this systemd won't defines some usually expected environment variables like LOGNAME... v2.1.1 : 2023-09-09 [наб] Fix: use ':' instead of ' ' to marshall OnSuccess/OnFailure unit names; distinguish them with :Success/:Failure. Fix: forward CRON_MAIL_* to /etc/cron.{daily,...} units. v2.1.0 : 2023-09-08 [наб] New feature: * possibly send e-mails more like traditional cron. The behaviour can be controlled by defining the MAILTO=, CRON_MAIL_SUCCESS= & CRON_MAIL_FORMAT= & parameters globally in /etc/crontab or in individual crontabs. Fix new 2.x feature: * treat "crontab -t | -T" argument not existing as an error Fix regressions from 1.x -> 2.x rewrite: * run-parts mode: re-enable processing of /etc/anacrontab and /var/spool/cron * fix systemd_bool() logic inversion, has impact on handling of PERSISTENT= and BATCH= overrides. v2.0.2 : 2023-09-01 [наб] * Makefile: split test target into test: test-{no,}unshare v2.0.1 : 2023-08-22 [наб & Dwayne Bent] * Improvements to the ./configure & Makefile.in v2.0 : 2023-08-16 [наб] * rewrite of the generator and crontab utility in C++ which gives a lot more performance * rewrite mail_on_failure in Shell * drop dependency on Python * again majors improvements in the configure/Makefile scripts * support for timezones with TZ= environment variable, this requires systemd 235 * an extensive testsuite (#51) [adetiste] * rewrite remove_stale_stamps in Shell; take into account the new "run-part-less" mode introduced in v1.5.15 * use pkgconf to get systemd $unitdir & $generatordir v1.16.7 : 2023-08-13 * THIS IS THE LAST VERSION BEFORE MERGING THE C++ REWRITE * after a survey of downstream users, "cron-yearly.{service|timer}" are now enabled by default. This can be disabled again with an ./configure option. * run-part-less-mode: read base starting hour of daily/weeky/monthly jobs from distro's /etc/crontab if present [наб ] * fix more regressions found since the 1.16 OO-rewrite * review crontab_setgid.c v1.16.7 : 2023-08-13 * discarded version v1.16.5 : 2023-08-09 * fix another test failure on Debian build machines that are not yet UsrMerge'd. v1.16.4 : 2023-08-09 * fix test failure: "module 'importlib' has no attribute 'util'", may even impact 'crontab' CLI. This is a regression introduced by: "Python 3.12+ compatibility: "load_module()" was deprecated" v1.16.3 : 2023-07-29 * ignore /etc/cron.{hourly|daily|weekly|monthly}/0anacron. These files are present when anacron has not been fully removed, and do absolutely nothing (status "removed & non-purged" on Debian). /etc/cron.d/anacron would already be masked by empty anacron.service * monitor the contents of /etc/cron.{hourly|daily|weekly|monthly}/ in run-parts-less mode. * make the build respect `libexecdir` configure variable https://www.gnu.org/prep/standards/html_node/Directory-Variables.html https://refspecs.linuxfoundation.org/FHS_3.0/fhs/ch04s07.html * UsrMerge: drop `runparts` configure variable, run-parts is now always /usr/bin/run-parts [наб ] * various improvements to the configure script & Makefile * rewrite boot_delay as a shell script for faster execution * review of the man pages * better error handling in crontab setgid helper v1.16.2 : 2023-07-27 * Python 3.12+ compatibility: "load_module()" was deprecated * fix other regressions brought by the rewrite: * fix expansion of commands starting with '~/' [Ben Hutchings] * regression in crontab that rejected valid timespecs v1.16.1 : 2023-07-24 * fix a syntax error in the crontab utility v1.16 : 2023-07-10 * major rewrite * MAILTO= : /etc/cron.*/ will inherit this from /etc/crontab if they don't provide their own * always explicitly set `User=root`, this way systemd will itself provide fallback for HOME=, LOGNAME=, USER= & SHELL= env. variables. (issue #65) * accept a days-of-week range that ends with '...-7' (#57) v1.15.22 : 2023-07-04 * improve runparts-less mode again v1.15.21 : 2023-06-23 * improve runparts-less mode v1.15.20 : 2023-03-10 * fix sending of unicode emails, thanks to 0xE1E10 v1.15.19 : 2022-07-16 * Align with previous botched version jump (#82) * crontab accepts $EDITOR with spaces (#83) v1.5.18 : 2020-12-26 Various improvements to email on error: * Revert "Use DynamicUser=yes for error email generator" * Use sysusers.d snippet instead * Support for MAILFROM variable [thanks MarcoCLA] v1.5.17 : 2020-01-26 * Use DynamicUser=yes for error email generator v1.5.16 : 2020-12-20 * ignore backup files in /etc/cron.d * fix run-partscondition v1.5.15 : 2020-10-17 * fix skipping OnFailure for empty MAILTO=, thanks to Richard Laager * make run-parts uses optional v1.5.14 : 2018-11-11 * Python 3.7 compatibility, thanks to enrico.detoma * Handle sending mail_on_error when the locale is not correctly set * Use KillMode=process for generated units v1.5.13 : 2018-03-28 * escape '%' in unit description (Thanks Mateusz Kowalewski) v1.5.12 : 2017-12-08 * fix typo in configure script introduced in 1.5.11 v1.5.11 : 2017-12-08 * add distinct configure option for systemd generator dir (Thanks Mike Gilbert) * add support for RandomizedDelaySec, this requires systemd ≥ 229 v1.5.10 : 2017-07-01 * fix regression in handling of masked/overridden timer units v1.5.9 : 2017-06-18 * allow runtime masking of units * log masked timers in a distinct way * drop cargo-culted RefuseManual[Start|Stop] * delay cron-boot.timer of 1 minute v1.5.8 : 2017-01-15 * handle weekly slices as expected * document that someone can use 'systemctl edit' to override (generated) units. v1.5.7 : 2017-01-11 * try to fix it again, hopefully finally, making parse_time_unit() even harder to understand v1.5.6 : 2017-01-10 * fix last off-by-one error in processing of - ranges. v1.5.5 : 2017-01-10 * pass $(CPPFLAGS) to compilator for setgid helper * almost always generate .sh scripts to avoid that systemd complains with "Invalid escape sequences in line, correcting:" when parsing some complex one-liners https://github.com/systemd/systemd/blob/master/src/basic/extract-word.c#L204 * fix off-by-one error in processing of '*/' for months & days. Issue #49 v1.5.4 : 2016-01-29 * generator: don't call OnFailure unit without a MTA available. * sync with systemd-cron-next: remove --stale-stamps configure option * quote "Environment=" keypairs only when necessary v1.5.3 : 2015-02-16 Minor bugfixes: * add crontab --show option that lists crontabs * crontab: try /usr/bin/editor, /usr/bin/vim, /usr/bin/nano, /usr/bin/mcedit if VISUAL and EDITOR are not set * avoid forgeting successful edits in /tmp/ v1.5.2 : 2014-12-21 Bug-fix release: * generator now process UTF-8 files correctly (generators are run with LANG=C) * global exception handler added to generator; now prints error in journal * make install : setgid helper will be chgrp cron / chmod 2755 if group cron exists ; but won't create this group by itself * now support BATCH=yes|no into crontabs; this is translasted into CPUSchedulingPolicy=idle & IOSchedulingClass=idle * the generator can now co-exist with a boilerplate /etc/crontab that does includes include definitions for /etc/cron.daily etc... these will only be processed if the matching native unit is not activated during configure step v1.5.1 : 2014-12-13 * make all writes in crontab (both Python & C parts) atomic * keep rejected crontabs in /tmp/crontab* for review * added support for /etc.cron.allow and /etc/cron.deny; without any of those, only root can create crontabs * turn the setuid/root helper into a setguid/crontab to let it run with the least privieges If you enable this feature in your package, some additional setup is needed after it has been unpacked: getent group crontab > /dev/null 2>&1 || addgroup --system crontab chgrp crontab /lib/systemd-cron/crontab_setgid chmod 2755 /lib/systemd-cron/crontab_setgid mkdir -p /var/spool/cron/crontabs chown root:crontab /var/spool/cron/crontabs chmod 1730 /var/spool/cron/crontabs cd /var/spool/cron/crontabs ls -1 | xargs -r -n 1 --replace=xxx chown 'xxx:crontab' 'xxx' ls -1 | xargs -r -n 1 chmod 600 The crontab program, when run as root, will also try to fixup file permissions; but won't create the crontab group. You can use sudo crontab -l -u $USER to fix-up your own crontab's permissions. THANKS to Lorenzo Beretta for review v1.5.0 : 2014-12-11 * added an optional C setuid helper to let non-root users use crontab v1.4.2 : 2014-11-25 Bug-Fix release * build now honor bindir for systemctl location * generator: better handling of quoted commands in crontabs v1.4.1 : 2014-11-11 * generator: will now log warnings & errors about bad crontabs in the journal * generator: try less aggressively to make jobs persistent: a job like 0 19 * * * root poweroff on a school/office PC would had run the next morning; which is not the expected behaviour; this can now be overridden with the PERSISTENT=yes|auto|no variable * new trivial internal utility boot_delay : this is needed because combining OnCalendar= & OnBootSec= in timer units doesn't have the needed behaviour (OR instead of AND). A boot delay is a standard feature of anacrontab; but can now also be used in crontab by specifying the DELAY= value. Thanks to @wavexx for the extensive review. v1.4.0 : 2014-11-04 * new utility mail_on_failure that is called by a new OnFailure= hook, both for static & generated units * add support for new time unit introduced in systemd 217: minutely, quarterly, semi-annually * review of man pages v1.3.3 : 2014-10-21 This release solves a bug introduced in crontab with the switch to Python3 v1.3.2 : 2014-10-20 * switch to Python3 for crontab & generator * add a manpage for /etc/anacrontab * add a new utility remove_stale_stamps than removes stales stamps from long deleted crontabs ( It can be called from /etc/cron.weekly/ ) * configure: statedir defaults now to '/var/spool/cron' * crontab: improved error handling * generator: add support for RANDOM_DELAY & START_HOURS_RANGE to anacrontab. This works in crontabs too. v1.3.1 : 2014-09-22 * turn Path watching unit in static unit * make Persistent=true smarter * make /var/spool configurable * put man page in right section v1.3.0 : 2014-09-04 merge in systemd-crontab-generator from @kstep https://github.com/kstep/systemd-crontab-generator v1.2.1 : 2014-04-01 * FIX: Build should now be parallelizable with make -j v1.2.0 : 2014-03-31 * NEW: Yearly timers with system ≥ 209. * NEW: Persistent timers with systemd ≥ 212. v1.1.2 : 2013-08-24 * FIX: Reconfigure units so that service units are automatically started by targets without needing to have them separately enabled. v1.1.1 : 2013-07-31 * Only activate service units if corresponding cron directory is non-empty. * Makefile does not automatically install cron directories. Thanks @WithHat v1.1.0 : 2013-07-16 * Add target units which allow you to write custom cron jobs. Thanks @WithHat v1.0.1 : 2013-05-31 * Update build mechanism with configure script and templated Makefile. v1.0.0 : 2013-05-30 * Support cron.boot for scripts to be executed at boot-up. v0.1.0 : 2013-05-27 * Initial release supporting hourly, daily, weekly, and monthly scripts. systemd-cron-2.5.1/LICENSE000066400000000000000000000023741475512744600152000ustar00rootroot00000000000000Copyright (C) 2013 Dwayne Bent Copyright (C) 2013 Dominik Peteler Copyright (C) 2014 Konstantin Stepanov (me@kstep.me) Copyright (C) 2014-2024 Alexandre Detiste (alexandre@detiste.be) Copyright (C) 2023-2024 наб (nabijaczleweli@nabijaczleweli.xyz) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. systemd-cron-2.5.1/Makefile.in000066400000000000000000000231241475512744600162340ustar00rootroot00000000000000CFLAGS ?= -O2 CXXFLAGS ?= -O2 SHELLCHECK ?= shellcheck CRONTAB ?= crontab PCH ?= y version := @version@ schedules := @schedules@ schedules_not := @schedules_not@ enable_runparts := @enable_runparts@ have_onsuccess := @have_onsuccess@ use_loglevelmax := @use_loglevelmax@ part2timer := @part2timer@ crond2timer := @crond2timer@ pamname := @pamname@ prefix := @prefix@ bindir := @bindir@ datadir := @datadir@ libdir := @libdir@ libexecdir := @libexecdir@ statedir := @statedir@ mandir := @mandir@ docdir := @docdir@ unitdir := @unitdir@ generatordir := @generatordir@ sysusersdir := @sysusersdir@ srcdir := $(CURDIR)/src outdir := $(CURDIR)/out builddir := $(outdir)/build distname := systemd-cron-$(version) distdir := $(outdir)/dist/$(distname) tarball := $(outdir)/dist/$(distname).tar.xz ifneq ($(enable_runparts),no) out_services := $(foreach schedule,$(schedules),$(builddir)/units/cron-$(schedule).service) out_timers := $(foreach schedule,$(schedules),$(builddir)/units/cron-$(schedule).timer) out_targets := $(foreach schedule,$(schedules),$(builddir)/units/cron-$(schedule).target) else out_services := $(builddir)/units/systemd-cron-cleaner.service out_timers := $(builddir)/units/systemd-cron-cleaner.timer out_targets := endif out_units := $(out_services) $(out_timers) $(out_targets) $(builddir)/units/cron.target \ $(builddir)/units/cron-update.path $(builddir)/units/cron-update.service \ $(builddir)/units/cron-mail@.service out_manuals := $(patsubst $(srcdir)/man/%.in,$(builddir)/man/%,$(wildcard $(srcdir)/man/*)) out_programs_sh := $(subst $(srcdir),$(builddir),$(patsubst %.sh,%,$(wildcard $(srcdir)/bin/*.sh))) out_programs_cpp := $(subst $(srcdir),$(builddir),$(patsubst %.cpp,%,$(wildcard $(srcdir)/bin/*.cpp))) out_programs_c := $(subst $(srcdir),$(builddir),$(patsubst %.c,%,$(wildcard $(srcdir)/bin/*.c))) outputs := $(out_units) $(out_manuals) $(out_programs_sh) $(out_programs_cpp) $(out_programs_c) define \n endef null := ifneq ($(enable_runparts),no) requires = $(subst ${null} ${null},\n,$(foreach schedule,$(schedules),Requires=cron-$(schedule).timer)) else requires = Requires=systemd-cron-cleaner.timer endif # $(call in2out,$input,$output,$schedule,$requires) define in2out sed \ -e "s|\@statedir\@|$(statedir)|g" \ -e "s|\@libexecdir\@|$(libexecdir)|g" \ -e "s|\@version\@|$(version)|g" \ -e "s|\@use_loglevelmax\@|$(use_loglevelmax)|g" \ -e "s|\@enable_runparts\@|$(enable_runparts)|g" \ -e "$(if $(filter $(have_onsuccess),yes),s|\@remove_if_no_onsuccess\@||g,/\@remove_if_no_onsuccess\@/d)" \ -e "s|\@schedule\@|$3|g" \ -e "s|\@requires\@|$4|g" \ $1 > $2 endef all: $(builddir) $(outputs) clean: rm -rf $(outdir) distprep: $(distdir) cp -at $(distdir) configure Makefile.in LICENSE README.md VERSION src test dist: $(tarball) test: test-unshare test-nounshare test-unshare: all unshare -rm test/test-generator $(enable_runparts) "$(statedir)" $(have_onsuccess) unshare -ru test/test-m_f_j $(version) unshare -rm test/test-r_s_s test-nounshare: all $(SHELLCHECK) $(out_programs_sh) configure $(foreach manpage,$(out_manuals),\ man --warnings --encoding=utf8 --local-file $(manpage) 2>&1 > /dev/null${\n}) install: all install -m755 -D $(builddir)/bin/crontab $(DESTDIR)$(bindir)/$(CRONTAB) install -m755 -D $(builddir)/bin/systemd-crontab-generator $(DESTDIR)$(generatordir)/systemd-crontab-generator install -m755 -D $(builddir)/bin/remove_stale_stamps $(DESTDIR)$(libexecdir)/systemd-cron/remove_stale_stamps install -m755 -D $(builddir)/bin/mail_for_job $(DESTDIR)$(libexecdir)/systemd-cron/mail_for_job install -m755 -D $(builddir)/bin/boot_delay $(DESTDIR)$(libexecdir)/systemd-cron/boot_delay install -m644 -D $(srcdir)/lib/sysusers.d/systemd-cron.conf $(DESTDIR)$(sysusersdir)/systemd-cron.conf install -m755 -D $(builddir)/bin/crontab_setgid $(DESTDIR)$(libexecdir)/systemd-cron/crontab_setgid install -m644 -D $(builddir)/man/systemd.cron.7 $(DESTDIR)$(mandir)/man7/systemd.cron.7 install -m644 -D $(builddir)/man/crontab.1 $(DESTDIR)$(mandir)/man1/$(CRONTAB).1 install -m644 -D $(builddir)/man/crontab.5 $(DESTDIR)$(mandir)/man5/$(CRONTAB).5 install -m644 -D $(builddir)/man/anacrontab.5 $(DESTDIR)$(mandir)/man5/anacrontab.5 install -m644 -D $(builddir)/man/systemd-crontab-generator.8 $(DESTDIR)$(mandir)/man8/systemd-crontab-generator.8 install -m644 -D $(builddir)/units/cron.target $(DESTDIR)$(unitdir)/cron.target install -m644 $(builddir)/units/cron-update.path $(DESTDIR)$(unitdir) install -m644 $(builddir)/units/cron-update.service $(DESTDIR)$(unitdir) install -m644 $(builddir)/units/cron-mail@.service $(DESTDIR)$(unitdir) ifneq ($(enable_runparts),no) $(foreach schedule,$(schedules),\ install -m644 $(builddir)/units/cron-$(schedule).timer $(DESTDIR)$(unitdir)${\n}) $(foreach schedule,$(schedules),\ install -m644 $(builddir)/units/cron-$(schedule).target $(DESTDIR)$(unitdir)${\n}) $(foreach schedule,$(schedules),\ install -m644 $(builddir)/units/cron-$(schedule).service $(DESTDIR)$(unitdir)${\n}) else install -m644 $(builddir)/units/systemd-cron-cleaner.timer $(DESTDIR)$(unitdir) install -m644 $(builddir)/units/systemd-cron-cleaner.service $(DESTDIR)$(unitdir) endif $(builddir)/units/cron-update.path: $(srcdir)/units/cron-update.path.in $(call in2out,$<,$@) ifeq ($(enable_runparts),no) echo 'PathChanged=/etc/cron.hourly' >> $@ echo 'PathChanged=/etc/cron.daily' >> $@ echo 'PathChanged=/etc/cron.weekly' >> $@ echo 'PathChanged=/etc/cron.monthly' >> $@ endif $(builddir)/units/cron-update.service: $(srcdir)/units/cron-update.service.in $(call in2out,$<,$@) $(builddir)/units/cron-mail@.service: $(srcdir)/units/cron-mail@.service.in $(call in2out,$<,$@) $(builddir)/units/cron-%.service: $(srcdir)/units/cron-schedule.service.in $(call in2out,$<,$@,$*) ifeq ($(use_loglevelmax),no) sed -i -e '/^LogLevelMax=/d' $@ endif $(builddir)/units/cron-boot.service: $(srcdir)/units/cron-boot.service.in $(call in2out,$<,$@,boot) $(builddir)/units/cron-%.timer: $(srcdir)/units/cron-schedule.timer.in $(call in2out,$<,$@,$*) $(builddir)/units/cron-boot.timer: $(srcdir)/units/cron-boot.timer.in $(call in2out,$<,$@,boot) $(builddir)/units/cron-%.target: $(srcdir)/units/cron-schedule.target.in $(call in2out,$<,$@,$*) $(builddir)/units/cron.target: $(srcdir)/units/cron.target.in $(call in2out,$<,$@,,$(requires)) $(builddir)/units/systemd-cron-cleaner.service: $(srcdir)/units/systemd-cron-cleaner.service.in $(call in2out,$<,$@) $(builddir)/units/systemd-cron-cleaner.timer: $(srcdir)/units/systemd-cron-cleaner.timer.in $(call in2out,$<,$@) $(builddir)/man/%: $(srcdir)/man/%.in $(call in2out,$<,$@) $(builddir)/man/systemd.cron.7: $(srcdir)/man/systemd.cron.7.in $(call in2out,$<,$@) ifneq ($(enable_runparts),yes) sed -i $(foreach sched,$(schedules) ,-e '1i.nr timer_$(sched) 0') $@ else sed -i $(foreach sched,$(schedules) ,-e '1i.nr timer_$(sched) 1') $@ endif ifneq ($(schedules_not),) sed -i $(foreach sched,$(schedules_not),-e '1i.nr timer_$(sched) 0') $@ endif sed -i $(foreach sched,$(schedules) ,-e '1i.nr etccron_$(sched) 1') $@ ifneq ($(schedules_not),) sed -i $(foreach sched,$(schedules_not),-e '1i.nr etccron_$(sched) 0') $@ endif $(builddir)/include/part2timer.hpp : $(part2timer) LC_LANG=C sort $< | awk '/^#/ || /^$$/ {next} { print "{\"" $$1 "\"sv, \"" $$2 "\"sv}," }' > $@ $(builddir)/include/crond2timer.hpp : $(crond2timer) LC_LANG=C sort $< | awk '/^#/ || /^$$/ {next} { print "{\"" $$1 "\"sv, \"" $$2 "\"sv}," }' > $@ $(builddir)/include/configuration.hpp : $(srcdir)/include/configuration.hpp.in mkdir -p $(builddir)/include sed -e 's:\@use_loglevelmax\@:$(use_loglevelmax):' \ -e 's:\@use_runparts\@:$(if $(filter $(enable_runparts),yes),true,false):' \ -e 's:\@version\@:$(version):' \ -e 's:\@libexecdir\@:$(libexecdir):' \ -e 's:\@statedir\@:$(statedir):' \ -e 's:\@pamname\@:$(pamname):g' \ -e 's:\@have_onsuccess\@:$(if $(filter $(have_onsuccess),yes),true,false):g' $< > $@ $(builddir)/include/%.hpp: $(srcdir)/include/%.hpp cp $< $@ CXXVER := $(shell $(CXX) --version | { read -r l; echo "$$l"; }) ifneq "$(findstring clang,$(CXXVER))" "" # clang doesn't use PCHs automatically PCH_ARG := $(if $(PCH),-include-pch $(builddir)/include/libvoreutils.hpp.gch) -Wno-gcc-compat else PCH_ARG := endif common_headers := $(builddir)/include/configuration.hpp $(builddir)/include/libvoreutils.hpp$(if $(PCH),.gch) $(builddir)/include/util.hpp CFLAGS += -Wall -Wextra -fno-exceptions -Wno-psabi CXXFLAGS += -Wall -Wextra -fno-exceptions -Wno-psabi $(builddir)/include/libvoreutils.hpp.gch : $(builddir)/include/libvoreutils.hpp $(CXX) $(CXXFLAGS) $(CPPFLAGS) -std=c++20 -I $(builddir)/include $< -o $@ $(builddir)/bin/systemd-crontab-generator: $(srcdir)/bin/systemd-crontab-generator.cpp $(common_headers) $(builddir)/include/part2timer.hpp $(builddir)/include/crond2timer.hpp $(CXX) $(CXXFLAGS) $(CPPFLAGS) -std=c++20 -I $(builddir)/include $(PCH_ARG) $< -o $@ $(LDFLAGS) @libmd@ $(builddir)/bin/crontab: $(srcdir)/bin/crontab.cpp $(common_headers) $(CXX) $(CXXFLAGS) $(CPPFLAGS) -std=c++20 -I $(builddir)/include $(PCH_ARG) $< -o $@ $(LDFLAGS) $(builddir)/bin/%: $(srcdir)/bin/%.sh $(call in2out,$<,$@) chmod +x $@ $(builddir)/bin/crontab_setgid: $(srcdir)/bin/crontab_setgid.c $(CC) $(CFLAGS) $(CPPFLAGS) $< -DCRONTAB_DIR='"$(statedir)"' -o $@ $(LDFLAGS) $(outputs): | $(builddir) $(outdir): mkdir -p $@ $(builddir): mkdir -p $@/bin $@/man $@/units $@/include $(distdir): mkdir -p $(distdir) $(tarball): distprep cd $(distdir)/..; tar -cJ --owner=root --group=root --file $(tarball) $(distname) .PHONY: all clean dist distprep install systemd-cron-2.5.1/README.md000066400000000000000000000174741475512744600154610ustar00rootroot00000000000000systemd-cron ================ [systemd][1] units to run [cron][2] scripts Description --------------- systemd units to provide cron daemon functionality by running scripts in cron directories. The crontabs are automatically translated using /usr/lib/systemd/system-generators/systemd-crontab-generator. Usage --------- Add executable scripts to the appropriate cron directory (e.g. `/etc/cron.daily`) and enable systemd-cron: # systemctl daemon-reload # systemctl enable cron.target # systemctl start cron.target The project also includes simple crontab command equivalent, which behaves like standard crontab command (and accepts the same main options). The scripts should now be automatically run by systemd. See man:systemd.cron(7) for more information. Dependencies ---------------- * systemd ≥ 236 * UsrMerged system * C and C++20 compilers * libmd (-lmd) * pkgconf (optional) * support for /usr/lib/sysusers.d/*.conf (optional) * [run-parts][3] (optional, disabled by default) * sendmail from $SENDMAIL or in $PATH or in /usr/sbin:/usr/lib (optional, evaluated at runtime) Dependencies history ------------------------ * systemd ≥ 197, first support for timers * systemd ≥ 209, yearly timers * systemd ≥ 212, persistent timers * systemd ≥ 217, minutely, quarterly & semi-annually timers * systemd ≥ 228, `RemainAfterElapse` option, might be useful for @reboot jobs * systemd ≥ 229, real random delay support with `RandomizedDelaySec` option [(bug)][90] * systemd ≥ 233, support for new `~` = 'last day of month' and '9..17/2' .timer syntax * systemd ≥ 235, support for timezones * systemd ≥ 236, `LogLevelMax` option * systemd ≥ 236, `StandardInputData` which is needed for '%' support. * systemd ≥ 251, OnFailure handler receives $MONITOR_UNIT and more. * systemd ≥ 255, `SetLoginEnvironment`: no change needed. * systemd ≥ 256, `run0`. Installation ---------------- There exists packages available for: * [Debian][7] * [Ubuntu][8] * [Arch][9] * [Gentoo][10] * [OpenMamba][11] (RPM based) There is also a old .spec file for Fedora in [contrib/][12]. Using [`contrib/sendmail-ala-vixie`][14] and the right `CRON_MAIL_*=` configuration, the e-mail format and firing conditions can be matched to vixie cron precisely. A complete list of all packages can be browsed at [Repology][13]. You can also build it manually from source. Packaging -------------- ### Building $ ./configure $ make ### Staging $ make DESTDIR="$destdir" install ### Configuration The `configure` script takes command line arguments to configure various details of the build. The following options follow the standard GNU [installation directories][4]: * `--prefix=` * `--bindir=` * `--datadir=` * `--libdir=` * `--libexecdir=` * `--statedir=` * `--mandir=` * `--docdir=` Other options include (`pkgconf` may be overridden with `$PKG_CONFIG`): * `--enable-runparts[=yes|no]` Use static units for /etc/cron.{hourly,daily,...} Default: `no`. * `--unitdir=` Path to systemd unit files. Default: `pkgconf systemd --variable=systemdsystemunitdir` or `/systemd/system`. * `--generatordir=` Path to systemd generators. Default: `pkgconf systemd --variable=systemdsystemgeneratordir` or `/systemd/system-generators`. * `--sysusersdir=` Path to systemd-sysusers snippets. Default: `pkgconf systemd --variable=sysusersdir` or `/sysusers.d`. * `--enable-boot[=yes|no]` Include support for the boot timer. Default: `yes`. * `--enable-minutely[=yes|no]` Include support for the minutely timer. Default: `no`. * `--enable-hourly[=yes|no]` Include support for the hourly timer. Default: `yes`. * `--enable-daily[=yes|no]` Include support for the daily timer. Default: `yes`. * `--enable-weekly[=yes|no]` Include support for the weekly timer. Default: `yes`. * `--enable-monthly[=yes|no]` Include support for the monthly timer. Default: `yes`. * `--enable-quarterly[=yes|no]` Include support for the quarterly timer. Default: `no`. * `--enable-semi_annually[=yes|no]` Include support for the semi-annually timer. Default: `no`. * `--enable-yearly[=yes|no]` Include support for the yearly timer. Default: `yes`. * `--libmd=` Compiler and linker flags required to build and link to libmd. Default: `pkgconf --cflags --libs libmd` or `-lmd`. * `--with-part2timer=file` Mapping from basename in /etc/cron.{daily,weekly,etc.) to unit name. Default: `/dev/null`. * `--with-crond2timer=file` Mapping from basename in /etc/cron.d to unit name. Default: `/dev/null`. * `--with-pamname=pamconfigname` Name of /etc/pam.d script to put into `PAMName=` directives. Default: not generated. If set, also forces `KillMode=control-group` instead of the default `=process`. * `--with-version=ver` Downstream version. Default: contents of `VERSION`. A typical configuration for the latest systemd would be: $ ./configure (the default settings are a common ground between what is seen on current Arch/Debian/Gentoo packaging) Alternatively you can also generate static .timer/.service to serialize the jobs in /etc/cron.{hourly,daily,weekly,monthly,...} through run-parts: $ ./configure --enable-runparts=yes `part2timer` and `crond2timer` are in the format basenameunitbasename[whatever[... (empty lines and start-of-line `#`-comments permitted; unitbasename doesn't include ".timer") and may be useful when, for example, `/etc/cron.daily/plocate` has a timer called `plocate-updatedb.timer` or `/etc/cron.d/ntpsec` has a timer called `ntpsec-rotate-stats.timer`: without the override, the jobs would run twice since native-timer detection would be looking for `plocate.timer` and `ntpsec.timer`. If there is already a perfect 1:1 mapping between `/etc/cron./` and `/usr/lib/systemd/system/.timer`, then it is not needed to add an entry to these tables. If your compiler's [PCH compilation is broken](https://github.com/systemd-cron/systemd-cron/issues/141), build with `make PCH=`. ### Caveat Your package should also run these extra commands before starting cron.target to ensure that @reboot scripts doesn't trigger right away: # touch /run/crond.reboot # touch /run/crond.bootdir See Also ------------ `systemd.cron(7)` or in source tree `man -l src/man/systemd.cron.7` License ----------- The project is licensed under MIT. It vendors a derived work of voreutils, which is available under the 0BSD licence. Copyright ------------- © 2014, Dwayne Bent : original package with static units © 2014, Konstantin Stepanov (me@kstep.me) : author of crontab [generator][6] © 2014, Daniel Schaal : review of crontab generator © 2014-2024, Alexandre Detiste (alexandre@detiste.be) : maintainer © 2023-2025, наб (nabijaczleweli@nabijaczleweli.xyz) : rewrite of generator in C++, maintainer [1]: http://www.freedesktop.org/wiki/Software/systemd/ "systemd" [2]: http://en.wikipedia.org/wiki/Cron "cron" [3]: https://tracker.debian.org/pkg/debianutils "debianutils" [4]: https://www.gnu.org/prep/standards/html_node/Directory-Variables.html "Directory Variables" [5]: http://www.freedesktop.org/software/systemd/man/systemd.timer.html#Persistent= "systemd.timer" [6]: https://github.com/kstep/systemd-crontab-generator "crontab generator" [7]: http://packages.debian.org/systemd-cron [8]: http://packages.ubuntu.com/search?suite=all&searchon=names&keywords=systemd-cron [9]: https://aur.archlinux.org/packages/systemd-cron [10]: https://packages.gentoo.org/package/sys-process/systemd-cron [11]: https://openmamba.org/en/packages/?tag=devel&pkg=systemd-cron.source [12]: https://github.com/systemd-cron/systemd-cron/blob/master/contrib/systemd-cron.spec [13]: https://github.com/systemd-cron/systemd-cron/blob/master/contrib/sendmail-ala-vixie [14]: https://repology.org/project/systemd-cron/packages [90]: https://bugs.freedesktop.org/show_bug.cgi?id=82084 systemd-cron-2.5.1/VERSION000066400000000000000000000000061475512744600152310ustar00rootroot000000000000002.5.1 systemd-cron-2.5.1/configure000077500000000000000000000146071475512744600161040ustar00rootroot00000000000000#!/bin/sh # shellcheck disable=SC2034 # We expand enable_* variables via eval set -e set -u pkgconf="${PKG_CONFIG-pkgconf}" # SC2016: the variable expansion is handled by Make prefix='/usr' # shellcheck disable=SC2016 bindir='$(prefix)/bin' # shellcheck disable=SC2016 datadir='$(prefix)/share' # shellcheck disable=SC2016 libdir='$(prefix)/lib' # shellcheck disable=SC2016 libexecdir='$(prefix)/libexec' statedir='/var/spool/cron' # shellcheck disable=SC2016 mandir='$(datadir)/man' # shellcheck disable=SC2016 docdir='$(datadir)/doc/systemd-cron' # shellcheck disable=SC2016 unitdir="$($pkgconf systemd --variable=systemdsystemunitdir)" 2>/dev/null || \ unitdir='$(libdir)/systemd/system' # shellcheck disable=SC2016 generatordir="$($pkgconf systemd --variable=systemdsystemgeneratordir)" 2>/dev/null || \ generatordir='$(libdir)/systemd/system-generators' # shellcheck disable=SC2016 sysusersdir="$($pkgconf systemd --variable=sysusersdir)" 2>/dev/null || \ sysusersdir='$(libdir)/sysusers.d' libmd="$($pkgconf --cflags --libs libmd)" 2>/dev/null || \ libmd='-lmd' enable_runparts=no use_loglevelmax=no part2timer=/dev/null crond2timer=/dev/null pamname= read -r version < VERSION enable_boot=yes enable_minutely=no enable_hourly=yes enable_daily=yes enable_weekly=yes enable_monthly=yes enable_quarterly=no enable_semi_annually=no enable_yearly=yes # TODO: roll back to straight OnSuccess= if we ever bump past the systemd ≥ 236 requirement (this is 249); cf. #165 have_onsuccess="$($pkgconf systemd --max-version=249 && echo no)" 2>/dev/null || \ have_onsuccess=yes orig_args="$*" ARGS=$(getopt -n "$(basename "${0}")" -o '' -l ' help::, prefix:, bindir:, confdir:, datadir:, libdir:, libexecdir:, statedir:, mandir:, docdir:, unitdir:, generatordir:, sysusersdir:, enable-boot::, enable-minutely::, enable-hourly::, enable-daily::, enable-weekly::, enable-monthly::, enable-quarterly::, enable-semi_annually::, enable-yearly::, enable-runparts::, use-loglevelmax::, with-part2timer:, with-crond2timer:, with-pamname:, with-version:, ' -- "${@}") usage() { grep ^'### Configuration' README.md -A 50 } # shellcheck disable=SC2181 if [ $? -ne 0 ] then usage exit 1 fi set_enable_flag() { if [ -z "${2}" ] then eval "enable_${1}=yes" elif [ "${2}" = 'yes' ] || [ "${2}" = 'no' ]; then eval "enable_${1}=${2}" else echo "ERROR: Unknown value for enable-${1}: '${2}'. Expected 'yes' or 'no'." fi } eval set -- "${ARGS}" while true; do case "${1}" in '--help') usage exit 0;; '--prefix') prefix="${2}" shift 2;; '--bindir') bindir="${2}" shift 2;; '--confdir') echo "--confdir ${2} is ignored" shift 2;; '--datadir') datadir="${2}" shift 2;; '--libdir') libdir="${2}" shift 2;; '--libexecdir') libexecdir="${2}" shift 2;; '--statedir') statedir="${2}" shift 2;; '--mandir') mandir="${2}" shift 2;; '--docdir') docdir="${2}" shift 2;; '--unitdir') unitdir="${2}" shift 2;; '--generatordir') generatordir="${2}" shift 2;; '--sysusersdir') sysusersdir="${2}" shift 2;; '--libmd') libmd="${2}" shift 2;; '--enable-boot') set_enable_flag boot "${2}" shift 2;; '--enable-minutely') set_enable_flag minutely "${2}" shift 2;; '--enable-hourly') set_enable_flag hourly "${2}" shift 2;; '--enable-daily') set_enable_flag daily "${2}" shift 2;; '--enable-weekly') set_enable_flag weekly "${2}" shift 2;; '--enable-monthly') set_enable_flag monthly "${2}" shift 2;; '--enable-quarterly') set_enable_flag quarterly "${2}" shift 2;; '--enable-semi_annually') set_enable_flag semi_annually "${2}" shift 2;; '--enable-yearly') set_enable_flag yearly "${2}" shift 2;; '--enable-runparts') set_enable_flag runparts "${2}" shift 2;; '--use-loglevelmax') case "${2}" in 'alert'|'crit'|'err'|'warning'|'notice'|'info'|'debug') use_loglevelmax="${2}";; *) echo "ERROR: Unknown value for use-loglevelmax: '${2}'. Expected 'alert', 'crit', 'err', 'warning', 'notice', 'info', or 'debug'.";; esac shift 2;; '--with-part2timer') part2timer="${2}" shift 2;; '--with-crond2timer') crond2timer="${2}" shift 2;; '--with-pamname') pamname="${2}" shift 2;; '--with-version') version="${2}" shift 2;; '--') shift break;; esac done check_sched_enabled() { if eval [ "\"\${enable_${1}}\"" = \'yes\' ]; then schedules="${schedules} ${2}" else schedules_not="${schedules_not} ${2}" fi } schedules="" schedules_not="" check_sched_enabled boot boot check_sched_enabled minutely minutely check_sched_enabled hourly hourly check_sched_enabled daily daily check_sched_enabled weekly weekly check_sched_enabled monthly monthly check_sched_enabled quarterly quarterly check_sched_enabled semi_annually semi-annually check_sched_enabled yearly yearly exec 2>&1 set -x sed " 1i\ # Generated by $0 ${orig_args} s|@schedules@|${schedules}|g s|@schedules_not@|${schedules_not}|g s|@enable_runparts@|${enable_runparts}|g s|@prefix@|${prefix}|g s|@bindir@|${bindir}|g s|@datadir@|${datadir}|g s|@libdir@|${libdir}|g s|@libexecdir@|${libexecdir}|g s|@statedir@|${statedir}|g s|@mandir@|${mandir}|g s|@docdir@|${docdir}|g s|@unitdir@|${unitdir}|g s|@generatordir@|${generatordir}|g s|@sysusersdir@|${sysusersdir}|g s|@libmd@|${libmd}|g s|@use_loglevelmax@|${use_loglevelmax}|g s|@part2timer@|${part2timer}|g s|@crond2timer@|${crond2timer}|g s|@pamname@|${pamname}|g s|@version@|${version}|g s|@have_onsuccess@|${have_onsuccess}|g " Makefile.in > Makefile systemd-cron-2.5.1/contrib/000077500000000000000000000000001475512744600156255ustar00rootroot00000000000000systemd-cron-2.5.1/contrib/0wait000077500000000000000000000022211475512744600165740ustar00rootroot00000000000000#!/bin/sh # wait for local apt-cacher-ng proxy to come online # before starting unattended-upgrades # (that may well never happen before shutdown, # but can happen later on the same day) # # /etc/cron.daily/0wait rollback() { echo "0wait aborted" touch -t 201401010000 /var/lib/systemd/timers/stamp-cron-daily.timer sleep 1 killall run-parts exit 0 } trap "rollback" TERM INT QUIT HUP while true do ping -qc 1 antec > /dev/null && exit 0 sleep 300 done # there should be a more systemd-ish way to do this # #systemd # |-atd -f # |-dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation # |-fancontrol /usr/sbin/fancontrol # | `-sleep 60 # |-kdm -nodaemon # | |-Xorg :0 vt7 -br -nolisten tcp -auth /var/run/xauth/A:0-86s7nb # | `-kdm # | `-kdm_greet # | `-{QProcessManager} # |-nullmailer-send # |-run-parts --report /etc/cron.daily # | `-0wait /etc/cron.daily/0wait # | `-sleep 300 # |-sshd # | `-sshd # | `-su - # | `-bash # | `-pstree -a # |-systemd --user # | `-(sd-pam) # |-systemd-journal # |-systemd-logind # `-systemd-udevd systemd-cron-2.5.1/contrib/census.txt000066400000000000000000000014461475512744600176730ustar00rootroot00000000000000# census on how s-d-c is configured on various distros # # https://repology.org/project/systemd-cron/packages --statedir Arch: /var/spool/cron/ Debian: /var/spool/cron/crontabs/ (managed by cron-daemon-common) Fedora: /var/spool/cron/ Gentoo: /var/spool/cron/crontabs/ (has sys-process/cronbase) OpenMamba: /var/spool/cron --enable-boot Arch: yes Debian: no Fedora: ? Gentoo: (optional) OpenMamba: yes --enable-runparts: Arch: no Debian: no Fedora: ? Gentoo: (optional) OpenMamba: yes creation of a system group for the setgid helper Arch: ? Debian: managed by cron-daemon-common Fedora: ? Gentoo: ? OpenMamba: ? use systemd per-user instances: Arch: no Debian: no Fedora: no Gentoo: no OpenMamba: no systemd-cron-2.5.1/contrib/sendmail-ala-vixie000077500000000000000000000023751475512744600212330ustar00rootroot00000000000000#!/bin/sh # https://github.com/systemd-cron/systemd-cron/issues/166 # # sd-cron default: # From: root # Subject: [$(hostname)] Failure: $USER's crontab "* * * * * cron-command" # # vixie cron: # From: (Cron Daemon) <$USER@$(hostname -f)> # Subject: Cron <$USER@$(hostname)> cron-command # # $user inherited in the environment from mail_for_job.sh fullname="$(hostname -f)" while read -r header rest; do case "$header" in "") echo exec cat ;; From:) printf 'From: (Cron Daemon) <%s@%s>\n' "$user" "$fullname" ;; Subject:) rest="${rest%\"}" rest="${rest#*\"}" # down to '0 0 * * * cd /home/...' or '@daily cd /home/...' if [ "${rest#@}" != "$rest" ]; then rest="${rest#*[ ]}" else rest="${rest#*[ ]}"; rest="${rest#*[! ]}" rest="${rest#*[ ]}"; rest="${rest#*[! ]}" rest="${rest#*[ ]}"; rest="${rest#*[! ]}" rest="${rest#*[ ]}"; rest="${rest#*[! ]}" rest="${rest#*[ ]}" fi while [ "${rest#[ ]}" != "$rest" ]; do rest="${rest#[ ]}" done printf 'Subject: Cron <%s@%s> %s\n' "$user" "${fullname%%.*}" "$rest" ;; *) printf '%s %s\n' "$header" "$rest" ;; esac done | "$(command -v sendmail || command -v /usr/sbin/sendmail || command -v /usr/lib/sendmail)" "$@" systemd-cron-2.5.1/contrib/sendmail-apprise000077500000000000000000000003511475512744600210070ustar00rootroot00000000000000#!/bin/sh from= subject= while read -r header rest; do case "$header" in "" ) break ;; From: ) from="$rest" ;; Subject:) subject="$rest" ;; esac done exec apprise -vv -t "$subject – $from" -b "$(cat)" systemd-cron-2.5.1/contrib/sendmail-cat000077500000000000000000000003171475512744600201150ustar00rootroot00000000000000#!/bin/sh # this is a trivial fake sendmail for development of "mail_for_job" tool # export SENDMAIL=$PWD/contrib/sendmail-cat # out/build/bin/mail_for_job cron-monthly-check-dfsg-status.service exec cat systemd-cron-2.5.1/contrib/sendmail-matrix000077500000000000000000000014571475512744600206600ustar00rootroot00000000000000#!/bin/sh TOKEN=systemd-cron # probably source this from a file only readable by user _cron-failure, and don't make it "systemd-cron" # The TO stanza should look something like # TO='!QQQQQqqQQqQQqqQQqq:qqqqqqqqqqqqqq.qqq@https://matrix.qqqqqqqqqqqqqq.qqq:8448' from= to= subject= while read -r header rest; do case "$header" in "" ) break ;; From: ) from="$rest" ;; To: ) to="$rest" ;; Subject:) subject="$rest" ;; esac done room="${to%@*}" server="${to#*@}" content="$(jq -Rs --arg from "$from" --arg subject "$subject" '$subject + " ‒ " + $from + "\n\n" + .')" exec curl --fail-with-body --no-progress-meter --oauth2-bearer "$TOKEN" \ --data-binary '{"msgtype": "m.text", "body": '"$content"'}' -XPUT "$server/_matrix/client/r0/rooms/$room/send/m.room.message/$$" systemd-cron-2.5.1/contrib/systemd-cron.cron.weekly000066400000000000000000000001751475512744600224410ustar00rootroot00000000000000#!/bin/sh test -x /usr/libexec/systemd-cron/remove_stale_stamps || exit 0 exec /usr/libexec/systemd-cron/remove_stale_stamps systemd-cron-2.5.1/contrib/systemd-cron.spec000066400000000000000000000066361475512744600211430ustar00rootroot00000000000000# https://fedoraproject.org/wiki/Packaging:ScriptletSnippets#Systemd # https://github.com/systemd/systemd/blob/42911a567dc22c3115fb3ee3c56a7dcfb034f102/src/core/macros.systemd.in # "If your package includes one or more systemd units that need # to be enabled by default on package installation, # they must be covered by the Fedora preset policy." Name: systemd-cron Version: 2.5.0 Release: 1 License: MIT Summary: systemd units to provide cron daemon & anacron functionality Url: https://github.com/systemd-cron/systemd-cron/ Group: System Environment/Base Source: https://github.com/systemd-cron/systemd-cron/archive/v%{version}.tar.gz Provides: cronie Provides: cronie-anacron Conflicts: cronie Conflicts: cronie-anacron BuildRoot: %{_tmppath}/%{name}-%{version}-build Requires: crontabs Requires: systemd %description Provides systemd units to run cron jobs in /etc/cron.hourly cron.daily cron.weekly and cron.monthly directories, without having cron or anacron installed. It also provides a generator that dynamically translate /etc/crontab, /etc/cron.d/* and user cronjobs in systemd units. %pre touch /run/crond.reboot %preun %systemd_preun cron.target %post # XXX this macro doesn't seems to do anything %systemd_post cron.target if [ $1 -eq 1 ] ; then systemctl daemon-reload systemctl enable cron.target systemctl start cron.target fi %postun %systemd_postun_with_restart cron.target %prep %setup -q %build ./configure \ --enable-boot=no \ --enable-runparts make %install make DESTDIR=$RPM_BUILD_ROOT install sed -i '/Persistent=true/d' $RPM_BUILD_ROOT/usr/lib/systemd/system/cron-hourly.timer mkdir -p $RPM_BUILD_ROOT/var/spool/cron mkdir -p $RPM_BUILD_ROOT/etc/cron.d/ mkdir -p $RPM_BUILD_ROOT/etc/cron.weekly/ cp contrib/systemd-cron.cron.weekly $RPM_BUILD_ROOT/etc/cron.weekly/systemd-cron mkdir -p $RPM_BUILD_ROOT/usr/lib/systemd/system-preset/ echo 'enable cron.target' > $RPM_BUILD_ROOT/usr/lib/systemd/system-preset/50-systemd-cron.preset %files %license LICENSE %doc README.md CHANGELOG %dir /etc/cron.d/ /etc/cron.weekly/ /usr/bin/crontab /usr/libexec/systemd-cron/mail_for_job /usr/libexec/systemd-cron/boot_delay /usr/libexec/systemd-cron/remove_stale_stamps /usr/libexec/systemd-cron/crontab_setgid /usr/lib/systemd/system-preset/50-systemd-cron.preset /usr/lib/systemd/system/cron.target /usr/lib/systemd/system/cron-weekly.service /usr/lib/systemd/system/cron-update.path /usr/lib/systemd/system/cron-monthly.timer /usr/lib/systemd/system/cron-hourly.target /usr/lib/systemd/system/cron-weekly.timer /usr/lib/systemd/system/cron-monthly.service /usr/lib/systemd/system/cron-weekly.target /usr/lib/systemd/system/cron-mail@.service /usr/lib/systemd/system/cron-daily.timer /usr/lib/systemd/system/cron-daily.service /usr/lib/systemd/system/cron-daily.target /usr/lib/systemd/system/cron-hourly.service /usr/lib/systemd/system/cron-update.service /usr/lib/systemd/system/cron-hourly.timer /usr/lib/systemd/system/cron-monthly.target /usr/lib/systemd/system/cron-yearly.service /usr/lib/systemd/system/cron-yearly.target /usr/lib/systemd/system/cron-yearly.timer /usr/lib/systemd/system-generators/systemd-crontab-generator /usr/lib/sysusers.d/systemd-cron.conf %{_mandir}/man1/crontab.* %{_mandir}/man5/crontab.* %{_mandir}/man5/anacrontab.* %{_mandir}/man7/systemd.cron.* %{_mandir}/man8/systemd-crontab-generator.* %dir /var/spool/cron systemd-cron-2.5.1/contrib/zz_desktop_notification000077500000000000000000000045511475512744600225220ustar00rootroot00000000000000#!/bin/bash # # this is a sample script that show a desktop notification # when your system upgrades are done # # a better GUI to systemd timers (like the KDE Weather/Calendar applet) # would be better # # in run-parts mode this would have gone to /etc/cron.daily/ # with a split cron-daily, it should be appended # to apt-daily-{upgrade}.service like this: # ExecStartPost=/etc/site/zz_desktop_notification # set -e [ -e /var/log/unattended-upgrades/unattended-upgrades.log ] || exit 0 line=$(grep "will be upgrade" /var/log/unattended-upgrades/unattended-upgrades.log | tail -n 1) if [ -z "$line" -a -e /var/log/unattended-upgrades/unattended-upgrades.log.1.gz ] then line=$(zgrep "will be upgraded" /var/log/unattended-upgrades/unattended-upgrades.log.1.gz | tail -n 1) fi updated=$(echo "$line" | grep ^$(date +%Y-%m-%d) | cut -d: -f4) # reboot-required isn't broad enough to my taste (mostly only kernel); # these are the usual suspects that might make a system # unbootable or make X doesn't come up for a in $updated; do if [[ $a == *grub* ]]; then grub="$grub $a"; fi if [[ $a == *systemd* ]]; then systemd="$systemd $a"; fi if [[ $a == *radeon* ]]; then radeon="$radeon $a"; fi if [[ $a == *mesa* ]]; then mesa="$mesa $a"; fi done; if [ -e /var/run/reboot-required -o -n "$grub" -o -n "$systemd" -o -n "$radeon" -o -n "$mesa" ] then # check if user is active if [ -n "$(users)" ] then echo "grub:$grub" echo "systemd:$systemd" echo "radeon:$radeon" echo "mesa:$mesa" echo "user(s) active:" who users=$(loginctl list-sessions | grep seat0 | awk '{print $3}') if [ -n "$users" ] then for user in $users do export DISPLAY=:0 su "$user" -c "notify-send 'REBOOT NEEDED' -i dialog-warning \"$updated\" " done else echo "REBOOT NEEDED: $updated" | wall fi else # reboot after 1 minute shutdown --reboot fi else # check if a graphical session exists users=$(loginctl list-sessions | grep seat0 | awk '{print $3}') if [ -n "$users" ] then export DISPLAY=:0 if [ -n "$updated" ] then for user in $users do echo "$user" su "$user" -c "notify-send 'daily upgrade completed' -i dialog-information \"$updated\" " done else for user in $users do su "$user" -c "notify-send 'cron-daily finished' -i dialog-information 'nothing to update today'" done fi fi fi systemd-cron-2.5.1/src/000077500000000000000000000000001475512744600147545ustar00rootroot00000000000000systemd-cron-2.5.1/src/bin/000077500000000000000000000000001475512744600155245ustar00rootroot00000000000000systemd-cron-2.5.1/src/bin/boot_delay.sh000077500000000000000000000003311475512744600202010ustar00rootroot00000000000000#!/bin/sh if ! { [ $# -eq 1 ] && delay=$(( $1 * 60 )); }; then printf 'Usage: %s minutes\n' "$0" exit 1 fi IFS=".$IFS" read -r uptime _ < /proc/uptime [ "$uptime" -ge "$delay" ] || exec sleep $(( delay - uptime )) systemd-cron-2.5.1/src/bin/crontab.cpp000066400000000000000000000517011475512744600176640ustar00rootroot00000000000000#include "configuration.hpp" #include "libvoreutils.hpp" #include "util.hpp" #include #include #define CRONTAB_DIR STATEDIR static const char * self; static const bool HAVE_SETGID = [] { struct stat sb; return geteuid() != 0 && !stat(SETGID_HELPER, &sb) && S_ISREG(sb.st_mode) && sb.st_uid == 0 && sb.st_gid != 0 && sb.st_mode & S_ISGID && sb.st_mode & S_IXGRP; }(); template static auto exec(const char * prog, A... args) -> int { execl(prog, self, static_cast(args)..., static_cast(nullptr)); auto exec_err = errno; std::fprintf(stderr, "%s: %s: %s\n", self, prog, std::strerror(exec_err)); return exec_err == ENOENT ? 127 : 126; } static const std::string_view current_user = getpass_getlogin(); static __attribute__((format(printf, 1, 2))) auto confirm(const char * fmt, ...) -> bool { for(char buf[128];;) { va_list args; va_start(args, fmt); std::vfprintf(stderr, fmt, args); va_end(args); if(!std::fgets(buf, sizeof(buf), stdin)) return false; if(int resp = rpmatch(buf); resp != -1) return resp; } } static auto copy_FILE(FILE * from, FILE * to) -> void { std::uint8_t buf[4096]; for(ssize_t rd; !std::feof(from) && (rd = std::fread(buf, 1, sizeof(buf), from));) std::fwrite(buf, 1, rd, to); } static const char * const generator_path = std::getenv("SYSTEMD_CRON_GENERATOR") ?: "/usr/lib/systemd/system-generators/systemd-crontab-generator"; static auto run_generator(const char * op, const char * file_or_line, bool file_is_file) -> int { vore::file::FILE copy; if(file_is_file) if(struct stat sb; file_or_line == "-"sv && (fstat(0, &sb) || !S_ISREG(sb.st_mode))) { if(!(copy = vore::file::FILE::tmpfile())) return std::fprintf(stderr, "%s: %s\n", self, std::strerror(errno)), 1; copy_FILE(stdin, copy); std::fflush(copy); std::rewind(copy); } switch(pid_t child = vfork()) { case -1: return std::fprintf(stderr, "%s: couldn't create child: %s\n", self, std::strerror(errno)), 125; case 0: // child if(copy) dup2(fileno(copy), 0); _exit(exec(generator_path, op, file_or_line)); default: { // parent int childret; while(waitpid(child, &childret, 0) == -1 && errno == EINTR) // no other errors possible ; return WIFSIGNALED(childret) ? 128 + WTERMSIG(childret) : WEXITSTATUS(childret); } } } static const bool want_colour = !*(std::getenv("NO_COLOR") ?: "") && (std::getenv("TERM") ?: ""sv).find("color"sv) != std::string_view::npos && isatty(1); #define COLOUR_GREEN "\033[1;32m" #define COLOUR_YELLOW "\033[1;33m" #define COLOUR_BLUE "\033[1;34m" #define COLOUR_RESET "\033[0m" static auto coloured(const std::string_view & data, const char * colour) -> void { if(colour) std::fputs(colour, stdout); std::fwrite(data.data(), 1, data.size(), stdout); if(colour) std::fputs(COLOUR_RESET, stdout); } static auto translate(const char * line) -> int { close(3); auto timer = vore::file::FILE::tmpfile(); if(!timer) return std::fprintf(stderr, "%s: %s\n", self, std::strerror(errno)), 1; assert(fileno(timer) == 3); close(4); auto service = vore::file::FILE::tmpfile(); if(!service) return std::fprintf(stderr, "%s: %s\n", self, std::strerror(errno)), 1; assert(fileno(service) == 4); auto err = run_generator("--translate", line, false); if(err) return err; coloured("# /etc/systemd/system/$unit.timer\n"sv, want_colour ? COLOUR_BLUE : nullptr); std::rewind(timer); copy_FILE(timer, stdout); std::fputc('\n', stdout); coloured("# /etc/systemd/system/$unit.service\n"sv, want_colour ? COLOUR_BLUE : nullptr); std::rewind(service); copy_FILE(service, stdout); std::fflush(stdout); // further optional analysis: // don't run if /dev/fd/3 doesn't exist (<=> /proc not mounted on Linux) if(access("/dev/fd/3", R_OK)) return 0; // ignore execlp() return if sd-analyze unavailable // oddly, missing /run/systemd is perfectly okay std::rewind(timer); std::rewind(service); // TODO: roll back to straight execlp() if we ever bump past the systemd ≥ 236 requirement (this is 250); cf. #161 // execlp("systemd-analyze", "systemd-analyze", "verify", "/dev/fd/3:input.timer", "/dev/fd/4:input.service", static_cast(nullptr)); execl("/bin/sh", "sh", "-c", "systemd-analyze --version 2>/dev/null |" "{ read -r _ v _ || v=0; [ \"$v\" -lt 250 ] || exec systemd-analyze verify /dev/fd/3:input.timer /dev/fd/4:input.service; }", static_cast(nullptr)); return 0; } static auto check(const char * file) -> bool { return run_generator("--check", file, true) == 0; } static auto test(const char * file) -> bool { auto rc = check(file); if(rc) std::puts("No syntax issues were found in the crontab file."); else std::puts("Invalid crontab file. Syntax issues were found."); return !rc; } static auto version() -> int { std::puts(VERSION); return 0; } // try to fix things up if running as root static auto try_chmod(const char * cron_file = nullptr, const char * user = nullptr) -> void { struct stat sb; if(stat(SETGID_HELPER, &sb)) return; if(!chown(CRONTAB_DIR, 0, sb.st_gid)) (void)chmod(CRONTAB_DIR, 01730); // rwx-wx--T if(cron_file && user) if(auto ent = getpwnam(user)) if(!chown(cron_file, ent->pw_uid, sb.st_gid)) (void)chmod(cron_file, 00600); // rw------- } // Divide the crontab into three colour-coded sexions: // blue for comments (metadata for the user) // green for time specs (metadata for cron) // yellow for variable names // default for everything else (actual data) static auto colour_crontab(FILE * f) -> void { char * line_raw{}; std::size_t linecap{}; for(ssize_t len; (len = getline(&line_raw, &linecap, f)) != -1;) { std::string_view line{line_raw, static_cast(len)}; const char * colour{}; if(line[0] == '#') colour = COLOUR_BLUE; else if(regmatch_t matches[2]{{.rm_so = 0, .rm_eo = static_cast(line.size())}}; !regexec(&ENVVAR_RE, line.data(), sizeof(matches) / sizeof(*matches), matches, REG_STARTEND)) { coloured({&line[matches[1].rm_so], &line[matches[1].rm_eo]}, COLOUR_YELLOW); line.remove_prefix(matches[1].rm_eo); } else { vore::soft_tokenise tokens{line, " \t\n"sv}; auto cur = std::begin(tokens); if(cur != std::end(tokens) && (*cur)[0] == '@') { // @daily echo dupa coloured(*cur, COLOUR_GREEN); line.remove_prefix(cur->size()); } else { // 0 * * * * echo dupa for(auto i = 0u; i < 5; ++i) { if(cur != std::end(tokens)) ++cur; } if(cur != std::end(tokens)) { std::string_view timespec{line.data(), cur->data()}; coloured(timespec, COLOUR_GREEN); line.remove_prefix(timespec.size()); } } } coloured(line, colour); } } static auto list(const char * cron_file, const char * user) -> int { if(vore::file::FILE f{cron_file, "r"}) { if(!want_colour) copy_FILE(f, stdout); else colour_crontab(f); check(cron_file); try_chmod(cron_file, user); return 0; } auto err = errno; if(err == ENOENT) return std::fprintf(stderr, "no crontab for %s\n", user), 1; if(user != current_user) return std::fprintf(stderr, "you can not display %s's crontab\n", user), 1; if(HAVE_SETGID) { if(!want_colour) return exec(SETGID_HELPER, "r"); int pipe[2]; if(pipe2(pipe, O_CLOEXEC)) return std::fprintf(stderr, "%s: %s\n", self, std::strerror(errno)), 125; switch(pid_t child = vfork()) { case -1: return std::fprintf(stderr, "%s: couldn't create child: %s\n", self, std::strerror(errno)), 125; case 0: // child dup2(pipe[1], 1); _exit(exec(SETGID_HELPER, "r")); default: { // parent close(pipe[1]); vore::file::FILE f{pipe[0], "r"}; if(!f) return std::fprintf(stderr, "%s\n", std::strerror(err)), 1; colour_crontab(f); int childret; while(waitpid(child, &childret, 0) == -1 && errno == EINTR) // no other errors possible ; return WIFSIGNALED(childret) ? 128 + WTERMSIG(childret) : WEXITSTATUS(childret); } } } return std::fprintf(stderr, "%s\n", std::strerror(err)), 1; } static auto remove(const char * cron_file, const char * user, bool ask) -> int { try_chmod(); if(ask && !confirm("Are you sure you want to delete %s? ", cron_file)) return 0; if(!unlink(cron_file)) return 0; auto err = errno; if(err == ENOENT || (err == EROFS && !access(cron_file, F_OK))) return std::fprintf(stderr, "no crontab for %s\n", user), 0; if(err == EROFS) return std::fprintf(stderr, "%s is on a read-only filesystem\n", cron_file), 1; if(user != current_user) return std::fprintf(stderr, "you can not delete %s's crontab\n", user), 1; if(HAVE_SETGID) return exec(SETGID_HELPER, "d"); if(err == EACCES) if(!truncate(cron_file, 0)) return std::fprintf(stderr, "couldn't remove %s, wiped it instead\n", cron_file), 0; return std::fprintf(stderr, "%s\n", std::strerror(err)), 1; } namespace { struct autodeleting_path { char buf[PATH_MAX]; bool armed = true; ~autodeleting_path() { if(this->armed) unlink(this->buf); } operator char *() noexcept { return this->buf; } }; } static auto replace_crontab(const char * cron_file, const char * user, FILE * from) -> bool { autodeleting_path final_tmp_path{.buf = {}, .armed = false}; std::snprintf(final_tmp_path, sizeof(final_tmp_path.buf), "" CRONTAB_DIR "/%s.XXXXXX", user); vore::file::FILE final_tmp; int final_tmp_fd = mkostemp(final_tmp_path, O_CLOEXEC); if(final_tmp_fd == -1 || !(final_tmp = {final_tmp_fd, "w"})) return false; final_tmp_path.armed = true; copy_FILE(from, final_tmp); if(std::fflush(final_tmp)) return false; if(rename(final_tmp_path, cron_file)) return false; final_tmp_path.armed = false; try_chmod(cron_file, user); return true; } static auto edit(const char * cron_file, const char * user) -> int { autodeleting_path tmp_path; vore::file::FILE tmp; { std::snprintf(tmp_path, sizeof(tmp_path.buf), "%s/crontab_XXXXXX", std::getenv("TMPDIR") ?: "/tmp"); int tmp_fd = mkostemp(tmp_path, O_CLOEXEC); if(tmp_fd == -1 || !(tmp = {tmp_fd, "w+"})) return std::fprintf(stderr, "%s\n", std::strerror(errno)), 1; if(vore::file::FILE crontab{cron_file, "re"}) copy_FILE(crontab, tmp); else { int err = errno; if(err == ENOENT) std::fputs("# min hour dom month dow command\n", tmp); else if(user != current_user) return std::fprintf(stderr, "you can not edit %s's crontab\n", user), 1; else if(HAVE_SETGID) switch(pid_t child = vfork()) { case -1: return std::fprintf(stderr, "%s: couldn't create child: %s\n", self, std::strerror(errno)), 125; case 0: // child dup2(tmp_fd, 1); _exit(exec(SETGID_HELPER, "r")); default: { // parent int childret; while(waitpid(child, &childret, 0) == -1 && errno == EINTR) // no other errors possible ; childret = WIFSIGNALED(childret) ? 128 + WTERMSIG(childret) : WEXITSTATUS(childret); if(childret) { if(childret == 127 || childret == ENOENT) // ENOENT || helper returned ENOENT std::fputs("# min hour dom month dow command\n", tmp); else { // helper will send error to stderr std::fprintf(stderr, "failed to read %s\n", cron_file); return childret; } } } } else return std::fprintf(stderr, "%s: %s: %s\n", self, cron_file, std::strerror(err)), 1; } std::fflush(tmp); } switch(pid_t child = vfork()) { case -1: return std::fprintf(stderr, "%s: couldn't create child: %s\n", self, std::strerror(errno)), 125; case 0: // child _exit(exec("/bin/sh", "-c", "[ -n \"${EDITOR}\" ] && exec ${EDITOR} \"$0\"\n" "[ -n \"${VISUAL}\" ] && exec ${VISUAL} \"$0\"\n" "for e in editor vim nano mcedit; do\n" " command -v \"$e\" > /dev/null && exec \"$e\" \"$0\"\n" "done\n" "echo No editor found >&2\n" "exit 127\n", tmp_path)); default: { // parent int childret; while(waitpid(child, &childret, 0) == -1 && errno == EINTR) // no other errors possible ; childret = WIFSIGNALED(childret) ? 128 + WTERMSIG(childret) : WEXITSTATUS(childret); if(childret) { if(childret != 127) { // !ENOENT tmp_path.armed = false; std::fprintf(stderr, "edit aborted, your edit is kept here: %s\n", tmp_path.buf); } return childret; } } } tmp_path.armed = false; if(!check(tmp_path)) return std::fprintf(stderr, "not replacing crontab, your edit is kept here: %s\n", tmp_path.buf), 1; if(!std::freopen(tmp_path, "re", tmp)) { // reopen in case editor replaced the file, treat removal as cancel std::fputs("edit aborted\n", stderr); tmp_path.armed = true; return 0; } if(replace_crontab(cron_file, user, tmp)) { tmp_path.armed = true; return 0; } int err = errno; if(user != current_user) return std::fprintf(stderr, "you can not edit %s's crontab, your edit is kept here: %s\n", user, tmp_path.buf), 1; if(err == ENOSPC) return std::fprintf(stderr, "no space left in " CRONTAB_DIR ", your edit is kept here: %s\n", tmp_path.buf), 1; if(err == EROFS) return std::fprintf(stderr, "" CRONTAB_DIR " is on a read-only filesystem, your edit is kept here: %s\n", tmp_path.buf), 1; if(HAVE_SETGID) { std::rewind(tmp); switch(pid_t child = vfork()) { case -1: return std::fprintf(stderr, "%s: couldn't create child: %s\n", self, std::strerror(errno)), 125; case 0: // child dup2(fileno(tmp), 0); _exit(exec(SETGID_HELPER, "w")); default: { // parent int childret; while(waitpid(child, &childret, 0) == -1 && errno == EINTR) // no other errors possible ; childret = WIFSIGNALED(childret) ? 128 + WTERMSIG(childret) : WEXITSTATUS(childret); if(childret) return std::fprintf(stderr, "your edit is kept here: %s\n", tmp_path.buf), childret; else { tmp_path.armed = true; return 0; } } } } return std::fprintf(stderr, "unexpected error %s, your edit is kept here: %s\n", std::strerror(err), tmp_path.buf), 1; } static auto show() -> int { if(geteuid() != 0) return std::fputs("must be privileged to use -s\n", stderr), 1; for(auto && ent : vore::file::DIR{CRONTAB_DIR}) { if(getpwnam(ent.d_name)) std::puts(ent.d_name); else std::fprintf(stderr, "WARNING: crontab found with no matching user: %s\n", ent.d_name); } return 0; } static auto replace(const char * cron_file, const char * user, const char * file) -> int { vore::file::FILE from_file{file, "re"}; if(!from_file) return std::fprintf(stderr, "%s: %s: %s\n", self, file, std::strerror(errno)), 1; if(!from_file.opened) if(struct stat sb; fstat(0, &sb) || !S_ISREG(sb.st_mode)) { auto copy = vore::file::FILE::tmpfile(); if(!copy) return std::fprintf(stderr, "%s: %s\n", self, std::strerror(errno)), 1; copy_FILE(stdin, copy); std::fflush(copy); dup2(fileno(copy), 0); std::rewind(from_file); } if(!check(file)) return std::fputs("not replacing crontab\n", stderr), 1; std::rewind(from_file); if(replace_crontab(cron_file, user, from_file)) return 0; int err = errno; if(user != current_user) return std::fprintf(stderr, "you can not replace %s's crontab\n", user), 1; if(errno == ENOSPC) return std::fputs("no space left in " CRONTAB_DIR "\n", stderr), 1; if(errno == EROFS) return std::fputs("" CRONTAB_DIR "is on a read-only filesystem\n", stderr), 1; if(HAVE_SETGID) { std::rewind(from_file); switch(pid_t child = vfork()) { case -1: return std::fprintf(stderr, "%s: couldn't create child: %s\n", self, std::strerror(errno)), 125; case 0: // child if(from_file.opened) dup2(fileno(from_file), 0); _exit(exec(SETGID_HELPER, "w")); default: { // parent int childret; while(waitpid(child, &childret, 0) == -1 && errno == EINTR) // no other errors possible ; return WIFSIGNALED(childret) ? 128 + WTERMSIG(childret) : WEXITSTATUS(childret); } } } return std::fprintf(stderr, "%s: %s: %s\n", self, file, std::strerror(err)), 1; } enum class action_t : char { replace, list = 'l', remove = 'r', edit = 'e', show = 's', translate = 't', test = 'T', version = 'V' }; #define USAGE \ "usage:\n" \ " %1$s -h Show this help message and exit.\n" \ " %1$s -s Show all user who have a crontab. (only for root)\n" \ " %1$s [-u USER] [FILE] Replace the current crontab, read it from STDIN or FILE\n" \ " %1$s -e [-u USER] Open the current crontab with an editor\n" \ " %1$s -r [-u USER] [-i] Remove a crontab, (-i: with confirmation)\n" \ " %1$s -l [-u USER] List current crontab.\n" \ " %1$s -t line Translate one crontab line.\n" \ " %1$s -T FILE Check one whole crontab file.\n" \ " %1$s -V Display systemd-cron version.\n" \ "\n" \ "long options:\n" \ " %1$s -e, --edit\n" \ " %1$s -l, --list\n" \ " %1$s -r, --remove\n" \ " %1$s -s, --show\n" \ " %1$s -T, --test\n" \ " %1$s -t, --translate\n" \ " %1$s -V, --version\n" \ " %1$s -u, --user\n" static const constexpr struct option longopts[] = {{"list", no_argument, nullptr, 'l'}, // {"remove", no_argument, nullptr, 'r'}, // {"ask", no_argument, nullptr, 'i'}, // {"edit", no_argument, nullptr, 'e'}, // {"show", no_argument, nullptr, 's'}, // {"translate", no_argument, nullptr, 't'}, // {"test", no_argument, nullptr, 'T'}, // {"version", no_argument, nullptr, 'V'}, // {"help", no_argument, nullptr, 'h'}, // {"user", required_argument, nullptr, 'u'}, // {}}; // auto main(int argc, char * const * argv) -> int { setlocale(LC_ALL, ""); self = argv[0]; bool ask{}; auto action = action_t::replace; const char * user{}; for(int arg; (arg = getopt_long(argc, argv, "lriestTVhu:", longopts, nullptr)) != -1;) switch(arg) { case 'l': case 'r': case 'e': case 's': case 't': case 'T': case 'V': action = static_cast(arg); break; case 'i': ask = true; break; case 'u': user = optarg; break; case 'h': default: return std::fprintf(stderr, USAGE, self), 1; } if(argv[optind] && argv[optind + 1]) return std::fprintf(stderr, USAGE, self), 1; const char * file{}; if(argv[optind]) switch(action) { case action_t::replace: case action_t::test: case action_t::translate: file = argv[optind]; break; default: return std::fprintf(stderr, USAGE, self), 1; } else if(action == action_t::translate || action == action_t::test) return std::fprintf(stderr, USAGE, self), 1; if(!user) user = current_user.data(); if(!user || !getpwnam(user)) return std::fprintf(stderr, "user '%s' unknown\n", user), 1; // try to fixup CRONTAB_DIR if it has not been handled in package script if(access(CRONTAB_DIR, F_OK)) if(!mkdirp(CRONTAB_DIR)) return std::fprintf(stderr, "" CRONTAB_DIR " doesn't exist!\n"), 1; vore::file::fd{REBOOT_FILE, O_WRONLY | O_CREAT | O_CLOEXEC, 0666}; char cron_file[sizeof(CRONTAB_DIR "/") + LOGIN_NAME_MAX]; std::snprintf(cron_file, sizeof(cron_file), CRONTAB_DIR "/%s", user); switch(action) { case action_t::replace: return replace(cron_file, user, file ?: "-"); case action_t::list: return list(cron_file, user); case action_t::remove: return remove(cron_file, user, ask); case action_t::edit: return edit(cron_file, user); case action_t::show: return show(); case action_t::test: return test(file); case action_t::translate: return translate(file); case action_t::version: return version(); default: __builtin_unreachable(); } } systemd-cron-2.5.1/src/bin/crontab_setgid.c000066400000000000000000000061361475512744600206650ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #include #define MAX_COMMAND 1000 #define MAX_LINES 1000 #ifndef DEFAULT_NOACCESS #define DEFAULT_NOACCESS 0 #endif void end(char * msg){ fprintf(stderr,"crontab_setgid: %s\n", msg); exit(1); } void rtrim(char *str){ int n=strlen(str); while((--n>0)&&(str[n]==' ' || str[n]=='\n')); str[n+1]='\0'; } int main(int argc, char *argv[]) { if (!getuid()) end("root doesn't need this helper"); if (argc != 2) end("bad argument"); struct passwd *pw; pw = getpwuid(getuid()); if (!pw) end("user doesn't exist"); char users[LOGIN_NAME_MAX]; char crontab[sizeof CRONTAB_DIR + 1 + LOGIN_NAME_MAX]; char temp[sizeof crontab + 7]; snprintf(crontab, sizeof crontab, "%s/%s", CRONTAB_DIR, pw->pw_name); FILE *file; char buffer[MAX_COMMAND]; switch(argv[1][0]) { case 'r': file = fopen(crontab, "r"); if (file == NULL) { if(errno == ENOENT) { fprintf(stderr, "no crontab for %s\n", pw->pw_name); return ENOENT; } else { perror("Cannot open input file"); return 1; } }; while(fgets(buffer, sizeof(buffer), file)) { fputs(buffer, stdout); } fclose(file); break; case 'w': file = fopen("/etc/cron.allow", "r"); if (file != NULL) { int allowed=0; while(fgets(users, sizeof(users), file)) { rtrim(users); if (!strcmp(pw->pw_name, users)) { allowed=1; break; } } fclose(file); if (!allowed) end("you're not in /etc/cron.allow"); } else { file = fopen("/etc/cron.deny", "r"); if (file != NULL) { while(fgets(users, sizeof(users), file)) { rtrim(users); if (!strcmp(pw->pw_name, users)) { fclose(file); end("you are in /etc/cron.deny"); } } fclose(file); } else if (DEFAULT_NOACCESS) end("without /etc/cron.allow or /etc/cron.deny; only root can install crontabs"); } snprintf(temp, sizeof temp, "%s.XXXXXX", crontab); // this file is created $user:crontab / 0600 int fd = mkstemp(temp); if (fd == -1) { perror("Cannot create output file"); return 1; } file = fdopen(fd, "w"); if (file == NULL) { perror("Cannot open output file"); unlink(temp); return 1; } int lines=0; while(fgets(buffer, sizeof(buffer), stdin)) { lines++; if (fputs(buffer, file) < 0) { perror("Cannot write to file"); fclose(file); unlink(temp); return 1; } if (lines > MAX_LINES) { fclose(file); unlink(temp); end("maximum lines reached"); } } if (fclose(file)) { perror("fclose"); unlink(temp); return 1; } if (rename(temp,crontab)) { perror("rename"); unlink(temp); return 1; } break; case 'd': if (unlink(crontab) == -1) { if(errno == ENOENT) { fprintf(stderr, "no crontab for %s\n", pw->pw_name); return 0; } perror("Cannot delete file"); return 1; } break; default: end("unknown action"); return 1; } return 0; } systemd-cron-2.5.1/src/bin/mail_for_job.sh000077500000000000000000000105361475512744600205120ustar00rootroot00000000000000#!/bin/sh -f [ $# -eq 1 ] || { printf 'usage: %s unit[:Success|:Failure][:nonempty][:nometadata][:verbose]\n' "$0" >&2 exit 1 } unit="$1" verbose= metadata=m nonempty= while :; do case "$unit" in *:verbose ) verbose=v; unit="${unit%:*}" ;; *:nonempty ) nonempty=n; unit="${unit%:*}" ;; *:nometadata) metadata=; unit="${unit%:*}" ;; *:[A-Z]* ) unit="${unit%:*}" ;; # https://github.com/systemd-cron/systemd-cron/issues/120 * ) break ;; esac done SENDMAIL="$(command -v "$SENDMAIL" || command -v sendmail || command -v /usr/sbin/sendmail || command -v /usr/lib/sendmail)" || { [ -n "$verbose" ] && printf '<3>can'\''t send mail for %s without a MTA\n' "$unit" exit 0 } systemctl show --property=User --property=Environment --property=SourcePath --property=Description --property=ActiveState --property=InvocationID "$unit" | { user= job_env= source_path= description= active_state= invocation_id= while IFS='=' read -r k v; do case "$k" in 'User' ) user="$v" ;; 'Environment' ) job_env="$v" ;; 'SourcePath' ) source_path="$v" ;; 'Description' ) description="$v" ;; 'ActiveState' ) active_state="$v" ;; 'InvocationID') invocation_id="$v" ;; esac done [ -z "$user" ] && user='root' [ -z "$source_path" ] && source_path="$unit" [ "${source_path#'@statedir@/'}" != "$source_path" ] && source_path="${source_path#'@statedir@/'}'s crontab" # Description is either »[Cron] "0 * * * * program"« or »[Cron] /etc/crontab«; we don't care about the latter [ "${description#'[Cron] "'}" != "$description" ] && source_path="$source_path ${description#'[Cron] '}" export user # used by custom sendmails! mailto="$user" mailfrom='root' sendmail="$SENDMAIL" for kv in $job_env; do case "$kv" in 'MAILTO='* ) mailto="${kv#'MAILTO='}" ;; 'MAILFROM='*) mailfrom="${kv#'MAILFROM='}" ;; 'SENDMAIL='*) sendmail="${kv#'SENDMAIL='}" ;; 'CRON_MAIL_FORMAT=nometadata') metadata=;; 'CRON_MAIL_FORMAT=no-metadata') metadata=;; esac done [ -z "$mailto" ] && { [ -n "$verbose" ] && printf 'This cron job (%s) opted out of email, therefore quitting\n' "$unit" >&2 exit 0 } [ -n "$nonempty" ] && { # INVOCATION_ID= matches messages from systemd # _SYSTEMD_INVOCATION_ID= matches messages from the service # SYSLOG_FACILITY=9 matches lines from the standard output and standard error only (we set SyslogFacility=cron, cron=9) journalctl -qu "$unit" _SYSTEMD_INVOCATION_ID="$invocation_id" SYSLOG_FACILITY=9 | read -r _ || { [ -n "$verbose" ] && printf 'This cron job (%s) produced no output, therefore quitting\n' "$unit" >&2 exit 0 } } [ "$active_state" = 'failed' ] && why='Failure' || why='Output' # '[tarta] Failure: /etc/cron.daily/whatever' or '[tarta] Output: /etc/cron.daily/whatever' { # Encode the message in raw 8-bit UTF-8. w/o base64 # Virtually all modern MTAs are 8-bit clean and send each other 8-bit data # without checking each other's 8BITMIME flag. printf '%s: %s\n' 'Content-Type' 'text/plain; charset="utf-8"' \ 'MIME-Version' '1.0' \ 'Content-Transfer-Encoding' 'binary' \ 'Date' "$(date -R)" \ 'From' "$mailfrom (systemd-cron)" \ 'To' "$mailto" \ 'Subject' "[$(uname -n)] $why: $source_path" \ 'X-Mailer' "systemd-cron @version@" \ 'Auto-Submitted' 'auto-generated' # https://datatracker.ietf.org/doc/html/rfc3834#section-5 echo case "${LC_ALL-"${LC_CTYPE-"${LANG}"}"}" in C|POSIX|*.utf8|*.utf-8|*.UTF-8) ;; # ok, we can safely use this locale as UTF-8 * ) LC_ALL=C.UTF-8 ;; # forced to comply with charset= esac if [ -n "$metadata" ]; then systemctl status -n0 "$unit" journalctl -u "$unit" -o short-iso _SYSTEMD_INVOCATION_ID="$invocation_id" + INVOCATION_ID="$invocation_id" else journalctl -u "$unit" -o cat _SYSTEMD_INVOCATION_ID="$invocation_id" SYSLOG_FACILITY=9 fi } 2>&1 | "$sendmail" -i -B 8BITMIME "$mailto" } systemd-cron-2.5.1/src/bin/remove_stale_stamps.sh000077500000000000000000000011661475512744600221430ustar00rootroot00000000000000#!/bin/sh set -e set -u # previously the {daily/weekly/monthly/..} stamps # were special-cased: they were to be purged only # once at 'systemd-cron' removal # now with the transition to run-part-less mode, # these become extraneous cruft too and # can/should be removed if the matching .timer # does not exist anymore find /var/lib/systemd/timers/ -name 'stamp-cron-*' -type f -mtime +10 | while read -r stamp do timer=${stamp##*/stamp-} if test -f "/run/systemd/generator/$timer" then : elif test -f "/lib/systemd/system/$timer" then : else rm -f "$stamp" fi done exit 0 systemd-cron-2.5.1/src/bin/systemd-crontab-generator.cpp000066400000000000000000001563321475512744600233440ustar00rootroot00000000000000#include "libvoreutils.hpp" #include "util.hpp" #include #include #include static const constexpr auto key_or_plain = [](auto && lhs, auto && rhs) { static const constexpr auto key = vore::overload{[](const std::string_view & s) { return s; }, [](const std::pair & kv) { return kv.first; }}; return key(lhs) < key(rhs); }; static const constexpr std::uint8_t MINUTES_SET[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59}; static const constexpr std::uint8_t HOURS_SET[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23}; static const constexpr std::uint8_t DAYS_SET[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31}; static const constexpr std::string_view DOWS_SET[] = {"Sun"sv, "Mon"sv, "Tue"sv, "Wed"sv, "Thu"sv, "Fri"sv, "Sat"sv, "Sun"sv}; static const constexpr std::uint8_t MONTHS_SET[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; static const char * const MINUTES_RANGE = "[0, 59]"; static const char * const HOURS_RANGE = "[0, 23]"; static const char * const DAYS_RANGE = "[1, 31] or 'L'"; static const char * const DOWS_RANGE = "[mon, sun] or [1, 7] or [1, 7]L"; static const char * const MONTHS_RANGE = "[jan, dec] or [1, 12]"; #include "configuration.hpp" static const char * SELF; static bool RUN_BY_SYSTEMD; static const constexpr std::string_view VALID_CHARS = "-" "0123456789" "ABCDEFGHIJKLMNOPQRSTUVWXYZ" "_" "abcdefghijklmnopqrstuvwxyz"sv; // keep sorted static const constexpr std::pair PART2TIMER[] = { // kept sorted by Makefile #include "part2timer.hpp" {"\xFF"sv, ""sv}, // we can't have an empty array (if no mapping set), so this sorts after everything }; static const constexpr std::pair CROND2TIMER[] = { // kept sorted by Makefile #include "crond2timer.hpp" {"\xFF"sv, ""sv}, // we can't have an empty array (if no mapping set), so this sorts after everything }; static auto which(const std::string_view & exe, std::optional paths = {}) -> std::optional { if(!paths) paths = std::getenv("PATH") ?: "/usr/bin:/bin"; for(auto path : vore::soft_tokenise{*paths, ":"sv}) { auto abspath = (std::string{path} += '/') += exe; if(!access(abspath.c_str(), X_OK)) return abspath; } return {}; } static const bool HAS_SENDMAIL = [] { if(auto sendmail = std::getenv("SENDMAIL")) if(!access(sendmail, X_OK)) return true; return which("sendmail"sv) || which("sendmail"sv, "/usr/sbin:/usr/lib"sv); }(); static std::string_view TARGET_DIR; static std::string TIMERS_DIR; static std::optional UPTIME; static auto systemd_bool(const std::string_view & string) -> bool { return !strncasecmp(string.data(), "1", string.size()) || // !strncasecmp(string.data(), "yes", string.size()) || // !strncasecmp(string.data(), "true", string.size()); } static auto systemd_bool_false(const std::string_view & string) -> bool { return !strncasecmp(string.data(), "0", string.size()) || // !strncasecmp(string.data(), "no", string.size()) || // !strncasecmp(string.data(), "false", string.size()); } enum class Log : std::uint8_t { EMERG, ALERT, CRIT, ERR, WARNING, NOTICE, INFO, DEBUG }; // there could be an IGNORE level for logging the .placeholder // with a priority even lower than DEBUG only log to stdout // but never to dmesg static __attribute__((format(printf, 2, 3))) void log(Log level, const char * fmt, ...) { va_list args; va_start(args, fmt); auto into = stderr; vore::file::FILE kmsg; if(RUN_BY_SYSTEMD && (kmsg = {"/dev/kmsg", "we"})) { std::fprintf(kmsg, "<%" PRIu8 ">%s[%d]", static_cast(level), SELF, getpid()); into = kmsg; } else std::fputs(SELF, into); std::fputs(": ", into); std::vfprintf(into, fmt, args); std::fputc('\n', into); va_end(args); } #define FORMAT_SV(sv) (int)(sv).size(), (sv).data() static auto int_map(const std::string_view & str, bool & err, bool = false) -> std::size_t; static auto month_map(const std::string_view & month, bool & err, bool = false) -> std::size_t; static auto dow_map(const std::string_view & dow_full, bool & err, bool to) -> std::size_t; template static auto parse_period(const std::string_view & value, const V & values, std::set & into, bool & raw_for_schedule, std::size_t (*mapping)(const std::string_view &, bool &, bool), std::size_t base) -> std::optional; static auto environment_write(const std::map & env, FILE * into) -> void; enum class withuser_t : std::uint8_t { from_cmd0, // system crontab from_basename, // users' crontabs initial, // --check/--translate }; enum class cron_mail_success_t : std::uint8_t { never, // no OnSuccess= always, // plain OnSuccess= nonempty, // OnSuccess=...\x20nonempty dflt = nonempty, }; enum class cron_mail_format_t : bool { normal, // systemctl status + journalctl nometadata, // journalctl -o cat dflt = normal, }; struct Job { std::string filename; std::string_view basename; std::string_view line; std::vector parts; std::map environment; std::string environment_PATH_storage; // borrowed into environment on expansion std::string_view shell; std::size_t random_delay; std::string period; // either period or timespec std::optional timezone; std::set timespec_minute; // 0-60 or TIMESPEC_ASTERISK std::set timespec_hour; // 0-24 or TIMESPEC_ASTERISK std::set timespec_dom; // 0-31 or TIMESPEC_ASTERISK std::set timespec_dow; // 0-7 (actually sun-mon-...-sun) or "*" std::set timespec_month; // 0-12 or TIMESPEC_ASTERISK std::optional timespec_minute_raw, timespec_hour_raw, timespec_dom_raw, timespec_dow_raw, timespec_month_raw; // for schedule for hashing bool sunday_is_seven; bool last_dom; bool last_dow; std::string schedule, schedule_raw; // first for systemd, second for hashing std::size_t boot_delay; std::size_t start_hour; bool persistent; bool batch; std::string jobid; std::string unit_name; std::string_view user; std::optional user_home; // 'static uid_t user_uid; gid_t user_gid; cron_mail_success_t cron_mail_success; cron_mail_format_t cron_mail_format; struct { vore::span command; // subview of parts std::optional command0; // except this is command[0] if set bool nopercent; struct command_iter { using iterator_category = std::input_iterator_tag; using difference_type = void; using value_type = const std::string_view; using pointer = const std::string_view *; using reference = const std::string_view &; vore::span command; std::optional command0; command_iter & operator++() noexcept { if(this->command.size()) this->command0 = *command.b++; else this->command0 = {}; return *this; } constexpr bool operator==(const command_iter & rhs) const noexcept { return this->command0 == rhs.command0 && this->command.b == rhs.command.b; } constexpr bool operator!=(const command_iter & rhs) const noexcept { return !(*this == rhs); } constexpr const std::string_view & operator*() noexcept { if(!this->command0) ++*this; return *this->command0; } }; constexpr command_iter begin() const noexcept { return {this->command, this->command0}; } constexpr command_iter end() const noexcept { return {{this->command.e, this->command.e}, {}}; } constexpr std::size_t size() const noexcept { return this->command.size() + static_cast(this->command0); } constexpr std::string_view operator[](std::size_t i) const noexcept { if(this->command0) { if(!i) return *this->command0; --i; } return this->command[i]; } } command; std::string execstart; std::string_view input_data; bool valid; Job(std::string_view filename, std::string_view line) { this->filename = filename; this->basename = vore::basename(filename); this->line = line; vore::soft_tokenise tokens{line, " \t\n"sv}; std::copy(std::begin(tokens), std::end(tokens), std::back_inserter(this->parts)); this->shell = "/bin/sh"sv; this->boot_delay = 0; this->start_hour = 0; this->random_delay = 0; this->persistent = false; this->user = "root"sv; this->user_uid = 0; this->user_gid = 0; this->command = {}; this->input_data = {}; this->valid = true; this->batch = false; this->sunday_is_seven = false; this->last_dom = false; this->last_dow = false; this->cron_mail_success = cron_mail_success_t::dflt; this->cron_mail_format = cron_mail_format_t::dflt; } auto log(Log priority, const char * message) -> void { ::log(priority, "%.*s: %.*s: %s", FORMAT_SV(this->filename), FORMAT_SV(this->line), message); } template #ifdef __clang__ __attribute__((format(printf, 3, 4))) #endif auto log(Log priority, const char * fmt, const T &... args) -> void { char buf[128]; std::string_view msg{buf, static_cast(std::snprintf(buf, sizeof(buf), fmt, args...))}; ::log(priority, "%.*s: %.*s: %.*s", FORMAT_SV(this->filename), FORMAT_SV(this->line), FORMAT_SV(msg)); } auto which(const std::string_view & pgm) -> std::optional { auto itr = this->environment.find("PATH"sv); return ::which(pgm, itr == std::end(this->environment) ? std::nullopt : std::optional{itr->second}); } // decode some environment variables that influence the behaviour of systemd-cron itself auto decode_environment(const std::map & environment, bool default_persistent, cron_mail_success_t default_cron_mail_success, cron_mail_format_t default_cron_mail_format) -> void { this->persistent = default_persistent; this->cron_mail_success = default_cron_mail_success; this->cron_mail_format = default_cron_mail_format; for(auto && [k, v] : environment) { if(k == "PERSISTENT"sv) this->persistent = systemd_bool(v); else if(k == "RANDOM_DELAY"sv) { bool err{}; this->random_delay = int_map(v, err); if(err) this->log(Log::WARNING, "invalid RANDOM_DELAY"); } else if(k == "START_HOURS_RANGE"sv) { bool err{}; if(auto idx = v.find('-'); idx == std::string_view::npos) err = true; else this->start_hour = int_map(v.substr(0, idx), err); if(err) this->log(Log::WARNING, "invalid START_HOURS_RANGE"); } else if(k == "DELAY"sv) { bool err{}; this->boot_delay = int_map(v, err); if(err) this->log(Log::WARNING, "invalid DELAY"); } else if(k == "BATCH"sv) this->batch = systemd_bool(v); else if(k == "CRON_MAIL_SUCCESS"sv) { if(v == "never"sv || systemd_bool_false(v)) this->cron_mail_success = cron_mail_success_t::never; else if(v == "always"sv || systemd_bool(v)) this->cron_mail_success = cron_mail_success_t::always; else if(v == "nonempty"sv || v == "non-empty"sv) this->cron_mail_success = cron_mail_success_t::nonempty; else if(v == "inherit"sv) this->cron_mail_success = default_cron_mail_success; else this->log(Log::WARNING, "unknown CRON_MAIL_SUCCESS value"); } else if(k == "CRON_MAIL_FORMAT"sv) { if(v == "normal"sv) this->cron_mail_format = cron_mail_format_t::normal; else if(v == "nometadata"sv || v == "no-metadata"sv) this->cron_mail_format = cron_mail_format_t::nometadata; else if(v == "inherit"sv) this->cron_mail_format = default_cron_mail_format; else this->log(Log::WARNING, "unknown CRON_MAIL_FORMAT value"); } else { // $CRON_INHERIT_VARIABLES handled externally if(k == "SHELL"sv) this->shell = v; if(k == "TZ"sv || k == "CRON_TZ"sv) { auto tz = v; if(tz.starts_with(':')) tz.remove_prefix(1); if(tz.empty()) this->timezone = {}; else if(!access(("/usr/share/zoneinfo/"s += tz).c_str(), F_OK)) this->timezone = tz; } if(k == "MAILTO"sv && !v.empty()) if(!HAS_SENDMAIL) this->log(Log::WARNING, "a MTA is not installed, but MAILTO is set"); this->environment.emplace(k, v); } } } auto parse_anacrontab() -> void { if(this->parts.size() < 4) { this->valid = false; return; } this->period = this->parts[0]; this->jobid = "anacron-"s += this->parts[2]; bool err{}; this->boot_delay = int_map(this->parts[1], err); if(err) this->log(Log::WARNING, "invalid DELAY"); this->command.command.b = &*(this->parts.begin() + 3); this->command.command.e = &*(this->parts.end()); this->command.nopercent = true; } // crontab --translate auto parse_crontab_auto() -> void { if(this->line[0] == '@') this->parse_crontab_at(withuser_t::initial); else this->parse_crontab_timespec(withuser_t::initial); if(this->command.size()) { if(this->command.size() > 1 && getpwnam(MAYBE_DUPA(this->command[0]))) { this->user = this->command[0]; ++this->command.command.b; } else this->user = getpass_getlogin(); this->decode_command(); auto first = true; for(auto && hunk : this->command) { if(hunk.empty()) continue; if(!std::exchange(first, false)) this->execstart += ' '; this->execstart += hunk; } } } // @daily (user) do something auto parse_crontab_at(withuser_t withuser) -> void { if(this->parts.size() < (2 + (withuser == withuser_t::from_cmd0))) { this->log(Log::ERR, "have %zu field%s, need ≥%d (@timespec%s program)", this->parts.size(), this->parts.size() == 1 ? "" : "s", 2 + (withuser == withuser_t::from_cmd0), (withuser == withuser_t::from_cmd0) ? " user" : ""); this->valid = false; return; } this->period = this->parts[0]; switch(withuser) { case withuser_t::from_cmd0: this->user = this->parts[1]; this->command.command.b = &*(this->parts.begin() + 2); break; case withuser_t::from_basename: this->user = this->basename; [[fallthrough]]; case withuser_t::initial: this->command.command.b = &*(this->parts.begin() + 1); break; } this->command.command.e = &*(this->parts.end()); this->jobid = (std::string{this->basename} += '-') += this->user; this->parse_percent(); } // 6 2 * * * (user) do something auto parse_crontab_timespec(withuser_t withuser) -> void { if(this->parts.size() < (6 + (withuser == withuser_t::from_cmd0))) { this->log(Log::ERR, "have %zu field%s, need ≥%d (minute hour day dow month%s program)", this->parts.size(), this->parts.size() == 1 ? "" : "s", 6 + (withuser == withuser_t::from_cmd0), (withuser == withuser_t::from_cmd0) ? " user" : ""); this->valid = false; return; } auto && minutes = this->parts[0]; auto && hours = this->parts[1]; auto && days = this->parts[2]; auto && months = this->parts[3]; auto && dows = this->parts[4]; this->timespec_minute = this->parse_time_unit(minutes, "minute", MINUTES_SET, MINUTES_RANGE, int_map, this->timespec_minute_raw); this->timespec_hour = this->parse_time_unit(hours, "hour", HOURS_SET, HOURS_RANGE, int_map, this->timespec_hour_raw); this->last_dom = days == "L"sv; this->last_dow = (dows.size() == 2 && dows.back() == 'L'); if(this->last_dom) days = "1"sv; else if(this->last_dow) { days = "7/1"sv; dows = dows.substr(0, 1); } this->timespec_dom = this->parse_time_unit(days, "day", DAYS_SET, DAYS_RANGE, int_map, this->timespec_dom_raw); this->timespec_dow = this->parse_time_unit(dows, "dow", DOWS_SET, DOWS_RANGE, dow_map, this->timespec_dow_raw); this->timespec_month = this->parse_time_unit(months, "month", MONTHS_SET, MONTHS_RANGE, month_map, this->timespec_month_raw); this->sunday_is_seven = dows.back() == '7' || [&] { if(dows.size() < 3) return false; char buf[3]{}; std::transform(std::end(dows) - 3, std::end(dows), buf, ::tolower); return std::string_view{buf, 3} == "sun"sv; }(); switch(withuser) { case withuser_t::from_cmd0: this->user = this->parts[5]; this->command.command.b = &*(this->parts.begin() + 6); break; case withuser_t::from_basename: this->user = this->basename; [[fallthrough]]; case withuser_t::initial: this->command.command.b = &*(this->parts.begin() + 5); break; } this->command.command.e = &*(this->parts.end()); this->jobid = (std::string{this->basename} += '-') += this->user; this->parse_percent(); } // For non-anacron jobs: find the first unescaped %, terminate this->command there, and put the rest into this->input_data auto parse_percent() -> void { for(auto itr = this->command.command.b; itr != this->command.command.e; ++itr) { std::size_t cur{}; for(std::size_t percent; (percent = itr->find('%', cur)) != std::string_view::npos;) { if(percent == 0 || (*itr)[percent - 1] != '\\') { this->input_data = {itr->data() + percent + 1, this->line.data() + this->line.size()}; itr->remove_suffix(itr->size() - percent); this->command.command.e = itr + 1; return; } cur = percent + 1; } } } static const constexpr std::uint8_t TIMESPEC_ASTERISK = -1; template auto parse_time_unit(const std::string_view & value, const char * field, const V & values, const char * range, std::size_t (*mapping)(const std::string_view &, bool &, bool), std::optional & raw_for_schedule) -> std::set { if(value == "*"sv) { if constexpr(DOW) return {"*"sv}; else return {TIMESPEC_ASTERISK}; } std::size_t base; if constexpr(DOW) base = 0; else base = *std::min_element(std::begin(values), std::end(values)); std::set result; bool raw{}; for(auto && subval : vore::soft_tokenise{value, ","sv}) // NOTE: this glides over consecutive commas so "0,,3" is accepted as-if "0,3" if(auto err = parse_period(subval, values, result, raw, mapping, base)) { if(*err) this->log(Log::ERR, "field %s=%.*s (%.*s): %s", field, FORMAT_SV(value), FORMAT_SV(subval), *err); else { if(auto slash = subval.find('/'); slash != std::string_view::npos) subval.remove_suffix(subval.size() - slash); this->log(Log::ERR, "field %s=%.*s (%.*s), may be * or %s", field, FORMAT_SV(value), FORMAT_SV(subval), range); } this->valid = false; return {}; } if(raw && this->persistent) raw_for_schedule = value; return result; } // decode & validate auto decode() -> void { this->jobid.erase(std::remove_if(std::begin(this->jobid), std::end(this->jobid), [](char c) { return !std::binary_search(std::begin(VALID_CHARS), std::end(VALID_CHARS), c); }), std::end(this->jobid)); this->decode_command(); } // perform smart substitutions for known shells auto decode_command() -> void { if(!this->command.size()) return; if(this->pull_user()) { if(this->command[0].starts_with("~/"sv)) { this->command.command0 = std::string{ * this->user_home} += this->command[0].substr(1); ++this->command.command.b; } if(auto itr = this->environment.find("PATH"sv); itr != std::end(this->environment)) if(itr->second.starts_with("~/") || itr->second.find(":~/"sv) != std::string_view::npos) { for(auto && path : vore::soft_tokenise{itr->second, ":"sv}) { if(path.starts_with("~/")) { this->environment_PATH_storage += *this->user_home; this->environment_PATH_storage += path.substr(1); } else this->environment_PATH_storage += path; this->environment_PATH_storage += ':'; } this->environment_PATH_storage.pop_back(); this->environment["PATH"sv] = this->environment_PATH_storage; } } } auto pull_user() -> bool { if(this->user_home) return true; static std::map, std::less<>> pwnam_cache; auto itr = pwnam_cache.find(this->user); if(itr == std::end(pwnam_cache)) { std::string user{this->user}; if(auto ent = getpwnam(user.data())) itr = pwnam_cache.emplace(std::move(user), std::tuple{ent->pw_dir, ent->pw_uid, ent->pw_gid}).first; } if(itr != std::end(pwnam_cache)) { this->user_home = std::get<0>(itr->second); this->user_uid = std::get<1>(itr->second); this->user_gid = std::get<2>(itr->second); } return !!this->user_home; } auto is_active() -> bool { if(this->schedule == "reboot"sv && !access(REBOOT_FILE, F_OK)) return false; return true; } auto generate_schedule_from_period() -> void { static const constexpr std::string_view TIME_UNITS_SET[] = {"daily"sv, "monthly"sv, "quarterly"sv, "semi-annually"sv, "weekly"sv, "yearly"sv}; // keep sorted if(auto i = this->period.find_first_not_of('@'); i != std::string::npos) this->period.erase(0, i); else this->period = {}; std::transform(std::begin(this->period), std::end(this->period), std::begin(this->period), ::tolower); static const constexpr std::pair replacements[] = { // keep sorted {"1"sv, "daily"sv}, {"30"sv, "monthly"sv}, {"31"sv, "monthly"sv}, {"365"sv, "yearly"sv}, {"7"sv, "weekly"sv}, {"annually"sv, "yearly"sv}, {"anually"sv, "yearly"sv}, {"bi-annually"sv, "semi-annually"sv}, {"biannually"sv, "semi-annually"sv}, {"boot"sv, "reboot"sv}, {"semiannually"sv, "semi-annually"sv}, }; if(auto itr = vore::binary_find(std::begin(replacements), std::end(replacements), this->period, key_or_plain); itr != std::end(replacements)) this->period = itr->second; char buf[128]; auto hour = this->start_hour; if(this->period == "reboot"sv) { this->boot_delay = std::max(this->boot_delay, static_cast(1)); this->schedule = this->period; this->persistent = false; } else if(this->period == "minutely"sv) { this->schedule = this->period; this->persistent = false; } else if(this->period == "hourly" && this->boot_delay == 0) this->schedule = "hourly"sv; else if(this->period == "hourly"sv) { this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-*-* *:%zu:0", this->boot_delay))}; this->boot_delay = 0; } else if(this->period == "midnight" && this->boot_delay == 0) this->schedule = "daily"sv; else if(this->period == "midnight") this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-*-* 0:%zu:0", this->boot_delay))}; else if(std::binary_search(std::begin(TIME_UNITS_SET), std::end(TIME_UNITS_SET), this->period) && hour == 0 && this->boot_delay == 0) this->schedule = this->period; else if(this->period == "daily"sv) this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-*-* %zu:%zu:0", hour, this->boot_delay))}; else if(this->period == "weekly"sv) this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "Mon *-*-* %zu:%zu:0", hour, this->boot_delay))}; else if(this->period == "monthly"sv) this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-*-1 %zu:%zu:0", hour, this->boot_delay))}; else if(this->period == "quarterly"sv) this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-1,4,7,10-1 %zu:%zu:0", hour, this->boot_delay))}; else if(this->period == "semi-annually"sv) this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-1,7-1 %zu:%zu:0", hour, this->boot_delay))}; else if(this->period == "yearly"sv) this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-1-1 %zu:%zu:0", hour, this->boot_delay))}; else { bool err{}; auto prd = int_map(this->period, err); if(err) { this->log(Log::ERR, "unknown schedule"); this->schedule = this->period; return; } if(prd > 31) { // workaround for anacrontab std::size_t divisor = std::round(static_cast(prd) / 30); this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-1/%zu-1 %zu:%zu:0", divisor, hour, this->boot_delay))}; } else this->schedule = {buf, static_cast(std::snprintf(buf, sizeof(buf), "*-*-1/%zu %zu:%zu:0", prd, hour, this->boot_delay))}; } if(this->persistent) this->schedule_raw = this->schedule; } auto generate_schedule() -> void { if(!this->period.empty()) this->generate_schedule_from_period(); else this->generate_schedule_from_timespec(); } auto generate_schedule_from_timespec() -> void { std::string dows; if(this->timespec_dow.size() != 1 || *std::begin(this->timespec_dow) != "*"sv) { // != ['*'] for(auto && dow : vore::span{std::begin(DOWS_SET) + this->sunday_is_seven, std::end(DOWS_SET) - !this->sunday_is_seven}) { if(this->timespec_dow.find(dow) == std::end(this->timespec_dow)) continue; if(!dows.empty()) dows += ','; dows += dow; } dows += ' '; } this->timespec_month.erase(0); this->timespec_dom.erase(0); if(this->timespec_month.empty() || this->timespec_dom.empty() || this->timespec_hour.empty() || this->timespec_minute.empty()) { this->valid = false; // errors already dumped by the parser return; } // %s*-%s-%s %s:%s:00 // if persistent, parts of schedule_raw which use ~s are copied directly from the input this->schedule = std::move(dows); this->schedule += "*-"sv; if(this->persistent) { if(this->timespec_dow_raw) { this->schedule_raw = *this->timespec_dow_raw; this->schedule_raw += "*-"sv; } else this->schedule_raw = this->schedule; } auto timespec_comma = [&](auto && field, auto && raw) { char buf[3 + 1]; // 255 auto first = true; for(auto f : field) { if(!std::exchange(first, false)) { this->schedule += ','; if(this->persistent && !raw) this->schedule_raw += ','; } std::string_view to_append; if(f == TIMESPEC_ASTERISK) to_append = "*"sv; else to_append = {buf, static_cast(std::snprintf(buf, sizeof(buf), "%" PRIu8 "", f))}; this->schedule += to_append; if(this->persistent && !raw) this->schedule_raw += to_append; } if(this->persistent && raw) this->schedule_raw += *raw; }; #define ADDBOTH(what) \ this->schedule += what; \ if(this->persistent) \ this->schedule_raw += what timespec_comma(this->timespec_month, this->timespec_month_raw); if(this->last_dow || this->last_dom) { ADDBOTH('~'); } else { ADDBOTH('-'); } timespec_comma(this->timespec_dom, this->timespec_dom_raw); if(this->last_dow) { ADDBOTH('/'); ADDBOTH('1'); } ADDBOTH(' '); timespec_comma(this->timespec_hour, this->timespec_hour_raw); ADDBOTH(':'); timespec_comma(this->timespec_minute, this->timespec_minute_raw); ADDBOTH(":00"sv); } template auto debackslashpercentise(std::string_view cmd, F && append) -> void { if(this->command.nopercent) { append(cmd, false); return; } while(cmd.size()) { auto backpct = cmd.find("\\%"sv); if(backpct == std::string_view::npos) { append(cmd, true); cmd = {}; } else { append(cmd.substr(0, backpct), true); append("%"sv, false); cmd.remove_prefix(backpct + 2); } } } auto generate_scriptlet() -> std::optional { // ...only if needed assert(!this->unit_name.empty()); if(this->command.size() == 1 && this->pull_user()) { this->debackslashpercentise(this->command[0], [&](auto && segment, auto) { this->execstart += segment; }); uid_t uid{}; gid_t gid{}; if(this->user_uid) uid = setfsuid(this->user_uid); if(this->user_gid) gid = setfsgid(this->user_gid); struct stat sb; auto statres = stat(this->execstart.c_str(), &sb); if(this->user_uid) setfsuid(uid); if(this->user_gid) setfsgid(gid); if(!statres && S_ISREG(sb.st_mode)) return {}; } auto scriptlet = ((std::string{TARGET_DIR} += '/') += this->unit_name) += ".sh"sv; this->execstart = (std::string{this->shell} += ' ') += scriptlet; return scriptlet; } auto generate_unit_header(FILE * into, const char * tp) -> void { std::fputs("[Unit]\n", into); std::fprintf(into, "Description=[%s] ", tp); if(this->line[0] != '/') std::fputc('\"', into); { auto desc = this->line; wchar_t c; mbstate_t st{}; for(std::size_t conv; !desc.empty() && (conv = std::mbrtowc(&c, desc.data(), desc.size(), &st)); desc.remove_prefix(conv)) switch(conv) { case static_cast(-1): // EILSEQ: reset, output byte as \xXX st = {}; conv = 1; std::fprintf(into, "\\x%02hhx", desc[0]); break; case static_cast(-2): // incomplete: truncate conv = desc.size(); break; default: if(c == '%') // % is special in systemd units, %% is literal std::fputs("%%", into); else std::fwrite(desc.data(), 1, conv, into); continue; } } if(this->line[0] != '/') std::fputc('\"', into); std::fputc('\n', into); std::fputs("Documentation=man:systemd-crontab-generator(8)\n", into); if(this->filename != "-"sv) std::fprintf(into, "SourcePath=%.*s\n", FORMAT_SV(this->filename)); } // TODO: roll back to straight OnSuccess= and drop onsuccess_shim if we ever bump past the systemd ≥ 236 requirement (this is 249); cf. #165 auto format_on_failure(FILE * into, const char * key, const char * state, bool nonempty = false) -> char * { size_t sp; char * onsuccess_shim{}; FILE * onsuccess_out{}; if(key[2] == 'S' && !HAVE_ONSUCCESS) { format_on_failure(into, "Before", "Success", nonempty); if((onsuccess_out = open_memstream(&onsuccess_shim, &sp))) into = onsuccess_out; std::fputs("ExecStart=-+/usr/bin/systemctl start --no-block cron-mail@%n:Success", into); } else std::fprintf(into, "%s=cron-mail@%%n:%s", key, state); if(nonempty) std::fputs(":nonempty", into); switch(this->cron_mail_format) { case cron_mail_format_t::normal: break; case cron_mail_format_t::nometadata: std::fputs(":nometadata", into); break; } std::fputs(".service\n", into); if(onsuccess_out) std::fclose(onsuccess_out); return onsuccess_shim; } auto generate_service(FILE * into) -> void { this->generate_unit_header(into, "Cron"); char * onsuccess_shim{}; if(auto itr = this->environment.find("MAILTO"sv); itr != std::end(this->environment) && itr->second.empty()) ; // mails explicitly disabled else if(!HAS_SENDMAIL) ; // mails automatically disabled else { this->format_on_failure(into, "OnFailure", "Failure"); switch(this->cron_mail_success) { case cron_mail_success_t::never: break; case cron_mail_success_t::always: onsuccess_shim = this->format_on_failure(into, "OnSuccess", "Success"); break; case cron_mail_success_t::nonempty: onsuccess_shim = this->format_on_failure(into, "OnSuccess", "Success", true); break; } } if(this->user != "root"sv || this->filename.find(STATEDIR) != std::string_view::npos) { std::fputs("Requires=systemd-user-sessions.service\n", into); if(this->user_home) std::fprintf(into, "RequiresMountsFor=%.*s\n", FORMAT_SV(*this->user_home)); } std::fputc('\n', into); std::fputs("[Service]\n", into); std::fprintf(into, "User=%.*s\n", FORMAT_SV(this->user)); if(*PAMNAME) std::fputs("PAMName=" PAMNAME "\n", into); // + default KillMode=control-group (can't have =process with PAMName) else std::fputs("KillMode=process\n", into); std::fputs("WorkingDirectory=-~\n", into); std::fputs("Type=oneshot\n", into); std::fputs("IgnoreSIGPIPE=false\n", into); std::fputs("SyslogFacility=cron\n", into); if(USE_LOGLEVELMAX != "no"sv) std::fprintf(into, "LogLevelMax=%.*s\n", FORMAT_SV(USE_LOGLEVELMAX)); if(!this->schedule.empty() && this->boot_delay) if(!UPTIME || this->boot_delay > *UPTIME) std::fprintf(into, "ExecStartPre=-%.*s %zu\n", FORMAT_SV(BOOT_DELAY), this->boot_delay); std::fprintf(into, "ExecStart=%.*s\n", FORMAT_SV(this->execstart)); if(onsuccess_shim) { std::fputs(onsuccess_shim, into); std::free(onsuccess_shim); } if(this->environment.size()) { std::fputs("Environment=", into); environment_write(this->environment, into); std::fputc('\n', into); } if(!this->input_data.empty()) { std::fputs("StandardInput=data\n", into); std::fputs("StandardInputData=", into); b64 data{into}; this->debackslashpercentise(this->input_data, [&](auto segment, auto replace_percent) { if(replace_percent) for(std::size_t pct; (pct = segment.find('%')) != std::string_view::npos;) { data.feed(segment.substr(0, pct)); data.feed("\n"sv); segment.remove_prefix(pct + 1); } data.feed(segment); }); data.finish(); std::fputc('\n', into); } if(this->batch) { std::fputs("CPUSchedulingPolicy=idle\n", into); std::fputs("IOSchedulingClass=idle\n", into); } } auto generate_timer(FILE * into) -> void { this->generate_unit_header(into, "Timer"); std::fputs("PartOf=cron.target\n", into); std::fputc('\n', into); std::fputs("[Timer]\n", into); if(this->schedule == "reboot"sv) std::fprintf(into, "OnBootSec=%zum\n", this->boot_delay); else { std::fprintf(into, "OnCalendar=%.*s", FORMAT_SV(this->schedule)); if(this->timezone) std::fprintf(into, " %.*s", FORMAT_SV(*this->timezone)); std::fputc('\n', into); } if(this->random_delay) std::fprintf(into, "RandomizedDelaySec=%zum\n", this->random_delay); if(this->persistent) std::fputs("Persistent=true\n", into); } auto generate_unit_name(std::uint64_t & seq) -> void { assert(!this->jobid.empty()); this->unit_name = ("cron-"s += this->jobid) += '-'; if(!this->persistent) { char buf[20 + 1]; // 18446744073709551615 this->unit_name += std::string_view{buf, static_cast(std::snprintf(buf, sizeof(buf), "%" PRIu64 "", seq++))}; } else { MD5_CTX ctx; MD5Init(&ctx); MD5Update(&ctx, reinterpret_cast(this->schedule_raw.data()), this->schedule_raw.size()); for(auto && hunk : this->command) { if(hunk.empty()) continue; MD5Update(&ctx, reinterpret_cast(""), 1); // NUL byte MD5Update(&ctx, reinterpret_cast(hunk.data()), hunk.size()); } char buf[((128 / 8) * 2) + 1]; MD5End(&ctx, buf); this->unit_name += std::string_view{buf, sizeof(buf) - 1}; } } // write the result in TARGET_DIR auto output() -> bool { auto output_err = [&](std::string_view f, const char * op) { char buf[512]; std::snprintf(buf, sizeof(buf), "%.*s: %s: %s", FORMAT_SV(f), op, std::strerror(errno)); this->log(Log::ERR, buf); }; assert(!this->unit_name.empty()); if(auto scriptlet = this->generate_scriptlet()) { // as a side-effect also changes this->execstart vore::file::FILE f{scriptlet->c_str(), "we"}; if(!f) return output_err(*scriptlet, "create"), false; auto first = true; for(auto && hunk : this->command) { if(hunk.empty()) continue; if(!std::exchange(first, false)) std::fputc(' ', f); this->debackslashpercentise(hunk, [&](auto && segment, auto) { std::fwrite(segment.data(), 1, segment.size(), f); }); } if(!first) std::fputc('\n', f); if(std::ferror(f) || std::fflush(f)) return output_err(*scriptlet, "write"), false; } auto timer = ((std::string{TARGET_DIR} += '/') += this->unit_name) += ".timer"sv; { vore::file::FILE t{timer.c_str(), "we"}; if(!t) return output_err(timer, "create"), false; this->generate_timer(t); if(std::ferror(t) || std::fflush(t)) return output_err(timer, "write"), false; } if(symlink(timer.c_str(), (((std::string{TIMERS_DIR} += '/') += this->unit_name) += ".timer"sv).c_str()) == -1 && errno != EEXIST) return output_err(timer, "link"), false; auto & service = timer; service.replace(service.size() - "timer"sv.size(), service.size(), "service"sv); { vore::file::FILE s{service.c_str(), "we"}; if(!s) return output_err(service, "create"), false; this->generate_service(s); if(std::ferror(s) || std::fflush(s)) return output_err(service, "write"), false; } return true; } }; template static auto for_each_file(const char * dirname, F && cbk) -> void { if(vore::file::DIR dir{dirname}) { auto fd = dirfd(dir); struct stat sb; for(auto && ent : dir) if(ent.d_type == DT_REG || (!fstatat(fd, ent.d_name, &sb, 0) && S_ISREG(sb.st_mode))) cbk(ent.d_name); } } static auto environment_write(const std::map & env, FILE * into) -> void { auto first = true; for(auto && [k, v] : env) { if(!std::exchange(first, false)) std::fputc(' ', into); auto quote = v.find(' ') != std::string::npos; if(quote) std::fputc('"', into); std::fwrite(k.data(), 1, k.size(), into); std::fputc('=', into); std::fwrite(v.data(), 1, v.size(), into); if(quote) std::fputc('"', into); } } template &), class H = void (*)(Job &&)> static auto parse_crontab(std::string_view filename, withuser_t withuser, std::map environment, bool anacrontab, cron_mail_success_t default_cron_mail_success, cron_mail_format_t default_cron_mail_format, F && cbk, std::optional> && preview_env = std::nullopt) -> bool { vore::file::mapping map; { vore::file::fd f{filename.data(), O_RDONLY | O_CLOEXEC}; if(f == -1) { if(withuser == withuser_t::initial) // Treat ENOENT as an error only for crontab -t and crontab -T, otherwise ignore return false; else return errno == ENOENT; } struct stat sb; fstat(f, &sb); if(!sb.st_size) return true; map = {nullptr, static_cast(sb.st_size), PROT_READ, MAP_PRIVATE, f, 0}; if(!map) return false; } std::string_view cron_inherit_variables; for(auto && line : vore::soft_tokenise{map, "\n"sv}) { while(!line.empty() && std::isspace(line[0])) line.remove_prefix(1); if(line.empty() || line[0] == '#') continue; while(!line.empty() && std::isspace(line.back())) line.remove_suffix(1); regmatch_t matches[3] = {{.rm_so = 0, .rm_eo = static_cast(line.size())}}; if(!regexec(&ENVVAR_RE, line.data(), sizeof(matches) / sizeof(*matches), matches, REG_STARTEND)) { auto key = line.substr(matches[1].rm_so, matches[1].rm_eo - matches[1].rm_so); auto value = line.substr(matches[2].rm_so, matches[2].rm_eo - matches[2].rm_so); for(char tostrip : {'\'', '\"', ' '}) { while(!value.empty() && value[0] == tostrip) value.remove_prefix(1); while(!value.empty() && value.back() == tostrip) value.remove_suffix(1); } if(key == "PERSISTENT"sv && value == "auto"sv) environment.erase("PERSISTENT"sv); else if(key == "CRON_INHERIT_VARIABLES"sv) cron_inherit_variables = value; else environment[key] = value; continue; } Job j{filename, line}; if(anacrontab) { j.decode_environment(environment, /*default_persistent=*/true, default_cron_mail_success, default_cron_mail_format); j.parse_anacrontab(); } else if(line[0] == '@') { j.decode_environment(environment, /*default_persistent=*/true, default_cron_mail_success, default_cron_mail_format); j.parse_crontab_at(withuser); } else { j.decode_environment(environment, /*default_persistent=*/false, default_cron_mail_success, default_cron_mail_format); j.parse_crontab_timespec(withuser); } j.decode(); j.generate_schedule(); cbk(j); } if(preview_env) { Job j{filename, ""sv}; preview_env->first(cron_inherit_variables, environment); j.decode_environment(environment, /*default_persistent=*/false, default_cron_mail_success, default_cron_mail_format); preview_env->second(std::move(j)); } return true; } static auto int_map(const std::string_view & str, bool & err, bool) -> std::size_t { std::size_t ret = -1; if(!vore::parse_uint<10>(MAYBE_DUPA(str), ret)) err = true; return ret; } static auto month_map(const std::string_view & month, bool & err, bool) -> std::size_t { static const constexpr std::string_view months[] = {"jan"sv, "feb"sv, "mar"sv, "apr"sv, "may"sv, "jun"sv, "jul"sv, "aug"sv, "sep"sv, "oct"sv, "nov"sv, "dec"sv}; if(auto ret = int_map(month, err); !err) return ret; else { auto mon = month.substr(0, 3); char buf[3]; std::transform(std::begin(mon), std::end(mon), buf, ::tolower); if(auto itr = std::find(std::begin(months), std::end(months), std::string_view{buf, mon.size()}); itr != std::end(months)) return (itr - std::begin(months)) + 1; else { err = true; return 0; } } } static auto dow_map(const std::string_view & dow_full, bool & err, bool to) -> std::size_t { static const constexpr std::string_view dows[] = {"sun"sv, "mon"sv, "tue"sv, "wed"sv, "thu"sv, "fri"sv, "sat"sv, "sun"sv}; auto dow = dow_full.substr(0, 3); char buf[3]; std::transform(std::begin(dow), std::end(dow), buf, ::tolower); if(auto itr = std::find(std::begin(dows) + to, std::end(dows), std::string_view{buf, dow.size()}); itr != std::end(dows)) return itr - std::begin(dows); else return int_map(dow_full, err); } template static auto parse_period(const std::string_view & value, const V & values, std::set & into, bool & raw_for_schedule, std::size_t (*mapping)(const std::string_view &, bool &, bool), std::size_t base) -> std::optional { std::string_view range = value; std::size_t step = 1; bool err{}; if(auto idx = value.find('/'); idx != std::string_view::npos) { range = value.substr(0, idx); auto rest = value.substr(idx + 1); if(rest.find('/') != std::string_view::npos) return "doubled /"; step = int_map(rest, err); if(err) return "/-skip not an integer"; } if(range == "*"sv) { for(ssize_t i = 0; i < std::distance(std::begin(values), std::end(values)); i += step) into.emplace(values[i]); return {}; } auto start = range, end = range; std::size_t max = std::distance(std::begin(values), std::end(values)) - 1; if(auto idx = range.find('~'); idx != std::string_view::npos) { start = range.substr(0, idx); end = range.substr(idx + 1); if(end.find('~') != std::string_view::npos) return "doubled ~"; raw_for_schedule = true; auto i_start = start.empty() ? base : mapping(start, err, false) - 1 + !base; auto i_end = end.empty() ? max : std::min(mapping(end, err, true) + !base, max); if(i_start > max || i_start > i_end) return nullptr; static std::default_random_engine rand{std::random_device{}()}; into.emplace(values[std::uniform_int_distribution{i_start, i_end}(rand)]); return std::nullopt; } if(auto idx = range.find('-'); idx != std::string_view::npos) { start = range.substr(0, idx); end = range.substr(idx + 1); if(end.find('-') != std::string_view::npos) return "doubled -"; } auto i_start = mapping(start, err, false) - 1 + !base; auto i_end = std::min(mapping(end, err, true) + !base, max + 1); if(err) return nullptr; bool any{}; for(std::size_t i = i_start; i < i_end; i += step) { into.emplace(values[i]); any = true; } return any ? std::nullopt : std::optional{nullptr}; } static auto generate_timer_unit(Job & job) -> void { static std::map seqs; if(job.valid && job.is_active()) { job.generate_unit_name(seqs[job.jobid]); job.output(); } } // schedule rerun of generators after /var is mounted static auto workaround_var_not_mounted() -> bool { auto service = std::string{TARGET_DIR} += "/cron-after-var.service"sv; if(vore::file::FILE f{service.c_str(), "we"}) { std::fputs("[Unit]\n" "Description=Rerun systemd-crontab-generator because /var is a separate mount\n" "Documentation=man:systemd.cron(7)\n" "After=cron.target\n" "ConditionDirectoryNotEmpty=" STATEDIR "\n" "\n" "[Service]\n" "Type=oneshot\n" "ExecStart=/bin/sh -c \"systemctl daemon-reload ; systemctl try-restart cron.target\"\n", f); if(std::ferror(f) || std::fflush(f)) return false; } else return false; auto MULTIUSER_DIR = std::string{TARGET_DIR} += "/multi-user.target.wants"sv; if(mkdir(MULTIUSER_DIR.c_str(), 0777) == -1 && errno != EEXIST) return false; if(symlink(service.c_str(), (MULTIUSER_DIR += "/cron-after-var.service"sv).c_str()) && errno != EEXIST) return false; return true; } // check if distribution also provide a native .timer static auto is_masked(const char * path, std::string_view name, vore::span *> distro_mapping) -> bool { auto unit_file = ("/___/systemd/system/"s += name) += ".timer"; for(auto root : {"lib", "etc", "run"}) { std::memcpy(unit_file.data() + 1, root, 3); if(!access(unit_file.c_str(), F_OK)) { const char * interjection = ""; struct stat sb; if(!stat(unit_file.c_str(), &sb) && sb.st_size == 0) interjection = " and masked"; log(Log::NOTICE, "ignoring %s/%.*s because it is overridden%s by native .timer unit", path, FORMAT_SV(name), interjection); return true; } } auto mapped_name = name; if(auto itr = vore::binary_find(std::begin(distro_mapping), std::end(distro_mapping), name, key_or_plain); itr != std::end(distro_mapping)) mapped_name = itr->second; auto name_distro = std::string{mapped_name} += ".timer"sv; if(!access(("/lib/systemd/system/"s += name_distro).c_str(), F_OK)) { log(Log::NOTICE, "ignoring %s/%.*s because there is %.*s", path, FORMAT_SV(name), FORMAT_SV(name_distro)); return true; } return false; } static auto is_backup(const char * path, const std::string_view & name) -> bool { if(name == ".placeholder"sv) return true; bool backup = name[0] == '.' || name.find('~') != std::string_view::npos || name.find(".dpkg-"sv) != std::string_view::npos || name.find(".rpm"sv) != std::string_view::npos || name == "0anacron"sv; if(backup) log(Log::DEBUG, "ignoring %s/%.*s", path, FORMAT_SV(name)); return backup; } static auto realmain() -> int { if(!mkdirp(TIMERS_DIR)) { log(Log::ERR, "making %.*s: %s", FORMAT_SV(TIMERS_DIR), std::strerror(errno)); return 1; } std::optional fallback_mailto; std::map inherit_variables_s; std::map distro_start_hour; auto toplevel_cron_mail_success = cron_mail_success_t::dflt; auto toplevel_cron_mail_format = cron_mail_format_t::dflt; if(!parse_crontab( "/etc/crontab", withuser_t::from_cmd0, {}, /*anacrontab=*/false, cron_mail_success_t::dflt, cron_mail_format_t::dflt, [&](auto && job) { if(!job.valid) { log(Log::ERR, "truncated line in /etc/crontab: %.*s", FORMAT_SV(job.line)); return; } // legacy boilerplate: ignore jobs that run /etc/cron.hourly, daily, weekly, monthly // (but save the starting hour for daily, weekly, and monthly) if(job.line.find("/etc/cron.hourly"sv) != std::string_view::npos) return; for(auto && disableable_dir : {"/etc/cron.daily"sv, "/etc/cron.weekly"sv, "/etc/cron.monthly"sv}) if(job.line.find(disableable_dir) != std::string_view::npos) { if(auto hour = *job.timespec_hour.begin(); hour != Job::TIMESPEC_ASTERISK) distro_start_hour[disableable_dir.substr("/etc/cron."sv.size())] = hour; return; } generate_timer_unit(job); }, std::make_optional(std::make_pair( [&](auto && cron_inherit_variables, auto && raw_variables) { for(auto var : vore::soft_tokenise{cron_inherit_variables, " \t"sv}) if(auto itr = raw_variables.find(var); itr != std::end(raw_variables)) inherit_variables_s.emplace(itr->first, itr->second); }, [&](auto && envjob) { if(auto itr = envjob.environment.find("MAILTO"sv); itr != std::end(envjob.environment)) fallback_mailto = itr->second; toplevel_cron_mail_success = envjob.cron_mail_success; toplevel_cron_mail_format = envjob.cron_mail_format; })))) log(Log::ERR, "%s: %s", "/etc/crontab", std::strerror(errno)); std::map inherit_variables; for(auto && [k, v] : inherit_variables_s) inherit_variables.emplace(k, v); for_each_file("/etc/cron.d", [&](std::string_view basename) { if(is_masked("/etc/cron.d", basename, {std::begin(CROND2TIMER), std::end(CROND2TIMER)})) return; if(is_backup("/etc/cron.d", basename)) return; auto filename = "/etc/cron.d/"s += basename; if(!parse_crontab(filename, withuser_t::from_cmd0, inherit_variables, /*anacrontab=*/false, toplevel_cron_mail_success, toplevel_cron_mail_format, [&](auto && job) { if(!job.valid) { log(Log::ERR, "truncated line in %.*s: %.*s", FORMAT_SV(filename), FORMAT_SV(job.line)); return; } if(fallback_mailto && job.environment.find("MAILTO"sv) == std::end(job.environment)) job.environment.emplace("MAILTO"sv, *fallback_mailto); generate_timer_unit(job); })) log(Log::ERR, "%s: %s", filename.c_str(), std::strerror(errno)); }); if(!USE_RUNPARTS) { auto i = 0u; for(auto period : {"hourly"sv, "daily"sv, "weekly"sv, "monthly"sv, "yearly"sv}) { ++i; auto directory = "/etc/cron."s += period; if(struct stat sb; stat(directory.c_str(), &sb) || !S_ISDIR(sb.st_mode)) continue; for_each_file(directory.c_str(), [&](std::string_view basename) { if(is_masked(directory.c_str(), basename, {std::begin(PART2TIMER), std::end(PART2TIMER)})) return; if(is_backup(directory.c_str(), basename)) return; auto filename = (directory + '/') += basename; if(access(filename.c_str(), X_OK)) return; std::string_view command = filename; Job job{filename, filename}; job.decode_environment(inherit_variables, /*default_persistent=*/true, toplevel_cron_mail_success, toplevel_cron_mail_format); job.period = period; job.start_hour = distro_start_hour[period]; // default 0 job.boot_delay = i * 5; job.command = {{&command, &command + 1}, {}, true}; job.jobid = (std::string{period} += '-') += basename; job.decode(); // ensure clean jobid job.generate_schedule(); if(fallback_mailto && job.environment.find("MAILTO"sv) == std::end(job.environment)) job.environment.emplace("MAILTO"sv, *fallback_mailto); job.unit_name = "cron-" + job.jobid; job.output(); }); } } if(!parse_crontab("/etc/anacrontab", withuser_t::from_basename, {}, /*anacrontab=*/true, toplevel_cron_mail_success, toplevel_cron_mail_format, [&](auto && job) { if(!job.valid) { log(Log::ERR, "truncated line in /etc/anacrontab: %.*s", FORMAT_SV(job.line)); return; } generate_timer_unit(job); })) log(Log::ERR, "%s: %s", "/etc/anacrontab", std::strerror(errno)); if(struct stat sb; !stat(STATEDIR, &sb) && S_ISDIR(sb.st_mode)) { // /var is available for_each_file(STATEDIR, [&](std::string_view basename) { if(basename.find('.') != std::string_view::npos) return; auto filename = (std::string{STATEDIR} += '/') += basename; if(!parse_crontab(filename, withuser_t::from_basename, inherit_variables, /*anacrontab=*/false, toplevel_cron_mail_success, toplevel_cron_mail_format, [&](auto && job) { generate_timer_unit(job); })) log(Log::ERR, "%s: %s", filename.c_str(), std::strerror(errno)); }); vore::file::fd{REBOOT_FILE, O_WRONLY | O_CREAT | O_CLOEXEC, 0666}; } else { if(!workaround_var_not_mounted()) log(Log::WARNING, "%s: %s", "cron-after-var.service", std::strerror(errno)); } return 0; } static auto check(const char * cron_file) -> int { bool err{}; if(!parse_crontab(cron_file, withuser_t::initial, {}, /*anacrontab=*/false, cron_mail_success_t::dflt, cron_mail_format_t::dflt, [&](auto && job) { if(!job.valid) { err = true; job.log(Log::ERR, "truncated line"); } else if(!job.period.empty()) { static const constexpr std::string_view valid_periods[] = {"annually"sv, "bi-annually"sv, "biannually"sv, "daily"sv, "hourly"sv, "midnight"sv, "minutely"sv, "monthly"sv, "quarterly"sv, "reboot"sv, "semi-annually"sv, "semiannually"sv, "weekly"sv, "yearly"sv}; // keep sorted if(!std::binary_search(std::begin(valid_periods), std::end(valid_periods), job.period)) { err = true; job.log(Log::ERR, "unknown schedule"); } } else if(job.timespec_month.contains(0) || job.timespec_dom.contains(0)) { err = true; job.log(Log::ERR, "month and day can't be 0"); } })) { err = true; log(Log::ERR, "%s: %s", cron_file, std::strerror(errno)); } return err; } static auto translate(const char * line) -> int { Job job{"-"sv, line}; job.parse_crontab_auto(); job.decode(); job.decode_command(); job.generate_schedule(); vore::file::FILE timer{3, "w"}; if(!timer) return log(Log::ERR, "%s", std::strerror(errno)), 3; job.generate_timer(timer); std::fputs("#Persistent=true\n", timer); vore::file::FILE service{4, "w"}; if(!service) return log(Log::ERR, "%s", std::strerror(errno)), 4; job.generate_service(service); return !job.valid; } int main(int argc, const char * const * argv) { std::setlocale(LC_ALL, "C.UTF-8"); if(argc == 1) { std::fprintf(stderr, "usage: %s destination_folder\n", argv[0]); return 1; } const char * file{}; bool file_check; SELF = vore::basename(std::string_view{argv[0]}).data(); TARGET_DIR = argv[1]; if(TARGET_DIR == "--check"sv || TARGET_DIR == "--translate"sv) { file_check = TARGET_DIR == "--check"sv; TARGET_DIR = "/ENOENT"sv; file = argv[2] ?: "-"; } TIMERS_DIR = std::string{TARGET_DIR} += "/cron.target.wants"sv; if(file) return file_check ? check(file) : translate(file); RUN_BY_SYSTEMD = argc == 4; if(RUN_BY_SYSTEMD) if(vore::file::FILE up{"/proc/uptime", "re"}) if(std::fscanf(up, "%" SCNu64 "", &UPTIME.emplace()) != 1) UPTIME = {}; return realmain(); } systemd-cron-2.5.1/src/include/000077500000000000000000000000001475512744600163775ustar00rootroot00000000000000systemd-cron-2.5.1/src/include/configuration.hpp.in000066400000000000000000000012731475512744600223670ustar00rootroot00000000000000#include using namespace std::literals; static const char * const VERSION = "@version@"; static const constexpr std::string_view USE_LOGLEVELMAX = "@use_loglevelmax@"sv; static const constexpr bool USE_RUNPARTS = @use_runparts@; static const constexpr bool HAVE_ONSUCCESS = @have_onsuccess@; static const constexpr std::string_view BOOT_DELAY = "@libexecdir@/systemd-cron/boot_delay"sv; #define STATEDIR "@statedir@" static const char * const SETGID_HELPER = "@libexecdir@/systemd-cron/crontab_setgid"; #define PAMNAME "@pamname@" // ignore if empty static const char * const REBOOT_FILE = "/run/crond.reboot"; systemd-cron-2.5.1/src/include/libvoreutils.hpp000066400000000000000000000260621475512744600216410ustar00rootroot00000000000000// SPDX-License-Identifier: 0BSD // Derived from voreutils headers #ifndef LIBVOREUTILS_HPP #define LIBVOREUTILS_HPP #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std::literals; namespace vore::file { namespace { template class mapping { public: constexpr mapping() noexcept {} mapping(void * addr, std::size_t length, int prot, int flags, int fd, off_t offset) noexcept { void * ret = mmap(addr, length, prot, flags, fd, offset); if(ret != MAP_FAILED) { static_assert(sizeof(C) == 1); this->map = {static_cast(ret), length}; this->opened = true; } } mapping(const mapping &) = delete; constexpr mapping(mapping && oth) noexcept : map(oth.map), opened(oth.opened) { oth.opened = false; } constexpr mapping & operator=(mapping && oth) noexcept { this->map = oth.map; this->opened = oth.opened; oth.opened = false; return *this; } ~mapping() { if(this->opened) munmap(const_cast(this->map.data()), this->map.size()); } constexpr operator bool() const noexcept { return !this->map.empty(); } constexpr operator std::basic_string_view() const noexcept { return this->map; } constexpr const std::basic_string_view & operator*() const noexcept { return this->map; } constexpr const std::basic_string_view * operator->() const noexcept { return &this->map; } private: std::basic_string_view map = {}; bool opened = false; }; } } namespace vore { namespace { template constexpr std::basic_string_view basename(std::basic_string_view str) noexcept { if(size_t idx = str.rfind('/'); idx != std::basic_string_view::npos) str.remove_prefix(idx + 1); return str; } } } namespace vore::file { namespace { template class fd { template friend class FILE; public: constexpr fd() noexcept = default; fd(const char * path, int flags, mode_t mode = 0, int from = AT_FDCWD) noexcept { if constexpr(allow_stdio) if(path[0] == '-' && !path[1]) { // path == "-"sv but saves a strlen() call on libstdc++ switch(flags & O_ACCMODE) { case O_RDONLY: this->desc = 0; return; case O_WRONLY: this->desc = 1; return; default: errno = EINVAL; return; } } while((this->desc = openat(from, path, flags, mode)) == -1 && errno == EINTR) ; this->opened = this->desc != -1; } fd(int desc) noexcept : desc(desc), opened(true) {} fd(const fd &) = delete; constexpr fd(fd && oth) noexcept { *this = std::move(oth); } constexpr fd & operator=(fd && oth) noexcept { this->swap(oth); return *this; } ~fd() { if(this->opened) close(this->desc); } constexpr operator int() const noexcept { return this->desc; } int take() & noexcept { this->opened = false; return this->desc; } constexpr void swap(fd & oth) noexcept { std::swap(this->desc, oth.desc); std::swap(this->opened, oth.opened); } private: int desc = -1; public: bool opened = false; }; template class FILE { public: static FILE tmpfile() noexcept { FILE ret; ret.stream = std::tmpfile(); ret.opened = ret.stream; return ret; } constexpr FILE() noexcept = default; FILE(const char * path, const char * opts) noexcept { if constexpr(allow_stdio) if(path[0] == '-' && !path[1]) { // path == "-"sv but saves a strlen() call on libstdc++ if(opts[0] && opts[1] == '+') { errno = EINVAL; return; } switch(opts[0]) { case 'r': this->stream = stdin; return; case 'w': case 'a': this->stream = stdout; return; default: errno = EINVAL; return; } } this->stream = std::fopen(path, opts); this->opened = this->stream; } FILE(const FILE &) = delete; constexpr FILE(FILE && oth) noexcept { *this = std::move(oth); } FILE(int oth, const char * opts) noexcept : stream(oth != -1 ? fdopen(oth, opts) : nullptr), opened(this->stream) {} FILE(fd && oth, const char * opts) noexcept : FILE(static_cast(oth), opts) { if(this->stream) oth.opened = false; } constexpr FILE & operator=(FILE && oth) noexcept { this->swap(oth); return *this; } ~FILE() { if(this->opened) std::fclose(this->stream); } constexpr operator ::FILE *() const noexcept { return this->stream; } constexpr void swap(FILE & oth) noexcept { std::swap(this->stream, oth.stream); std::swap(this->opened, oth.opened); } private: ::FILE * stream = nullptr; public: bool opened = false; }; template FILE(fd &&, const char *) -> FILE; } } namespace vore::file { namespace { struct DIR_iter { ::DIR * stream{}; struct dirent * entry{}; DIR_iter & operator++() noexcept { if(this->stream) do this->entry = readdir(this->stream); while(this->entry && ((this->entry->d_name[0] == '.' && this->entry->d_name[1] == '\0') || // this->entry == "."sv || this->entry == ".."sv, but saves trips to libc (this->entry->d_name[0] == '.' && this->entry->d_name[1] == '.' && this->entry->d_name[2] == '\0'))); return *this; } DIR_iter operator++(int) noexcept { const auto ret = *this; ++(*this); return ret; } constexpr bool operator==(const DIR_iter & rhs) const noexcept { return this->entry == rhs.entry; } constexpr bool operator!=(const DIR_iter & rhs) const noexcept { return !(*this == rhs); } constexpr const dirent & operator*() const noexcept { return *this->entry; } }; class DIR { public: using iterator = DIR_iter; DIR(const char * path) noexcept { this->stream = opendir(path); this->opened = this->stream; } DIR(const DIR &) = delete; constexpr DIR(DIR && oth) noexcept : stream(oth.stream), opened(oth.opened) { oth.opened = false; } ~DIR() { if(this->opened) closedir(this->stream); } constexpr operator ::DIR *() const noexcept { return this->stream; } iterator begin() const noexcept { return ++iterator{this->stream}; } constexpr iterator end() const noexcept { return {}; } private: ::DIR * stream = nullptr; bool opened = false; }; } } namespace vore { namespace { struct soft_tokenise_iter { // merge_seps = true using iterator_category = std::input_iterator_tag; using difference_type = void; using value_type = std::string_view; using pointer = std::string_view *; using reference = std::string_view &; std::string_view delim; std::string_view remaining; std::string_view token = {}; soft_tokenise_iter & operator++() noexcept { auto next = this->remaining.find_first_not_of(this->delim); if(next != std::string_view::npos) this->remaining.remove_prefix(next); auto len = this->remaining.find_first_of(this->delim); if(len != std::string_view::npos) { this->token = {this->remaining.data(), len}; this->remaining.remove_prefix(len); } else { this->token = this->remaining; this->remaining = {}; } return *this; } soft_tokenise_iter operator++(int) noexcept { const auto ret = *this; ++(*this); return ret; } constexpr bool operator==(const soft_tokenise_iter & rhs) const noexcept { return this->token == rhs.token; } constexpr bool operator!=(const soft_tokenise_iter & rhs) const noexcept { return !(*this == rhs); } constexpr std::string_view operator*() const noexcept { return this->token; } constexpr const std::string_view * operator->() const noexcept { return &this->token; } }; struct soft_tokenise { using iterator = soft_tokenise_iter; std::string_view str; std::string_view delim; iterator begin() noexcept { return ++iterator{this->delim, this->str}; } constexpr iterator end() const noexcept { return {}; } }; } } #ifndef strndupa #define strndupa(str, maxlen) \ __extension__({ \ auto _strdupa_str = str; \ auto len = strnlen(_strdupa_str, maxlen); \ auto ret = reinterpret_cast(alloca(len + 1)); \ std::memcpy(ret, _strdupa_str, len); \ ret[len] = '\0'; \ ret; \ }) #endif #define MAYBE_DUPA(strv) \ __extension__({ \ auto && _strv = strv; \ _strv.data()[_strv.size()] ? strndupa(_strv.data(), _strv.size()) : _strv.data(); \ }) namespace vore { namespace { template bool parse_uint(const char * val, T & out) { if(val[0] == '\0') return errno = EINVAL, false; if(val[0] == '-') return errno = ERANGE, false; char * end{}; errno = 0; auto res = std::strtoull(val, &end, base); out = res; if(errno) return false; if(res > std::numeric_limits::max()) return errno = ERANGE, false; if(*end != '\0') return errno = EINVAL, false; return true; } } } namespace vore { namespace { template struct span { T b, e; constexpr T begin() const noexcept { return this->b; } constexpr T end() const noexcept { return this->e; } constexpr std::size_t size() const noexcept { return this->e - this->b; } constexpr decltype(*(T{})) & operator[](std::size_t i) const noexcept { return *(this->b + i); } }; template span(T, T) -> span; } } namespace vore { namespace { template I binary_find(I begin, I end, const T & val) { // std::binary_search() but returns the iterator instead begin = std::lower_bound(begin, end, val); return (!(begin == end) && !(val < *begin)) ? begin : end; } template I binary_find(I begin, I end, const T & val, Compare comp) { begin = std::lower_bound(begin, end, val, comp); return (!(begin == end) && !comp(val, *begin)) ? begin : end; } } } namespace vore { namespace { template struct overload : Ts... { using Ts::operator()...; }; template overload(Ts...) -> overload; } } #endif systemd-cron-2.5.1/src/include/util.hpp000077500000000000000000000043341475512744600200740ustar00rootroot00000000000000#include "libvoreutils.hpp" #include static auto mkdirp(const std::string_view & path) -> bool { for(auto && seg : vore::soft_tokenise{path, "/"}) { std::string_view up_to_now{std::begin(path), std::end(seg)}; if(mkdir(MAYBE_DUPA(up_to_now), 0777) == -1 && errno != EEXIST) return false; } return true; } // Matches https://github.com/python/cpython/blob/3.11/Lib/getpass.py static auto getpass_getlogin() -> std::string_view { for(auto var : {"LOGNAME", "USER", "LNAME", "USERNAME"}) if(auto val = std::getenv(var)) return val; static char pwusername[LOGIN_NAME_MAX + 1]; if(auto ent = getpwuid(getuid())) return std::strncpy(pwusername, ent->pw_name, LOGIN_NAME_MAX); return {}; } static const regex_t ENVVAR_RE = [] { regex_t ret; assert(!regcomp(&ret, R"regex(^([A-Za-z_0-9]+)[[:space:]]*=[[:space:]]*(.*)$)regex", REG_EXTENDED | REG_NEWLINE)); assert(ret.re_nsub == 2); return ret; }(); // https://git.sr.ht/~nabijaczleweli/fzifdso/tree/a05110a75aa8ab1abf92a0193b5772b74036b80a/item/src/fido2.cpp#L220 struct b64 { static const constexpr char alphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_"; static const constexpr std::uint8_t alphabet_bits = 6 /*__builtin_ctz(alphabet.size())*/; FILE * into; std::uint16_t acc{}; std::uint8_t accsz{}; auto feed(const std::string_view & data) -> void { // fprintf(stderr, "feed='%.*s'\n", (int)data.size(), data.data()); this->feed(reinterpret_cast(data.data()), reinterpret_cast(data.data() + data.size())); } auto feed(const std::uint8_t * beg, const std::uint8_t * end) -> void { for(;;) { if(this->accsz < alphabet_bits) { if(beg == end) break; this->acc |= static_cast(*beg++) << ((16 - 8) - this->accsz); this->accsz += 8; } std::fputc(alphabet[this->acc >> (16 - alphabet_bits)], this->into); this->acc <<= alphabet_bits; this->accsz -= alphabet_bits; } } auto finish() -> void { if(this->accsz) std::fputc(alphabet[this->acc >> (16 - alphabet_bits)], this->into); if(this->accsz) { if(this->accsz <= 4) std::fputc('=', this->into); if(this->accsz == 2) std::fputc('=', this->into); } } }; systemd-cron-2.5.1/src/lib/000077500000000000000000000000001475512744600155225ustar00rootroot00000000000000systemd-cron-2.5.1/src/lib/sysusers.d/000077500000000000000000000000001475512744600176445ustar00rootroot00000000000000systemd-cron-2.5.1/src/lib/sysusers.d/crontab.conf000066400000000000000000000000161475512744600221400ustar00rootroot00000000000000g crontab - - systemd-cron-2.5.1/src/lib/sysusers.d/systemd-cron.conf000066400000000000000000000001031475512744600231340ustar00rootroot00000000000000u _cron-failure -:systemd-journal - /nonexistent /usr/sbin/nologin systemd-cron-2.5.1/src/man/000077500000000000000000000000001475512744600155275ustar00rootroot00000000000000systemd-cron-2.5.1/src/man/anacrontab.5.in000066400000000000000000000050651475512744600203400ustar00rootroot00000000000000.Dd 2023-08-19 .Dt ANACRONTAB 5 .Os systemd-cron @version@ . .Sh NAME .Nm /etc/anacrontab .Nd monotonic jobs . .Sh DESCRIPTION The file .Nm follows the rules previously set by \fBanacron(8)\fR. .Pp Lines starting with .Sq Sy # are comments. .Pp Environment variables can be set using .Va variable Ns = Ns Fa value alone on a line. .Pp The special .Sy RANDOM_DELAY (in minutes) environment variable is translated to .Sy RandomizedDelaySec= . .Pp The special .Sy START_HOURS_RANGE (in hours) environment variable is translated to the hour component of .Sy OnCalendar= . anacron expects a range in the format .Va start Ns - Ns Va end , but .Nm systemd-crontab-generator only uses .Va start . .Pp The other lines are job-descriptions in the white-space-separated format .D1 Va period delay job-identifier command where .Bl -tag -compact -width ".Va job-identifier" -offset 6n .It Va period is a number of days to wait between each job execution, or one of the special values .Sy @reboot , .Sy @minutely , .Sy @hourly , .Sy @midnight , .Sy @daily , .Sy @weekly , .Sy @monthly , .Sy @quarterly , .Sy @semi-annually , .Sy @yearly . . .It Va delay is the number of extra minutes to wait before starting job, translated to .Sy OnBootSec= , . .It Va job-identifier is a single word used by .Nm systemd-crontab-generator to construct dynamic unit names in the form .Sy cron-anacron- Ns Va job-identifier Ns Sy - Ns Ar MD5 Ns Sy .\& Ns Brq Sy timer , service , . .It Va command is the program to run by the shell. .El . .Sh BUGS .Nm systemd-crontab-generator doesn't support multiline commands. .Pp Any .Va period greater than .Em 30 is rounded to the closest month. .Pp There are subtle differences on how anacron and systemd handle persistent timers: anacron will run a weekly job at most once a week, with a guaranteed minimum delay of 6 days between runs, whereas systemd will try to run it every monday at midnight, or at system boot. In the most extreme case, if a system was booted on sunday, weekly jobs will run that day and the again the next (mon)day. .Pp There is no difference for the daily job. . .Sh NOTES Real anacron only accepts .Sy @monthly and .Sy @yearly as .Va period ; all others listed above are .Xr systemd.cron 7 Ns 's extensions. .Pp Unlike .Xr crontab 5 , every .Xr anacrontab 5 job is persistent by default. . .Sh DIAGNOSTICS After editing .Nm , you can run .Nm journalctl Fl n and .Nm systemctl Cm list-timers to see if the timers have well been updated. . .Sh SEE ALSO .Xr systemd.timer 5 , .Xr systemd-crontab-generator 8 . .\".Sh AUTHOR .\".An Alexandre Detiste Aq Mt alexandre@detiste.be systemd-cron-2.5.1/src/man/crontab.1.in000066400000000000000000000057241475512744600176560ustar00rootroot00000000000000.Dd 2023-08-13 .Dt CRONTAB 1 .Os systemd-cron @version@ . .Sh NAME .Nm crontab .Nd maintain crontab files for individual users . .Sh SYNOPSIS .Nm .Op Fl u Ar user .Op Ar newtab . .Nm .Fl l .Op Fl u Ar user . .Nm .Fl r .Op Fl i .Op Fl u Ar user . .Nm .Fl e .Op Fl u Ar user . .Nm .Fl s . .Nm .Fl t .Ar line . .Nm .Fl T .Ar crontab . .Sh DESCRIPTION .Nm lets users install, uninstall, view, and edit recurrent jobs in the .Xr crontab 5 format, as well as pre-view and convert them to .Xr systemd.timer 5 pairs. root may also spy on who which users have installed crontabs. .Pp .\" Assumes DEFAULT_NOACCESS=0 Each user may have their own crontab, but this can be limited by .Pa /etc/cron.allow to create an explicit allow-list or .Pa /etc/cron.deny to deny access to individual users. .Pp Crontabs are checked before installing \(em if they are found to be invalid, installation is aborted and a summary of errors is written to the standard error stream. . .Sh OPTIONS .Bl -tag -compact -width ".Fl u , -user Ns = Ns Ar user" .It (by default) replace the user's crontab from .Ar newtab .Pq standard input stream if Qo Sy - Qc , the default . . .It Fl l , -list Copy user's crontab to the standard output stream, or error if there is none. . .It Fl r , -remove Remove user's crontab. .It Fl i , -ask Output a confirmation prompt before doing so. . .It Fl e , -edit Let user edit crontab, install when they're done. . .It Fl s , -show List which users have a crontab installed. Nonexistent users are warned about to the standard error stream. Only root can do this. . .It Fl t , -translate Validate and translate a .Xr crontab 5 .Ar line into a native .Xr systemd.timer 5 pair to the standard output stream. . .It Fl T , -test Validate whether .Ar crontab is a valid .Xr crontab 5 file. .Pp . .It Fl u , -user Ns = Ns Ar user Edit .Ar user Ns 's crontab instead of the currently-logged-in user's. Only root can do this, and they should be careful about using .Nm without this option \(em the current user is determined by .Pf $ Ev LOGNAME , .Pf $ Ev USER , .Pf $ Ev LNAME .Pf $ Ev USERNAME , and only then by the real UID! .El . .Sh FILES .Bl -tag -compact -width ".Pa @statedir@" .\" Assumes DEFAULT_NOACCESS=0 .It Pa /etc/cron.allow If exists, only users listed here (one username per line) can install their own crontabs. Otherwise, everyone can. . .It Pa /etc/cron.deny Users listed here aren't allowed to install their own crontabs. . .It Pa @statedir@ Crontabs live here. .El . .Sh ENVIRONMENT .Ev EDITOR , .Ev VISUAL , .Nm editor , .Nm vim , .Nm nano , and .Nm mcedit are tried, in order, when using .Fl e . . .Sh SEE ALSO .Xr crontab 5 , .Xr systemd.cron 7 for a summary of the format and how to tweak installed cronjobs \(em .Nm systemctl Cm edit Li cron- Ns Ar schedule Ns Li .\& Ns Brq Li timer Ns |\& Ns Li service . . .Sh LIMITATIONS SELinux is not supported. . .\".Sh AUTHOR .\".An -split .\".An Konstantin Stepanov Aq Mt me@kstep.me .\".An Alexandre Detiste Aq Mt alexandre@detiste.be .\"\(em manual and setgid helper systemd-cron-2.5.1/src/man/crontab.5.in000066400000000000000000000342551475512744600176630ustar00rootroot00000000000000.\"/* Copyright 1988,1990,1993,1994 by Paul Vixie .\" * All rights reserved .\" * .\" * Distribute freely, except: don't remove my name from the source or .\" * documentation (don't take credit for my work), mark your changes (don't .\" * get me blamed for your possible bugs), don't alter or remove this .\" * notice. May be sold if buildable source is provided to buyer. No .\" * warrantee of any kind, express or implied, is included with this .\" * software; use at your own risk, responsibility for damages (if any) to .\" * anyone resulting from the use of this software rests entirely with the .\" * user. .\" * .\" * Send bug reports, bug fixes, enhancements, requests, flames, etc., and .\" * I'll try to keep a version up to date. I can be reached as follows: .\" * Paul Vixie uunet!decwrl!vixie!paul .\" */ .\" .\" $Id: crontab.5,v 2.4 1994/01/15 20:43:43 vixie Exp $ .\" .TH CRONTAB 5 "03 July 2014" "systemd-cron @version@" "crontab" .UC 4 .SH NAME crontab \- tables for driving systemd-cron .SH DESCRIPTION A .I crontab file contains instructions to .IR systemd-cron of the general form: ``run this command at this time on this date''. Each user has their own crontab, and commands in any given crontab will be executed as the user who owns the crontab. .PP Blank lines and leading spaces and tabs are ignored. Lines whose first non-space character is a hash-sign (#) are comments, and are ignored. Note that comments are not allowed on the same line as cron commands, since they will be taken to be part of the command. Similarly, comments are not allowed on the same line as environment variable settings. .PP An active line in a crontab will be either an environment setting or a cron command. The crontab file is parsed from top to bottom, so any environment settings will affect only the cron commands below them in the file. An environment setting is of the form, .PP name = value .PP where the spaces around the equal-sign (=) are optional, and any subsequent non-leading spaces in .I value will be part of the value assigned to .IR name . The .I value string may be placed in quotes (single or double, but matching) to preserve leading or trailing blanks. The .I value string is .B not parsed for environmental substitutions or replacement of variables, thus lines like .PP PATH = $HOME/bin:$PATH .PP will not work as you might expect. And neither will this work .PP A=1 B=2 C=$A $B .PP There will not be any substitution for the defined variables in the last value. .PP In PATH, tilde-expansion is performed on elements starting with "~/", so this works as expected: .PP SHELL=/bin/bash PATH=~/bin:/usr/bin/:/bin .PP .I Special variables: .TP .BR SHELL ", " PATH ", " USER ", " LOGNAME ", " HOME ", " LANG Those are set up automatically by systemd itself, see .IR systemd.exec (5) SHELL defaults to /bin/sh. SHELL and PATH may be overridden by settings in the crontab. .TP .B MAILTO .br When sending output from a job, .IR systemd.cron (7) will look at MAILTO. If MAILTO is defined mail is sent to this email address. MAILTO may also be used to direct mail to multiple recipients by separating recipient users with a comma. If MAILTO is defined but empty (MAILTO=""), no mail will be sent. Otherwise mail is sent to the owner of the crontab. .br By default, this mail contains .B systemctl status and the full log for the failed run, copied from the journal. .TP .B MAILFROM .br When sending output from a job, .IR systemd.cron (7) will look at MAILFROM. If MAILFROM is defined, mail is sent from this email address. Otherwise it's seen as being sent by "root". .TP .B CRON_MAIL_SUCCESS Control if (when) to send mail with output from successful jobs. .br .BR 'nonempty' ,\ 'non-empty' : mail is only sent if the job left anything in the journal (i.e. wrote something to the standard output or error streams); this is the default, and matches classic cron .br .BR 'always' ,\ 'yes' ,\ 'true' ,\ '1' : always send mail .br .BR 'never' ,\ 'no' ,\ 'false' ,\ '0' : never send mail for a successful job .IP Mail is .I always sent for failed jobs. .TP .B CRON_MAIL_FORMAT Control the format of the content of cron-job-related messages. .br .BR 'normal' : .B systemctl status + .B journalctl output (incl. time, process names, the usual) for the run; this is the default .br .BR 'nometadata' ,\ 'no-metadata' : raw journal contents .RB ( -o\ cat : just standard output + error streams); this matches classic cron .IP .B CRON_MAIL_SUCCESS and .BR CRON_MAIL_FORMAT , if changed in .IR /etc/crontab , are remembered for all other crontabs .RI ( /etc/cron.d ", " /etc/anacron ", users'\ crontabs)" and act as an administrator-controlled default. They can be set to .BR 'inherit' to get that default back. .TP .B CRON_INHERIT_VARIABLES In the top-level .IR /etc/crontab : a whitespace-separated list of variables .RI ( including " control statements that get removed from the environment otherwise)" to remember into other crontabs .RI ( /etc/cron.d ", users'\ crontabs; not " /etc/anacron ). This allows instituting a global .BR RANDOM_DELAY / SHELL /&c.\& default policy. .br Elsewhere: ignored. .TP .B RANDOM_DELAY (in minutes) environment variable is translated to .BR RandomizedDelaySec= . .TP .B DELAY (in minutes) environment variable is translated to .BR OnBootSec= . This works like the 'delay' field of anacrontab(5) and make systemd wait # minutes after boot before starting the unit. This value can also be used to spread out the start times of @daily/@weekly/@monthly... jobs on a 24/24 system. .TP .B START_HOURS_RANGE (in hours) environment variable is translated to the .I 'hour' component of .BR OnCalendar= . This variable is inherited from anacrontab(5), but also supported in crontab(5) by systemd-crontab-generator. Anacron expect a time range in the START-END format (eg: 6-9), systemd-crontab-generator will only use the starting hour of the range as reference. Unless you set this variable, all the @daily/@weekly/@monthly/@yearly jobs will run at midnight. If you set this variable and the system was off during the ours defined in the range, the (persistent) job will start at boot. .TP .B PERSISTENT With this flag, you can override the generator default heuristic. .br .BR 'yes' : force all further jobs to be persistent .br .BR 'auto' : only recognize @ keywords to be persistent (this is the default) .br .BR 'no' : force all further jobs not to be persistent .TP .BR TZ ", " CRON_TZ The job is scheduled in this time-zone instead of in the system time-zone. Must be a full IANA time-zone name (as found under .IR /usr/share/zoneinfo ), or empty to reset to the default timezone, otherwise no special semantics. Always passed to the job. .TP .B BATCH This boolean flag is translated to options .B CPUSchedulingPolicy=idle and .B IOSchedulingClass=idle when set. .PP The format of a .B cron command is the same as the one defined by the cron daemon. Each line has five time and date fields, followed by a command, followed by a newline character ('\\n'). The system crontab (/etc/crontab) and the packages crontabs (/etc/cron.d/*) use the same format, except that the username for the command is specified after the time and date fields and before the command. The fields may be separated by spaces or tabs. .PP Commands are executed by .IR systemd when the minute, hour, and month of year fields match the current time, .I and when at least one of the two day fields (day of month, or day of week) match the current time (see ``Note'' below). The time and date fields are: .IP .ta 1.5i field allowed values .br ----- -------------- .br minute 0-59 .br hour 0-23 .br day of month 1-31 .br month 1-12 (or names, see below) .br day of week 0-7 (0 or 7 is Sun, or use names) .br .PP A field may be an asterisk (*), which always stands for ``first\-last''. .PP Ranges of numbers are allowed. Ranges are two numbers separated with a hyphen. The specified range is inclusive. For example, 8-11 for an ``hours'' entry specifies execution at hours 8, 9, 10 and 11. .PP A random value (within the legal range) may be obtained by using the `~' character in a field. The interval of the random value may be specified explicitly, for example ``0~30'' will result in a random value between 0 and 30 inclusive. If either (or both) of the numbers on either side of the `~' are omitted, the appropriate limit (low or high) for the field will be used. .PP Lists are allowed. A list is a set of numbers (or ranges) separated by commas. Examples: ``1,2,5,9'', ``0-4,8-12''. .PP Step values can be used in conjunction with ranges. Following a range with ``/'' specifies skips of the number's value through the range. For example, ``0-23/2'' can be used in the hours field to specify command execution every other hour (the alternative in the V7 standard is ``0,2,4,6,8,10,12,14,16,18,20,22''). Steps are also permitted after an asterisk, so if you want to say ``every two hours'', just use ``*/2''. .PP Names can also be used for the ``month'' and ``day of week'' fields. Use the first three letters of the particular day or month (case doesn't matter). Ranges or lists of names are not allowed. .PP The ``sixth'' field (the rest of the line) specifies the command to be run. The entire command portion of the line will be executed by /bin/sh or by the shell specified in the SHELL variable of the crontab file. .PP If the command contains an unescaped .B % character, it is instead split thereon: the part before is run by the shell, the part after is given on the standard input stream, with each subsequent .B % replaced by a newline. .BR % s can be escaped as .BR \e% , and produce a literal .RB % . .PP systemd-crontab-generator doesn't handle multi-line command split by the .B % character like vixie-cron. .PP Note: The day of a command's execution can be specified by two fields \(em day of month, and day of week. If both fields are restricted (i.e., aren't .BR * ), the command will be run when .I either field matches the current time. For example, .br ``30 4 1,15 * 5'' would cause a command to be run at 4:30 am on the 1st and 15th of each month, plus every Friday. One can, however, achieve the desired result by adding a test to the command (see the last example in EXAMPLE CRON FILE below). .PP Instead of the first five fields, one of eight special strings may appear: .IP .ta 1.5i string meaning .br ------ ------- .br @reboot Run once, at startup. .br @yearly Run once a year, "0 0 1 1 *". .br @annually (same as @yearly) .br @monthly Run once a month, "0 0 1 * *". .br @weekly Run once a week, "0 0 * * 0". .br @daily Run once a day, "0 0 * * *". .br @midnight (same as @daily) .br @hourly Run once an hour, "0 * * * *". .br .PP Please note that startup, as far as @reboot is concerned, may be before some system daemons, or other facilities, were startup. This is due to the boot order sequence of the machine. .SH EXAMPLE CRON FILE The following lists an example of a user crontab file. .nf # use /bin/bash to run commands, instead of the default /bin/sh SHELL=/bin/bash # mail errors to `paul', no matter whose crontab this is MAILTO=paul # # run five minutes after midnight, every day 5 0 * * * $HOME/bin/daily.job >> $HOME/tmp/out 2>&1 # run at 2:15pm on the first of every month .\" -- output mailed to paul 15 14 1 * * $HOME/bin/monthly # run at 10 pm on weekdays, annoy Joe # runs 'mail \-s "It's 10 pm" joe', with 'Joe,\en\enWhere are your kids?\en' on stdin 0 22 * * 1-5 mail \-s "It's 10pm" joe%Joe,%%Where are your kids?% 23 0-23/2 * * * echo "run 23 minutes after midn, 2am, 4am ..., everyday" 5 4 * * sun echo "run at 5 after 4 every sunday" # Run on every second Saturday of the month 0 4 8-14 * * test $(date +\\%u) \-eq 6 && echo "2nd Saturday" .fi .SH EXAMPLE SYSTEM CRON FILE The following lists the content of a regular system-wide crontab file. Unlike a user's crontab, this file has the username field, as used by /etc/crontab. .nf # /etc/crontab: system-wide crontab # Unlike any other crontab you don't have to run the `crontab' # command to install the new version when you edit this file # and files in /etc/cron.d. These files also have username fields, # that none of the other crontabs do. SHELL=/bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin # m h dom mon dow user command 17 * * * * root cd / && run-parts \-\-report /etc/cron.hourly 25 6 * * * root test \-x /usr/sbin/anacron || ( cd / && run-parts \-\-report /etc/cron.daily ) 47 6 * * 7 root test \-x /usr/sbin/anacron || ( cd / && run-parts \-\-report /etc/cron.weekly ) 52 6 1 * * root test \-x /usr/sbin/anacron || ( cd / && run-parts \-\-report /etc/cron.monthly ) # .fi .PP This is only an example, .B systemd-cron uses native units instead for those jobs. .br If you add those lines, your jobs will run twice. .SH SEE ALSO systemd.cron(7), systemd-crontab-generator(8), crontab(1) Some extra settings can only be tweaked with .PP systemctl edit cron-.[timer|service] .TP see systemd.cron(7) for more details. .SH LIMITATIONS The .I crontab syntax does not make it possible to define all possible periods one could imagine off. For example, it is not straightforward to define the last weekday of a month. If a task needs to be run in a specific period of time that cannot be defined in the .I crontab syntaxs the best approach would be to have the program itself check the date and time information and continue execution only if the period matches the desired one. .B systemd-crontab-generator doesn't support these .B vixie-cron features: .TP * spawning forking daemons, the 'Service' units are all set with 'Type=oneshot' .TP * vixie-cron requires that each entry in a crontab end in a newline character. If the last entry in a crontab is missing a newline (ie, terminated by EOF), vixie-cron will consider the crontab (at least partially) broken. .br systemd-crontab-generator considers this crontab as valid .SH DIAGNOSTICS You can see how your crontab where translated by typing: .br .B systemctl cat cron--* .PP .B systemctl cat does support command-line completion. .SH AUTHOR Paul Vixie is the author of .I cron and original creator of this manual page. This page has also been modified for Debian by Steve Greenland, Javier Fernandez-Sanguino and Christian Kastner. .br This page has been reworded by Alexandre Detiste for inclusion in systemd-cron. systemd-cron-2.5.1/src/man/systemd-crontab-generator.8.in000066400000000000000000000041741475512744600233350ustar00rootroot00000000000000.Dd 2023-08-13 .Dt SYSTEMD-CRONTAB-GENERATOR 8 .Os systemd-cron @version@ . .Sh NAME .Nm systemd-crontab-generator .Nd translate cron schedules in systemd Units . .Sh SYNOPSIS .Li /usr/lib/systemd/system-generators/ Ns Nm systemd-crontab-generator .Ar output-dir . .Sh DESCRIPTION .Nm systemd-crontab-generator is a .Xr systemd.generator 7 translating classic cron .Sx FILES into native .Xr systemd.timer 5 Ns / Ns Xr systemd.service 5 pairs. .Pp It runs automatically .Bl -bullet -compact -offset 2n -width 1n .It during early boot, . .It a second time by .Ic cron-after-var.service if .Pa /var is a separate mount-point, in order to process user crontabs in .Pa @statedir@ , . .It after each update to .Pa /etc/crontab Ns \(dg and .Pa /etc/anacrontab Ns \(dg, and . .It when packages add files under .Pa /etc/cron.d Ns \(dg .El (\(dg: monitored by .Ic cron-update.path ) . . .Sh FILES .Bl -tag -compact -width ".Pa @statedir@" .\" Starts same as in systemd.cron.7 .It Pa /etc/crontab Administrator's system crontab, see .Xr crontab 5 . . .It Pa /etc/cron.d System crontabs managed by packages live here. . .It Pa /etc/anacrontab .Xr anacrontab 5 . .It Pa @statedir@ Users' crontabs live here. . .It Pa /run/systemd/generator Automatically generated units go here. . .It Pa /run/crond.reboot If this file exists, .Sy @reboot jobs aren't re-generated. Managed automatically. . .It Pa /var/lib/systemd/timers .Xr systemd.timer 5 Ns s with the .Fa Persistent flag set store their timestamps here. .El . .Sh DIAGNOSTICS .\" First para same as in systemd.cron.7 .Nm systemctl Cm list-timers shows an overview of current timers and when they'll elapse. .Pp If you see something to the effect of .Dl /usr/lib/systemd/system-generators/systemd-crontab-generator failed with error code 1. in the journal, you can run .Li /usr/lib/systemd/system-generators/ Ns Nm systemd-crontab-generator Pa /tmp/test for more verbose output. . .Sh SEE ALSO .Xr crontab 5 , .Xr systemd.unit 5 , .Xr systemd.timer 5 , .Xr systemd.cron 7 . .\".Sh AUTHOR .\".An -split .\".An Konstantin Stepanov Aq Mt me@kstep.me .\"\(em generator .\".An Alexandre Detiste Aq Mt alexandre@detiste.be .\"\(em manual systemd-cron-2.5.1/src/man/systemd.cron.7.in000066400000000000000000000134171475512744600206620ustar00rootroot00000000000000.Dd 2023-08-13 .Dt SYSTEMD.CRON 7 .Os systemd-cron @version@ . .Sh NAME .Nm systemd.cron .Nd systemd units for cron periodic jobs . .Sh SYNOPSIS .de headline .if \\n[timer_\\$1] \\{ . .br cron-\\$1.timer, cron-\\$1.target, cron-\\$1.service . \\} .. cron.target .headline boot .headline minutely .headline hourly .headline daily .headline weekly .headline monthly .headline quarterly .headline semi-annually .headline yearly .br cron-update.path, cron-update.service .br cron-mail@.service . .Sh DESCRIPTION These units provide the functionality usually afforded by the cron daemon \(em running scripts in .Pa /etc/cron. Ns Ar schedule directories and sending mail on failure. .Pp Crontabs are monitored by .Nm cron-update.path and are automatically translated by .Xr systemd-crontab-generator 8 . . .Sh FILES .Bl -tag -compact -width ".Pa @statedir@" .\" Starts same as in systemd-crontab-generator.8 .It Pa /etc/crontab Administrator's system crontab, see .Xr crontab 5 . . .It Pa /etc/cron.d System crontabs managed by packages live here. . .It Pa /etc/anacrontab .Xr anacrontab 5 . .It Pa @statedir@ Users' crontabs live here. . .Pp .de etccron .if \\n[etccron_\\$1] \{ . .It Pa /etc/cron.\\$1 Directory for scripts to be executed \\$2. . \} .. .etccron boot "on boot" .etccron minutely "every minute" .etccron hourly "every hour" .etccron daily "every day" .etccron weekly "every week" .etccron monthly "every month" .etccron quarterly "every 3 months" .etccron semi-annually "every 6 months" .etccron yearly "every year" .Pp . .It Pa /usr/lib/systemd/system/ Ns Ar schedule Ns Pa .timer .It Pa /etc/systemd/system/ Ns Ar schedule Ns Pa .timer Native systemd timers will override cron jobs with the same name. .Pp You can also use this mechanism to mask an unneeded crontab provided by a package via .Nm systemctl Cm mask Ar package Ns Li .timer . .El . .Sh UNITS .Bl -tag -compact -width ".Pa /etc/cron.semi-annually" .It Nm cron.target Target unit which starts the others, needs to be enabled to use systemd-cron. . .It Nm cron-update.path Monitors .Sx FILES and calls . .It Nm cron-update.service which runs .Nm systemctl Cm daemon-reload to re-run the generator. .Pp . .if '@enable_runparts@'yes' \{ . .It Nm cron- Ns Ar schedule Ns Nm .timer Triggers .Nm cron- Ns Ar schedule Ns Nm .service on .Ar target . Started and stopped by the cron.target unit. . .It Nm cron- Ns Ar schedule Ns Nm .target Pulls in all service units wanted by the target, i.a.\& .Nm cron- Ns Ar schedule Ns Nm .service . . .It Nm cron- Ns Ar schedule Ns Nm .service Runs scripts in the .Pa /etc/cron. Ns Ar * directories. You can use .Xr journalctl 1 to view the output of scripts run from these units. .Pp . \} . .It Nm cron-mail@.service Sends mail (via .Xr sendmail 1 , which can be overridden with .Pf $ Ev SENDMAIL ) in case a cron service unit fails, succeeds, or succeeds-but-only-if-it-wrote-something. The instance name .Pq the bit after the Sy @ is the unit name, followed by optional arguments delimited by colons .Pq Sq Sy \&: : .Bl -tag -compact -width ".Sy nometadata" .It Sy nonempty exit silently if the unit produced no output .Pq equivalent to Ev CRON_MAIL_SUCCESS Ns = Ns Sy nonempty for .Va OnSuccess Ns = ) , . .It Sy nometadata don't include .Nm systemctl Cm status output, don't add usual .Nm journalctl metadata to the output .Pq equivalent to Ev CRON_MAIL_FORMAT Ns = Ns Sy nometadata , and . .It Sy verbose log reason before exiting silently. .El (upper-case arguments are ignored). .Pp Overriding this via .Nm systemctl Cm edit can be useful, especially for units under .Pa /etc/cron. Ns Ar * . .El . .Sh BUGS Do .Em not use with a cron daemon or anacron, otherwise scripts may be executed multiple times. .Pp All services are run with .Fa Type Ns = Ns Sy oneshot , which means you can't use systemd-cron to launch long lived forking daemons. . .Sh EXTENSIONS The generator can optionally turn any crontabs in persistent timers with the .Ev PERSISTENT Ns = Ns Sy true flag, while a regular cron and anacron setup won't catch up on the missed executions of crontabs on reboot. . .Sh EXAMPLES .Ss Start cron units .Bd -literal -compact .Li # Nm systemctl Cm start Li cron.target .Ed . .Ss Start cron units on boot .Bd -literal -compact .Li # Nm systemctl Cm enable Li cron.target .Ed . .Ss View script output .Bd -literal -compact .Li # Nm journalctl -u cron-daily .Ed . .Ss Override some generated timer start time .Bd -literal -compact -offset 4n .Li # Nm systemctl Cm edit Li cron-geoip-database-contrib-root-1.timer .Ed and add .Bd -literal -compact -offset 4n [Timer] OnCalendar= OnCalendar=*-*-* 18:36:00 .Ed . .Ss "Override cron-daily.service priority, useful for old computers" .Bd -literal -compact -offset 4n .Li # Nm systemctl Cm edit Li cron-daily.service .Ed and add .Bd -literal -compact -offset 4n [Service] CPUSchedulingPolicy=idle IOSchedulingClass=idle .Ed . .Ss "Example service file executed every hour" .Bd -literal -compact [Unit] Description=Update the man db [Service] Nice=19 IOSchedulingClass=2 IOSchedulingPriority=7 ExecStart=/usr/bin/mandb --quiet [Install] WantedBy=cron-hourly.target .Ed . .Sh NOTES The exact times scripts are executed is determined by the values of the special calendar events .Sy hourly , .Sy daily , .Sy weekly , .Sy monthly , and .Sy yearly defined in .Xr systemd.time 7 . .if '@enable_runparts@'yes' \{ . .Pp .Xr run-parts 8 is used to run scripts, which must be executable by root. . \} . .Sh DIAGNOSTICS .\" First para same as in systemd-crontab-generator.8 .Nm systemctl Cm list-timers shows an overview of current timers and when they'll elapse. . .Sh SEE ALSO .Xr crontab 1 , .Xr systemd 1 , .Xr crontab 5 , .Xr systemd.service 5 , .Xr systemd.timer 5 , .Xr systemd.unit 5 , .Xr systemd.time 7 , .Xr run-parts 8 , .Xr systemd-crontab-generator 8 . .\".Sh AUTHOR .\".An Dwayne Bent systemd-cron-2.5.1/src/units/000077500000000000000000000000001475512744600161165ustar00rootroot00000000000000systemd-cron-2.5.1/src/units/cron-boot.service.in000066400000000000000000000006451475512744600220140ustar00rootroot00000000000000[Unit] Description=systemd-cron @schedule@ script service Documentation=man:systemd.cron(7) PartOf=cron-@schedule@.target ConditionDirectoryNotEmpty=/etc/cron.@schedule@ ConditionPathExists=!/run/crond.bootdir OnFailure=cron-mail@%n:Failure.service [Service] User=root Type=oneshot IgnoreSIGPIPE=false SyslogFacility=cron ExecStart=/usr/bin/run-parts /etc/cron.@schedule@ ExecStartPost=/usr/bin/touch /run/crond.bootdir systemd-cron-2.5.1/src/units/cron-boot.timer.in000066400000000000000000000002301475512744600214620ustar00rootroot00000000000000[Unit] Description=systemd-cron @schedule@ timer Documentation=man:systemd.cron(7) PartOf=cron.target [Timer] OnBootSec=60 Unit=cron-@schedule@.target systemd-cron-2.5.1/src/units/cron-mail@.service.in000066400000000000000000000011031475512744600220610ustar00rootroot00000000000000[Unit] Description=systemd-cron job mail — %i Documentation=man:systemd.cron(7) # TODO: drop conditional and roll back to straight OnSuccess= if we ever bump past the systemd ≥ 236 requirement (this is 249); cf. #165 @remove_if_no_onsuccess@RefuseManualStart=true RefuseManualStop=true [Service] Type=oneshot PassEnvironment=SENDMAIL ExecStart=@libexecdir@/systemd-cron/mail_for_job %i User=_cron-failure # TODO: drop Group= systemd ≥ 236 requirement (this is 245); cf. https://github.com/systemd-cron/systemd-cron/issues/165#issuecomment-2661583788 Group=systemd-journal systemd-cron-2.5.1/src/units/cron-schedule.service.in000066400000000000000000000005531475512744600226430ustar00rootroot00000000000000[Unit] Description=systemd-cron @schedule@ script service Documentation=man:systemd.cron(7) PartOf=cron-@schedule@.target ConditionDirectoryNotEmpty=/etc/cron.@schedule@ OnFailure=cron-mail@%n:Failure.service [Service] User=root Type=oneshot IgnoreSIGPIPE=false SyslogFacility=cron LogLevelMax=@use_loglevelmax@ ExecStart=/usr/bin/run-parts /etc/cron.@schedule@ systemd-cron-2.5.1/src/units/cron-schedule.target.in000066400000000000000000000002121475512744600224610ustar00rootroot00000000000000[Unit] Description=systemd-cron @schedule@ target Documentation=man:systemd.cron(7) Requires=cron-@schedule@.service StopWhenUnneeded=yes systemd-cron-2.5.1/src/units/cron-schedule.timer.in000066400000000000000000000002611475512744600223170ustar00rootroot00000000000000[Unit] Description=systemd-cron @schedule@ timer Documentation=man:systemd.cron(7) PartOf=cron.target [Timer] Persistent=true OnCalendar=@schedule@ Unit=cron-@schedule@.target systemd-cron-2.5.1/src/units/cron-update.path.in000066400000000000000000000002731475512744600216240ustar00rootroot00000000000000[Unit] Description=systemd-cron path monitor Documentation=man:systemd.cron(7) [Path] PathChanged=/etc/crontab PathChanged=/etc/cron.d PathChanged=/etc/anacrontab PathChanged=@statedir@ systemd-cron-2.5.1/src/units/cron-update.service.in000066400000000000000000000003651475512744600223320ustar00rootroot00000000000000[Unit] Description=systemd-cron update units Documentation=man:systemd.cron(7) [Service] Type=oneshot ExecStart=/bin/sh -c '>> /run/crond.reboot ; systemctl daemon-reload ; systemctl restart cron.target ; systemctl reset-failed "cron-*.timer"' systemd-cron-2.5.1/src/units/cron.target.in000066400000000000000000000002131475512744600206700ustar00rootroot00000000000000[Unit] Description=systemd-cron Documentation=man:systemd.cron(7) @requires@ Wants=cron-update.path [Install] WantedBy=multi-user.target systemd-cron-2.5.1/src/units/systemd-cron-cleaner.service.in000066400000000000000000000003471475512744600241470ustar00rootroot00000000000000[Unit] Description=systemd-cron, clean loose timestamps Documentation=man:systemd-crontab-generator(8) [Service] Type=oneshot ExecStart=@libexecdir@/systemd-cron/remove_stale_stamps CapabilityBoundingSet= RestrictAddressFamilies= systemd-cron-2.5.1/src/units/systemd-cron-cleaner.timer.in000066400000000000000000000002761475512744600236300ustar00rootroot00000000000000[Unit] Description=systemd-cron, clean loose timestamps Documentation=man:systemd-crontab-generator(8) [Timer] OnCalendar=Mon *-*-* 0:15:0 Persistent=true [Install] WantedBy=timers.target systemd-cron-2.5.1/test/000077500000000000000000000000001475512744600151445ustar00rootroot00000000000000systemd-cron-2.5.1/test/crontab000066400000000000000000000016721475512744600165250ustar00rootroot00000000000000# this is a corpus of crontabs that tries # to test all most code paths #dow 0-6 0=Vasárnap 1=Hétfő 2=Kedd 3=Szerda 4=Csütörtök 5=Péntek 6=Szombat 1 2 3 4 5 henry echo 0=Vasárnap 1=Hétfő 2=Kedd 3=Szerda 4=Csütörtök 5=Péntek 6=Szombat START_HOURS_RANGE=3-22 # Example of job definition: # .---------------- minute (0 - 59) # | .------------- hour (0 - 23) # | | .---------- day of month (1 - 31) # | | | .------- month (1 - 12) OR jan,feb,mar,apr ... # | | | | .---- day of week (0 - 6) (Sunday=0 or 7) OR sun,mon,tue,wed,thu,fri,sat # | | | | | # * * * * * user-name command to be executed 21 5 * * * root test -x /etc/cron.daily/popularity-contest && /etc/cron.daily/popularity-contest --crond */5 * * * * root true 00 0 * * 1-5 eroot echo Monday to Friday, double space, leading zero 00 0 * * 0-3 root echo test US mode 0 0 * * 4-7 root echo test EU mode @daily root ls PERSISTENT=true ~ ~ ~ ~ ~ root echo ~ systemd-cron-2.5.1/test/m_f_j/000077500000000000000000000000001475512744600162165ustar00rootroot00000000000000systemd-cron-2.5.1/test/m_f_j/bothMAIL.output000066400000000000000000000007521475512744600211030ustar00rootroot00000000000000-i -B 8BITMIME chlupsko@henry.jpeg Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: binary Date: Fri, 01 Dec 2000 23:00:00 +0000 From: henry@zbysiopatia.jpeg (systemd-cron) To: chlupsko@henry.jpeg Subject: [tarta] Failure: m_f_j/bothMAIL X-Mailer: systemd-cron @VERSION@ Auto-Submitted: auto-generated User= Environment="PATH=żupan:/etc" MAILFROM=henry@zbysiopatia.jpeg TZ=UTC0 MAILTO=chlupsko@henry.jpeg ActiveState=failed SourcePath=m_f_j/bothMAIL systemd-cron-2.5.1/test/m_f_j/bothMAIL.service000066400000000000000000000002251475512744600211760ustar00rootroot00000000000000User= Environment="PATH=żupan:/etc" MAILFROM=henry@zbysiopatia.jpeg TZ=UTC0 MAILTO=chlupsko@henry.jpeg ActiveState=failed SourcePath=m_f_j/bothMAIL systemd-cron-2.5.1/test/m_f_j/date000077500000000000000000000000711475512744600170570ustar00rootroot00000000000000#!/bin/sh PATH="${PATH#*:}" exec date -ud@975711600 "$@" systemd-cron-2.5.1/test/m_f_j/empty-mailto.output000066400000000000000000000001141475512744600221150ustar00rootroot00000000000000This cron job (empty-mailto.service) opted out of email, therefore quitting systemd-cron-2.5.1/test/m_f_j/empty-mailto.service000066400000000000000000000001151475512744600222160ustar00rootroot00000000000000User=henry Environment="PATH=żupan:/etc" MAILTO= TZ=UTC0 ActiveState=failed systemd-cron-2.5.1/test/m_f_j/journalctl000077500000000000000000000000121475512744600203120ustar00rootroot00000000000000#!/bin/sh systemd-cron-2.5.1/test/m_f_j/noconfig.output000066400000000000000000000005151475512744600213030ustar00rootroot00000000000000-i -B 8BITMIME root Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: binary Date: Fri, 01 Dec 2000 23:00:00 +0000 From: root (systemd-cron) To: root Subject: [tarta] Failure: noconfig.service X-Mailer: systemd-cron @VERSION@ Auto-Submitted: auto-generated User= Environment= ActiveState=failed systemd-cron-2.5.1/test/m_f_j/noconfig.service000066400000000000000000000000461475512744600214020ustar00rootroot00000000000000User= Environment= ActiveState=failed systemd-cron-2.5.1/test/m_f_j/ok.output000066400000000000000000000005101475512744600201050ustar00rootroot00000000000000-i -B 8BITMIME root Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: binary Date: Fri, 01 Dec 2000 23:00:00 +0000 From: root (systemd-cron) To: root Subject: [tarta] Output: ok.service X-Mailer: systemd-cron @VERSION@ Auto-Submitted: auto-generated User= Environment= ActiveState=inactive systemd-cron-2.5.1/test/m_f_j/ok.service000066400000000000000000000000501475512744600202040ustar00rootroot00000000000000User= Environment= ActiveState=inactive systemd-cron-2.5.1/test/m_f_j/sendmail000077500000000000000000000000721475512744600177370ustar00rootroot00000000000000#!/bin/sh echo "$@" cat #tee /dev/stderr | b2sum | sponge systemd-cron-2.5.1/test/m_f_j/systemctl000077500000000000000000000000731475512744600201730ustar00rootroot00000000000000#!/bin/sh while [ $# -gt 1 ]; do shift done exec cat "$1" systemd-cron-2.5.1/test/m_f_j/user.output000066400000000000000000000005201475512744600204530ustar00rootroot00000000000000-i -B 8BITMIME henry Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: binary Date: Fri, 01 Dec 2000 23:00:00 +0000 From: root (systemd-cron) To: henry Subject: [tarta] Failure: user.service X-Mailer: systemd-cron @VERSION@ Auto-Submitted: auto-generated User=henry Environment= ActiveState=failed systemd-cron-2.5.1/test/m_f_j/user.service000066400000000000000000000000531475512744600205540ustar00rootroot00000000000000User=henry Environment= ActiveState=failed systemd-cron-2.5.1/test/test-generator000077500000000000000000000667621475512744600200560ustar00rootroot00000000000000#!/bin/sh # expects to run as root of a mountns S_C_G="${S_C_G:-"$PWD/out/build/bin/systemd-crontab-generator"}" TESTDIR="$PWD/test/" PATH="/usr/bin:$PATH" SENDMAIL='/bin/true' export SENDMAIL # s-c-g won't generate On{Success,Failure}= lines if it doesn't have sendmail USE_RUN_PARTS="${1:-no}" echo "USE_RUN_PARTS: $USE_RUN_PARTS" HAVE_ONSUCCESS="${3:-yes}" echo "HAVE_ONSUCCESS: $HAVE_ONSUCCESS" mount -t tmpfs tmpfs /etc || exit STATEDIR="${2:-"/var/spool/cron"}" if [ -d "$STATEDIR" ]; then mount -t tmpfs tmpfs "$STATEDIR" || exit fi err=0 assert_key() { # file k v testname # TODO: roll back to no special-case if we ever bump past the systemd ≥ 236 requirement (this is 249); cf. #165 [ "$2" = ExecStart ] && [ "$HAVE_ONSUCCESS" = no ] && greparg=-m1 || greparg= [ "$2" = OnSuccess ] && [ "$HAVE_ONSUCCESS" = no ] && set -- "$1" "ExecStart" "-+/usr/bin/systemctl start --no-block $3" "$4" && postgrep() { tail -n+2; } || postgrep() { cat; } val="$(grep $greparg "^$2=" "$1" | postgrep)" [ "${val#"$2="}" = "$3" ] || { err=1; echo "$4: $1: $val != $3" >&2; } } assert_ney() { # file k testname val="$(grep "^$2=" "$1")" && { err=1; echo "$3: $1: $val exists" >&2; } } assert_dat() { # file content testname printf '%s\n' "$2" | cmp -s - "$1" || { err=1; echo "$3: $(cat "$1") != $2" >&2; } } { mkdir -p /etc/home/dummy > /etc/a%b%c > /etc/home/dummy/a%b%c echo 'dummy:x:12:34:dummy:/etc/home/dummy:/bin/sh' > /etc/passwd mkdir /etc/cron.d printf '%s\n' '# test_no_scriptlet' \ '@daily dummy /bin/true' \ \ '# test_period_basic' \ 'SHELL=/bin/dash' \ '@daily dummy true' \ 'SHELL=/bin/sh' \ \ '# test_userpath_expansion' \ '@daily dummy ~/fake >/dev/null' \ \ '# test_timespec_basic' \ '5 6 * * * dummy true' \ \ '# test_timespec_slice' \ '*/5 * * * * dummy true' \ \ '# test_timespec_range' \ '1 * * * mon-wed dummy true' \ \ '# test_tz' \ 'TZ=:Europe/Warsaw' \ 'BATCH=1' \ '1 * * * mon-wed dummy echo in zoneinfo' \ 'TZ=UTC0' \ 'BATCH=0' \ '1 * * * mon-wed dummy echo valid $TZ, but not in zoneinfo' \ 'TZ=' \ \ '# test_cron_mail_success' \ 'CRON_MAIL_SUCCESS=always' \ '* * * * * dummy :' \ 'CRON_MAIL_SUCCESS=yes' \ '* * * * * dummy :' \ 'CRON_MAIL_SUCCESS=true' \ '* * * * * dummy :' \ 'CRON_MAIL_SUCCESS=1' \ '* * * * * dummy :' \ \ 'CRON_MAIL_SUCCESS=never' \ '* * * * * dummy :' \ 'CRON_MAIL_SUCCESS=no' \ '* * * * * dummy :' \ 'CRON_MAIL_SUCCESS=false' \ '* * * * * dummy :' \ 'CRON_MAIL_SUCCESS=0' \ '* * * * * dummy :' \ \ 'CRON_MAIL_SUCCESS=inherit' \ '* * * * * dummy :' \ \ 'CRON_MAIL_SUCCESS=nonempty' \ '* * * * * dummy :' \ 'CRON_MAIL_SUCCESS=non-empty' \ '* * * * * dummy :' \ \ '# test_cron_mail_format' \ 'CRON_MAIL_FORMAT=normal' \ '* * * * * dummy :' \ 'CRON_MAIL_FORMAT=nometadata' \ '* * * * * dummy :' \ 'CRON_MAIL_FORMAT=inherit' \ '* * * * * dummy :' \ 'CRON_MAIL_FORMAT=no-metadata' \ '* * * * * dummy :' > /etc/crontab printf '%s\n' '# test_MAIL_inherit_default' \ '* * * * * dummy !' \ '# test_MAIL_inherit_partial' \ 'CRON_MAIL_SUCCESS=never' \ '* * * * * dummy !' \ '# test_MAIL_inherit_explicit' \ 'CRON_MAIL_SUCCESS=inherit' \ '* * * * * dummy !' \ '' \ '# test_fri-sun' \ '* * * * fri-sun dummy !' \ \ '# test_percent' \ '* * * * * dummy /etc/a\%b\%c' \ '* * * * * dummy ~/a\%b\%c' \ '* * * * * dummy echo a\%b\%c' \ '* * * * * dummy echo a; cat%b%c\%d' \ '* * * * * dummy echo a; cat%b %c\%d%' \ '* * * * * dummy echo a; cat%b % c\%d%%' \ '* * * * * dummy echo a; cat%b % c\%d%%%' \ '* * * * * dummy echo a; cat%b % c\%d%%%%' > /etc/cron.d/crondtab printf '%s\n' '# test_percent_anacrontab' \ '1 0 anacrussy echo a\%b\%c' \ '1 0 anacrussy echo a; cat%b%c\%d' > /etc/anacrontab "$S_C_G" /etc/out || { err=$?; echo "$S_C_G" /etc/out = $err >&2; exit $err; } cd /etc/out || exit assert_key cron-crontab-dummy-e0b1d8dd8cff486372199f7c71b842d1.timer 'OnCalendar' 'daily' test_no_scriptlet assert_key cron-crontab-dummy-e0b1d8dd8cff486372199f7c71b842d1.service 'ExecStart' '/bin/true' test_no_scriptlet assert_key cron-crontab-dummy-d441f91bec8513ca5d4aa275d61ba4d5.timer 'OnCalendar' 'daily' test_period_basic assert_key cron-crontab-dummy-d441f91bec8513ca5d4aa275d61ba4d5.service 'ExecStart' '/bin/dash /etc/out/cron-crontab-dummy-d441f91bec8513ca5d4aa275d61ba4d5.sh' test_period_basic assert_dat cron-crontab-dummy-d441f91bec8513ca5d4aa275d61ba4d5.sh 'true' test_period_basic assert_key cron-crontab-dummy-de72613675650ff5241aa33a51102b29.service 'ExecStart' '/bin/sh /etc/out/cron-crontab-dummy-de72613675650ff5241aa33a51102b29.sh' test_userpath_expansion assert_dat cron-crontab-dummy-de72613675650ff5241aa33a51102b29.sh '/etc/home/dummy/fake >/dev/null' test_userpath_expansion assert_key cron-crontab-dummy-0.timer 'OnCalendar' '*-*-* 6:5:00' test_timespec_basic assert_key cron-crontab-dummy-1.timer 'OnCalendar' '*-*-* *:0,5,10,15,20,25,30,35,40,45,50,55:00' test_timespec_slice assert_key cron-crontab-dummy-2.timer 'OnCalendar' 'Mon,Tue,Wed *-*-* *:1:00' test_timespec_range assert_key cron-crontab-dummy-3.timer 'OnCalendar' 'Mon,Tue,Wed *-*-* *:1:00 Europe/Warsaw' test_tz # systemd accepts zones it finds in the zoneinfo file, but only by exact match assert_key cron-crontab-dummy-4.timer 'OnCalendar' 'Mon,Tue,Wed *-*-* *:1:00' test_tz # TZ=UTC0 is valid (as is TZ=UTC2), but systemd doesn't accept it as a suffix assert_key cron-crontab-dummy-3.service 'CPUSchedulingPolicy' 'idle' test_BATCH assert_ney cron-crontab-dummy-4.service 'CPUSchedulingPolicy' test_BATCH assert_key cron-crontab-dummy-4.service 'WorkingDirectory' '-~' test_BATCH assert_key cron-crontab-dummy-4.service 'OnSuccess' 'cron-mail@%n:Success:nonempty.service' test_cron_mail_success_default assert_key cron-crontab-dummy-4.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success_default assert_key cron-crontab-dummy-5.service 'OnSuccess' 'cron-mail@%n:Success.service' test_cron_mail_success=always assert_key cron-crontab-dummy-5.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=always assert_key cron-crontab-dummy-6.service 'OnSuccess' 'cron-mail@%n:Success.service' test_cron_mail_success=yes assert_key cron-crontab-dummy-6.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=yes assert_key cron-crontab-dummy-7.service 'OnSuccess' 'cron-mail@%n:Success.service' test_cron_mail_success=true assert_key cron-crontab-dummy-7.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=true assert_key cron-crontab-dummy-8.service 'OnSuccess' 'cron-mail@%n:Success.service' test_cron_mail_success=1 assert_key cron-crontab-dummy-8.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=1 assert_ney cron-crontab-dummy-9.service 'OnSuccess' test_cron_mail_success=never assert_key cron-crontab-dummy-9.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=never assert_ney cron-crontab-dummy-10.service 'OnSuccess' test_cron_mail_success=no assert_key cron-crontab-dummy-10.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=no assert_ney cron-crontab-dummy-11.service 'OnSuccess' test_cron_mail_success=false assert_key cron-crontab-dummy-11.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=false assert_ney cron-crontab-dummy-12.service 'OnSuccess' test_cron_mail_success=0 assert_key cron-crontab-dummy-12.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=0 assert_key cron-crontab-dummy-13.service 'OnSuccess' 'cron-mail@%n:Success:nonempty.service' test_cron_mail_success=inherit # same as default at toplevel assert_key cron-crontab-dummy-13.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=inherit assert_key cron-crontab-dummy-14.service 'OnSuccess' 'cron-mail@%n:Success:nonempty.service' test_cron_mail_success=nonempty assert_key cron-crontab-dummy-14.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=nonempty assert_key cron-crontab-dummy-15.service 'OnSuccess' 'cron-mail@%n:Success:nonempty.service' test_cron_mail_success=non-empty assert_key cron-crontab-dummy-15.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_success=non-empty assert_key cron-crontab-dummy-16.service 'OnSuccess' 'cron-mail@%n:Success:nonempty.service' test_cron_mail_format=normal assert_key cron-crontab-dummy-16.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_format=normal assert_key cron-crontab-dummy-17.service 'OnSuccess' 'cron-mail@%n:Success:nonempty:nometadata.service' test_cron_mail_format=nometadata assert_key cron-crontab-dummy-17.service 'OnFailure' 'cron-mail@%n:Failure:nometadata.service' test_cron_mail_format=nometadata assert_key cron-crontab-dummy-18.service 'OnSuccess' 'cron-mail@%n:Success:nonempty.service' test_cron_mail_format=inherit # same as default at toplevel assert_key cron-crontab-dummy-18.service 'OnFailure' 'cron-mail@%n:Failure.service' test_cron_mail_format=inherit assert_key cron-crontab-dummy-19.service 'OnSuccess' 'cron-mail@%n:Success:nonempty:nometadata.service' test_cron_mail_format=no-metadata assert_key cron-crontab-dummy-19.service 'OnFailure' 'cron-mail@%n:Failure:nometadata.service' test_cron_mail_format=no-metadata assert_key cron-crondtab-dummy-0.service 'OnSuccess' 'cron-mail@%n:Success:nonempty:nometadata.service' test_MAIL_inherit_default assert_key cron-crondtab-dummy-0.service 'OnFailure' 'cron-mail@%n:Failure:nometadata.service' test_MAIL_inherit_default assert_ney cron-crondtab-dummy-1.service 'OnSuccess' test_MAIL_inherit_partial assert_key cron-crondtab-dummy-1.service 'OnFailure' 'cron-mail@%n:Failure:nometadata.service' test_MAIL_inherit_partial assert_key cron-crondtab-dummy-2.service 'OnSuccess' 'cron-mail@%n:Success:nonempty:nometadata.service' test_MAIL_inherit_explicit assert_key cron-crondtab-dummy-2.service 'OnFailure' 'cron-mail@%n:Failure:nometadata.service' test_MAIL_inherit_explicit assert_key cron-crondtab-dummy-3.timer 'OnCalendar' 'Fri,Sat,Sun *-*-* *:*:00' test_fri-sun assert_key cron-crondtab-dummy-4.service 'ExecStart' '/etc/a%b%c' test_percent assert_key cron-crondtab-dummy-5.service 'ExecStart' '/etc/home/dummy/a%b%c' test_percent assert_key cron-crondtab-dummy-6.service 'ExecStart' '/bin/sh /etc/out/cron-crondtab-dummy-6.sh' test_percent assert_dat cron-crondtab-dummy-6.sh 'echo a%b%c' test_percent assert_key cron-crondtab-dummy-7.service 'ExecStart' '/bin/sh /etc/out/cron-crondtab-dummy-7.sh' test_percent assert_key cron-crondtab-dummy-7.service 'StandardInput' 'data' test_percent assert_key cron-crondtab-dummy-7.service 'StandardInputData' "$(printf 'b\nc%%d' | base64 -w0)" test_percent assert_dat cron-crondtab-dummy-7.sh 'echo a; cat' test_percent assert_key cron-crondtab-dummy-8.service 'ExecStart' '/bin/sh /etc/out/cron-crondtab-dummy-8.sh' test_percent assert_key cron-crondtab-dummy-8.service 'StandardInput' 'data' test_percent assert_key cron-crondtab-dummy-8.service 'StandardInputData' "$(printf 'b \nc%%d\n' | base64 -w0)" test_percent assert_dat cron-crondtab-dummy-8.sh 'echo a; cat' test_percent assert_key cron-crondtab-dummy-9.service 'ExecStart' '/bin/sh /etc/out/cron-crondtab-dummy-9.sh' test_percent assert_key cron-crondtab-dummy-9.service 'StandardInput' 'data' test_percent assert_key cron-crondtab-dummy-9.service 'StandardInputData' "$(printf 'b \n c%%d\n\n' | base64 -w0)" test_percent assert_dat cron-crondtab-dummy-9.sh 'echo a; cat' test_percent assert_key cron-crondtab-dummy-10.service 'ExecStart' '/bin/sh /etc/out/cron-crondtab-dummy-10.sh' test_percent assert_key cron-crondtab-dummy-10.service 'StandardInput' 'data' test_percent assert_key cron-crondtab-dummy-10.service 'StandardInputData' "$(printf 'b \n c%%d\n\n\n' | base64 -w0)" test_percent assert_dat cron-crondtab-dummy-10.sh 'echo a; cat' test_percent assert_key cron-crondtab-dummy-11.service 'ExecStart' '/bin/sh /etc/out/cron-crondtab-dummy-11.sh' test_percent assert_key cron-crondtab-dummy-11.service 'StandardInput' 'data' test_percent assert_key cron-crondtab-dummy-11.service 'StandardInputData' "$(printf 'b \n c%%d\n\n\n\n' | base64 -w0)" test_percent assert_dat cron-crondtab-dummy-11.sh 'echo a; cat' test_percent assert_key cron-anacron-anacrussy-626d2e0fd1f5e39a4b4425d53d92559d.service 'ExecStart' '/bin/sh /etc/out/cron-anacron-anacrussy-626d2e0fd1f5e39a4b4425d53d92559d.sh' test_percent_anacrontab assert_dat cron-anacron-anacrussy-626d2e0fd1f5e39a4b4425d53d92559d.sh 'echo a\%b\%c' test_percent_anacrontab assert_key cron-anacron-anacrussy-90ac7985e1a331e6fd95807d7fd2bc25.service 'ExecStart' '/bin/sh /etc/out/cron-anacron-anacrussy-90ac7985e1a331e6fd95807d7fd2bc25.sh' test_percent_anacrontab assert_dat cron-anacron-anacrussy-90ac7985e1a331e6fd95807d7fd2bc25.sh 'echo a; cat%b%c\%d' test_percent_anacrontab } { rm -f /etc/cron.d/* /etc/anacrontab printf '%s\n' 'CRON_MAIL_SUCCESS=always' \ 'CRON_MAIL_FORMAT=nometadata' \ 'MAILTO=root@test.owcy' > /etc/crontab printf '%s\n' '* * * * * dummy true' > /etc/cron.d/crondtab "$S_C_G" /etc/out-nojob || { err=$?; echo "$S_C_G" /etc/out-nojob = $err >&2; exit $err; } cd /etc/out-nojob || exit assert_key cron-crondtab-dummy-0.service 'OnSuccess' 'cron-mail@%n:Success:nometadata.service' test_nojob_OnSuccess assert_key cron-crondtab-dummy-0.service 'OnFailure' 'cron-mail@%n:Failure:nometadata.service' test_nojob_OnFailure assert_key cron-crondtab-dummy-0.service 'Environment' 'MAILTO=root@test.owcy' test_nojob_Environment } { rm -r /etc/cron.* mkdir /etc/cron.d /etc/cron.daily printf '%s\n' 'CRON_INHERIT_VARIABLES=RANDOM_DELAY NORMAL_VARIABLE' \ 'RANDOM_DELAY=10' \ 'NORMAL_VARIABLE=real' \ '* * * * * dummy /bin/true' > /etc/crontab printf '%s\n' '* * * * * dummy /bin/true' \ 'NORMAL_VARIABLE=over' \ '* * * * * dummy /bin/true' > /etc/cron.d/crondtab ln -s /bin/true /etc/cron.daily/true "$S_C_G" /etc/outI || { err=$?; echo "$S_C_G" /etc/outI = $err >&2; exit $err; } cd /etc/outI || exit assert_key cron-crontab-dummy-0.service 'Environment' 'NORMAL_VARIABLE=real' toplevel_control assert_key cron-crontab-dummy-0.timer 'RandomizedDelaySec' '10m' toplevel_control assert_key cron-crondtab-dummy-0.service 'Environment' 'NORMAL_VARIABLE=real' inherited assert_key cron-crondtab-dummy-0.timer 'RandomizedDelaySec' '10m' inherited assert_key cron-crondtab-dummy-1.service 'Environment' 'NORMAL_VARIABLE=over' inherited_overridden assert_key cron-crondtab-dummy-1.timer 'RandomizedDelaySec' '10m' inherited_overridden if [ "$USE_RUN_PARTS" = 'no' ] then assert_key cron-daily-true.service 'Environment' 'NORMAL_VARIABLE=real' inherited_runparts assert_key cron-daily-true.timer 'RandomizedDelaySec' '10m' inherited_runparts fi } { rm -r /etc/cron.* mkdir /etc/cron.d printf '%s\n' 'CRON_INHERIT_VARIABLES=RANDOM_DELAY NORMAL_VARIABLE' \ 'RANDOM_DELAY=10' \ 'NORMAL_VARIABLE=real' > /etc/crontab printf '%s\n' '* * * * * dummy /bin/true' > /etc/cron.d/crondtab "$S_C_G" /etc/outI2 || { err=$?; echo "$S_C_G" /etc/outI2 = $err >&2; exit $err; } cd /etc/outI2 || exit assert_key cron-crondtab-dummy-0.service 'Environment' 'NORMAL_VARIABLE=real' inherited_nojob assert_key cron-crondtab-dummy-0.timer 'RandomizedDelaySec' '10m' inherited_nojob } { rm -r /etc/cron.* printf '%s\n' 'root:x:0:0:root:/root:/bin/sh' 'eroot:x:0:0:root:/root:/bin/ed' > /etc/passwd cp "${TESTDIR}crontab" /etc mkdir /etc/cron.daily echo id > /etc/cron.daily/id chmod +x /etc/cron.daily/id > /etc/cron.daily/nonexecutable "$S_C_G" /etc/out2 || { err=$?; echo "$S_C_G" /etc/out2 = $err >&2; exit $err; } cd /etc/out2 || exit assert_key cron-crontab-henry-0.service 'User' 'henry' henry-unknown-user assert_key cron-crontab-henry-0.service 'Requires' 'systemd-user-sessions.service' henry-unknown-user assert_ney cron-crontab-henry-0.service 'RequiresMountsFor' henry-unknown-user assert_key cron-crontab-henry-0.service 'OnFailure' 'cron-mail@%n:Failure.service' onfailure_escaped assert_key cron-crontab-henry-0.service 'Description' '[Cron] "1 2 3 4 5 henry echo 0=Vasárnap 1=Hétfő 2=Kedd 3=Szerda 4=Csütörtök 5=Péntek 6=Szombat"' henry-the-hungarian assert_key cron-crontab-henry-0.timer 'OnCalendar' 'Fri *-4-3 2:1:00' henry\'s-calendar assert_dat cron-crontab-henry-0.sh 'echo 0=Vasárnap 1=Hétfő 2=Kedd 3=Szerda 4=Csütörtök 5=Péntek 6=Szombat' henry\'s-hungarian-program } test_test_crontab_common() { assert_key cron-crontab-root-0.service 'User' 'root' root-special assert_ney cron-crontab-root-0.service 'Requires' root-special assert_ney cron-crontab-root-0.service 'RequiresMountsFor' root-special assert_key cron-crontab-root-0.timer 'OnCalendar' '*-*-* *:0,5,10,15,20,25,30,35,40,45,50,55:00' root-slashed-calendar assert_key cron-crontab-eroot-0.service 'User' 'eroot' eroot-known-user assert_key cron-crontab-eroot-0.service 'Requires' 'systemd-user-sessions.service' eroot-known-user assert_key cron-crontab-eroot-0.service 'RequiresMountsFor' '/root' eroot-known-user assert_key cron-crontab-eroot-0.timer 'OnCalendar' 'Mon,Tue,Wed,Thu,Fri *-*-* 0:0:00' eroot-mon-fri-\ \ -00 assert_key cron-crontab-root-1.timer 'Description' '[Timer] "00 0 * * 0-3 root echo test US mode"' root-US-mode-timer assert_key cron-crontab-root-1.timer 'OnCalendar' 'Sun,Mon,Tue,Wed *-*-* 0:0:00' root-US-mode-timer assert_key cron-crontab-root-2.timer 'Description' '[Timer] "0 0 * * 4-7 root echo test EU mode"' root-EU-mode-timer assert_key cron-crontab-root-2.timer 'OnCalendar' 'Thu,Fri,Sat,Sun *-*-* 0:0:00' root-EU-mode-timer assert_key 'cron-crontab-root-e78207bda09f301009747d39e25432fa.timer' 'Description' '[Timer] "@daily root ls"' root-@daily assert_key 'cron-crontab-root-e78207bda09f301009747d39e25432fa.timer' 'OnCalendar' '*-*-* 3:0:0' root-@daily assert_key 'cron-crontab-root-e78207bda09f301009747d39e25432fa.timer' 'Persistent' 'true' root-@daily assert_key 'cron-crontab-root-68d29d8cf7f485d7c7821ca686d61ffd.timer' 'Description' '[Timer] "~ ~ ~ ~ ~ root echo ~"' root-\~ assert_key 'cron-crontab-root-68d29d8cf7f485d7c7821ca686d61ffd.timer' 'Persistent' 'true' root-\~ if [ "$USE_RUN_PARTS" = 'no' ] then [ -e cron-daily-nonexecutable.service ] && { err=1; echo "cron-daily-nonexecutable.service exists!"; } assert_key cron-daily-id.service 'Description' '[Cron] /etc/cron.daily/id' /etc/cron.daily assert_key cron-daily-id.service 'ExecStartPre' '-/usr/libexec/systemd-cron/boot_delay 10' /etc/cron.daily # hourly.daily = 2nd, *5min for each longer one assert_key cron-daily-id.service 'ExecStart' '/etc/cron.daily/id' /etc/cron.daily assert_key cron-daily-id.timer 'OnCalendar' '*-*-* 5:10:0' /etc/cron.daily assert_key cron-daily-id.timer 'Persistent' 'true' /etc/cron.daily fi } test_test_crontab_common { iconv() { command iconv -f utf-8 -t iso-8859-2; } iconv < "${TESTDIR}crontab" > /etc/crontab "$S_C_G" /etc/out3 || { err=$?; echo "$S_C_G" /etc/out3 = $err >&2; exit $err; } cd /etc/out3 || exit # All the same except Description (escaped) and program (not escaped) assert_key cron-crontab-henry-0.service 'User' 'henry' henry-unknown-user-iconv assert_key cron-crontab-henry-0.service 'Requires' 'systemd-user-sessions.service' henry-unknown-user-iconv assert_ney cron-crontab-henry-0.service 'RequiresMountsFor' henry-unknown-user-iconv assert_key cron-crontab-henry-0.service 'Description' '[Cron] "1 2 3 4 5 henry echo 0=Vas\xe1rnap 1=H\xe9tf\xf5 2=Kedd 3=Szerda 4=Cs\xfct\xf6rt\xf6k 5=P\xe9ntek 6=Szombat"' henry-the-hungarian-iconv assert_key cron-crontab-henry-0.timer 'OnCalendar' 'Fri *-4-3 2:1:00' henry\'s-calendar-iconv assert_dat cron-crontab-henry-0.sh "$(echo 'echo 0=Vasárnap 1=Hétfő 2=Kedd 3=Szerda 4=Csütörtök 5=Péntek 6=Szombat' | iconv)" henry\'s-hungarian-program-iconv } test_test_crontab_common exit $err systemd-cron-2.5.1/test/test-m_f_j000077500000000000000000000010631475512744600171210ustar00rootroot00000000000000#!/bin/sh # needs to run as root of a UTS namespace to set hostname # uses fake "systemctl"/"sendmail" programs to validate the output set -u M_F_J="${M_F_J:-"$PWD/out/build/bin/mail_for_job"}" trap 'rm -rf "$tmpdir"' EXIT INT tmpdir="$(mktemp -d)/" if [ $# -eq 0 ] then read -r V < VERSION else V="$1" fi cd test/m_f_j || exit hostname tarta PATH=".:$PATH" err=0 for f in *.service; do of="${f%.service}.output" sed "s#@VERSION@#${V}#" < "$of" > "${tmpdir}$of" "$M_F_J" "$f":verbose 2>&1 | diff -u "${tmpdir}$of" - || err=1 done exit $err systemd-cron-2.5.1/test/test-r_s_s000077500000000000000000000032731475512744600171610ustar00rootroot00000000000000#!/bin/sh # expects to run as root of a mountns R_S_S="${R_S_S:-"$PWD/out/build/bin/remove_stale_stamps"}" mount -t tmpfs tmpfs /lib/systemd/system || exit mount -t tmpfs tmpfs /var/lib || exit mount -t tmpfs tmpfs /run || exit mkdir -p /var/lib/systemd/timers /run/systemd/generator || exit touch /lib/systemd/system/cron-daily.timer \ /lib/systemd/system/cron-weekly.timer \ /lib/systemd/system/cron-monthly.timer \ /lib/systemd/system/cron-yearly.timer \ /run/systemd/generator/cron-yearly.timer touch -d@975711600 /var/lib/systemd/timers/stamp-cron-daily.timer \ /var/lib/systemd/timers/stamp-cron-weekly.timer \ /var/lib/systemd/timers/stamp-cron-monthly.timer \ /var/lib/systemd/timers/stamp-cron-quarterly.timer \ /var/lib/systemd/timers/stamp-cron-semi-annually.timer \ /var/lib/systemd/timers/stamp-cron-yearly.timer \ /var/lib/systemd/timers/stamp-cron-crontab-root-0.timer \ /var/lib/systemd/timers/stamp-cron-zfsutils-linux-root-242bc1b186a7d177d1fc05adc24b8249.timer > /run/systemd/generator/cron-crontab-root-0.timer printf '%s\n' /var/lib/systemd/timers/stamp-cron-daily.timer \ /var/lib/systemd/timers/stamp-cron-weekly.timer \ /var/lib/systemd/timers/stamp-cron-monthly.timer \ /var/lib/systemd/timers/stamp-cron-yearly.timer \ /var/lib/systemd/timers/stamp-cron-crontab-root-0.timer | sort > /run/expected "$R_S_S" || exit find /var/lib -type f | sort | diff -u /run/expected -