Running only a single instance of a process

Redowan Delowar December 31, 2024
Source

I've been having a ton of fun fiddling with Tailscale over the past few days. While setting it up on a server, I came across this ufw firewall script that configures the firewall on Linux to ensure direct communication across different nodes in my tailnet. It has the following block of code that I found interesting (added comments for clarity):

Here, flock is a Linux command that ensures only one instance of the script runs at a time by locking a specified file (e.g., PIDFILE) through a file descriptor (e.g., 200). If another process already holds the lock, the script either waits or exits immediately. Above, it bails with an error message and exit code 1.

If you try running two instances of this script, the second one will exit with this message:

On most Linux distros, flock comes along with the coreutils. If not, it's easy to install with your preferred package manager.

A more portable version

On macOS, the file locking mechanism is different, and flock doesn't work there. To make your script portable, you can use mkdir in the following manner to achieve a similar result:

This works because mkdir is atomic. It creates the lock directory (LOCKDIR) in /tmp or fails if the directory already exists. This acts as a marker for the running instance. If successful, the script sets up a trap to remove the directory on exit and continues to the main logic. If mkdir fails, it means another instance of the process is running, and the script exits with a message.

This is almost as effective as the flock version. Since I rarely write scripts for non-Linux environments, either option is fine!

With Python

Oftentimes, I opt for Python when I need to write larger scripts. The same can be achieved in Python like this:

The script uses fcntl.flock to prevent multiple instances from running. It creates a lock file (LOCKFILE) in the /tmp directory, named after the scripts filename. When the script starts, it opens the file in write mode and tries to lock it with fcntl.flock using an exclusive lock (LOCK_EX). The LOCK_NB flag makes the operation non-blocking. If another process holds the lock, the script exits with a message.

This approach works on both Linux and macOS, as both support fcntl for file-based locks. The lock is automatically released when the file is closed, either at the end of the script or the with block.

With Go

I was curious about doing it in Go. It's quite similar to Python:

Like the Python example, this uses syscall.Flock to prevent multiple script instances. It creates a lock file based on the script's name using filepath.Base(os.Args[0]) and stores it in /tmp. The script tries to acquire an exclusive, non-blocking lock (LOCK_EX | LOCK_NB). If unavailable, it exits with a message. The lock is automatically released when the file is closed in the defer block.

Underneath, Go makes sure that syscall.Flock works on both macOS and Linux.

Discussion in the ATmosphere

Loading comments...