03 HOST RUNTIME // Linux X11 — services, displays, audio SRC connector-app/resources/*.py
03.A

Four programs, no installer

qrc:/connector/*.py

The agent that isn't installed

Connector controls remote Linux hosts without deploying anything to them. The four Python helpers are compiled into the client binary as Qt resources, base64-encoded at call time, and piped through an SSH heredoc into a six-line bootstrap that writes them to ~/.cache/connector-host-runtime/ and executes them. There is no package, no daemon to install, no version to keep in sync — the helper on the host is always exactly the helper in the client you are running.

The four staged helpers, by size and responsibility
HelperLinesInterfaceResponsibility
connector-linux-hostctl.py1,146 state | start | restart | stop | mic-start | mic-stop | approve-pair systemd supervision of Sunshine and the mic service, environment repair, pair approval
connector-x11-workspace.py901 apply | update | restore, --runtime-dir dedicated Xvfb workspace with its own desktop and its own Sunshine instance
connector-host-overlay.py544 --runtime-dir GTK control bar drawn inside the streamed desktop
connector-linux-michost.py313 serve UDP receiver that injects client microphone audio into PipeWire
connector-app/src/connectorbackend.cpp buildRemoteLinuxHostControlCommand() the bootstrap that lands on the host
   1CONNECTOR_HOSTCTL_ACTION='state' CONNECTOR_HOSTCTL_ARGS_B64='W10=' \
   2CONNECTOR_HOSTCTL_SCRIPT_B64='…' CONNECTOR_MICHOST_SCRIPT_B64='…' \
   3CONNECTOR_MIC_PORT='48200' python3 - <<'PY'
   4import base64, json, os, pathlib, subprocess, sys
   5runtime_dir = pathlib.Path.home() / ".cache" / "connector-host-runtime"
   6runtime_dir.mkdir(parents=True, exist_ok=True)
   7script_path = runtime_dir / "linux-hostctl.py"
   8script_path.write_bytes(base64.b64decode(os.environ["CONNECTOR_HOSTCTL_SCRIPT_B64"]))
   9args = json.loads(base64.b64decode(os.environ.get("CONNECTOR_HOSTCTL_ARGS_B64", "W10=")).decode("utf-8"))
  10completed = subprocess.run([sys.executable, str(script_path), os.environ["CONNECTOR_HOSTCTL_ACTION"], *args], check=False)
  11raise SystemExit(completed.returncode)
  12PY
Arguments travel as a base64-encoded JSON array rather than as shell words — W10= is the encoding of []. That removes an entire class of quoting bug from a code path that carries user-supplied device names and PINs.
03.B

Finding the session

best_session_env()

An SSH login has no DISPLAY. Every helper therefore begins by walking /proc to steal the environment of a real desktop process owned by the same uid.

Keys harvested

DISPLAY · WAYLAND_DISPLAY · XDG_RUNTIME_DIR · DBUS_SESSION_BUS_ADDRESS · SWAYSOCK · HYPRLAND_INSTANCE_SIGNATURE · XAUTHORITY · XDG_SESSION_TYPE · XDG_CURRENT_DESKTOP

Processes considered

gnome-shell · gnome-session · plasmashell · startplasma · kwin · xfce4-session · cinnamon · mate-session · sway · hyprland · xorg · xwayland

Candidates are ranked by display_rank(), which prefers a low, real display number over a scratch one — so a helper never accidentally adopts the environment of a previous Connector workspace running on :98.

Why this matters

Sunshine started from an SSH session without these variables will launch, bind its ports, and capture nothing. The connector-env.conf systemd drop-in exists precisely to write the harvested values back into the unit so the next start is correct even without Connector present.

03.C

Runtime supervision and its fallbacks

action_start() · connector-linux-hostctl.py:936

hostctl start is not a wrapper around systemctl start. It repairs the unit first, then starts it, then verifies, then falls back to a direct spawn if systemd could not produce a responding runtime.

Host runtime start sequence with systemd fallback A flow from the SSH bootstrap through unit repair, service stop, systemd start, readiness verification, an optional direct process spawn fallback, then microphone provisioning, and finally a state collection that produces the JSON answer. SSH → python3 bootstrap linux-hostctl.py start 1 · REPAIR THE UNIT BEFORE TOUCHING IT write_direct_sunshine_service only if ExecStart is empty ~/.config/systemd/user/ write_drop_in sunshine.service.d/ connector-env.conf 7 session vars + LD_LIBRARY_PATH write_mic_service connector-mic-host.service Wants= / After= wired in systemctl --user daemon-reload · force_stop_unit(mic) · force_stop_unit(sunshine) · wait_for_stop(3 s) force_stop_unit escalates: stop → SIGTERM → SIGKILL on every pid it can still see 2 · START AND VERIFY systemctl --user start sunshine.service · 8 s timeout wait_for_start(8 s) poll :47989 until it answers sunshine_start_ready(state) runtime_responding && process_count == 1 && no duplicates FALLBACK — systemd did not produce a responding runtime force_stop_unit(sunshine) → wait_for_stop(3 s) → start_direct_sunshine(env) spawns the binary with the harvested session env; records direct_sunshine_path / error not ready 3 · MICROPHONE PROVISIONING — only when a mic service was staged ensure_pipewire_virtual_mic unload stale modules module-null-sink → connector_virtual_mic_sink module-remap-source → connector_virtual_mic pipewire-pulse config fallback ~/.config/pipewire/pipewire-pulse.conf.d/ connector-virtual-mic.conf used when pactl is unusable; survives reboots start connector-mic-host.service reset-failed → start → wait_for_mic_ready verify default == connector_virtual_mic failure downgrades to a warning, not an error 4 · collect_state(env) → 26-key JSON object printed on stdout unit_file_state · active_state · sub_state · main_pid · process_count · pids · duplicate_processes runtime_responding · admin_responding · http_port · admin_port · mic_port · environment_ready mic_active_state · mic_ready · mic_default_source · mic_default_source_is_connector · pactl_usable …
Figure 03.1 — hostctl start, including the systemd escape hatch Every branch converges on the same collect_state() snapshot, so the client always receives a complete picture regardless of which path succeeded. ok is not "the command ran" — it is sunshine_start_ready(state), which requires the HTTP port to answer and exactly one runtime process to exist.
host — staged helper, state action
ian@linux-host:~$ python3 ~/.cache/connector-host-runtime/linux-hostctl.py state
{
  "ok": true,
  "command": "state",
  "session_env": {
    "DISPLAY": ":1",
    "XDG_SESSION_TYPE": "x11",
    "XDG_CURRENT_DESKTOP": "ubuntu:GNOME",
    "XDG_RUNTIME_DIR": "/run/user/1000",
    "DBUS_SESSION_BUS_ADDRESS": "unix:path=/run/user/1000/bus",
    "XAUTHORITY": "/run/user/1000/.mutter-Xwaylandauth.XXXXXX"
  },
  "environment_ready": true,
  "unit_file_state": "enabled",
  "active_state": "active",
  "sub_state": "running",
  "main_pid": 4471,
  "process_count": 1,
  "duplicate_processes": false,
  "runtime_responding": true,
  "admin_responding": true,
  "http_port": 47989,
  "admin_port": 47990,
  "mic_port": 48200,
  "mic_active_state": "active",
  "mic_virtual_source_ready": true,
  "mic_ready": true,
  "mic_default_source": "connector_virtual_mic",
  "mic_default_source_is_connector": true,
  "pipewire_pulse_active_state": "active",
  "pactl_available": true,
  "pactl_usable": true,
  "wpctl_available": true,
  "pw_cli_available": true
}

admin_port is not a separate constant — it is read_config_port() + 1, where read_config_port() parses port out of the Sunshine configuration file and defaults to 47989.

03.D

Display switching

switchSessionDisplay() · connectorxrandrparse.cpp

Pressing a display button in the session overlay does not change what the client requests — it changes what the host captures. Connector treats the host as authoritative and refuses to fake a switch it could not confirm.

Display switch decision flow A decision flow starting from a display button press, branching on whether the session uses a dedicated virtual workspace or the host's physical displays, then either updating the workspace or issuing sunshine switch-display over SSH, aligning the pointer, updating the in-stream overlay, and finally either committing or refusing the switch. DISPLAY SLOT PRESSED session overlay window, or Session ▸ Displays menu, or session.switch_display RPC RESOLVE resolveDisplayValue(displays, idx) remoteDisplayForValue(...) status → "Switching to HDMI-0..." BRANCH ON m_sessionUsesDedicatedWorkspace A · DEDICATED WORKSPACE updateRemoteWorkspace(device, displays, resolvedDisplayValue, &warning) → ssh: x11-workspace.py update → rewrites the workspace sunshine.conf in "exclusive" mode a failure aborts the switch outright and reports the warning B · PHYSICAL HOST DISPLAYS ssh: sunshine --switch-display <name> 12 000 ms timeout then alignRemotePointerToDisplay(...) 5 000 ms timeout, warp the X pointer no sunshine on PATH → exit 127 with a structured JSON error, not a shell error WHERE DISPLAY NAMES COME FROM ssh: xrandr --query (or wlr-randr --json) parseXrandrMonitorsFromText(text, currentDefault) regex: ^([A-Za-z0-9_.:-]+)\s+(connected|disconnected) (\s+primary)?(?:\s+(\d+x\d+\+\d+\+\d+))? → name · resolution · configValue · outputIndex · isPrimary · isDefault WHY configValue == name Verbatim comment in connectorxrandrparse.cpp: "Sunshine on X11 matches output_name by xrandr output name (e.g. HDMI-0). Use the output name as configValue so writes never produce an unmatchable value." covered by the connector_xrandr_parse ctest suite IF THE SWITCH APPLIED AND THE IN-STREAM OVERLAY IS IN USE updateRemoteHostOverlay(device, displays, resolvedDisplayValue) — rewrites state.json so the overlay repositions itself COMMIT — displaySwitchApplied m_sessionCurrentDisplayIndex = resolvedDisplayValue markCurrentRemoteDisplay(...) rememberPreferredRemoteDisplay(deviceId, display) saveDeviceLibrary() — remembered for next session REFUSE — authoritative attempt failed "Connector could not confirm the live display switch." The client-side stream is deliberately NOT switched as a consolation prize. A wrong picture is worse than no change.
Figure 03.2 — display switching, host-authoritative The client-side m_sessionManager->switchDisplay() path still exists, but it is only used when no authoritative attempt was made at all — that is, when the host has no shell automation. The moment Connector can reach the host's shell, the host decides.
03.E

Dedicated X11 workspaces

connector-x11-workspace.py

Streaming a host's physical desktop means taking it over: whoever is sitting at that machine sees your mouse. The workspace helper builds a second, headless desktop sized to your client, runs its own Sunshine against it, and leaves the physical session untouched. connector-x11-workspace.py apply --runtime-dir <dir> is the entry point; update re-applies a changed display selection and restore tears everything back down.

Dedicated X11 workspace construction The helper snapshots the host state, allocates a free X display number between 98 and 120, starts Xvfb at the client resolution, launches either a GNOME or a minimal XFCE-style desktop, stops the existing Sunshine, writes a workspace-specific config, starts a workspace Sunshine, and records metadata for a later restore. PHYSICAL SESSION — LEFT RUNNING IN "WORKSPACE" MODE :0 or :1 — the real seat GNOME / KDE / XFCE session physical monitors via xrandr snapshot_current_state() records: outputs · currentDesktop · activeWindow serviceActive · configPath "exclusive" mode blanks these outputs DEDICATED WORKSPACE — BUILT PER SESSION choose_display_number() scan :98 … :120 skip if /tmp/.X11-unix/X<n> or /tmp/.X<n>-lock exists Xvfb :<n> -screen 0 WxHx24 -ac -nolisten tcp size = client display, rounded down to even, floor 640×360 start_workspace_desktop() host is GNOME/Ubuntu → gnome-shell otherwise → xfwm4 | openbox | metacity plus xfdesktop and xfce4-panel if present workspace env DISPLAY=:<n> XDG_SESSION_TYPE=x11 GDK_BACKEND=x11 WAYLAND_DISPLAY, SWAYSOCK, HYPRLAND_… dropped WORKSPACE SUNSHINE stop_existing_sunshine() → wait_sunshine_ports_released() on 47984 · 47989 · 47990 · 48010 start_workspace_sunshine(env, workspace config) — a copy of the host config repointed at :<n> RUNTIME DIRECTORY — THE ONLY STATE THAT SURVIVES A CRASH meta.json mode · displayNumber · xvfbPid · desktopPids · sunshinePid serviceWasActive · exclusiveApplied workspaceWidth · workspaceHeight · desktopEnvironment read by stale_restore_if_needed() and cleanup_stale_sibling_workspaces() restore() terminate workspace sunshine → desktop pids → Xvfb restore_host_outputs(snapshot) if exclusive mode blanked them restart the original sunshine.service if it was active before also runs automatically on the next apply() if a stale meta.json is found Failure inside apply() unwinds in reverse — sunshine pid, desktop pids, Xvfb pid, then output restore — and returns {"ok": false, "error": …}
Figure 03.3 — building a second desktop on someone else's machine The display range :98:120 is scanned rather than assumed, and sibling workspaces left behind by a previous crashed session are cleaned up before a new one is built. Everything needed to undo the operation is written to meta.json before the operation happens.

"Client-matched virtual display active. Use DISCONNECT to end the session."

Main.qml — session overlay status text when a dedicated workspace is in use
03.F

The in-stream overlay

connector-host-overlay.py

The controls you need most — disconnect, switch monitor — are hardest to reach while a stream has your pointer captured. Connector's answer is to draw the control bar on the host, inside the picture you are already looking at. It is a GTK 3 window that talks to the client through two files in the runtime directory.

state.json — client writes
title, subtitle, the display list with slot numbers, and which display is current. The overlay polls it, hashes it, and rebuilds its buttons only when the hash changes.
command.json — overlay writes
{"action":"switch_display","display_value":1} or {"action":"disconnect"}. The client picks these up and executes them through the normal session path.

Fixed controls

  • LOCATE — warps the pointer onto the overlay and re-runs the sequence at 120 ms, 360 ms and 820 ms, because compositors move windows after they are mapped.
  • HIDE — collapses the bar without killing the process.
  • DISCONNECT — writes the disconnect command.
  • One button per display, labelled by slot, with the active one styled overlay-display.active.

The overlay also carries a drag handle, a pointer guard that keeps the cursor on the current display, and a pid file it cleans up on exit. Its title defaults to "Connector controls" with the subtitle "IN-STREAM".

Graceful degradation

If the overlay cannot be launched, Connector says so rather than silently losing a control surface: "Connector could not launch the in-stream host controls, so the local Session menu stays as fallback."

03.G

Microphone bridge — CMIC v1

connectormicrophonepacket.h · connector-linux-michost.py

Moonlight streams audio from host to client. It does not stream a microphone the other way. Connector adds a small, fully specified one-way UDP protocol to close that gap on Linux hosts.

CMIC microphone bridge packet layout and audio path The 20-byte CMIC packet header is laid out byte by byte, followed by the audio path from the client capture device through UDP port 48200 into the michost receiver, a pw-cat playback process, a PipeWire null sink and a remapped source that Sunshine reads as the default microphone. PACKET — 20-BYTE HEADER, LITTLE ENDIAN, + 1920-BYTE PAYLOAD 0–3 magic "CMIC" 4 ver 1 5 chans 1 6–7 frame 960 samples 8–11 rate 48000 12–15 seq uint32 16–17 bytes 1920 18–19 rsvd 0 20–1939 payload — 960 signed 16-bit mono samples = 20 ms of audio at 48 kHz PATH — CLIENT CAPTURE TO HOST DEFAULT INPUT client capture macOS: AVFoundation connectormicrophonestreamer _macos.mm UDP → host:48200 50 packets / second no ack, no retransmit rides the tailnet, not the internet michost serve binds :: and 0.0.0.0 first sender wins, then locked idle release after 10 s pw-cat --playback --format s16 --rate 48000 --media-role Communication Sunshine reads the default PipeWire source PIPEWIRE GRAPH BUILT BY hostctl module-null-sink sink_name=connector_virtual_mic_sink module-remap-source master=…sink.monitor source_name=connector_virtual_mic VALIDATION — parse_packet() rejects anything that is not exactly right len < 20 · magic != b"CMIC" · version != 1 channels != 1 · frame_samples != 960 · sample_rate != 48000 payload length != declared · declared != 1920 a malformed datagram is dropped silently; there is no error channel SENDER PINNING normalize_sender(addr) records the first source address that sends a valid packet; later packets from any other address are ignored until the stream has been idle for SOURCE_IDLE_TIMEOUT = 10 s
Figure 03.4 — the CMIC bridge, wire format and audio path Both ends of this protocol are authored here: the encoder in connectormicrophonepacket.h and the decoder in connector-linux-michost.py. The header layout is asserted from both sides, and the packet builder has its own ctest suite (connector_microphone_packet).
03.H

Continue