@@ -1,0 +1,139 @@
#!/usr/bin/env bash
# Serve several VMs from one Sunshine, each as an app in Moonlight's list.
#
# Boots the desktop guest and, when it's built, the SteamOS-style guest, then starts one Sunshine that
# offers both. A client pairs once and picks which VM to stream; one streams at a time.
#
# Usage: serve_apps.sh
#
# Environment:
# SUNSHINE_BIN Sunshine binary (default: <repo>/cmake-build-release-cuda/sunshine)
# QEMU QEMU binary (default: qemu-system-x86_64)
# SERVE_DIR Sunshine state, logs and the web UI password (default: ~/.local/state/sunshine-qemu/apps)
# PORT Sunshine base port (default: 47989, what Moonlight expects)
# ENCODER nvenc, vaapi, vulkan or software (default: nvenc)
# GUEST_WIDTH/GUEST_HEIGHT size the guests boot at (default: 1920x1080)
# SERVE_EXIT_AFTER seconds to run before shutting down, for automated checks
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo="$(cd "${script_dir}/../../.." && pwd)"
sunshine_bin="${SUNSHINE_BIN:-${repo}/cmake-build-release-cuda/sunshine}"
serve_dir="${SERVE_DIR:-${HOME}/.local/state/sunshine-qemu/apps}"
port="${PORT:-47989}"
width="${GUEST_WIDTH:-1920}"
height="${GUEST_HEIGHT:-1080}"
[[ -x "${sunshine_bin}" ]] || { echo "missing ${sunshine_bin}; build Sunshine first" >&2; exit 2; }
mkdir -p "${serve_dir}"
chmod 700 "${serve_dir}"
work="$(mktemp -d "${TMPDIR:-/tmp}/sunshine-apps.XXXXXX")"
sunshine_pid=""
declare -a vm_pids=()
cleanup() {
set +e
echo
echo "stopping"
[[ -n "${sunshine_pid}" ]] && kill "${sunshine_pid}" 2> /dev/null
for pid in "${vm_pids[@]}"; do
kill "${pid}" 2> /dev/null
done
rm -rf "${work}"
}
trap cleanup EXIT INT TERM
remember_vm() {
local dir="$1"
for pidfile in "${dir}/qemu.pid" "${dir}/dbus.pid"; do
[[ -f "${pidfile}" ]] && vm_pids+=("$(cat "${pidfile}")")
done
}
apps='{"env": {}, "apps": ['
separator=''
# the desktop guest keeps its changes between runs, like the machine someone actually uses
desktop_cache="${DESKTOP_CACHE:-${HOME}/.cache/sunshine-qemu/desktop}"
if [[ -f "${desktop_cache}/desktop-base.qcow2" ]]; then
overlay="${serve_dir}/desktop.qcow2"
[[ -f "${overlay}" ]] || qemu-img create -q -f qcow2 -F qcow2 -b "${desktop_cache}/desktop-base.qcow2" "${overlay}"
echo "booting the desktop guest..."
desktop_bus="$(SNAPSHOT=off DESKTOP_IMAGE="${overlay}" "${script_dir}/desktop/run_desktop.sh" "${work}/desktop" "${width}" "${height}")"
remember_vm "${work}/desktop"
apps+="${separator}"'{"name": "Desktop", "qemu-vm": "'"${desktop_bus}"'", "image-path": "desktop.png"}'
separator=', '
echo " desktop: ssh -p $(cat "${work}/desktop/ssh.port") -i ${desktop_cache}/ssh_key sq@127.0.0.1"
fi
steamos_cache="${STEAMOS_CACHE:-${HOME}/.cache/sunshine-qemu/steamos}"
if [[ -f "${steamos_cache}/steamos.qcow2" ]]; then
steam_overlay="${serve_dir}/steamos.qcow2"
[[ -f "${steam_overlay}" ]] || qemu-img create -q -f qcow2 -F qcow2 -b "${steamos_cache}/steamos.qcow2" "${steam_overlay}"
echo "booting the SteamOS guest..."
steamos_bus="$(SNAPSHOT=off STEAMOS_IMAGE_FILE="${steam_overlay}" "${script_dir}/steamos/run_steamos.sh" "${work}/steamos" "${width}" "${height}")"
remember_vm "${work}/steamos"
apps+="${separator}"'{"name": "SteamOS", "qemu-vm": "'"${steamos_bus}"'", "image-path": "steamos.png"}'
separator=', '
echo " steamos: ssh -p $(cat "${work}/steamos/ssh.port") -i ${steamos_cache}/ssh_key sq@127.0.0.1"
else
echo "no SteamOS guest yet; run ${script_dir}/steamos/build_steamos.sh to add it" >&2
fi
apps+=']}'
[[ "${separator}" == ", " ]] || { echo "no guest images; build one first" >&2; exit 2; }
printf '%s\n' "${apps}" > "${serve_dir}/apps.json"
cat > "${serve_dir}/sunshine.conf" <<EOF
capture = qemu
encoder = ${ENCODER:-nvenc}
port = ${port}
sunshine_name = sunshine-qemu-vms
file_state = ${serve_dir}/sunshine_state.json
credentials_file = ${serve_dir}/sunshine_credentials.json
pkey = ${serve_dir}/sunshine.key
cert = ${serve_dir}/sunshine.crt
file_apps = ${serve_dir}/apps.json
log_path = ${serve_dir}/sunshine.log
min_log_level = ${LOG_LEVEL:-debug}
upnp = disabled
system_tray = disabled
origin_web_ui_allowed = lan
EOF
if [[ ! -f "${serve_dir}/web-ui-password" ]]; then
head -c 12 /dev/urandom | od -An -tx1 | tr -d ' \n' > "${serve_dir}/web-ui-password"
chmod 600 "${serve_dir}/web-ui-password"
"${sunshine_bin}" "${serve_dir}/sunshine.conf" --creds sunshine "$(cat "${serve_dir}/web-ui-password")" > "${serve_dir}/creds.log" 2>&1
fi
"${sunshine_bin}" "${serve_dir}/sunshine.conf" > "${serve_dir}/sunshine.stdout" 2>&1 &
sunshine_pid=$!
for _ in $(seq 1 300); do
kill -0 "${sunshine_pid}" 2> /dev/null || { echo "Sunshine exited; see ${serve_dir}/sunshine.log" >&2; exit 1; }
curl -s --max-time 1 "http://127.0.0.1:${port}/serverinfo" | grep -q 'status_code="200"' && break
sleep 0.2
done
address="$(hostname -I 2>/dev/null | awk '{print $1}')"
cat <<EOF
Sunshine is serving these VMs as apps:
$(python3 -c 'import json,sys; [print(" -", a["name"]) for a in json.load(open(sys.argv[1]))["apps"]]' "${serve_dir}/apps.json")
Moonlight: add the host ${address:-<IP of this machine>} (port ${port}), pair, then pick a VM
Web UI: https://${address:-localhost}:$((port + 1)) user: sunshine password: $(cat "${serve_dir}/web-ui-password")
Logs: ${serve_dir}/sunshine.log
One VM streams at a time: stop the stream to switch to the other.
EOF
if [[ -n "${SERVE_EXIT_AFTER:-}" ]]; then
sleep "${SERVE_EXIT_AFTER}"
else
echo "Ctrl+C to stop"
while kill -0 "${sunshine_pid}" 2> /dev/null; do
sleep 2
done
fi