Tuesday, January 25, 2011

Linux: Schedule command to run once after reboot (RunOnce equivalent)

I'd like to schedule a command to run after reboot on a Linux box. I know how to do this so the command consistently runs after every reboot with a @reboot crontab entry, however I only want the command to run once. After it runs, it should be removed from the queue of commands to run. I'm essentially looking for a Linux equivalent to RunOnce in the Windows world.

In case it matters:

$ uname -a
Linux devbox 2.6.27.19-5-default #1 SMP 2009-02-28 04:40:21 +0100 x86_64 x86_64 x86_64 GNU/Linux
$ bash --version
GNU bash, version 3.2.48(1)-release (x86_64-suse-linux-gnu)
Copyright (C) 2007 Free Software Foundation, Inc.
$ cat /etc/SuSE-release
SUSE Linux Enterprise Server 11 (x86_64)
VERSION = 11
PATCHLEVEL = 0

Is there an easy, scriptable way to do this?

  • Create an @reboot entry in your crontab to run a script called /usr/local/bin/runonce.

    Create a directory structure called /etc/local/runonce.d/ran using mkdir -p.

    Create the script /usr/local/bin/runonce as follows:

    #!/bin/sh
    for file in /etc/local/runonce.d/*
    do
        if [[ ! -f "$file" ]]
        then
            continue
        fi
        "$file"
        mv "$file" "/usr/local/runonce.d/ran/$file.$(date +%Y%m%dT%H%M%S)"
        logger -t runonce -p local3.info "$file"
    done
    

    Now place any script you want run at the next reboot (once only) in the directory /etc/local/runonce.d and chown and chmod +x it appropriately. Once it's been run, you'll find it moved to the ran subdirectory and the date and time appended to its name. There will also be an entry in your syslog.

    pra : for file in /etc/local/runonce.d/*, no?
    Dennis Williamson : @pra: Thanks for spotting that.
    Christopher Parker : Thanks for your answer. This solution is great. It technically solves my problem, however it seems like there's a lot of preparation of infrastructure required to make this work. It's not portable. I think your solution would ideally be baked into a Linux distribution (I'm not sure why it isn't!). Your answer inspired my ultimate solution, which I've also posted as an answer. Thanks again!
    Christopher Parker : What made you choose local3, versus any of the other facilities between 0 and 7?
    Dennis Williamson : @Christopher: A dice roll is always the best method. Seriously, though, for an example it didn't matter and that's the key my finger landed on. Besides, I don't own any eight-sided die.
    Christopher Parker : @Dennis: Got it, thanks. Coincidentally, local3 is the local facility that appears in `man logger`.
  • RunOnce is an artifact of Windows resulting from problems completing configuration before a reboot. Is there any reason you can't run your script before reboot? The above solution appears to be a reasonable clone of RunOnce.

    From BillThor
  • in redhat and debian systems you can do that from /etc/rc.local, it's a kind of autoexec.bat.

    Dennis Williamson : That's going to get executed at *each* boot not just the next one only.
    natxo asenjo : my bad, I misread the question. Thanks for the correction
  • I really appreciate the effort put into Dennis Williamson's answer. I wanted to accept it as the answer to this question, as it is elegant and simple, however:

    • I ultimately felt that it required too many steps to set up.
    • It requires root access.

    I think his solution would be great as an out-of-the-box feature of a Linux distribution.

    That being said, I wrote my own script to accomplish more or less the same thing as Dennis's solution. It doesn't require any extra setup steps and it doesn't require root access.

    #!/bin/bash
    
    if [[ $# -eq 0 ]]; then
        echo "Schedules a command to be run after the next reboot."
        echo "Usage: $(basename $0) <command>"
        echo "       $(basename $0) -p <path> <command>"
        echo "       $(basename $0) -r <command>"
    else
        REMOVE=0
        COMMAND=${!#}
        SCRIPTPATH=$PATH
    
        while getopts ":r:p:" optionName; do
            case "$optionName" in
                r) REMOVE=1; COMMAND=$OPTARG;;
                p) SCRIPTPATH=$OPTARG;;
            esac
        done
    
        SCRIPT="${HOME}/.$(basename $0)_$(echo $COMMAND | sed 's/[^a-zA-Z0-9_]/_/g')"
    
        if [[ ! -f $SCRIPT ]]; then
            echo "PATH=$SCRIPTPATH" >> $SCRIPT
            echo "cd $(pwd)"        >> $SCRIPT
            echo "logger -t $(basename $0) -p local3.info \"COMMAND=$COMMAND ; USER=\$(whoami) ($(logname)) ; PWD=$(pwd) ; PATH=\$PATH\"" >> $SCRIPT
            echo "$COMMAND | logger -t $(basename $0) -p local3.info" >> $SCRIPT
            echo "$0 -r \"$(echo $COMMAND | sed 's/\"/\\\"/g')\""     >> $SCRIPT
            chmod +x $SCRIPT
        fi
    
        CRONTAB="${HOME}/.$(basename $0)_temp_crontab_$RANDOM"
        ENTRY="@reboot $SCRIPT"
    
        echo "$(crontab -l 2>/dev/null)" | grep -v "$ENTRY" | grep -v "^# DO NOT EDIT THIS FILE - edit the master and reinstall.$" | grep -v "^# ([^ ]* installed on [^)]*)$" | grep -v "^# (Cron version [^$]*\$[^$]*\$)$" > $CRONTAB
    
        if [[ $REMOVE -eq 0 ]]; then
            echo "$ENTRY" >> $CRONTAB
        fi
    
        crontab $CRONTAB
        rm $CRONTAB
    
        if [[ $REMOVE -ne 0 ]]; then
            rm $SCRIPT
        fi
    fi
    

    Save this script (e.g.: runonce), chmod +x, and run:

    $ runonce foo
    $ runonce "echo \"I'm up. I swear I'll never email you again.\" | mail -s \"Server's Up\" $(whoami)"
    

    In the event of a typo, you can remove a command from the runonce queue with the -r flag:

    $ runonce fop
    $ runonce -r fop
    $ runonce foo
    

    Using sudo works the way you'd expect it to work. Useful for starting a server just once after the next reboot.

    myuser@myhost:/home/myuser$ sudo runonce foo
    myuser@myhost:/home/myuser$ sudo crontab -l
    # DO NOT EDIT THIS FILE - edit the master and reinstall.
    # (/root/.runonce_temp_crontab_10478 installed on Wed Jun  9 16:56:00 2010)
    # (Cron version V5.0 -- $Id: crontab.c,v 1.12 2004/01/23 18:56:42 vixie Exp $)
    @reboot /root/.runonce_foo
    myuser@myhost:/home/myuser$ sudo cat /root/.runonce_foo
    PATH=/usr/sbin:/bin:/usr/bin:/sbin
    cd /home/myuser
    foo
    /home/myuser/bin/runonce -r "foo"
    

    Some notes:

    • This script replicates the environment (PATH, working directory, user) it was invoked in.
    • It's designed to basically defer execution of a command as it would be executed "right here, right now" until after the next boot sequence.
    Dennis Williamson : Your script looks really handy. One thing to note is that it destructively strips comments out of the crontab.
    Christopher Parker : @Dennis: Thanks. I originally didn't have that extra grep call in there, but all of the comments were piling up; three for every time I ran the script. I think I'll change the script to just always remove comment lines that look like those three auto-generated comments.
    Christopher Parker : @Dennis: Done. The patterns could probably be better, but it works for me.
    Christopher Parker : @Dennis: Actually, based on crontab.c, I think my patterns are just fine. (Search for "DO NOT EDIT THIS FILE" at http://www.opensource.apple.com/source/cron/cron-35/crontab/crontab.c.)
  • Set up the script in /etc/rc5.d with an S99 and have it delete itself after running.

    From mpez0

0 comments:

Post a Comment