Files
rustdesk/.github/workflows/drm-capture.yml
Mariano Abad cff25dc4d8 drm: never latch the uinput refresh slot, and bound the source stride
The uinput refresh worker released UINPUT_REFRESH_BUSY on its two normal
exits only. The body locks several process-wide mutexes and does a Wayland
roundtrip, so an unwind there left the flag set for the process lifetime,
and every later hotplug then skipped the spawn and never reapplied the
uinput ABS range: the stale-range, wrong-output symptom the refresh exists
to prevent. This file already had the answer for the probe flag, one screen
away, and the hazard is called out in wayland.rs. Fixing one site and not
the other is the same miss as the hotplug maps.

The slot is deliberately handed back and re-taken mid-loop, so the guard
tracks ownership rather than releasing unconditionally: a plain RAII drop
would clear a flag a replacement worker owns.

drm_reader bounded only the destination (w*4*h) while the row loop reads up
to (h-1)*stride + w*4, so a large stride read past the mapping and could
overflow usize in y*stride. drm_render::convert already bounds stride*h;
the privileged half must not be the weaker of the two.

Also give the drm CI jobs a timeout, so a hung meson or vcpkg step fails in
an hour instead of six.
2026-07-28 23:25:18 -03:00

299 lines
12 KiB
YAML

name: DRM capture (opt-in drm feature)
# Least-privilege GITHUB_TOKEN. Every job here only checks out, builds and tests; the artifact
# up/download used by the deb job authenticates with the runtime token, not this one. Declared at
# the workflow level so the reusable bridge workflow called below inherits the same bound.
permissions:
contents: read
# Supersede a stale run when a PR is pushed again; never cancel a master run, whose whole job is to
# record that a given commit on master was verified.
concurrency:
group: drm-capture-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# Everything CI-side about the opt-in `drm` backend lives here, so the stock CI and release workflows
# stay byte-identical to a build with the feature off. Nothing in this file runs unless a drm-related
# path changes (or someone dispatches it by hand), so a PR that does not touch the backend pays nothing.
#
# The stock `CI` workflow deliberately does NOT compile with `--features drm`: the shipped default is
# the drm-off configuration and that stays the primary verified one.
on:
workflow_dispatch:
pull_request:
paths:
- "libs/scrap/src/common/drm_reader.rs"
- "libs/scrap/src/common/drm_render.rs"
- "libs/scrap/src/common/drmtap_dl.rs"
- "libs/scrap/src/common/mod.rs"
- "libs/scrap/Cargo.toml"
- "src/ipc.rs"
- "src/ipc/**"
- "src/server/drm_capturer.rs"
- "src/server/wayland.rs"
- "src/server/display_service.rs"
- "build.py"
- ".github/workflows/drm-capture.yml"
push:
branches:
- master
# Deliberately the SAME list as the pull_request trigger above: a shorter one here means a push
# that touches only the missing paths (a squash merge, a direct push) skips re-verification.
paths:
- "libs/scrap/src/common/drm_reader.rs"
- "libs/scrap/src/common/drm_render.rs"
- "libs/scrap/src/common/drmtap_dl.rs"
- "libs/scrap/src/common/mod.rs"
- "libs/scrap/Cargo.toml"
- "src/ipc.rs"
- "src/ipc/**"
- "src/server/drm_capturer.rs"
- "src/server/wayland.rs"
- "src/server/display_service.rs"
- "build.py"
- ".github/workflows/drm-capture.yml"
env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
FLUTTER_VERSION: "3.24.5"
jobs:
drm-tests:
name: drm unit tests (linux)
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: false
swap-storage: false
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
persist-credentials: false
- name: Install prerequisites
shell: bash
run: |
sudo apt-get -y update
sudo apt-get install -y \
clang cmake curl gcc git g++ \
libpam0g-dev libasound2-dev libunwind-dev \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
libgtk-3-dev libpulse-dev libva-dev libvdpau-dev \
libxcb-randr0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
libxdo-dev libxfixes-dev nasm wget
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
shell: bash
run: $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
targets: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
# The whole rustdesk-crate test set with the feature ON, not just the `_drm` ones by name: a name
# filter would skip the sibling asserts that also matter in this configuration, notably the one
# bounding `size_of::<Data>()`, which the new DmabufDesc variant grows.
# The two skips are the same ones the stock CI applies: both need a real display server and fail
# on a headless runner regardless of this feature.
- name: Run rustdesk crate tests with the drm feature
shell: bash
run: |
cargo test --locked --target x86_64-unknown-linux-gnu -p rustdesk --features drm \
--no-fail-fast -- --skip test_get_cursor_pos --skip test_get_key_state
# The capture backend itself lives in the scrap crate, so its unit tests are a separate
# package. `--lib` keeps this to unit tests; none of them touch a device or a display server.
- name: Run scrap crate tests with the drm feature
shell: bash
run: |
cargo test --locked --target x86_64-unknown-linux-gnu -p scrap --features drm --lib
libdrmtap:
name: libdrmtap pin, build and .so contract
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Install libdrmtap build deps
shell: bash
run: |
sudo apt-get -y update
sudo apt-get install -y meson ninja-build pkg-config libdrm-dev \
libegl1-mesa-dev libgles2-mesa-dev
# Exercises the real fetch-and-build path in build.py, which pins the commit by sha, so a bad or
# moved pin fails here rather than in a release job.
- name: Fetch the pinned libdrmtap and build the .so
shell: bash
run: |
python3 - <<'PY'
import importlib.util, sys
spec = importlib.util.spec_from_file_location("b", "build.py")
b = importlib.util.module_from_spec(spec)
sys.argv = ["build.py"]
spec.loader.exec_module(b)
so = b.build_libdrmtap_so()
print(f"::notice::built {so}")
open("so_path", "w").write(so)
PY
# The shipped hot path is the EGL detile. libdrmtap degrades to a CPU-only stub when the egl or
# glesv2 pkg-config files are missing on the build host, and nothing else in the pipeline notices,
# so assert here that the object we would ship really carries EGL and really exports every symbol
# the runtime loader resolves.
- name: Assert the .so contract (EGL enabled, loader symbols present)
shell: bash
run: |
SO="$(cat so_path)"
echo "checking $SO"
missing=0
# Every symbol drmtap_dl.rs resolves, derived from the loader itself so the two cannot drift.
for sym in $(grep -oE 'b"drmtap_[a-z_]+"' libs/scrap/src/common/drmtap_dl.rs \
| sed 's/^b"//; s/"$//' | sort -u); do
if ! nm -D --defined-only "$SO" | grep -q " T $sym\$"; then
echo "::error::libdrmtap does not export $sym, which the runtime loader resolves"
missing=1
fi
done
# EGL is reached by lazy dlopen, on purpose, so that the privileged process never links the
# vendor GL stack. That means there is NO DT_NEEDED entry and no undefined egl* symbol to look
# for: the naive ELF check reports "no EGL" on a perfectly good library. What a CPU-only stub
# build really lacks is the dlopen target name and the import call itself.
for s in "libEGL.so.1" "eglCreateImageKHR"; do
if ! strings "$SO" | grep -qF "$s"; then
echo "::error::libdrmtap looks like a CPU-only stub (no $s): the EGL detile hot path is missing"
missing=1
fi
done
test "$missing" -eq 0
echo "::notice::libdrmtap .so contract ok (13 loader symbols, EGL detile present)"
# The bridge generator is a reusable workflow, so this calls the stock one instead of duplicating it.
generate-bridge:
uses: ./.github/workflows/bridge.yml
drm-deb:
name: unattended-wayland deb (verification build)
needs: generate-bridge
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1
with:
tool-cache: false
android: true
dotnet: true
haskell: true
large-packages: false
swap-storage: false
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
persist-credentials: false
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Install prerequisites
shell: bash
run: |
sudo apt-get -y update
# Same list the stock linux job needs, plus the flutter desktop toolchain and the three
# libdrmtap build deps (libdrm and the mesa-specific EGL/GLES dev packages).
sudo apt-get install -y \
clang cmake curl gcc git g++ ninja-build meson pkg-config \
libpam0g-dev libasound2-dev libunwind-dev liblzma-dev \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \
libgtk-3-dev libpulse-dev libva-dev libvdpau-dev \
libxcb-randr0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
libxdo-dev libxfixes-dev nasm wget \
libdrm-dev libegl1-mesa-dev libgles2-mesa-dev
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
- name: Install vcpkg dependencies
shell: bash
run: $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: stable
targets: x86_64-unknown-linux-gnu
- uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
- name: Setup flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
shell: bash
run: |
cd $(dirname $(dirname $(which flutter)))
[[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
- name: Build the unattended-wayland deb
shell: bash
run: |
cargo build --locked --lib --release --features hwcodec,flutter,unix-file-copy-paste,drm
python3 ./build.py --flutter --drm --skip-cargo
# build.py exits 0 on some inner failures, so assert the artifact instead of trusting the status,
# and assert the two things that make it the drm variant at all.
- name: Assert the deb is a real drm build
shell: bash
run: |
deb="$(ls rustdesk-unattended-wayland-*.deb)"
test -n "$deb"
echo "::notice::built $deb ($(stat -c %s "$deb") bytes)"
dpkg -c "$deb" | grep -E 'usr/lib/rustdesk/libdrmtap\.so\.0\.[0-9]+\.[0-9]+$'
dpkg -c "$deb" | grep -E 'usr/lib/rustdesk/libdrmtap\.so\.0 ->'
mv "$deb" "${deb%.deb}-x86_64.deb"
# Verification artifact, deliberately NOT a release deliverable: it is built on the runner rather
# than in the old container the stock release debs use, so its glibc floor is higher. The consent
# free variant stays out of the published release either way.
- name: Upload the deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unattended-wayland-x86_64.deb
path: rustdesk-unattended-wayland-*-x86_64.deb