LinkedIn Sourceforge

Vincent's Blog

Pleasure in the job puts perfection in the work (Aristote)

vdcron 0.9: a cron for laptops that now survives shutdowns, power loss and reboots

Posted on 2026-10-03 22:47:00 from Vincent in vdcron

Give a shoutout to DAVIDCOHEN on unsplash.com vdcron is my small cron-like scheduler for machines that are not always on, such as laptops. It is a plain shell script that runs the tasks that are due each time the machine resumes, boots or someone logs in. At the end of September I reviewed the whole script, and the result is version 0.9. It fixes several bugs that could lose tasks from the queue, corrects the date calculations, and adds an option to edit the configuration safely. Above all, progress is now saved after each task, so a shutdown in the middle of a run no longer starts everything again.


vdcron 0.9: a scheduler that survives a shutdown

vdcron is my small cron-like scheduler for machines that are not always on. It is a plain shell script: no daemon, just a queue of tasks in a text file, read each time the laptop resumes, boots or someone logs in. Between 24 and 27 September I went through the whole script again, and the result is version 0.9. It is 23 commits, most of them small, but together they change how much I trust the tool. This post explains what changed and why.

Tasks that damaged the queue

The most serious problems were the ones where vdcron damaged its own configuration file. The file is a queue: the script reads it line by line, runs what is due, and writes back what remains. Anything that disturbs that loop loses tasks.

The worst case was standard input. The loop reads the configuration file on stdin, and the tasks inherited it. A task that reads stdin itself, such as ssh or cat, swallowed the rest of the file, and the lines it swallowed were then deleted from the queue. Tasks now run with stdin connected to /dev/null.

Three other fixes belong to the same family. The last line of the file was dropped when it had no trailing newline. Whitespace was trimmed with xargs, which also removed quotes and failed on a command containing an apostrophe; this is now done with parameter expansion. And lines were written back with echo, which under dash interprets backslashes and so corrupted any command containing one; printf replaces it everywhere. A smaller bug in the same area: a rescheduled task with the -q flag was written back with a 1 glued to the command instead of the flag.

Dates that land where they should

Rescheduling had its own set of errors. Monthly tasks were moved forward by adding 31 days, or 10 days near the end of a month, which gave the wrong month for most repeat values: a January/12 task run on 26 January came back as February. The next month is now computed by month arithmetic, so /1 is monthly, /3 quarterly and /12 yearly.

Day-based tasks had a similar weakness around daylight saving changes: adding a multiple of 86400 seconds in local time could run a task twice or move it to the neighbouring day. The calculation is now done on calendar dates in UTC, where a day is always a day.

The last one I found on my own machine. Weekday and month names in the configuration are English, but date answered in the system locale. With LANG=fr_BE.UTF-8 it said "jeudi" and "septembre", and nothing matched. The C locale is now forced for these names.

A stricter, more forgiving parser

The parsing of date specifications moved from expr to case patterns that accept exactly 8 or 12 digits. A typo such as 2026101O, with a letter O, used to produce an error from test(1); it is now logged as an invalid date spec and the line is kept. The test -a forms were replaced by && between separate tests.

Variable lines changed too. NAME=value lines were executed with eval behind a filter that only accepted alphanumeric values, which ruled out something as ordinary as a PATH. They are now set with export: the value is taken literally, with no expansion at all, so any value is allowed and nothing in it can be executed. Names that vdcron uses internally are refused and logged.

Two small comforts complete this part: tabs are accepted as separators after the date spec and after -q, and -s 0 is allowed for those who want no initial sleep.

Locking, editing and being killed

The temporary file is now created next to the configuration file rather than in /tmp. /tmp is often another filesystem, where mv copies and then deletes; in the same directory it is a single atomic rename(2).

dash and ksh do not run the EXIT trap when the shell is killed by a signal, so a killed vdcron left its lock and temporary file behind. HUP, INT and TERM are now turned into a normal exit, and the cleanup runs.

There is also a new option, -e, which opens the configuration file in $VISUAL, $EDITOR or vi while holding the lock. Before this, a run triggered during an edit could rewrite the file under the editor, and one of the two versions was lost. A run triggered during an edit now logs "Already running" and skips.

Finally, the log is trimmed with tail instead of ed, which is not installed on every Linux system, and vdcron exits with 0 when there is nothing to trim.

The reason for 0.9: progress is saved after each task

Until 0.8 the configuration file was rewritten once, at the end of the run. If the laptop was shut down in the middle of a long task, nothing was saved, and every task of that run started again at the next boot.

In 0.9 the queue is saved after each executed task. With five daily tasks and a shutdown during the fourth, the file now contains:

+20260928/1 sh ~/bin/task1
+20260928/1 sh ~/bin/task2
+20260928/1 sh ~/bin/task3
+20260927/1 sh ~/bin/task4
+20260927/1 sh ~/bin/task5

At the next boot only task4 and task5 run. The interrupted task restarts from the beginning, so long tasks should be safe to restart. Each save is an atomic rename preceded by a sync, to lower the risk of an empty file after a power loss.

A shutdown also leaves a lock file behind, and here 0.8 had a real flaw. The lock contained only a PID, and after a reboot that PID often belongs to another process. vdcron then believed it was already running and skipped. The lock now stores the PID and the start time of the process, so a lock from before the reboot is recognised as stale. Temporary files left by an interrupted run are removed by the next one.

Packaging and documentation

The Makefile now works with both BSD make and GNU make 4.0 or later, which is what I needed on Void Linux. Man pages go to share/man on Linux, DESTDIR is honoured, every path can be overridden on the command line, and a tgz target builds the release tarball. Both man pages and the README were rewritten to follow the code, including a description of what repeat values mean for weekdays and months.

Upgrading from 0.8

The configuration format does not change. Install when no run is in progress, because 0.9 treats a lock written by 0.8 as stale. If 0.8 crashed in the past, it may have left files named vdcron.conf.XXXXXX next to the configuration; delete them once by hand. The lock check needs a ps that supports -o lstart, which is the case on OpenBSD, FreeBSD and GNU/Linux with procps, but not with BusyBox.

The code is available with got or git:

got clone ssh://anon@repo.vincentdelft.be/vdcron
git clone ssh://anon@repo.vincentdelft.be/vdcron

Conclusion

With these changes, vdcron now runs perfectly on my FreeBSD, OpenBSD and Void Linux machines, from the same script and the same Makefile. Feedback from other users is very welcome: a bug report, a system where it does not behave as described, or simply a note on how you use it.



👍 0, 👎 0
displayed: 380



What is the second letter of the word Moon?