Files
Clayton Craft 1b20c249cf systemd/postmarketos-base-systemd: fix preset template instance handling
This fixes a bug where template presets with a specified instance were
not being handled. The install script previously parsed the entire line,
including the instance(s) and ran 'systemctl preset' on it all, e.g.
'systemctl preset unudhcpd@.service usb0'.

The script will now:

- detect lines that contain a template + instance(s)
- diff the instances
- run preset on template@instance for all added or removed instances

Template presets that don't have any instances specified in the preset
file (because they have a DefaultInstance= set) are treated just like
any other unit since we can run preset on those and it enables the
template with the default instance.

Signed-off-by: Clayton Craft <craftyguy@postmarketos.org>
Part-of: <https://gitlab.postmarketos.org/postmarketOS/pmaports/-/merge_requests/9026>
2026-07-28 22:57:00 +00:00

177 lines
4.9 KiB
Bash

#!/bin/sh
system_preset_path="/usr/lib/systemd/system-preset/90-base.preset"
user_preset_path="/usr/lib/systemd/user-preset/90-base.preset"
# $1: path to file (without .old)
remove_old() {
rm "$1.old" 2>/dev/null || true
}
# https://www.freedesktop.org/software/systemd/man/latest/systemd.preset.html
# tldr: preset file format
# - discard line when:
# - empty line
# - first character is # or :
# - expect line to be
# - keyword + whitespace + unit name + [template unit directive]
# - keywords are: 'enable', 'disable', 'ignore'
# - unit name can contain shell-style wild-cards
# - unit name must match to a real file
# - when template unit directives are defined:
# - doesn't match to DefaultInstance
# - template unit gets expanded by directives
# - example: `enable dirsrv@.service foo bar baz` equivalent to:
# `systemctl preset dirsrv@foo.service`
# `systemctl preset dirsrv@bar.service`
# `systemctl preset dirsrv@baz.service`
# $1: path to preset
units_affected_by_preset() {
# https://stackoverflow.com/questions/4198138/printing-everything-except-the-first-field-with-awk
if [ ! -f "$1" ]; then
# If the file doesn't exist, then there's no policy
echo ""
return
fi
# "enable unit.service abc def" becomes "unit.service abc def"
cat "$1" |
busybox awk '$1 == "enable" || $1 == "disable" || $1 == "ignore"' |
busybox sed 's/(\|enable \|disable \|ignore )//' |
uniq |
sort
}
# $1: path to file (without .old)
units_affected_by_old_and_new_preset() {
# step one: get list of services in general
local new="$(units_affected_by_preset "$1")"
local old="$(units_affected_by_preset "$1.old")"
printf "$new\n$old" | sort | uniq
}
# $1: path to file
# $2: entry name
# returns: string oder exitcode
get_policy_for_unit() {
# printf '%q' isn't available in busybox
# so we hope only * as an wildcard is used for now
# FIXME: better shell escaping
local escaped="$(echo "$2" | sed 's/\*/\\\*/')"
if [ ! -f "$1" ]; then
# If the file doesn't exist, then there's no policy
echo ""
return
fi
cat $1 |
grep "$escaped"'$' |
awk '$1 == "enable" || $1 == "disable" || $1 == "ignore" {print $1}'
}
# $1: path to file
# $2: entry name
# returns: exitcode
policy_changed() {
# we don't have to care about the exitcode altough it's faster
# "" != "enable"
local new="$(get_policy_for_unit $1 "$2")"
local old="$(get_policy_for_unit $1.old "$2")"
[ "$new" != "$old" ]
}
# $1: takes system or user
check_and_apply_presets() {
local type="$1"
local preset_path=""
local extra_args=""
if [ "$type" = user ]; then
preset_path="$user_preset_path"
extra_args="--global"
else
preset_path="$system_preset_path"
type="system"
fi
local entries="$(units_affected_by_old_and_new_preset $preset_path)"
local old_ifs=$IFS
IFS=$'\n'
for entry in $entries; do
if ! policy_changed "$preset_path" "$entry"; then
continue
fi
echo "unit policy has changed for $entry"
if echo "$entry" | grep -qF '*'; then
echo " wildcards are not supported yet, skipping"
continue
fi
# Handle template units with instance arguments, e.g. "unudhcpd@.service usb0"
# See systemd.preset(5) "Enable multiple template instances"
case "$entry" in
*@.*)
# Only act if there are instances listed after the template name
instances="${entry#* }"
if [ "$instances" = "$entry" ]; then
# handle templates listed without instances in the preset just like any other
# unit, this assumes they have a DefaultInstance= set
break
fi
template="${entry%% *}"
if [ ! -f "/usr/lib/systemd/$type/$template" ]; then
echo " no unit file found, skipping"
continue
fi
# e.g. "unudhcpd@" and "service"
base="${template%.*}"
suffix="${template##*.}"
# extract instance lists from new and old preset files for diffing
new_instances=""
if [ -f "$preset_path" ]; then
new_instances="$(grep " $template " "$preset_path" | sed "s/.* $template //")"
fi
old_instances=""
if [ -f "$preset_path.old" ]; then
old_instances="$(grep " $template " "$preset_path.old" | sed "s/.* $template //")"
fi
# only preset instances that were added or removed should be preset
# note: IFS is changed to split on words, so we can pick up multiple instances
# that might be listed after the template
local saved_ifs="$IFS"
IFS=' '
for inst in $new_instances $old_instances; do
if echo " $new_instances " | grep -qF " $inst " && \
echo " $old_instances " | grep -qF " $inst "; then
continue
fi
systemctl --no-reload preset $extra_args "${base}${inst}.${suffix}"
done
IFS="$saved_ifs"
# done processing this line
continue
;;
esac
if [ -f "/usr/lib/systemd/$type/$entry" ]; then
systemctl --no-reload preset $extra_args "$entry"
else
echo " no unit file found, skipping"
fi
done
IFS="$old_ifs"
}
check_and_apply_presets system
check_and_apply_presets user
remove_old "$system_preset_path"
remove_old "$user_preset_path"