mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-06 08:01:03 +03:00
libc::dup() does not copy the close-on-exec flag, so the dup'd _drm socket fd was inherited by every child this process forks. This process is the ROOT service and it does fork synchronously elsewhere (the loginctl active-uid lookup), and that fd is an ALREADY-AUTHORIZED channel to the one thing on the box that hands out scanout dma-bufs. F_DUPFD_CLOEXEC instead. Measured the difference rather than assuming it: dup() leaves FD_CLOEXEC clear, F_DUPFD_CLOEXEC sets it. Also the last two artifact sources without the stub check: - --package + --drm stages the .so straight out of a bundle somebody else produced, with no _assert_so_has_egl. Third source, same exposure as DRMTAP_PREBUILT_DIR, now asserted like the other two. All three artifact paths are covered. - the workflow triggers omitted src/server.rs, src/server/input_service.rs and src/platform/linux.rs, which all carry DRM wiring (warm_availability, the cursor path in run_cursor, the producer start and get_cursor/get_cursor_data), so a PR touching only those skipped the entire drm verification. Added to BOTH mirrored lists and asserted equal (15 == 15).
345 lines
15 KiB
YAML
345 lines
15 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"
|
|
# These three carry DRM wiring too (warm_availability, the cursor path in run_cursor, and the
|
|
# producer start + get_cursor/get_cursor_data), so a PR touching only them must not skip the
|
|
# whole drm verification.
|
|
- "src/server.rs"
|
|
- "src/server/input_service.rs"
|
|
- "src/platform/linux.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"
|
|
# These three carry DRM wiring too (warm_availability, the cursor path in run_cursor, and the
|
|
# producer start + get_cursor/get_cursor_data), so a PR touching only them must not skip the
|
|
# whole drm verification.
|
|
- "src/server.rs"
|
|
- "src/server/input_service.rs"
|
|
- "src/platform/linux.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. The character class allows digits (a drmtap_grab_desc2 would otherwise be
|
|
# silently dropped from the loop), and the count is asserted below so a refactor of the
|
|
# loader away from b"..." literals cannot quietly turn this whole check into a no-op that
|
|
# iterates zero times and passes.
|
|
syms=$(grep -oE 'b"drmtap_[a-z0-9_]+"' libs/scrap/src/common/drmtap_dl.rs \
|
|
| sed 's/^b"//; s/"$//' | sort -u)
|
|
nsyms=$(echo "$syms" | grep -c .)
|
|
if [ "$nsyms" -lt 13 ]; then
|
|
echo "::error::extracted only $nsyms loader symbols from drmtap_dl.rs (expected >= 13); the extraction pattern no longer matches the loader"
|
|
missing=1
|
|
fi
|
|
for sym in $syms; 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 ($nsyms 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)))
|
|
# `[[ ... ]] && cmd` as the last line makes the STEP fail once FLUTTER_VERSION moves off
|
|
# the pinned value, because the failed test becomes the script's exit status. An explicit
|
|
# if/else skips instead. Reading the values from the environment rather than interpolating
|
|
# ${{ }} into the script also keeps this off zizmor's template-injection list.
|
|
if [[ "$FLUTTER_VERSION" == "3.24.5" ]]; then
|
|
git apply "$GITHUB_WORKSPACE/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
|
|
else
|
|
echo "::notice::flutter $FLUTTER_VERSION is not 3.24.5; skipping the dropdown patch"
|
|
fi
|
|
|
|
- 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"
|
|
|
|
# MEASURE the glibc floor rather than describing it. This job builds on the runner instead of the
|
|
# ubuntu18.04 container the stock release debs use, so the artifact only runs on a host at least
|
|
# as new as the runner -- and that number belongs in the artifact NAME, because a comment in this
|
|
# file is not visible to whoever downloads it from the Actions UI.
|
|
- name: Measure the deb glibc floor
|
|
id: floor
|
|
shell: bash
|
|
run: |
|
|
deb="$(ls rustdesk-unattended-wayland-*-x86_64.deb)"
|
|
rm -rf /tmp/debfloor && dpkg-deb -R "$deb" /tmp/debfloor
|
|
floor="$(objdump -T /tmp/debfloor/usr/share/rustdesk/lib/librustdesk.so \
|
|
| grep -oE 'GLIBC_2\.[0-9]+' | sort -uV | tail -1)"
|
|
test -n "$floor"
|
|
echo "floor=${floor#GLIBC_}" >> "$GITHUB_OUTPUT"
|
|
echo "::notice::deb requires ${floor} or newer (built on the runner, not the ubuntu18.04 release container)"
|
|
|
|
# Verification artifact, deliberately NOT a release deliverable. The consent-free variant stays
|
|
# out of the published release either way; the name states the floor so nobody installs it on an
|
|
# older distro and hits a bare loader error.
|
|
- name: Upload the deb
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: rustdesk-unattended-wayland-x86_64-verification-glibc${{ steps.floor.outputs.floor }}.deb
|
|
path: rustdesk-unattended-wayland-*-x86_64.deb
|