1

I already have implemented a way to identify if a script is running from a cron job. It uses ps to clim up the process tree and identify if any (recursive) parent command contains "cron" or "CRON".

This solution, while working, is slow (say, around one second) and impacts all the scripts I have, every time I call them. I am looking for a faster solution.

I do not want to add any option to the script inside the crontab, as my goal is precisely to define default notification behavior when no notification option is provided on the command line, and have that default behavior different for cron jobs.

Is there a reasonably reliable, fast way to find out if the script (or one of its recursive parents) was launched from a cron job?

After my initial post, I have reworked my code and been able to improve it, though ps is still used. The code is below, any suggestion is welcome.

is_cron_job()
{
  local PS
  local CMD
  local PID=$$
  while :
  do
    PS="$(ps -h -o ppid,comm -p $PID)"
    [[ "$PS" =~ ^[[:space:]]*([0-9]+)[[:space:]]+(.*)$ ]] || return 1
    PID="${BASH_REMATCH[1]}"
    [[ "$PID" -ge 1 ]] || return 1
    CMD="${BASH_REMATCH[2]}"
    ! [[ "$CMD" =~ crond|CROND ]] || return 0
  done
  return 1
}
Fred
  • 6,590
  • 9
  • 20
  • 4
    You could set an environment variable in the `crontab` file, and check for it in your scripts. – Barmar Jan 23 '17 at 02:25
  • 2
    [This question](http://stackoverflow.com/questions/190759/can-php-detect-if-its-run-from-a-cron-job-or-from-the-command-line) asks for PHP, but most of the answers are applicable to bash scripts as well. In short, no, by default cron by itself doesn't give you anything to identify itself, but it is rather easy to detect interactive use; e.g. `[[ -t 0 && -t 1 ]]` will evaluate to true if both stdout and stdin connect to a terminal. – Amadan Jan 23 '17 at 02:25
  • @Barmar That would work, but I would prefer not having to configure something on the system in case my script is installed on a server I do not control. – Fred Jan 23 '17 at 02:27
  • @Amadan I will look at this question, thanks for the pointer. For the solution you suggest, would it not consider as "run from cron" any script that is connected at both ends to a pipe? – Fred Jan 23 '17 at 02:29
  • @Fred Yes, connecting both input and output to pipes or files would also fail that test. Is that a likely way to use your scripts? Maybe they should use cron-style notification in this case, since normal output won't be seen by the user. – Barmar Jan 23 '17 at 02:30
  • 1
    Maybe this is an XY problem? What's so special about running from cron? Maybe you should use a different criteria that's easier to test? – Barmar Jan 23 '17 at 02:31
  • 2
    "in case my script is insstalled on a server I do not control". The installation instructions should say to set the variable. Or you should provide an installation script. – Barmar Jan 23 '17 at 02:32
  • @Barmar I have dozens of script all using the same framework and yes, many of them could be used for building pipes. The only thing special about cron is that I would like my scripts to automatically use a "notify in case of failure" mode when called from cron (if no notification option is otherwise provided). About the variable in the crontab, I would say (1) forcing people to put assignments in a crontab is not very elegant I think and (2) even in my own crontab, I would prefer not having to do that if there is another solution. – Fred Jan 23 '17 at 02:39
  • 1
    Then I think you're stuck with the ps solution, or maybe something that uses `/proc` to do something similar. But thiere's no simple method. – Barmar Jan 23 '17 at 02:44
  • Thanks for the help. – Fred Jan 23 '17 at 02:46
  • I would prepend a variable to the "cmd-line" in the crontab, i.e. `59 12 30 12 * isCroned=true /path/to/script arg1 arg2 ... > /tmp/myScript.log 2>&1` then use `if ! ${isCroned:-false} ; then echo croned processing ; .... ; else echo non-crontab processing; fi` in the code to delegate actions. Good luck. – shellter Jan 23 '17 at 03:15
  • A solution based on `ps` will not work reliably as it's possible for the process executing the script to become disowned and no longer have a `cron` process in its process tree. – Grisha Levit Jan 23 '17 at 04:50
  • 1
    You will find your answers here: http://unix.stackexchange.com/questions/46789/check-if-script-is-started-by-cron-rather-than-invoked-manually – codeforester Jan 23 '17 at 05:27

3 Answers3

2

How about setting an environment variable in crontab before running the script?

If it is set inside the script that means it was run from cron.

Harald Nordgren
  • 11,693
  • 6
  • 41
  • 65
0

If your OS is using systemd, you actually have a very reliable method of finding out if you are running from cron or not.

loginctl show-session $(</proc/self/sessionid) | sed -n '/^Service=/s/.*=//p'

The result will be crond if and only if the process was started by cron.


Other methods typically have false positives/false negatives:

  • Environment variables can be manipulated by intermediate processes (e.g. if crond starts a wrapper script, which then starts your script, it's harder to guarantee that environment variables set in the crontab will be preserved)
  • Methods involving testing for an interactive shell or presence of a tty will have false positives in conditions like pipelines, startup scripts, etc.
  • Methods relying on parsing the process tree can have false positives since not all processes called crond actually are the cron daemon and can have false negatives since a disowned process will no longer be a child of crond.
Grisha Levit
  • 8,194
  • 2
  • 38
  • 53
0

The limiting speed factor is probably the bash loop, because bash (like other shells to) is interpreted and slow. The following commands avoid the loop and may be faster.

Unsafe Version

This version may lead to false positives if there are other processes which contain the string cron. I think your script has the same problem.

function is_cron_job {
    pstree -s $$ | grep -Fip 'cron'
}

pstree -s $$ prints only the current process and its ancestors (that is, their names).

grep -Fip 'cron' searches for the fixed (-F), case-insensitive (-i) string "cron" and returns 0 if there was a match and 1 if there was no match (-q).

Safer Version

Instead of using the cron's process name, we search for its PID.

function is_cron_job {
    local cronpid="$(pgrep cron)"
    if [ $(wc -w <<< "$cronpid") -ne 1 ]; then
        echo "No or multiple PIDs for cron!"
        exit 1
    fi
    pstree -sp $$ | grep -Po '\(\d+\)' | grep -Fq "($cronpid)"
}

Problems to be fixed:

  • Determine cron's PID in a reliable way.

  • Extract PIDs from pstree in a safe way.
    It would be very uncommon, but a process could be named something(123)funny. If such a process is ancestor of the current process and 123 is crons PID we may produce false positive again.

Socowi
  • 25,550
  • 3
  • 32
  • 54
  • Both of your solutions use `pstree`, which is not available on the operating systems I work on. Your safer solution uses multiple invocations of external programs and command substitutions (and therefore subshells), so it is not the kind of "fast" I am hoping for. – Fred Feb 04 '17 at 11:31
  • This being said, it is true that my code could cause false positives, but I consider that risk reasonable, as not only would there have to be many matching processes on the system, but my script would have to be the child of one of them (other than the true `cron`), which does not seem very likely to me. If it happens, I may get a difference in behavior, but no crash or data loss, so no big deal. – Fred Feb 04 '17 at 11:33