Search code examples
bashinotifyinotifywait

`inotifywait` does not terminate in bash script with `-e delete_self`; but does in interactive shell


I'm trying to write a script which restarts a python3 -m http.server process when a certain directory (_site) is deleted and then recreated. The script is below. There's an inotify command in the waitdel function which is supposed to only block until the directory is deleted. When it is deleted, the execution goes on with a simple polling wait until the directory is created, then the server restarts, and finally we're back to waiting.

The trouble is, when _site is deleted, inotifywait never exits in the shell script, even tho the exact same command does exit when I run it at the very same bash prompt I run this script in, both on DELETE and DELETE_SELF.

I've verified that the correct inotifywait command is run, and that the server process is not blocking the execution of the script. So, why is it not exiting in the script?

#!/usr/bin/env bash
# serve.bash --- serve content, respawn server when _site/ is deleted

# bash strict mode
set -euo pipefail
IFS=$'\n\t'

PID=0
DIR="$PWD/_site"                # NO FOLLOWING SLASH OR BREAKS INOTIFYWAIT
PIDFILE="$PWD/.server.pid"

die () {
    echo $0: error: $@
    exit 2
}

say () {
    echo $0 \[$(date)\]: $@
}

serve () {
    cleanup
    old="$PWD"
    cd "$DIR" || die Could not cd to "$DIR"
    python3 -m http.server 8000 --bind 127.0.0.1 2>&1 &
    echo $! > "$PIDFILE"
    cd "$old"
}

waitdel () {
    while true; do
        say Set up watcher for "$DIR"...
        inotifywait -e delete_self "$DIR"
        say "$DIR" deleted, restarting server...

        # Wait&poll till the directory is recreated.
        while [ ! -e "$DIR" ]; do
            sleep 0.1
        done

        serve
    done
}

cleanup () {
    if [[ ! -e "$PIDFILE" ]]; then
        return
    fi
    sync
    PID="$(cat $PIDFILE)" && rm "$PIDFILE"
    say Kill pid="$PID"...
    [ "0" = "$PID" ] || kill -9 "$PID" \
        || die Failed to kill preexisting server on pid "$PID"
}


trap cleanup SIGINT SIGTERM EXIT

if [ -e "$PIDFILE" ]; then
    if pgrep -a python3 | grep http\\.server >/dev/null; then
        trap - SIGINT SIGTERM EXIT
        die Stale pidfile found at "$PIDFILE", a potentially orphaned \
            server might be running.  Please kill it before proceeding.
    else
        rm "$PIDFILE"               # remove stale pidfile when no server proc found
    fi
fi


serve

waitdel

Solution

  • As per @oguz ismail's suggestion, I've tried to produce a minimal version of the script that can reproduce the issue, and here it is:

    DIR="$PWD/_site"
    mkdir -p $DIR
    old="$PWD"
    cd "$DIR" || die Could not cd to "$DIR"
    python3 -m http.server 8000 --bind 127.0.0.1 2>&1 &      # (1)
    cd "$old"
    pid=$!
    inotifywait -e delete_self "$DIR" &                      # (2)
    sleep 1
    rmdir $DIR
    sleep 1
    kill $pid
    echo kill                                                # (3)
    

    What's going on here: past the boilerplate stuff, in expression (1) we start a python http.server process whose CWD is $DIR. If it is not such, i.e. the CWD is $(dirname $DIR), inotifywait does successfully terminate. In (3) we clean up the server process. If we kill the python process, inotifywait terminates, if we don't, it doesn't. The output from the process

    Setting up watches.
    Watches established.
    Serving HTTP on 127.0.0.1 port 8000 (http://127.0.0.1:8000/) ...
    kill
    /home/g/co/gkayaalp.com/www/_site/ DELETE_SELF 
    

    suggests that inotifywait terminates after (3).

    So inotifywait hangs because $DIR is busy (I guess it is because inotify works with inodes and the python process hangs on to the inode, delaying the propagation of the deletion event). A quick way then to remedy this is to watch the parent directory instead. I modified waitdel as such:

    waitdel () {
        while true; do
            say Set up watcher for "$DIR"...
            while inotifywait -qq -e delete "$DIR/.."; do
                if [ ! -e $DIR ]; then
                    say "$DIR" deleted, restarting server...
    
                    # Wait&poll till the directory is recreated.
                    while [ ! -e "$DIR" ]; do
                        sleep 0.1
                    done
    
                    serve
                fi
            done
        done
    }
    

    where now it tracks DELETE events on $DIR/.., and on each, checks if $DIR is deleted. When it is, it waits for the directory to be regenerated, and then runs serve which kills the existing python process and spawns a new one.