Run Script Atomically

bashlaunchdscript

I have a launchd task that runs a bash script periodically, and this works great. However, if I change the script while the task is running, either by editing it directly, or swapping it with an updated copy, it can cause the running task to spew out a load of syntax errors or worse, have unpredictable behaviour.

The problem appears to be that instead of loading the script into memory, or holding open the file as it was at the time of execution, launchd (or probably bash itself) is loading the live file line-by-line, this means that any changes to the script appear to be reflected in real time, leading to errors.

To try to illustrate the problem, consider the following:

if [ "$foo" = '1' ]; then
    do_something_that_takes_time
fi

Now, imagine the script is running at line 2 (do_something_that_takes_time) and I swap out the script, removing the if/fi block entirely. When the script moves onto line 3, it will no longer find fi, and will continue executing expecting to find a fi but never will, resulting in an eventual error (unexpected end of file). It may also end up skipping commands (that were moved up as a result of removing lines).

Of course I can avoid this issue by unloading the launchd task, updating, then reloading it, but this isn't very convenient, especially for tasks that can take a long time to complete (as I'd prefer to let them finish, but there seems no way to do so and swap in the file at that point). Otherwise I have to add in some mechanism for prevent the task from re-launching, so I can swap in the new script at my leisure, but this seems like an unnecessary extra step when Bash should just be running the script I told it to, at the time it started.

So my question is this; is there some way that I can force launchd (or Bash) to execute the script only as it was when the task began? For example by fully loading it into memory before executing, or loading from an open file-handle that won't change?

I'm posting this here because I'm not entirely sure if this is a quirk of Bash in general, or potentially Mac specific in some way. The volume I'm executing from is using APFS, so should be copy-on-write, so I wouldn't expect this behaviour to occur (as an open file handle should continue to point to the old file, ignoring any changes that are made).

Best Answer

So my question is this; is there some way that I can force launchd (or bash) to execute the script only as it was when the task began?

No. It’s not a quirk of bash or launchd. launchd loads the job (plist) into memory, not the script. The script gets called per it’s “start” definition like RunAtLoad or CalendarInterval.

Also, the nuance here is when the task began. Well, that depends on when the task was configured to be started. Just because the job definition is loaded doesn’t mean the task has started.

If you define the job within the job definition using Program and ProgramArguments it can theoretically, do what you want. However, there’s no script logic; it’s just a command and it’s arguments.

Now if your script takes time or to put it another way, it is still running, modifying it will of course cause errors. This would be like you executing your script, then simultaneously in another bash session modifying it. It’s going to have issues.

launchd just executes the script like you would if you typed the command manually.