by clemens (06.1010.2021)

Beware of Zombie Watchers in Elixir

tl;dr

Running custom background watchers with e.g. watchexec will lead to zombie processes if not wrapped in custom shell script that kills the process once elixir quits.

In a project I wanted to run rsync to copy my static assets to the /priv/static folder, so that I can easily clean that up on each build (i.e. delete old digest files).

So we need the following for this to work:

First the wrapper script in assets/watcher, don’t forget to chmod 744 the file to make it executable.

!/usr/bin/env bash

# See https://hexdocs.pm/elixir/Port.html#module-zombie-operating-system-processes

# Start the program in the background
exec "$@" &
pid1=$!

# Silence warnings from here on
exec >/dev/null 2>&1

# Read from stdin in the background and
# kill running program when stdin closes
exec 0<&0 $(
  while read; do :; done
  kill -KILL $pid1
) &
pid2=$!

# Clean up
wait $pid1
ret=$?
kill -KILL $pid2
exit $ret
fatal: unable to read 57c1b0bcd5c9d60f7b85ec2eb79981a66042fc37

Then add the following config to your config/dev.ex to watch all files in assets/static/ and rsync them to priv/static/ on each change:

assets_dir = Path.expand("../assets", __DIR__)

config :my_app, MyAppWeb.Endpoint,
  ...
  watchers: [
    ...
    {Path.join(assets_dir, "watcher"),
     [
       "watchexec",
       "-w",
       "static",
       "rsync -r --delete --exclude /assets static/ ../priv/static/",
       cd: assets_dir
     ]}
  ]