Skip to content

Instantly share code, notes, and snippets.

@UndeadDevel
Last active February 13, 2024 01:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save UndeadDevel/82e5f61c9a5065ab9fc5d23a74ae5045 to your computer and use it in GitHub Desktop.
Save UndeadDevel/82e5f61c9a5065ab9fc5d23a74ae5045 to your computer and use it in GitHub Desktop.
QubesOS DVM caching mechanism + easy screenshots, open file manager & terminal in focused qube etc.
#!/bin/bash
# counts the number of open windows of a qube that the user can actually interact with
# can be called with a qube name as argument; otherwise uses the name of the qube whose window is currently in focus
#
# interesting info from specifications.freedesktop.org/wm-spec/1.3/ar01s05.html
# uniquely set xprop properties of the actually displayed window, when testing an online firefox:
#_NET_WM_ICON_GEOMETRY (optional)
#_NET_FRAME_EXTENTS (mandatory)
#_NET_WM_ALLOWED_ACTIONS(mandatory)
#_NET_WM_STATE (mandatory, but can contain values, e.g. _NET_WM_STATE_ABOVE if window is always_on_top)
#WM_STATE (?? - probably an array with the values)
#_NET_WM_DESKTOP (optional)
ARG=$1 # qube name
if [ -z $ARG ];then
CUR_WIN_ID=$(xdotool getwindowfocus)
VM=$(xprop _QUBES_VMNAME -id $CUR_WIN_ID | cut -d \" -f 2)
else
VM=$ARG
fi
NUM_WIN=0
if [[ "$VM" != *"not found"* ]]; then
readarray WIN_ID_ARR < <(xdotool search --classname "^$VM:.+")
for id in "${WIN_ID_ARR[@]}"; do
if [[ $(xprop _NET_WM_ALLOWED_ACTIONS -id $id) != *"not found"* ]]; then
let NUM_WIN++
fi
done
if [ -z $ARG ];then
notify-send "Open '$VM' windows" "#: $NUM_WIN"
else
echo $NUM_WIN
fi
exit $NUM_WIN
fi
These three scripts together implement a QubesOS DVM caching mechanism,
allow easy screenshotting with the screenshot automatically copied to
the qube in focus,
launching a terminal or file manager in the qube in focus and
restarting qubes in focus, while also restarting their in-focus app and
moving it back to the workspace it was on!
#!/bin/bash
#
# runs commands in the qube whose window is currently in focus and implements a kind of DVM caching mechanism for lightening fast startup of a fresh DVM terminal or web browser
# note that the latter feature set requires the existence of a web-connected, named DVM called 'online' and a non-networked, named DVM called 'offline'
# requires Qubes 4.2+ (due to notify-send actions); not compatible with having a sys-gui unless significantly reworking the script
# doesn't require additional packages to be installed, but requires the 'count-open-windows-vm' and 'wait-for-and-hide-first-window' scripts to be on your PATH (e.g. ~/bin/ in dom0)!
# it makes sense, though it's not required, to put the following rules into an RPC file (e.g. use the Qubes Policy Editor and create a new file named '30-user'):
# qubes.OpenInVM * offline @dispvm ask default_target=@dispvm
# qubes.OpenInVM * @anyvm @dispvm ask default_target=offline
# qubes.OpenInVM * @anyvm @anyvm ask
# qubes.OpenURL * online @dispvm ask default_target=@dispvm
# qubes.OpenURL * @anyvm @dispvm ask default_target=online
# qubes.OpenURL * @anyvm @anyvm ask
#
# Observe the following caveats:
# 1a) Originally this script was implemented to automatically pause newly started cached DVMs ('online' and 'offline') after some seconds, but due to QubesOS issue #8572 (paused DVMs are not
# unpaused when using qvm-open-in-dvm) and what looks like a change in Xen behavior starting with QubesOS 4.2, where if a qube was paused for a while, unpausing it will make it useable
# only after several seconds, so not much faster than starting a new DVM, thus defeating the purpose of the caching mechanism, I decided to not pause 'offline' and 'online' anymore...
# This change can be undone by uncommenting the partially commented line 263 (in that case I recommend also uncommenting line 228)...this also means that a start-up script should be
# created that will pause those DVMs after launching them (see line 263 for a mechanism that makes sure they're not in use).
# 1b) An entry should be added to startup in any case, however, if the first start of the "insta-firefox" is supposed to be "insta"...use e.g. the following as the command:
# /usr/bin/sh -c 'qvm-start offline online && qvm-run online "firefox" & wait-for-and-hide-first-window online save'
# 1c) One possible experiment would be to run yet another script, which unpauses 'offline' and 'online' for a few seconds every 15 minutes or so to see if that fixes the new Xen issue...
# but it seems that not much CPU time is taken by 'offline' and 'online' in their unpaused status when nothing is running in them (except a firefox without active tabs for online)
# 2. Note also that with the current "insta-firefox" implementation it makes sense to map something like the following to a keyboard shortcut:
# sh -c 'qvm-unpause online & sleep 0.3 && ID=$(cat /tmp/.run-in-focused-vm-online-firefox-window-id); (xdotool key super+alt+Insert set_desktop --relative 1 windowmap --sync $ID windowstate --add MAXIMIZED_HORZ $ID windowstate --add MAXIMIZED_VERT $ID) || qvm-run online "firefox"'
# while also mapping "Add adjacent workspace" in the dom0 Window Manager to Super + Alt + Insert
# When opening a web URL in a disposable then, a dialog will pop up with 'online' pre-selected; simply press 'OK' and use the keyboard shortcut bound to the above command,
# which will create a new adjacent workspace, switch to it, open the insta-firefox in it, maximize it, and then open the link that was clicked on in that firefox.
# For slow typists the sleep command's numerical parameter should be increased.
# 3. Note that due to issue #8931 opening multiple documents of the same type in the same qube (such as in 'offline') with the intention of editing some of them can cause data loss.
# This can be fixed by following the instructions in that issue thread (see comment 4 by UndeadDevel for specific instructions).
#
# the intention behind this script is to have it be called via keyboard shortcuts set up in the dom0 "Keyboard" app, e.g. use:
# 'run-in-focused-vm shutdown' as the command for a keyboard shortcut (if this script is on your PATH); recommended keyboard shortcuts:
# Ctrl + Alt + Q shutdown (easy to remember, combines well with 'Ctrl + Q', which is a common shutdown shortcut for many programs)
# Ctrl + Alt + T terminal (easy to remember; beware that this launches the "insta-terminal" of the DVM 'offline' if dom0 is in focus while pressing the shortcut)
# Ctrl + Alt + K admin-terminal (sometimes the default shortcut for launching a terminal; it makes sense to set up another, e.g. 'Super + K', for the dom0 dropdown terminal)
# Ctrl + Alt + F filemanager (easy to remember; beware that this launches the "insta-firefox" instead of a file manager, if dom0 is in focus while pressing the shortcut)
# Super + Print screenshot (easy to remember; doesn't conflict with the other standard Xfce screenshot shortcuts)
#
# since this script (and the two scripts it depends on) need to be placed and executed in dom0, you should read through and verify these scripts before using them!
CUR_WIN_ID=$(xdotool getwindowfocus)
CUR_VM=$(xprop _QUBES_VMNAME -id $CUR_WIN_ID | cut -d \" -f 2)
# the following list of qube names and their associated "preferred apps" relates to AppVMs, which when shut down with this script can be restarted and will launch their PREF_APP upon restart
# while also moving that window back to the workspace of the window the shutdown command was called on; the default ('autodetect') will try to detect the kind of app that is shown in the
# window and restart it once the qube has restarted; in this case it will not only move it back to its workspace, but also try to restore the size and position of the window
# this section does not apply to the standard DVMs 'offline' and 'online' (which have their own restart mechanism) or unnamed DVMs (which are never restarted)
# modify according to your needs!
case "$CUR_VM" in
# "personal")
# PREF_APP="sh -c \"(exec nautilus) || (exec thunar)\""
# ;;
# "work")
# PREF_APP="soffice"
# ;;
# "vault")
# PREF_APP="keepassxc"
# ;;
"doc")
PREF_APP="xfce4-terminal --title=\"Jekyll Server\" --working-directory=\"/home/user/qubesos.github.io\" -x bash -c \"/home/user/bin/jekyll serve & firefox -offline\""
NUM_WINDOWS_APP=2 # optional parameter (default: 1) that specifies how many windows the above command is expected to open so the script will move all to their workspace
;;
*)
PREF_APP="autodetect" # this will make the script try to relaunch the app that was in focus when the shutdown command was issued
;;
esac
ARG=$1 # first command line argument to this script; usage: see below switch statement for standard options; otherwise any command
ARG2=$2 # usage: additional modifier(s) for select standard options (see quoted lower-case words being compared to $ARG2 and $ARG3 in below switch statement)
ARG3=$3 # usage: additional modifier(s) for select standard options (see quoted lower-case words being compared to $ARG2 and $ARG3 in below switch statement)
case "$ARG" in # main uses of this script; it makes sense to set up a keyboard shortcut each for calling this script with one of the following arguments
"filemanager")
CMD="sh -c \"(exec nautilus) || (exec thunar)\""
[[ "$ARG2" == "quiet" ]] && QUIET=true # don't push a notification about the command being executed
;;
"shutdown")
CMD="sleep 2 && poweroff"
if [[ "$ARG2" == "restart" ]] || [[ "$ARG3" == "restart" ]];then RESTART=true;fi # restart VM automatically after shutting down (not for unnamed DVMs or offline & online)
if [[ "$ARG2" == "force" ]] || [[ "$ARG3" == "force" ]];then FORCE=true;fi # does not wait for all windows to be closed or check for open files, just waits 2 seconds
if [[ "$ARG2" == "removews" ]] || [[ "$ARG3" == "removews" ]] || [[ "$4" == "removews" ]];then REMOVE_WS=true;fi # only for DVMs offline & online and unnamed DVMs;
CUR_WS=$(xdotool get_desktop_for_window $CUR_WIN_ID) # removes the workspace of the VM being shutdown if no windows of other VMs are on it
;;
"terminal")
CMD="sh -c \"(exec gnome-terminal) || (exec xfce4-terminal) || (exec konsole) || (exec xterm)\""
[[ "$ARG2" == "quiet" ]] && QUIET=true # don't push a notification about the command being executed
;;
"admin-terminal")
CMD="qvm-console-dispvm"
;;
"screenshot")
CMD="xfce4-screenshooter -r"
;;
*)
CMD=$ARG
;;
esac
# Functions
# called when restarting the standard DVM online only; used to switch the window currently in focus and all windows on other workspaces to "Always on Top" for a few seconds
# so there's no "popping" of the restarted firefox (there seems to be no other way to not have it "pop" briefly; using '-silent' has undesireable side effects and shortcomings, such as
# problems having the address field selected right away, having to deal with recovery as firefox thinks it crashed and startup time won't be instant)
# seems to require switching on 'focus stealing prevention' in Window Manager Tweaks app, "Focus" tab
switch_on_AOT() {
WIN_ID_ARR=()
CUR_FOCUS=$(xdotool getwindowfocus)
CUR_APP=$(xprop WM_CLASS -id $CUR_FOCUS | cut -d \" -f 2)
if [[ "$CUR_APP" != "xfdesktop" ]];then
FOCUS_DESKTOP=$(xdotool get_desktop_for_window $CUR_FOCUS)
if [[ $(xprop _NET_WM_STATE -id $CUR_FOCUS) == *"ABOVE"* ]];then # stickied windows will have _NET_WM_STATE(ATOM) = _NET_WM_STATE_ABOVE
HAD_ABOVE=true
else
HAD_ABOVE=false
xdotool windowstate --add ABOVE $CUR_FOCUS # add AOT to window in focus so that when the online firefox pops up it won't (briefly) cover that window
fi
else
HAD_ABOVE=true
FOCUS_DESKTOP=$(xdotool get_desktop)
fi
for ((i=$(xdotool get_num_desktops)-1; i>=0; i--));do
if [[ "$i" != "$FOCUS_DESKTOP" ]];then # we only force AOT for all windows on a WS outside the current workspace, as otherwise we introduce more issues than we solve
readarray OUTPUT < <(xdotool search --desktop $i --classname "") # further down the list in xdotool search means further "above"
WIN_ID_ARR+=("${OUTPUT[@]}")
fi
done
VALID_WIN_ARR=()
AOT_WIN_ARR=()
CMD=(xdotool)
for id in "${WIN_ID_ARR[@]}";do
if [[ $(xprop _NET_WM_ALLOWED_ACTIONS -id $id) != *"not found"* ]];then
if [[ $(xprop _NET_WM_STATE -id $id) == *"ABOVE"* ]];then
AOT_WIN_ARR+=("$id") # will have to toggle these twice so they end up on top again
else
VALID_WIN_ARR+=("$id")
fi
CMD+=( windowstate --toggle ABOVE $id ) # command chaining allows executing multiple tasks in the same process (much better performance)
fi
done
"${CMD[@]}"
CMD=(xdotool)
for id in "${AOT_WIN_ARR[@]}";do
CMD+=( windowstate --add ABOVE $id )
done
"${CMD[@]}"
}
# called once a new online DVM firefox has started and been hidden to restore prior AOT status
undo_switch_on_AOT() {
for id in "${VALID_WIN_ARR[@]}";do # don't chain commands here in case of failure (e.g. window was closed in meantime)
xdotool windowstate --remove ABOVE $id
done
[[ "$HAD_ABOVE" == "false" ]] && xdotool windowstate --remove ABOVE $CUR_FOCUS
}
remove_workspace() {
readarray WIN_ARR < <(xdotool search --desktop $CUR_WS --classname "")
NUM_WIN=0
for id in "${WIN_ARR[@]}"; do # don't remove a workspace where windows from qubes other than the one shutting down are located
if [[ "$(xprop _NET_WM_ALLOWED_ACTIONS -id $id)" != *"not found"* ]] && [[ "$(xprop _QUBES_VMNAME -id $id | cut -d \" -f 2)" != "$CUR_VM" ]]; then
NUM_WIN=1
break
fi
done
if [[ "$NUM_WIN" == "0" ]];then
CUR_WS_UPDATED=$(xdotool get_desktop)
NUM_DESKTOPS=$(xdotool get_num_desktops)
for ((i=CUR_WS+1; i<NUM_DESKTOPS; i++));do # xdotool aborts command chaining if the search command yields no results
if [ $i -eq $CUR_WS_UPDATED ];then
xdotool search --desktop $i --classname "" set_desktop_for_window %@ $((i-1)) set_desktop $((i-1))
else
xdotool search --desktop $i --classname "" set_desktop_for_window %@ $((i-1))
fi
done
xdotool set_num_desktops $((NUM_DESKTOPS-1))
fi
}
# don't shut down until all data has been pushed back to the origin qube...the command used here is fairly conservative, so in some cases it will keep the VM running needlessly, but
# that's why a 'force restart' button will also be shown; this function is only used for DVMs, as data loss can occur there if files weren't pushed back to their origin qube
wait_for_files() {
qvm-run -u root "$CUR_VM" -- bash -c \
'while [ -n "$(find /tmp/* -maxdepth 0 -type d -not -empty -not -name "ssh-XXXXX*" -not -name "systemd-private*" -not -name "jdtls-*" -not -name "LibreOffice_*" -not -name "hsperfdata_user" -not -name ".*" -print)" ];do rmdir --ignore-fail-on-non-empty -p /tmp/*/*/*/* || rmdir --ignore-fail-on-non-empty -p /tmp/*/*/* || rmdir -p /tmp/*/*;sleep 1;done;poweroff' &
i=0
while qvm-check --running "$CUR_VM";do
if [ $i -lt 10 ];then
((i++))
sleep 1
elif [ $i -eq 10 ];then
i=11
ID=$(notify-send -p -t 3000 "Open Documents?" "WARNING! There may be open documents in qube $CUR_VM!\nResolve the situation and/or $RESTART_STRING manually.")
else
ANSWER=$(notify-send -r $ID -u critical -A "Continue" -A R="Force $RESTART_STRING" -A O="Open relevant folder" \
"Open Documents?" "WARNING! There may be open documents in qube $CUR_VM!\nResolve the situation and/or $RESTART_STRING manually.")
if [[ "$ANSWER" == "R" ]];then
break
elif [[ "$ANSWER" == "O" ]];then
qvm-run "$CUR_VM" -- sh -c '(exec nautilus "/tmp/") || (exec thunar "/tmp/")' &
fi
fi
done
}
# Main logic
if [[ "$CUR_VM" == *"not found"* ]];then # dom0 has focus (e.g. a dom0 window has focus OR the active workspace is empty)
case "$ARG" in
"filemanager") # used for insta-firefox on an empty workspace; no need for yet another keyboard shortcut, as we never open thunar in dom0 anyway
ID=$(cat /tmp/.run-in-focused-vm-online-firefox-window-id)
qvm-unpause online
xdotool windowmap --sync $ID || qvm-run online -- firefox & # go-to disposable web browser
;;
"terminal") # the associated keyboard shortcut is only for domUs, while for dom0 terminals either the admin-terminal shortcut or another one (dropdown terminal) is used
qvm-run offline -- "$CMD" & # go-to disposable for offline activities
;;
"admin-terminal")
xfce4-terminal & # regular dom0 terminal
;;
"shutdown")
#qvm-unpause online offline # see issue #8580 (shutting down QubesOS takes a long time if some qubes were still paused); use if pausing DVMs (line 263)
xfce4-session-logout # displays the chooser with options for logout, suspend, reboot, shutdown
;;
"screenshot")
$CMD -s ~/Pictures # don't display the preview or chooser, as we either want to save it in dom0's Pictures (this command) or send it to another VM (see below)
;;
esac
elif [[ "$ARG" == "admin-terminal" ]];then # (for this and all below if-brackets) a domU is in focus
$CMD "$CUR_VM" & # qvm-console-dispvm, i.e. starts a disposable with shell access to the qube; you should log in as 'root'
elif [[ "$ARG" == "screenshot" ]];then
FILE=$($CMD -o echo)
if [[ -n $FILE ]];then # user may have aborted taking a screenshot
qvm-copy-to-vm "$CUR_VM" "$FILE"
ANSWER=$(notify-send -A O="Open destination folder" "Screenshot sent!" "Your selection has been sent as a screenshot to $CUR_VM!")
[[ "$ANSWER" == "O" ]] && qvm-run "$CUR_VM" -- sh -c '(exec nautilus "QubesIncoming/dom0/") || (exec thunar "QubesIncoming/dom0/")' &
fi
elif [[ "$ARG" == "shutdown" ]];then
if [[ "$CUR_VM" == "offline" || "$CUR_VM" == "online" ]];then # centerpiece of the DVM caching mechanism
notify-send "Restarting DVM $CUR_VM"
RESTART_STRING="restart"
[ -z $FORCE ] && wait_for_files
[[ -n "$REMOVE_WS" ]] && remove_workspace
if qvm-check --running "$CUR_VM";then
qvm-unpause "$CUR_VM"
qvm-shutdown --wait --timeout 5 "$CUR_VM"
fi
if [[ "$CUR_VM" == "online" ]];then
qvm-run online -- firefox &
switch_on_AOT # so the firefox doesn't pop into focus (at least if focus stealing prevention is on, which is recommended with QubesOS anyway)
wait-for-and-hide-first-window $CUR_VM save # hides the firefox shortly after it appears and saves the window ID to /tmp so we can unhide later
undo_switch_on_AOT # revert AOT changes
fi
qvm-start --skip-if-running "$CUR_VM" #&& sleep 5 && [[ $(count-open-windows-vm "$CUR_VM") == "0" ]] && qvm-pause "$CUR_VM" &
# unfortunately Xen does something to qubes that have been paused for longer that causes them to take a few seconds to "recover" from being paused
else
if ! [[ "$CUR_VM" =~ ^disp[0-9]{1,4}$ ]];then
if [[ "$PREF_APP" == "autodetect" ]];then
PREF_APP=$(xprop WM_CLASS -id $CUR_WIN_ID | cut -d \" -f 4 | cut -d : -f 2 | { read V; echo "${V,,}"; })
eval $(xdotool getwindowgeometry --shell $CUR_WIN_ID) # currently ignores window titlebar and border due to issue #176 in github.com/jordansissel/xdotool
STRING="last" # wmctrl has same problem; xwininfo could be used to fix, but not distributed in QubesOS dom0
else
STRING="preferred"
fi
fi
ID=987654321 # needs to have some value; the below notify-send command makes sure to always display only one notification, but continuously updates it
if [ -z $FORCE ];then
if [[ -n "$STRING" ]];then
NUM_WIN=$(count-open-windows-vm "$CUR_VM")
while [[ "$NUM_WIN" != "0" ]];do
ID=$(notify-send -p -r $ID "Shutdown command received for $CUR_VM" "Waiting for all windows of that qube ($NUM_WIN) to be closed...")
sleep 1
NUM_WIN=$(count-open-windows-vm "$CUR_VM")
done
else
notify-send "Shutdown command received for $CUR_VM" "Close the window of the app you started this DVM with to have it shut down."
fi
fi
if [[ -n "$STRING" ]];then
qvm-run -u root "$CUR_VM" -- "$CMD" &
if [ -z $RESTART ];then
ANSWER=$(notify-send -r $ID -A R="Restart with $STRING app!" "Shutting down $CUR_VM")
else
notify-send -r $ID "Restarting $CUR_VM"
fi
if [[ -n "$RESTART" ]] || [[ "$ANSWER" == "R" ]];then
qvm-shutdown --wait --dry-run "$CUR_VM" && qvm-run "$CUR_VM" -- "$PREF_APP" &
[ -z $NUM_WINDOWS_APP ] && NUM_WINDOWS_APP=1
for ((i=0; i<NUM_WINDOWS_APP; i++));do
NEW_ID=$(wait-for-and-hide-first-window $CUR_VM nohide)
xdotool set_desktop_for_window $NEW_ID $CUR_WS
done
[[ "$STRING" == "last" ]] && xdotool windowsize --sync $NEW_ID $WIDTH $HEIGHT windowmove $NEW_ID $X $Y
fi
else # unnamed disposable
RESTART_STRING="shutdown"
[ -z $FORCE ] && wait_for_files
[[ -n "$REMOVE_WS" ]] && remove_workspace
if qvm-check --running "$CUR_VM";then
notify-send "Shutting down $CUR_VM"
qvm-shutdown --wait --timeout 5 "$CUR_VM"
fi
fi
fi
else
[ -z $QUIET ] && notify-send "Running command in $CUR_VM" "$CMD"
qvm-run "$CUR_VM" -- "$CMD" &
fi
#!/bin/bash
# waits for a new window of a qube that's starting to pop up and then hides it (unless 'nohide' is specified)
# returns the windows ID of that window (or stores it in a tmp file if 'save' is specified)
# the first argument must be the name of the qube
VM=$1 # qube name
if [[ "$2" == "save" ]] || [[ "$3" == "save" ]];then SAVE=true;fi # indicates that we want the script to store the new window id in a tmp file
if [[ "$2" == "nohide" ]] || [[ "$3" == "nohide" ]];then NOHIDE=true;fi # indicates that the found window shouldn't be hidden after it's detected
readarray WIN_ID_ARR < <(xdotool search --sync --onlyvisible --classname "^$VM:.+") # will wait until the first window of that VM pops up
for id in "${WIN_ID_ARR[@]}"; do
if [[ $(xprop _NET_WM_ALLOWED_ACTIONS -id $id) != *"not found"* ]]; then
NEW_ID=$id
break
fi
done
[ -z $NOHIDE ] && xdotool windowunmap --sync $NEW_ID
if [ -z $SAVE ];then
echo $NEW_ID
else
echo "$NEW_ID" > /tmp/.run-in-focused-vm-online-firefox-window-id
fi
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment