@@ -1,0 +1,187 @@
#!/usr/bin/env bash
# Check that the VM renders at the size the client streams at, and gets its own size back afterwards.
#
# The guest boots at one size, a client streams at another, and this asserts three things: the guest's
# display really changed (xrandr inside it and QEMU's own framebuffer agree), the client decoded frames
# at that size with nothing scaled, and the guest is back to its own size once streaming stops.
#
# The guest needs something that applies its preferred mode; build_desktop.sh installs
# sq-follow-resolution for that, as GNOME and KDE do it themselves.
#
# Usage: follow_resolution.sh
#
# Environment:
# SUNSHINE_BIN Sunshine binary (default: <repo>/cmake-build-debug/sunshine)
# QEMU QEMU binary (default: qemu-system-x86_64)
# DESKTOP_CACHE desktop guest image (default: ~/.cache/sunshine-qemu/desktop)
# GUEST_WIDTH/GUEST_HEIGHT size the guest boots at (default: 1920x1080)
# CLIENT_WIDTH/CLIENT_HEIGHT size the client asks for (default: 1600x900)
# PORT Sunshine base port (default: 48489)
# RESIZE_ARTIFACTS logs and frames (default: <repo>/ci-artifacts/follow-resolution)
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo="$(cd "${script_dir}/../../../.." && pwd)"
sunshine_bin="${SUNSHINE_BIN:-${repo}/cmake-build-debug/sunshine}"
client_bin="${repo}/cmake-build-e2e-client/moonlight_e2e_client"
desktop_cache="${DESKTOP_CACHE:-${HOME}/.cache/sunshine-qemu/desktop}"
guest_width="${GUEST_WIDTH:-1920}"
guest_height="${GUEST_HEIGHT:-1080}"
client_width="${CLIENT_WIDTH:-1600}"
client_height="${CLIENT_HEIGHT:-900}"
port="${PORT:-48489}"
artifacts="${RESIZE_ARTIFACTS:-${repo}/ci-artifacts/follow-resolution}"
[[ -x "${sunshine_bin}" ]] || { echo "resize: missing ${sunshine_bin}" >&2; exit 2; }
[[ -x "${client_bin}" ]] || { echo "resize: missing ${client_bin}" >&2; exit 2; }
if [[ ! -f "${desktop_cache}/desktop-base.qcow2" ]]; then
echo "resize: SKIPPED: no desktop guest image; run ${script_dir}/build_desktop.sh" >&2
exit 77
fi
rm -rf "${artifacts}"
mkdir -p "${artifacts}"
work="$(mktemp -d "${TMPDIR:-/tmp}/sunshine-resize.XXXXXX")"
sunshine_pid=""
failure=""
cleanup() {
set +e
[[ -n "${sunshine_pid}" ]] && kill "${sunshine_pid}" 2> /dev/null
for pidfile in "${work}/vm/qemu.pid" "${work}/vm/dbus.pid"; do
[[ -f "${pidfile}" ]] && kill "$(cat "${pidfile}")" 2> /dev/null
done
cp "${work}/sunshine.log" "${artifacts}/" 2> /dev/null
rm -rf "${work}"
}
trap cleanup EXIT INT TERM
fail() {
echo "resize: FAIL: $*" >&2
failure="$*"
}
echo "resize: booting the guest at ${guest_width}x${guest_height}"
bus_address="$(SNAPSHOT=on "${script_dir}/run_desktop.sh" "${work}/vm" "${guest_width}" "${guest_height}" 2> "${artifacts}/boot.log")"
ssh_port="$(cat "${work}/vm/ssh.port")"
guest() {
ssh -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=20 \
-i "${desktop_cache}/ssh_key" -p "${ssh_port}" sq@127.0.0.1 "$@"
}
# what the guest thinks its screen is, as "<width>x<height>"
guest_size() {
guest 'DISPLAY=:0 xrandr 2>/dev/null | sed -n "s/^Screen 0:.*current \([0-9]*\) x \([0-9]*\),.*/\1x\2/p"' 2> /dev/null
}
for _ in $(seq 1 60); do
[[ -n "$(guest_size)" ]] && break
sleep 2
done
# an image built before the agent existed doesn't have it; a rebuilt one starts it from XDG autostart
if ! guest 'pgrep -f '[s]q-follow-resolution' > /dev/null' 2> /dev/null; then
echo "resize: the guest has no display agent running; installing it for this run"
scp -q -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR \
-i "${desktop_cache}/ssh_key" -P "${ssh_port}" "${script_dir}/sq-follow-resolution" sq@127.0.0.1:/tmp/
# systemd-run returns as soon as the unit starts; backgrounding over SSH holds the channel open instead
# shellcheck disable=SC2016 # $(id -u) is for the guest's shell, not this one
guest 'sudo install -m755 /tmp/sq-follow-resolution /usr/local/bin/ && XDG_RUNTIME_DIR=/run/user/$(id -u) systemd-run --user --unit=sq-follow-resolution --setenv=DISPLAY=:0 /usr/local/bin/sq-follow-resolution' > /dev/null 2>&1
sleep 3
guest 'pgrep -f '[s]q-follow-resolution' > /dev/null' 2> /dev/null || fail "the display agent didn't start in the guest"
fi
before="$(guest_size)"
echo "resize: the guest starts at ${before}"
[[ "${before}" == "${guest_width}x${guest_height}" ]] || fail "the guest booted at ${before}, not ${guest_width}x${guest_height}"
cat > "${work}/apps.json" <<'EOF'
{"env": {}, "apps": [{"name": "Desktop", "image-path": "desktop.png"}]}
EOF
cat > "${work}/sunshine.conf" <<EOF
capture = qemu
qemu_dbus_address = ${bus_address}
encoder = software
port = ${port}
sunshine_name = sunshine-resize-${port}
file_state = ${work}/sunshine_state.json
credentials_file = ${work}/sunshine_credentials.json
pkey = ${work}/sunshine.key
cert = ${work}/sunshine.crt
file_apps = ${work}/apps.json
log_path = ${work}/sunshine.log
min_log_level = info
upnp = disabled
system_tray = disabled
origin_web_ui_allowed = pc
notify_pre_releases = disabled
EOF
api_user="e2e"
api_pass="$(head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n')"
"${sunshine_bin}" "${work}/sunshine.conf" --creds "${api_user}" "${api_pass}" > "${work}/creds.log" 2>&1
"${sunshine_bin}" "${work}/sunshine.conf" > "${work}/sunshine.stdout" 2>&1 &
sunshine_pid=$!
for _ in $(seq 1 300); do
kill -0 "${sunshine_pid}" 2> /dev/null || { echo "resize: Sunshine exited early" >&2; tail -20 "${work}/sunshine.stdout" >&2; exit 1; }
curl -s --max-time 1 "http://127.0.0.1:${port}/serverinfo" | grep -q 'status_code="200"' && break
sleep 0.1
done
echo "resize: streaming at ${client_width}x${client_height}"
"${client_bin}" --port "${port}" --api-user "${api_user}" --api-pass "${api_pass}" --state-dir "${work}/client" \
--width "${client_width}" --height "${client_height}" --fps 30 --bitrate 20000 --frames 600 --timeout 120 \
--out "${artifacts}/frame.ppm" --summary "${artifacts}/summary.json" > "${artifacts}/client.log" 2>&1 &
client_pid=$!
during=""
for _ in $(seq 1 30); do
during="$(guest_size)"
[[ "${during}" == "${client_width}x${client_height}" ]] && break
sleep 2
done
echo "resize: during the stream the guest is at ${during}"
[[ "${during}" == "${client_width}x${client_height}" ]] || fail "the guest stayed at ${during} while the client streamed ${client_width}x${client_height}"
# QEMU's own framebuffer, so this doesn't rest on what the guest reports
if [[ -S "${work}/vm/qmp.sock" ]]; then
python3 - "${work}/vm/qmp.sock" "${artifacts}/framebuffer.ppm" <<'PY' > /dev/null 2>&1 || true
import json, socket, sys
s = socket.socket(socket.AF_UNIX)
s.connect(sys.argv[1])
f = s.makefile('rw')
f.readline()
def cmd(c):
f.write(json.dumps(c) + "\n")
f.flush()
while True:
line = json.loads(f.readline())
if 'return' in line or 'error' in line:
return line
cmd({"execute": "qmp_capabilities"})
cmd({"execute": "screendump", "arguments": {"filename": sys.argv[2]}})
PY
if [[ -f "${artifacts}/framebuffer.ppm" ]]; then
fb="$(head -2 "${artifacts}/framebuffer.ppm" | tr '\n' ' ' | awk '{print $2 "x" $3}')"
echo "resize: QEMU's framebuffer is ${fb}"
[[ "${fb}" == "${client_width}x${client_height}" ]] || fail "QEMU renders ${fb}, not ${client_width}x${client_height}"
fi
fi
wait "${client_pid}" || fail "the client failed; see ${artifacts}/client.log"
frame="$(head -2 "${artifacts}/frame.ppm" 2> /dev/null | tr '\n' ' ' | awk '{print $2 "x" $3}')"
echo "resize: the client decoded ${frame}"
[[ "${frame}" == "${client_width}x${client_height}" ]] || fail "the client decoded ${frame}"
after=""
for _ in $(seq 1 30); do
after="$(guest_size)"
[[ "${after}" == "${before}" ]] && break
sleep 2
done
echo "resize: after the stream the guest is at ${after}"
[[ "${after}" == "${before}" ]] || fail "the guest was left at ${after} instead of its own ${before}"
if [[ -n "${failure}" ]]; then
exit 1
fi
echo "resize: PASS: the guest followed the client to ${client_width}x${client_height} and went back to ${before}"