TipTopTips

Mostly technical musings on open source software development

This project is maintained by liborty

Timing is Everything

by Liborty Rustafarian

Introduction

As comedians and nymphomaniacs will tell you, timing is everything. So, in this installment of my popular bash (retro) programming series, we will build a bash script called sleeper for suspending and waking up a (Linux) computer at a predetermined time. We will then invoke it from crontab for automated repeated suspensions and awakenings. Some applications:

  1. “Saving the planet and your computer” i.e., suspending machines when they are not being used for some time. Suppose that you want to run some regular backup and maintenance at 3am, though? This script will awaken the machine just in time to run the maintenance cron job and then put it back to sleep.

  2. Regular downloads/uploads of data from the cloud/internet/sensor.

  3. Regular generation and publishing of (static) websites.

  4. Dedicated hardware that only does one job at regular intervals. For example, time-lapse photography by a computer controlled camera. Perhaps a Raspberry Pi running in a remote location on batteries? This will definitely make the batteries last a lot longer.

My application (Automated Web Trader) that motivated me to write sleeper ticks boxes 1.-3.

There are some existing facilities for machines self-suspending after some period of inactivity but they are somewhat unreliable in practice. That is why admins prefer to err on the side of caution and leave everything on all the time. Clearly, the superior solution is to wake up and to suspend again at exactly the required times under explicit own control.

Disclaimer: No warranties, implied or otherwise, are given. No responsibilities are accepted. This article is for educational purposes only. If you lock your computer into a state of permanent suspension, you will have only yourself to blame.

Suspend Immediately, Wake up at Later, with rtcwake

The Linux facility rtcwake has various modes (-m) of saving the state of the machine. The most useful ones are mem and disk, -m mem being the faster one. However, for longer suspensions and more energy saving, consider -m disk. Other main options of interest are -t (wake up at a specified time) or -s (wake up in specified number of seconds). We invoke it within sleeper script in this way:

sudo rtcwake -m mem -u -s $(( $G-($(date +%s)-$S)%$G-$A ))

Some observations:

  • rtcwake can only be invoked with sudo privileges, as it affects the whole machine. It should be a single user computer, or its users must be warned in advance of the pending suspension. Sudo has repercussions for invoking it from cron. It is inadvisable, for security reasons, to call it from other existing user crontabs, as they would all have to be run under root. It is better to invoke rtcwake from its own dedicated root crontab and just synchronise its timings with the user’s normal tasks crontabs.

  • -u means use UTC clock. This avoids many potential headaches with timezones an Summer times and whatnot. UTC achieves safer synchronisation with crontab, which should also run on UTC clock by setting environment variable TIMEZONE=UTC as the first line in the crontab.

  • the rest of the line specifies the number of seconds to be spent in the suspended state before re awakening. The calculation will be explained presently in the next section. In a recurring scenario this is simpler than using -t specific-time, as the specific times will be changing all the time.

  • The time resolution of rtcwake is seconds, whereas the smallest time unit of crontab is a minute. Accordingly, sleeper will work in seconds, whereas crontab in minutes. We need to bear this in mind when synchronising between them.

Sleep Time Calculation

The actual usage we want is the inverse of the way that rtcwake works. We want sleeper to be waking-centric, rather than sleep-centric. That is to say, the logical sequence of events is to be: wake up, do some communications and calculations tasks, then go back to sleep.

There is a potential problem with this. Due to lag etc, those communications will take somewhat unpredictable amounts of time. So, it is only when they are actually finished that we can calculate exactly how many seconds remain till the next regular waking time.

The beauty of this formula lies in its use of modular arithmetic to solve this problem:

$(( $G-($(date +%s)-$S)%$G-$A ))

where

  • the bash variables in this expression, $G,$S,$A, are supplied to sleeper as option values. Any that are left unspecified are assigned default values.
  • $S is the start offset for the first cron invocation in any hour, in seconds.
  • $G is the gap between subsequent regular cron invocations ($S < $G).
  • $(date +%s) returns the unix time (continuous count of seconds since 1.1.1970 till present).
  • We subtract $S from the unix seconds to get the time it would be be now if the start offset delay was zero.
  • %$G finds the remainder modulo $G, which is precisely the time spent on tasks up to this moment. Subtracting this from $G gives the time to run till the next scheduled tasks crontab invocation. In other words, adding the result to the present time, which is what rtcwake does internally, takes it exactly to the next regular $S+n*$G waking up milestone.
  • However, the system must be fully awake by then, so we finally subtract $A (advance), shortening the suspension by a few seconds to give the system time to wake up in time.

Now we are at the stage where we can invoke sudo sleeper manually at any time. It will suspend the computer immediately and wake up again $A seconds before the next scheduled crontab invocation of some regular tasks.

Crontab Synchronisation

Ideally, we would want to call sudo sleeper at the end of our main recurring tasks crontab script. However, because of the difficulties with permissions and security discussed above, we will make it build its own root crontab to run alongside the main user one. We add option -r for this purpose: sudo sleeper -r needs to be invoked only once, at the beginning. Thereafter, sleeper will be periodically reinvoked from its own crontab. To cancel and regain permanently wakeful state, use sudo sleeper -w (have to act quickly, while still awake).

One subtle problem with this is that sleeper can only be invoked by crontab at whole minutes, so when the usual tasks are finished, we have to wait a little (and our precise calculation of the remaining sleep time above is no longer strictly necessary). My main task, written in super fast Rust, usually finishes in a few seconds and the computer then has to idle till one whole minute is up. In practice, this is not much of a problem and it does give us time to issue sudo sleeper -w when needed. Should your applications need more than one minute to run, then it will be necessary to add the relevant number of “waking” minutes, rounded up. Here is the -r part of the script:

# set up and start crontab which will suspend one minute after the regular $S+$G time
# and reawaken $A seconds before the next one, using this script (this time without -r)
if [ $R ]; then
  SM=$(( $S/60+1 )) # start in minutes, plus one, to allow one awake minute
  GM=$(( $G/60 ))   # gaps in minutes
  printf "TZ=UTC\n$SM-59/$GM * * * * sudo $PD/bin/sleeper -a $A -s $S -g $G"\
  " >> $PD/data/sleepercronlog\n" | crontab -u root -  
fi

Note how the $SM and $GM parameters ($S and $M translated into minutes) map directly into the first entry (for minutes) in the crontab table: $SM-59/$GM. Similar thing can be done for hours and days in the following fields, should longer intervals be needed.

Cron is activated by piping the two lines crontab as standard input (-) into crontab -u root -. Meaning that the user (owner) of this crontab will be root.

I had permission problems until I have put sudo also in front of the invocations of sleeper and rtcwake in sleeper. This fussiness took me by surprise, considering that the script and the crontab were already invoked with sudo.

References

The full source is here: sleeper