Skip to content

Instantly share code, notes, and snippets.

@mandhor
Created April 7, 2017 13:55
Show Gist options
  • Save mandhor/2f1b04f2ad3815cced7fcaf03486a7be to your computer and use it in GitHub Desktop.
Save mandhor/2f1b04f2ad3815cced7fcaf03486a7be to your computer and use it in GitHub Desktop.

Revisions

  1. mandhor created this gist Apr 7, 2017.
    411 changes: 411 additions & 0 deletions lockrun.c
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,411 @@
    /*
    * $Id: //websites/unixwiz/unixwiz.net/webroot/tools/lockrun.c#8 $
    *
    * written by : Stephen J. Friedl
    * Software Consultant
    * Southern California USA
    * [email protected]
    * http://www.unixwiz.net/tools/
    *
    * ===================================================================
    * ======== This software is in the public domain, and can be ========
    * ======== used by anybody for any purpose ========
    * ===================================================================
    *
    * Lockrun: This program is used to launch a program out with a lockout
    * so that only one can run at a time. It's mainly intended for use out
    * of cron so that our five-minute running jobs which run long don't get
    * walked on. We find this a *lot* with Cacti jobs which just get hung
    * up: it's better to miss a polling period than to stack them up and
    * slow each other down.
    *
    * So we use a file which is used for locking: this program attempts to
    * lock the file, and if the lock exists, we have to either exit with
    * an error, or wait for it to release.
    *
    * lockrun --lockfile=FILE -- my command here
    *
    * COMMAND LINE
    * ------------
    *
    * --lockfile=F
    *
    * Specify the name of a file which is used for locking. The file is
    * created if necessary (with mode 0666), and no I/O of any kind is
    * done. The file is never removed.
    *
    * --maxtime=N
    *
    * The script being controlled should run for no more than <N> seconds,
    * and if it's beyond that time, we should report it to the standard
    * error (which probably gets routed to the user via cron's email).
    *
    * --wait
    *
    * When a lock is hit, we normally exit with error, but --wait causes
    * it to loop until the lock is released.
    *
    * --sleep=N
    *
    * When using --wait, will sleep <N> seconds between attempts to acquire
    * the lock.
    *
    * --retries=N
    *
    * When using --wait, will try only <N> attempts of acquiring the lock.
    *
    * --verbose
    *
    * Show a bit more runtime debugging.
    *
    * --quiet
    *
    * Don't show "run is locked" error if things are busy; keeps cron from
    * overwhelming you with messages if lockrun overlap is not uncommon.
    *
    * --
    *
    * Mark the end of the options: the command to run follows.
    *
    *
    * VERSION HISTORY
    *
    * 1.1.3 2014/11/03; now handle exit status correctly (thanks to Richard Faasen)
    * 1.1.2 2013/04/26; now we use lockf() if the platform supports it
    * 1.1.1 2012/09/05; added setsid() to make the controlled process a process
    * group leader
    * 1.1.0 2012/09/05; added --version and --help; added --retries (thanks Dov Murik)
    * 1.0.3 2010/11/20; added --quiet parameter
    * 1.0.2 2009/06/25; added lockf() support for Solaris 10 (thanks Michal Bella)
    * 1.0.1 2006/06/03; initial release
    *
    */

    #include <stdio.h>
    #include <stdarg.h>
    #include <errno.h>
    #include <stdlib.h>
    #include <string.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <time.h>
    #include <sys/types.h>
    #include <sys/wait.h>
    #include <sys/file.h>

    static const char Version[] = "lockrun 1.1.3 - 2014-11-03 - [email protected]";

    #ifndef __GNUC__
    # define __attribute__(x) /* nothing */
    #endif


    #define STRMATCH(a,b) (strcmp((a),(b)) == 0)

    #define UNUSED_PARAMETER(v) ((void)(v))

    #define TRUE 1
    #define FALSE 0

    static const char *lockfile = 0;
    static int wait_for_lock = FALSE;
    static mode_t openmode = 0666;
    static int sleeptime = 10; /* seconds */
    static int retries = 0;
    static int Verbose = FALSE;
    static int Maxtime = 0;
    static int Quiet = FALSE;

    static char *getarg(char *opt, char ***pargv);

    static void die(const char *format, ...)
    __attribute__((noreturn))
    __attribute__((format(printf, 1, 2)));

    #ifdef F_TLOCK
    # define WAIT_AND_LOCK(fd) lockf(fd, F_TLOCK,0)
    #else
    # define WAIT_AND_LOCK(fd) flock(fd, LOCK_EX | LOCK_NB)
    #endif

    static const char *const helptext[] = {
    "Usage: lockrun [options] -- command args...",
    "",
    " --help Show this brief help listing",
    " --version Report the version info",
    " -- Mark the end of lockrun options; command follows",
    " --verbose Show a bit more runtime debugging",
    " -V",
    " --quiet Exit quietly (and with success) if locked",
    " -q",
    " --lockfile=F Specify lockfile as file <F>",
    " -L=F",
    " --wait Wait for lockfile to appear (else exit on lock)",
    " -W",
    "",
    " Options with --wait:",
    "",
    " --sleep=T Sleep for <T> seconds on each wait loop",
    " -S=T",
    " --retries=N Attempt <N> retries in each wait loop",
    " -R=N",
    " --maxtime=T Wait for at most <T> seconds for a lock, then exit",
    0,
    };

    static void show_help(const char *const *);

    int main(int argc, char **argv)
    {
    char *Argv0 = *argv;
    int rc;
    int lfd;
    pid_t childpid;
    time_t starttime;
    int attempts = 0;

    UNUSED_PARAMETER(argc);

    time(&starttime);

    for ( argv++ ; *argv ; argv++ )
    {
    char *arg = *argv;
    char *opt = strchr(arg, '=');

    /* the -- token marks the end of the list */

    if ( strcmp(*argv, "--") == 0 )
    {
    argv++;
    break;
    }

    if (opt) *opt++ = '\0'; /* pick off the =VALUE part */

    if ( STRMATCH(arg, "--version") )
    {
    puts(Version);
    exit(EXIT_SUCCESS);
    }

    else if ( STRMATCH(arg, "--help"))
    {
    show_help(helptext);
    exit(EXIT_SUCCESS);
    }

    else if ( STRMATCH(arg, "-L") || STRMATCH(arg, "--lockfile"))
    {
    lockfile = getarg(opt, &argv);
    }

    else if ( STRMATCH(arg, "-W") || STRMATCH(arg, "--wait"))
    {
    wait_for_lock = TRUE;
    }

    else if ( STRMATCH(arg, "-S") || STRMATCH(arg, "--sleep"))
    {
    sleeptime = atoi(getarg(opt, &argv));
    }

    else if ( STRMATCH(arg, "-R") || STRMATCH(arg, "--retries"))
    {
    retries = atoi(getarg(opt, &argv));
    }

    else if ( STRMATCH(arg, "-T") || STRMATCH(arg, "--maxtime"))
    {
    Maxtime = atoi(getarg(opt, &argv));
    }

    else if ( STRMATCH(arg, "-V") || STRMATCH(arg, "--verbose"))
    {
    Verbose++;
    }

    else if ( STRMATCH(arg, "-q") || STRMATCH(arg, "--quiet"))
    {
    Quiet = TRUE;
    }

    else
    {
    die("ERROR: \"%s\" is an invalid cmdline param", arg);
    }
    }

    /*----------------------------------------------------------------
    * SANITY CHECKING
    *
    * Make sure that we have all the parameters we require
    */
    if (*argv == 0)
    die("ERROR: missing command to %s (must follow \"--\" marker) ", Argv0);

    if (lockfile == 0)
    die("ERROR: missing --lockfile=F parameter");

    /*----------------------------------------------------------------
    * Open or create the lockfile, then try to acquire the lock. If
    * the lock is acquired immediately (==0), then we're done, but
    * if the lock is not available, we have to wait for it.
    *
    * We can either loop trying for the lock (for --wait), or exit
    * with error.
    */

    if ( (lfd = open(lockfile, O_RDWR|O_CREAT, openmode)) < 0)
    die("ERROR: cannot open(%s) [err=%s]", lockfile, strerror(errno));

    while ( WAIT_AND_LOCK(lfd) != 0 )
    {
    attempts++;

    if ( ! wait_for_lock )
    {
    if ( Quiet)
    exit(EXIT_SUCCESS);
    else
    die("ERROR: cannot launch %s - run is locked", argv[0]);
    }

    if ( retries > 0 && attempts >= retries )
    {
    die("ERROR: cannot launch %s - run is locked (after %d attempts)", argv[0], attempts);
    }

    /* waiting */
    if ( Verbose ) printf("(locked: sleeping %d secs, after attempt #%d)\n", sleeptime, attempts);

    sleep(sleeptime);
    }

    fflush(stdout);

    /* run the child */


    if ( (childpid = fork()) == 0 )
    {
    /* IN THE CHILD */

    close(lfd); /* don't need the lock file */

    /* Make ourselves a process group leader so that this and all
    * child processes can be sent a signal as a group. This may
    * be used someday to support a --kill option, though this is
    * is still tricky.
    *
    * PORTING NOTE: if "setsid" is undefined on your platform,
    * just comment it out and send me an email with info about
    * the build environment.
    */
    (void) setsid();

    execvp(argv[0], argv);
    }
    else if ( childpid > 0 )
    {
    /* IN THE PARENT */

    time_t endtime;
    pid_t pid;
    int exitstatus = 0;

    if ( Verbose )
    printf("Waiting for process %ld\n", (long) childpid);

    pid = waitpid(childpid, &exitstatus, 0);

    time(&endtime);

    endtime -= starttime;

    if ( Verbose || (Maxtime > 0 && endtime > Maxtime) )
    printf("pid %d exited with status %d (time=%ld sec)\n", pid, exitstatus, endtime);

    /* Reflect subprogram's exit status in lockrun's exit status */

    if ( WIFEXITED(exitstatus) )
    {
    rc = WEXITSTATUS(exitstatus);
    }
    else
    {
    rc = 1; /* SHOULD NEVER GET HERE */
    }
    }
    else
    {
    die("ERROR: cannot fork [%s]", strerror(errno));
    }

    if ( Verbose ) printf("Exiting rc=%d\n", rc);

    exit(rc);
    }


    /*! \fn static char *getarg(char *opt, char ***pargv)
    * \brief A function to parse calling parameters
    *
    * This is a helper for the main arg-processing loop: we work with
    * options which are either of the form "-X=FOO" or "-X FOO"; we
    * want an easy way to handle either one.
    *
    * The idea is that if the parameter has an = sign, we use the rest
    * of that same argv[X] string, otherwise we have to get the *next*
    * argv[X] string. But it's an error if an option-requiring param
    * is at the end of the list with no argument to follow.
    *
    * The option name could be of the form "-C" or "--conf", but we
    * grab it from the existing argv[] so we can report it well.
    *
    * \return character pointer to the argument
    *
    */
    static char *getarg(char *opt, char ***pargv)
    {
    const char *const optname = **pargv;

    /* option already set? */
    if (opt) return opt;

    /* advance to next argv[] and try that one */
    if ((opt = *++(*pargv)) == 0)
    die("ERROR: option %s requires a parameter", optname);

    return opt;
    }

    /*
    * die()
    *
    * Given a printf-style argument list, format it to the standard error,
    * append a newline, then exit with error status.
    */

    static void die(const char *format, ...)
    {
    va_list args;

    va_start(args, format);
    vfprintf(stderr, format, args);
    putc('\n', stderr);
    va_end(args);

    exit(EXIT_FAILURE);
    }

    static void show_help(const char * const *p)
    {
    puts(Version);
    puts("");

    for ( ; *p; p++ )
    {
    puts(*p);
    }
    }