Correctly spawning apps from scripts in sway/i3

What I wanted to do is to bind a key in sway that would execute a script, and that script should run a graphical app. And this never worked reliably: sometimes it will just not run an app, and I have to repeat the action again.

More specifically, I've written a launcher that allows you to fuzzy-select a candidate app to run, and then press Enter to run it. The launcher is mostly written in Bash, and usez FZF for the selection part. When the app is selected, it does roughly the following:


app_to_run=...
nohup "$app_to_run" &>/dev/null &
sleep 0.01

Let's try to understand what it's doing. First, you have nohup. Its purpose is to prevent an app from being terminated when a "hangup" signal is sent to it. A hangup signal is usually sent when you close the terminal where the app is running. Most of the time, this is a right thing to do, except when you're trying to start a background app which should persist.

The &>/dev/null is just to ignore the output of the app and prevent nohup from creating the nohup.out file. Just to keep things clean.

And the most interesting part (and the source of the problem) is &. It runs the program in background mode. You'd expect that nohup together with & would do the trick, but there's a race condition here. It takes time for nohup to start, set the signal handlers, and pass control to the app (even if small amount). But at the same time, the bash script continues to run, so if it reaches the end faster than nohup can set signal handlers, then nohup itself will be terminated.

Hence I came up with a dirty hack. Some time ago I've added sleep 0.01 to make sure that bash doesn't terminate that quickly and gives nohup time to do its thing. It worked reasonably well most of the time, but occasionally when the system is under load, I'll still get the same problem.

Now, after digging a bit, I've found an alternative solution that seems to have fixed everything. So if you're implementing a terminal-based launcher and have to add sleep to your nohup, you can do this instead:


app_to_run=...
setsid -f "$app_to_run"

What setsid does is it "runs a program in a new session". This means that whatever it runs will not be attached to the current session (and to job control by extension). And thus, when current session is terminated, the app will persist in background. This approach is a bit different from nohup, because here the signal handlers are not touched at all.

But most importantly, setsid sets a new session before returning control back to Bash. So Bash is free to terminate as soon as it wishes. The app will still continue to run as you expect.

This approach not only works better, but is also more correct, as in case of nohup, you're messing with the SIGHUP signal handlers, which some programs will reset back, and your apps will surprisingly still not be able to run in the background.