Run a program once, or keep it running

websocketd starts one fresh process per connection and starts nothing at all until the first client arrives, so neither "run this once" nor "keep this running" happens on its own. Both are things you arrange around it.

Two opposite problems live on this page. Go to the one you have:

  • Only one instance may exist at a time, because it holds a device, a lock, or a resource that does not tolerate a second copy. Use a wrapper that refuses to start twice .
  • The program must survive for the whole connection, or longer, without being cut off mid-write. Give it signal handling .

Enforce a single instance

Wrap the real program in a script that takes a lock before handing over. Every connection still gets its own wrapper process. Only one wrapper ever reaches the program.

#!/bin/sh
# run-once.sh
LOCKDIR=/tmp/control-motor.lock

if ! mkdir "$LOCKDIR" 2>/dev/null; then
  echo "busy: another connection holds the motor"
  exit 0
fi
trap 'rmdir "$LOCKDIR"' EXIT INT TERM

exec ./control-motor.sh
websocketd --port=8080 ./run-once.sh

Creating a directory is atomic on every POSIX filesystem, which is what makes this a lock rather than a race. A second connection arriving while the first holds it gets a message and nothing else:

busy: another connection holds the motor

When the first connection closes, the wrapper's trap removes the lock directory, and the next connection acquires it normally.

On Linux you can use flock instead, which releases the lock when the file descriptor closes and so survives a wrapper that dies without running its trap:

#!/bin/bash
exec 9>/tmp/control-motor.lock
flock -n 9 || { echo "busy"; exit 0; }
exec ./control-motor.sh

flock ships with util-linux and is not present on macOS or the BSDs, which is why the portable version above uses mkdir.

Let the newest connection win instead

If the right answer is "the latest connection takes over" rather than "the second connection is refused", kill the incumbent instead of refusing the newcomer:

#!/bin/sh
# takeover.sh
PIDFILE=/tmp/control-motor.pid

if [ -f "$PIDFILE" ]; then
  kill -TERM "$(cat "$PIDFILE")" 2>/dev/null
  sleep 1
fi

echo $$ > "$PIDFILE"
exec ./control-motor.sh

Pick whichever matches what should happen when two people reach for the same resource. websocketd has no opinion, because a chat room and a motor controller need opposite answers.

Keep a program running

A program that loops rather than exiting when its input runs out has to handle the signals websocketd sends when the connection closes. This is the whole contract: handle SIGINT and SIGTERM, and exit promptly on either.

#!/usr/bin/env python3
import signal, sys, time

def shutdown(signum, frame):
    flush_everything()
    sys.exit(0)

signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)

while True:
    do_work()
    sys.stdout.flush()
    time.sleep(1)

SIGINT arrives first, so a program that handles only SIGTERM gets one fewer chance to clean up than it thinks. Handle both.

A program that ignores both, and does not read stdin either, does not survive: websocketd escalates to SIGKILL, and the process is gone under a second after the connection closes, having run no cleanup at all. It rides websocketd's teardown ladder to the bottom on every disconnect. Process lifecycle has the ladder and the --closems flag that lengthens it, which is what you want if your cleanup needs a network round trip.

Reading stdin is the other way to notice. websocketd closes your program's stdin first, so a loop of the form while read -r line ends by itself when the connection does, with no signal handling at all.

Keep it running past the connection

Anything still in the process group when the connection ends is killed. If a program has to outlive the connection that started it, do not start it from websocketd at all. Run it as a service in its own right, and let each connection bridge to it:

#!/bin/sh
exec socat - UNIX-CONNECT:/var/run/hub.sock

That is the third pattern in share state across connections , and it is the answer to "one process for the whole server" as well as to "shared state". Use systemd , or whatever supervises services on your host, to start and restart it.

Next