#!/bin/bash # Invoked only by the package's Remove/PreTransaction ALPM hook. set -eu # systemd's configuration parser throws the whitespace around an assignment away # (parse_line() runs both halves through strstrip()), so a drop-in written as # ExecStart = /home/alice/build/omawake daemon # picks the executable just as surely as the unspaced form does. readonly execstart_assignment='^[[:space:]]*ExecStart[[:space:]]*=' owned_unit() { local commands [[ -f $1 && ! -L $1 ]] || return 1 # Only a unit in the very shape the application generates is ever deleted; # anything else, however it is spaced, stays somebody's own file. commands=$(grep '^ExecStart=' "$1") || return 1 [[ $commands != *$'\n'* ]] || return 1 grep -Eq "^ExecStart=\"?/usr/bin/$2\"?([[:space:]]|$)" <<< "$commands" } # Decode systemd's shell_maybe_quote() output, including cescape_char() escapes. # Never evaluate manager-controlled values as shell syntax. unquote_manager_value() { local text=$1 decoded= character octal if [[ $text != '$'* ]]; then unquoted_value=$text return 0 fi [[ $text == \$\'*\' ]] || return 1 text=${text:2:${#text}-3} while [[ -n $text ]]; do if [[ ${text:0:1} != \\ ]]; then decoded+=${text:0:1} text=${text:1} continue fi case ${text:1:1} in \\ | "'") decoded+=${text:1:1} ;; a | b | f | n | r | t | v) printf -v character '%b' "\\${text:1:1}" decoded+=$character ;; [0-3]) octal=${text:1:3} [[ $octal =~ ^[0-3][0-7]{2}$ && $octal != 000 ]] || return 1 printf -v character '%b' "\\0$octal" decoded+=$character text=${text:4} continue ;; *) return 1 ;; esac text=${text:2} done unquoted_value=$decoded } remove_for_user() { local app=$1 user_home=$2 runtime=$3 config="$2/.config" unit="$1.service" local online=false effective path target manager_environment load_state if [[ -S $runtime/bus || -S $runtime/systemd/private ]]; then online=true # Do not evaluate shell syntax from a user manager's environment. manager_environment=$(systemctl --user show-environment) || return 1 while IFS= read -r line; do case $line in XDG_CONFIG_HOME=*) if ! unquote_manager_value "${line#XDG_CONFIG_HOME=}"; then echo ":: Cannot tell which directory $user_home's manager reads $unit from; removal aborted." >&2 return 1 fi # An XDG_CONFIG_HOME that is not an absolute path is no setting at all: # the manager itself falls back to the home's .config directory then. if [[ $unquoted_value == /* ]]; then config=$unquoted_value; fi ;; esac done <<< "$manager_environment" effective=$(systemctl --user show "$unit" --property=ExecStart --value) || return 1 if [[ -n $effective && $effective != *"path=/usr/bin/$app ;"* ]]; then echo ":: Preserving $unit for $user_home: it uses another executable." return 0 fi fi path="$config/systemd/user/$unit" if ! $online && grep -qs "$execstart_assignment" "$path.d/"*.conf; then echo ":: Preserving offline service with an executable override: $path." return 0 fi if [[ -e $path || -L $path ]]; then if ! owned_unit "$path" "$app"; then echo ":: Preserving custom or masked unit $path." return 0 fi fi if $online; then # Stop first, and fail the package transaction if stopping fails. load_state=$(systemctl --user show "$unit" --property=LoadState --value) || return 1 if [[ $load_state != not-found ]]; then systemctl --user stop "$unit" || return 1 # A unit that had failed stays failed once it is stopped, which is the one # state systemd will reset. For every other state it answers that the unit # is not loaded and exits non-zero, so the reset is asked for only where it # applies: a healthy unit leaving the transaction never aborts a removal. if [[ $(systemctl --user show "$unit" --property=ActiveState --value) == failed ]]; then systemctl --user reset-failed "$unit" || return 1 fi # Disabling also removes enablement links outside the normal target. systemctl --user disable "$unit" || return 1 fi fi # Offline users have no bus. Remove only exact enablement links for this unit. # All filesystem operations run as the owning user, never as pacman's root. for target in "$config/systemd/user/"*.wants/"$unit" "$config/systemd/user/"*.requires/"$unit"; do [[ -L $target ]] || continue case $(realpath -m -- "$target") in "$path"|"/usr/lib/systemd/user/$unit") rm -- "$target" ;; esac done if owned_unit "$path" "$app"; then rm -- "$path"; fi if $online; then systemctl --user daemon-reload || return 1; fi } if [[ ${1-} == --user ]]; then shift case ${1-} in omawake|omaspeak) ;; *) exit 2 ;; esac [[ $# == 3 ]] || exit 2 remove_for_user "$@" exit fi [[ $# == 1 ]] || exit 2 case $1 in omawake|omaspeak) ;; *) exit 2 ;; esac app=$1 result=0 # Include logged-out users as well as active/lingering user managers. accounts=$(getent passwd) || exit 1 while IFS=: read -r account _ user_id _ _ user_home _; do [[ $user_home == /* ]] || continue runtime="/run/user/$user_id" if [[ ! -d $user_home/.config/systemd/user && ! -S $runtime/bus && ! -S $runtime/systemd/private ]]; then continue fi if ! runuser -u "$account" -- env -u XDG_CONFIG_HOME \ XDG_RUNTIME_DIR="$runtime" DBUS_SESSION_BUS_ADDRESS="unix:path=$runtime/bus" \ "$0" --user "$app" "$user_home" "$runtime"; then echo ":: Could not clean up $app for $account; removal aborted. Stop/remove the user service and retry." >&2 result=1 fi done <<< "$accounts" exit "$result"