single-instance
single-instance copied to clipboard
fix: Set SOCK_CLOEXEC on Linux to prevent socket from being passed down to children.
This fixes a bug where forking and running a command from a Rust program could cause single-instance to mistakenly think a copy of the app is running when it isn't, when running on Linux. We can recreate this problem with a very simple example:
$ cargo init single-instance-problem
$ cd single-instance-problem
$ cargo add single-instance
And then in our main.rs
:
use std::process::Command;
use single_instance::SingleInstance;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let instance = SingleInstance::new("my-cool-app")?;
if !instance.is_single() {
eprintln!("Instance already running!");
} else {
println!("Forking...");
Command::new("sleep").arg("6000").spawn()?.wait()?;
}
Ok(())
}
Let's build and run this:
$ cargo build
$ ./target/debug/single-instance-problem
Forking...
At this point, if you CTRL-C
, then everything works exactly as you expect it would - you can restart single-instance-problem
and it will print out "Forking..." again. But, if we run single-instance-problem
, then open a second terminal and run:
$ ps -ef | grep single-instance-problem
ubuntu 568517 564675 0 21:16 pts/4 00:00:00 ./target/debug/single-instance-problem
$ kill -9 568517
Then we go back to our first window:
$ ./target/debug/single-instance-problem
Instance already running!
Ohs noes! We can use lsof
to see who has the abstract socket still open:
$ lsof | grep my-cool-app
sleep 568518 ubuntu 3u unix 0x0000000000000000 0t0 4218288 @my-cool-app type=STREAM
$ ps -ef | grep 568518
ubuntu 568518 1 0 21:16 pts/4 00:00:00 sleep 6000
Yup! It's the sleep 6000
we spawned in our Rust app.
To understand what's going on here (you probably know a lot of this already, but just in case...) when our app does Command::new(...).spawn()
, what's actually happening in Linux is that we're doing a fork and exec. When we fork in Linux, as the man page says:
The child inherits copies of the parent's set of open file descriptors. Each file descriptor in the child refers to the same open file description (see open(2)) as the corresponding file descriptor in the parent.
This means the sleep 6000
gets a copy of the open file descriptor for our abstract socket, which we probably don't want. The fix here is to set SOCK_CLOEXEC on the abstract socket. To quote from the socket man page:
SOCK_CLOEXEC - Set the close-on-exec (FD_CLOEXEC) flag on the new file descriptor. See the description of the O_CLOEXEC flag in open(2) for reasons why this may be useful.
What this means is that when we run our child command, we'll still fork
and get a copy of the file descriptor, but when the child exec
s the "sleep 6000" this file descriptor will automatically be closed. The parent process will still have it open, so we'll still get our "single instance" behavior, but if someone kill -9
s our single instance, we can recreate it immediately instead of having to wait for all the execed child processes to die.