ref:a8c9585c538b23150d306cb7c8ead258754bb100

fix(packaging): start sunshine-qemu@ units for VM names with dashes on systemd 255

Found by running the units for real: with the escaped instance name in StateDirectory=sunshine-qemu/%i (sq\x2de2e\x2dhook for the libvirt domain sq-e2e-hook), systemd 255's executor exits before running anything ('Failed to deserialize: Invalid argument'), so the unit never started for any VM whose name contains '-' or a space. systemd-analyze verify accepts it. Bisected to StateDirectory=; the units now use the shared parent as StateDirectory and create the per-VM directory, named after the VM itself (%I), in ExecStartPre. The state is in /var/lib/sunshine-qemu/<vm>, which also simplifies the install steps. Verified on Ubuntu 24.04 (systemd 255, libvirt 10.0): the libvirt hook starts sunshine-qemu@sq\x2de2e\x2dhook.service when the domain starts and stops it when the domain stops; the unit runs as sunshine-qemu in its sandbox, opens the p2p display through libvirt and the headless client streams the guest pattern. The packaging test also accepts shellcheck 0.9's SC2317. Refs #6 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BPNw4PCgkEfhyCjQT19wsb
SHA: a8c9585c538b23150d306cb7c8ead258754bb100
Author: Cole Christensen <cole.christensen@gmail.com>
Date: 2026-09-13 10:45
Parents: 9cc1e1a
4 files changed +30 -17
Type
docs/qemu.md +6 −7
@@ -149,17 +149,16 @@
```bash
vm=win11
instance="$(systemd-escape "${vm}")" # names with '-' or spaces are escaped in unit names and state paths
sudo install -Dm640 -g sunshine-qemu packaging/linux/qemu/example.conf "/etc/sunshine-qemu/${vm}.conf"
# edit /etc/sunshine-qemu/${vm}.conf (qemu_dbus_address, sunshine_name), then pick its base port and check:
sudo /usr/lib/sunshine-qemu/sunshine-qemu-port --dir /etc/sunshine-qemu --write "${vm}"
sudo /usr/lib/sunshine-qemu/sunshine-qemu-port --dir /etc/sunshine-qemu --check
# the state directory the unit uses, created now so the web UI credentials can be set before the first start
sudo install -d -o sunshine-qemu -g sunshine-qemu -m 0750 /var/lib/sunshine-qemu "/var/lib/sunshine-qemu/${instance}"
sudo install -d -o sunshine-qemu -g sunshine-qemu -m 0750 /var/lib/sunshine-qemu "/var/lib/sunshine-qemu/${vm}"
sudo -u sunshine-qemu CONFIGURATION_DIRECTORY="/var/lib/sunshine-qemu/${vm}" \
sudo -u sunshine-qemu CONFIGURATION_DIRECTORY="/var/lib/sunshine-qemu/${instance}" \
sunshine "/etc/sunshine-qemu/${vm}.conf" --creds <user> <password>
sudo systemctl daemon-reload
sudo systemctl enable --now "$(systemd-escape --template sunshine-qemu@.service "${vm}")" # unit names escape '-' and spaces
sudo systemctl enable --now "$(systemd-escape --template sunshine-qemu@.service "${vm}")"
```
Set the credentials before the unit first starts: until then the web UI (on `port + 1`) accepts whoever sets them
@@ -169,6 +168,6 @@
web UI fails; edit `/etc/sunshine-qemu/<vm>.conf` as root and restart the unit.
The unit runs `sunshine /etc/sunshine-qemu/<vm>.conf` as `sunshine-qemu` with its state in
`/var/lib/sunshine-qemu/<escaped vm>` (see `systemd-escape` above), restarts it on failure, and has `After=` on libvirt and Avahi. It uses
`/var/lib/sunshine-qemu/<vm>`, restarts it on failure, and has `After=` on libvirt and Avahi. It uses
`PrivateTmp=yes` and `ProtectHome=yes`, so keep sockets Sunshine connects to under `/run`, not `/tmp` or home
directories.
@@ -272,7 +271,7 @@
### Hook
`libvirt-hook-qemu` starts `sunshine-qemu@<domain>.service` (the domain name escaped with `systemd-escape`, so its
state is in `/var/lib/sunshine-qemu/$(systemd-escape <domain>)`) when a domain starts (and when libvirt reconnects to
`libvirt-hook-qemu` starts `sunshine-qemu@<domain>.service` (the domain name escaped with `systemd-escape`; its
state is in `/var/lib/sunshine-qemu/<domain>`) when a domain starts (and when libvirt reconnects to
a running domain) and stops it when the domain stops. It only acts on domains that have `/etc/sunshine-qemu/<domain>.conf`,
never waits for Sunshine and never fails the domain operation.
packaging/linux/qemu/sunshine-qemu@.service +9 −4
@@ -23,13 +23,18 @@
# log_path) and everything Sunshine keeps in its own directory land in /var/lib/sunshine-qemu/<vm>/sunshine.
# CONFIGURATION_DIRECTORY moves only Sunshine's directory; XDG_CONFIG_HOME would also move the
# configuration of libvirt and the other libraries Sunshine and virsh load.
# StateDirectory= only names the shared parent. With the escaped instance name in it (%i, for example
# win\x2d11 for the VM "win-11"), systemd 255's executor refuses to start the unit ("Failed to
# deserialize: Invalid argument"), so the per-VM directory, named after the VM itself (%I), is created
# by ExecStartPre instead.
StateDirectory=sunshine-qemu/%i
StateDirectory=sunshine-qemu
StateDirectoryMode=0750
WorkingDirectory=%S/sunshine-qemu
WorkingDirectory=%S/sunshine-qemu/%i
Environment=CONFIGURATION_DIRECTORY=%S/sunshine-qemu/%i
Environment="CONFIGURATION_DIRECTORY=%S/sunshine-qemu/%I"
# ProtectSystem=strict leaves only the state directory writable, so caches (for example the GPU
# driver's shader cache under $HOME) go there too.
Environment=HOME=%S/sunshine-qemu/%i
Environment="HOME=%S/sunshine-qemu/%I"
ExecStartPre=/usr/bin/mkdir -p -m 0750 "%S/sunshine-qemu/%I"
# platf::init() verifies QEMU's display once at startup, so wait until the VM is up.
ExecStartPre=/usr/lib/sunshine-qemu/sunshine-qemu-wait-vm /etc/sunshine-qemu/%I.conf
# The config is read-only here (root-owned and ProtectSystem=strict): settings saved in the web UI
packaging/linux/qemu/user/sunshine-qemu@.service +8 −3
@@ -13,9 +13,14 @@
# Per-VM state in ~/.local/state/sunshine-qemu/<vm>/sunshine, so instances don't share ~/.config/sunshine.
# CONFIGURATION_DIRECTORY moves only Sunshine's directory: XDG_CONFIG_HOME would also be inherited by
# virsh and libvirt, and a session libvirt they start would look for your VMs in the wrong place.
StateDirectory=sunshine-qemu/%i
WorkingDirectory=%S/sunshine-qemu/%i
Environment=CONFIGURATION_DIRECTORY=%S/sunshine-qemu/%i
# StateDirectory= only names the shared parent. With the escaped instance name in it (%i, for example
# win\x2d11 for the VM "win-11"), systemd 255's executor refuses to start the unit ("Failed to
# deserialize: Invalid argument"), so the per-VM directory, named after the VM itself (%I), is created
# by ExecStartPre instead.
StateDirectory=sunshine-qemu
WorkingDirectory=%S/sunshine-qemu
Environment="CONFIGURATION_DIRECTORY=%S/sunshine-qemu/%I"
ExecStartPre=/usr/bin/mkdir -p -m 0750 "%S/sunshine-qemu/%I"
# platf::init() verifies QEMU's display once at startup, so wait until the VM is up.
ExecStartPre=/usr/lib/sunshine-qemu/sunshine-qemu-wait-vm %E/sunshine-qemu/%I.conf
ExecStart=/usr/bin/sunshine %E/sunshine-qemu/%I.conf
tests/packaging/linux/qemu/test_sunshine_qemu.sh +7 −3
@@ -11,7 +11,7 @@
# Environment:
# SUNSHINE_QEMU_PKG_REQUIRE_TOOLS 1 turns skips (systemd-analyze or shellcheck missing) into failures,
# for CI where the tools must be there
# shellcheck disable=SC2329 # test functions are called by name from the runner at the end
# shellcheck disable=SC2317,SC2329 # test functions are called by name from the runner at the end (SC2317 before shellcheck 0.10)
set -uo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -90,8 +90,12 @@
test_units_keep_instances_apart_and_wait_for_the_vm() {
local unit
for unit in "${pkg}/sunshine-qemu@.service" "${pkg}/user/sunshine-qemu@.service"; do
# an escaped instance name (%i) in StateDirectory= makes systemd 255's executor fail to start the unit
grep -qE '^StateDirectory=sunshine-qemu/%i$' "${unit}" || fail "$(basename "$(dirname "${unit}")")/$(basename "${unit}"): no per-instance StateDirectory" || return 1
grep -qE '^Environment=CONFIGURATION_DIRECTORY=%S/sunshine-qemu/%i$' "${unit}" || fail "${unit}: Sunshine's appdata isn't per instance" || return 1
# ("Failed to deserialize: Invalid argument"); systemd-analyze verify doesn't notice
grep -qE '^StateDirectory=sunshine-qemu$' "${unit}" || fail "$(basename "$(dirname "${unit}")")/$(basename "${unit}"): StateDirectory isn't the shared parent" || return 1
! grep -qE '^StateDirectory=.*%i' "${unit}" || fail "${unit}: StateDirectory uses the escaped instance name" || return 1
grep -qE '^ExecStartPre=/usr/bin/mkdir -p -m 0750 "%S/sunshine-qemu/%I"$' "${unit}" || fail "${unit}: doesn't create the per-VM state directory" || return 1
grep -qE '^Environment="CONFIGURATION_DIRECTORY=%S/sunshine-qemu/%I"$' "${unit}" || fail "${unit}: Sunshine's appdata isn't per instance" || return 1
# XDG_CONFIG_HOME would be inherited by virsh and libvirt too (a session libvirt they start would
# read its domains from the per-VM directory)
! grep -qE '^Environment=XDG_CONFIG_HOME=' "${unit}" || fail "${unit}: sets XDG_CONFIG_HOME for every child process" || return 1