Programmatic clean qube shutdown

I’m writing a utility script which given a qube name will execute a graceful exit of likely processes followed by qvm-shutdown. Several of my app qubes are simple non-disposable hosts of browser + terminals, and this script is mostly intended for that simple case, so it doesn’t need to be too clever.

Both bash and zsh exit gracefully upon SIGHUP, so for them:

qvm-run --pass-io $QUBE 'pkill --signal HUP -x "\<bash\>"' || true
qvm-run --pass-io $QUBE 'pkill --signal HUP -x "\<zsh\>"' || true

(I think the -x helps isolate the signal to interactive shells- imperfectly, but good enough)

Google Chrome appears to make a clean exit upon SIGTERM, so:

qvm-run --pass-io $QUBE 'pgrep -x chrome >/dev/null 2>&1 && pkill --oldest --signal TERM -x chrome' || true

Firefox also:

qvm-run --pass-io $QUBE 'pgrep firefox >/dev/null 2>&1 && pkill --oldest --signal TERM firefox' || true

Tor Browser is trickier. If I kill it in the same way as for Firefox, I get a warning popup after the browser exits. I can circumvent this by first killing the torbrowser parent process, but the workaround makes the whole approach feel skeevy and I wish for a more robust solution.

I was thinking I could simulate a normal, user-initiated exit by sending a Ctrl-Q key with xdotool from dom0:

xdotool search --onlyvisible --class "${QUBE}.*Tor Browser" key --window '%1' ctrl+q

Alas, though the search works the key appears to be ignored by the browser window. Maybe it would work running xdotool within the qube itself, but not all these qubes have xdotool installed (notably e.g. whonix-workstation-17).

Does someone have advice for a better way to programmatically exit Tor Browser specifically, or browsers more generally, or app VMs even more generally?

1 Like

I found this QoL post linking to this script:

And I tested that Tor Browser similarly can be exited by way of a local xdotool invocation:

[app-qube]$ xdotool search --onlyvisible --name 'Tor Browser' key --window '%1' ctrl+q

But I’d still prefer philosophically to leave the Whonix template unmodified, no additional xdotool package. It’s unfortunate that sending the Ctrl-Q to the remote window on dom0 doesn’t appear to propagate the key to the app qube’s X session.

I never was quite happy terminating browsers by way of SIGTERM so I played around some more with xdotool in dom0. These invocations do seem to work consistently, at least for me, at least at present:

[dom0]$ xdotool search --onlyvisible --class "${QUBE}.*Firefox" windowfocus --sync '%1' key --window '%1' --clearmodifiers ctrl+q
[dom0]$ xdotool search --onlyvisible --class "${QUBE}.*Tor Browser" windowfocus --sync '%1' key --window '%1' --clearmodifiers ctrl+q
[dom0]$ xdotool search --onlyvisible --class "${QUBE}.*Google-chrome" windowfocus --sync '%1' key --window '%1' --clearmodifiers alt+f x

Notes:

  • These commands only succeed when executed viewing a virtual desktop on which at least one matching window appears. I think you could workaround this limitation but it’s good enough for my workflow.
  • --onlyvisible circumvents this unknown X error:
  Major opcode of failed request:  42 (X_SetInputFocus)
  Serial number of failed request:  4866
  Current serial number in output stream:  4867
  • Without windowfocus, the keypresses are (sometimes) ignored. For me it works (consistently?) without the --sync '%1' but that seems a sensible addition.
  • For Firefox, "${QUBE}.*Mozilla Firefox" will not match on --class.
  • For Chrome, "${QUBE}.*Google Chrome" will not match on --class.
  • If you use these in a script, the value of $QUBE should probably be regex-escaped
  • xdotool is pretty finicky, and not just because X is finicky. It’s a useful tool but buggy.
1 Like

A few more notes.


Terminating shells cleanly:

I suspect the simultaneous SIGHUPs could lead to competing writes to $HISTFILE, so I do this a little differently now:

qvm-run -u root --pass-io $QUBE 'for pid in $(pgrep -x "\<(bash|zsh)\>") ; do [ "$pid" ] && [ $pid != $ ] && kill -s HUP $pid ; sleep 0.1 ; done' || true
  • The $pid != $ check is to prevent HUP of the shell running the scriptlet before it’s done HUP’ing the other shells

Terminating Emacs:

If you launch the Emacs server process in your Emacs session: M-x server-start you can then script a clean shutdown like this:

emacsclient -e '(save-buffers-kill-terminal)'

You may want to check for modified buffers first, something like:

#!/bin/bash

#...

maybe_dirty_str="$(emacsclient -e '
  (when-let ((dirty-buffers
              (cl-loop for b in (buffer-list)
                      when (and (buffer-file-name b)
                                (buffer-modified-p b))
                      collect (buffer-name b))))
    (format "Dirty buffers:%s"
            (seq-reduce (lambda (a b) (concat a "\n- " b))
                        dirty-buffers
                        "")))
')"

if [ -z "$maybe_dirty_str" ] ; then
  echo "Unexpected empty result from Emacs" >&2
  exit 1
elif [ "$maybe_dirty_str" != 'nil' ] ; then
  # Have a list of dirty buffers
  echo -e "$maybe_dirty_str" >&2
  exit 1
else
  # OK
  :
fi

Terminating tmux:

Pretty simple, just

if tmux has-session >/dev/null 2>&1 ; then
  tmux kill-session
fi

However, I think this kills child shells with SIGTERM rather than SIGHUP, so you may want to terminate them yourself first. You can get the list of running commands and pids like this (and then process how you wish):

$ tmux list-panes -a -F '#{pane_current_command} #{pane_pid}'
emacs 665
bash 4174
man 7484
bash 7536
bash 7959
bash 15459
bash 16466
tmux 19859

Scheduling VM shutdown inside the VM:

/sbin/shutdown accepts a -t option to schedule a shutdown in the near future but the granularity is in minutes, i.e. /sbin/shutdown -t +1 means one minute from now. If you want to schedule a shutdown at a smaller granularity you can hack it together like this:

#!/bin/bash

#...

temp_script="${XDG_RUNTIME_DIR:-/tmp}/shutdown.sh"
echo '#!/bin/sh

sleep 3.0
sudo /sbin/shutdown now
' > "$temp_script"
chmod u+x "$temp_script"
setsid "$temp_script" >/dev/null 2>&1 </dev/null &
disown %1

# Then do whatever self-cleanup with your remaining 3 secs.
  • The setsid and disown are to prevent the shutdown from being interrupted in the case that a parent or the process group gets signaled.