mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-08-18 17:16:01 +08:00
Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c8f0a38d | ||
|
|
21eee5ccdc | ||
|
|
e6c26511cd | ||
|
|
c0b880e1eb | ||
|
|
c10f1499f2 | ||
|
|
d7a9c26ff9 | ||
|
|
f809ad173c | ||
|
|
34155d0266 | ||
|
|
9d544788b0 | ||
|
|
dd16e65b68 | ||
|
|
91eac61a90 | ||
|
|
941cc79af5 | ||
|
|
0d6ecfc730 |
@@ -26,8 +26,9 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_NAME: crossdesk/ubuntu22.04
|
||||
STACK_TAG: buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
IMAGE_NAME: crossdesk/ubuntu20.04
|
||||
STACK_TAG: buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
CMAKE_VERSION: 3.31.6
|
||||
XMAKE_VERSION: 3.0.9
|
||||
RUST_VERSION: 1.92.0
|
||||
RUSTUP_VERSION: 1.28.2
|
||||
@@ -44,11 +45,11 @@ jobs:
|
||||
- arch: amd64
|
||||
platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
base_image: nvidia/cuda:12.6.3-devel-ubuntu22.04
|
||||
base_image: nvidia/cuda:12.6.3-devel-ubuntu20.04
|
||||
- arch: arm64
|
||||
platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
base_image: ubuntu:22.04
|
||||
base_image: ubuntu:20.04
|
||||
steps:
|
||||
- name: Set candidate tag
|
||||
id: image
|
||||
@@ -86,6 +87,7 @@ jobs:
|
||||
sbom: false
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ matrix.base_image }}
|
||||
CMAKE_VERSION=${{ env.CMAKE_VERSION }}
|
||||
XMAKE_VERSION=${{ env.XMAKE_VERSION }}
|
||||
RUST_VERSION=${{ env.RUST_VERSION }}
|
||||
RUSTUP_VERSION=${{ env.RUSTUP_VERSION }}
|
||||
|
||||
+82
-10
@@ -13,8 +13,14 @@ on:
|
||||
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
||||
required: false
|
||||
default: "0"
|
||||
sign_windows_test:
|
||||
description: "Submit the Windows installer to SignPath using test-signing."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: write
|
||||
|
||||
env:
|
||||
@@ -29,12 +35,12 @@ jobs:
|
||||
include:
|
||||
- arch: amd64
|
||||
runner: ubuntu-24.04
|
||||
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_amd64.sh
|
||||
|
||||
- arch: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
image: crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
image: crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
package_script: ./scripts/linux/pkg_arm64.sh
|
||||
|
||||
container:
|
||||
@@ -106,26 +112,30 @@ jobs:
|
||||
- name: Ensure Wayland build dependencies
|
||||
shell: bash
|
||||
run: |
|
||||
if pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2; then
|
||||
if pkg-config --exists dbus-1 libdrm && \
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h && \
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends \
|
||||
libdbus-1-dev \
|
||||
libdrm-dev \
|
||||
libpipewire-0.3-dev \
|
||||
libspa-0.2-dev
|
||||
libdrm-dev
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
pkg-config --exists dbus-1 libdrm libpipewire-0.3 libspa-0.2
|
||||
install-crossdesk-pipewire-sdk
|
||||
pkg-config --exists dbus-1 libdrm
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h
|
||||
test -f /opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h
|
||||
|
||||
- name: Build CrossDesk
|
||||
env:
|
||||
CUDA_PATH: /usr/local/cuda
|
||||
XMAKE_GLOBALDIR: /data
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true --root -y
|
||||
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --USE_WAYLAND=true \
|
||||
--cc=gcc-10 --cxx=g++-10 --root -y
|
||||
xmake b -vy --root crossdesk
|
||||
|
||||
- name: Package
|
||||
@@ -133,13 +143,19 @@ jobs:
|
||||
chmod +x ${{ matrix.package_script }}
|
||||
${{ matrix.package_script }} ${LEGAL_VERSION}
|
||||
|
||||
- name: Verify packaged Ubuntu 22.04 glibc baseline
|
||||
- name: Verify Ubuntu 20.04 package dependency resolution
|
||||
run: |
|
||||
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
|
||||
apt-get update
|
||||
apt-get install --simulate "${PACKAGE_FILE}"
|
||||
|
||||
- name: Verify packaged Ubuntu 20.04 glibc baseline
|
||||
run: |
|
||||
PACKAGE_FILE="${GITHUB_WORKSPACE}/crossdesk-linux-${{ matrix.arch }}-${LEGAL_VERSION}.deb"
|
||||
VERIFY_DIR="${RUNNER_TEMP}/crossdesk-glibc-${{ matrix.arch }}"
|
||||
install -d "${VERIFY_DIR}"
|
||||
dpkg-deb --extract "${PACKAGE_FILE}" "${VERIFY_DIR}"
|
||||
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.35
|
||||
verify-crossdesk-glibc-baseline "${VERIFY_DIR}" 2.31
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v6
|
||||
@@ -400,6 +416,52 @@ jobs:
|
||||
run: |
|
||||
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Upload unsigned Windows installer for test signing
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
id: upload-unsigned-windows-installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ github.workspace }}/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Sign Windows installer with SignPath test certificate
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: crossdesk
|
||||
signing-policy-slug: test-signing
|
||||
github-artifact-id: ${{ steps.upload-unsigned-windows-installer.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: ${{ github.workspace }}\signpath\signed-installer
|
||||
skip-decompress: true
|
||||
parameters: |
|
||||
version: ${{ toJSON(env.VERSION_NUM) }}
|
||||
|
||||
- name: Verify and use test-signed Windows installer
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
shell: pwsh
|
||||
run: |
|
||||
$installerName = "crossdesk-win-x64-${{ env.VERSION_NUM }}.exe"
|
||||
$signedInstaller = Join-Path "${{ github.workspace }}\signpath\signed-installer" $installerName
|
||||
$destination = Join-Path "${{ github.workspace }}\scripts\windows" $installerName
|
||||
|
||||
if (!(Test-Path $signedInstaller)) {
|
||||
throw "SignPath did not return the signed installer: $signedInstaller"
|
||||
}
|
||||
|
||||
$signature = Get-AuthenticodeSignature $signedInstaller
|
||||
if (!$signature.SignerCertificate -or $signature.Status -eq "NotSigned") {
|
||||
throw "SignPath returned an installer without an Authenticode signature."
|
||||
}
|
||||
|
||||
Write-Host "Signer: $($signature.SignerCertificate.Subject)"
|
||||
Write-Host "Signature status: $($signature.Status)"
|
||||
Copy-Item $signedInstaller $destination -Force
|
||||
|
||||
- name: Build Portable CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
|
||||
@@ -534,6 +596,10 @@ jobs:
|
||||
body: |
|
||||
Binary release only. Source code is not included.
|
||||
|
||||
## Code signing policy
|
||||
|
||||
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
|
||||
|
||||
- name: Create or update 'latest' tag
|
||||
run: |
|
||||
git config user.name "github-actions[bot]"
|
||||
@@ -550,6 +616,12 @@ jobs:
|
||||
prerelease: false
|
||||
files: release/*
|
||||
generate_release_notes: false
|
||||
body: |
|
||||
Binary release only. Source code is not included.
|
||||
|
||||
## Code signing policy
|
||||
|
||||
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).
|
||||
|
||||
- name: Upload artifacts to server
|
||||
uses: burnett01/rsync-deployments@5.2
|
||||
|
||||
@@ -1,38 +1,51 @@
|
||||
# CrossDesk
|
||||
|
||||
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||
[]()
|
||||
|
||||
[](https://www.crossdesk.cn/)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||
[](https://github.com/kunkundi/crossdesk/actions)
|
||||
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
[](https://github.com/kunkundi/crossdesk/issues)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
[](https://github.com/kunkundi/crossdesk/forks)
|
||||
|
||||
[ [English](README_EN.md) / 中文 ]
|
||||
|
||||
PC 客户端
|
||||
---
|
||||
|
||||
## 界面预览
|
||||
|
||||
### PC 客户端
|
||||
|
||||

|
||||
|
||||
Web 客户端
|
||||
### Web 客户端
|
||||
|
||||
<p align="center">
|
||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||
</p>
|
||||
|
||||
---
|
||||
|
||||
## 简介
|
||||
|
||||
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备。
|
||||
|
||||
CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频传输库的实验性应用。MiniRTC 是一个轻量级的跨平台实时音视频传输库。它具有网络透传([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)),视频软硬编解码(H264/AV1),音频编解码([Opus](https://github.com/xiph/opus)),信令交互,网络拥塞控制,传输加密([SRTP](https://tools.ietf.org/html/rfc3711))等基础能力。
|
||||
|
||||
---
|
||||
|
||||
## 系统要求
|
||||
|
||||
| 平台 | 最低版本 |
|
||||
|----------------|---------------------------|
|
||||
| --- | --- |
|
||||
| **Windows** | Windows 10 及以上 (64 位) |
|
||||
| **macOS** | macOS Intel 15.0 及以上 ( 大于 14.0 小于 15.0 的版本可自行编译实现兼容 )<br> macOS Apple Silicon 14.0 及以上 |
|
||||
| **Linux** | Ubuntu 22.04 及以上 ( 低版本可自行编译实现兼容 ) |
|
||||
| **Linux** | Ubuntu 20.04 及以上 |
|
||||
|
||||
---
|
||||
|
||||
## 使用
|
||||
|
||||
@@ -47,14 +60,17 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|
||||
发起连接前,可在设置中自定义配置项,如语言、视频编码格式等。
|
||||

|
||||
|
||||
### Web 客户端
|
||||
### 使用 Web 客户端
|
||||
|
||||
浏览器访问 [CrossDesk Web Client](https://web.crossdesk.cn/)。
|
||||
输入 **远程设备 ID** 与 **密码**,点击连接即可接入远程设备。如图,**iOS Safari 远程控制 Win11**:
|
||||
|
||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||
|
||||
### Windows 服务(CrossDesk Service)
|
||||
|
||||
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
|
||||
|
||||
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
|
||||
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
|
||||
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
||||
@@ -62,13 +78,15 @@ CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服
|
||||
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
||||
|
||||
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
||||
```
|
||||
|
||||
```powershell
|
||||
# 安装
|
||||
.\CrossDesk.exe --service-install
|
||||
# 启动
|
||||
.\CrossDesk.exe --service-start
|
||||
# 查看状态
|
||||
.\CrossDesk.exe --service-status
|
||||
# 停止
|
||||
.\CrossDesk.exe --service-stop
|
||||
# 卸载
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
@@ -76,20 +94,25 @@ Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_
|
||||
|
||||
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
||||
|
||||
---
|
||||
|
||||
## 如何编译
|
||||
|
||||
依赖:
|
||||
### 构建依赖
|
||||
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/)
|
||||
- [cmake](https://cmake.org/download/) 3.21 及以上(系统版本过低时由 xmake 自动安装)
|
||||
|
||||
### Linux
|
||||
|
||||
Linux 构建目前支持 Ubuntu 22.04 及以上版本的 amd64 和 arm64。先安装基础编译依赖:
|
||||
Linux 构建支持 Ubuntu 20.04 及以上版本的 amd64 和 arm64。发布安装包以
|
||||
Ubuntu 20.04/glibc 2.31 为兼容基线。先安装基础编译依赖:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
|
||||
git curl unzip build-essential gcc-10 g++-10 python3-pip \
|
||||
pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
@@ -97,10 +120,19 @@ sudo apt-get install -y \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
```
|
||||
|
||||
Ubuntu 20.04 自带的 CMake 3.16 不满足要求时,xmake 会自动下载并使用
|
||||
CMake 3.21 或更高版本,无需手动替换系统 CMake。
|
||||
|
||||
Ubuntu 20.04 默认的 GCC 9 缺少 Slint C++ API 所需的部分 C++20 标准库,
|
||||
因此上述命令同时安装并在后续配置中指定 GCC 10。
|
||||
|
||||
需要启用 Wayland 捕获或 DRM 捕获时,再安装对应依赖:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
|
||||
sudo apt-get install -y libdbus-1-dev libdrm-dev
|
||||
|
||||
# Ubuntu 22.04 及以上也可以直接安装系统开发包
|
||||
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
|
||||
```
|
||||
|
||||
下载子模块并编译 Release 版本:
|
||||
@@ -112,16 +144,25 @@ cd crossdesk
|
||||
# 已经克隆过仓库时执行这一行
|
||||
git submodule update --init --recursive
|
||||
|
||||
xmake f -m release --USE_CUDA=false -y
|
||||
# Ubuntu 20.04 没有 PipeWire 0.3 开发包,安装仅用于编译的头文件 SDK。
|
||||
# 已安装发行版 PipeWire 0.3 开发包时可跳过此步骤。
|
||||
sudo ./docker/linux-build/install-pipewire-sdk.sh
|
||||
|
||||
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
这里应使用 `--toolchain=gcc-10`,以确保 CrossDesk 及 xmake 在配置阶段构建的
|
||||
libyuv 等第三方包都使用 GCC 10。只指定 `--cc`/`--cxx` 时,依赖安装可能仍回退到
|
||||
系统默认的 `/bin/cc` 和 `/bin/c++`。`-c` 会清除旧配置,切换编译器时必须保留。
|
||||
|
||||
amd64 构建结果位于 `build/linux/x86_64/release/crossdesk`,arm64 构建结果位于 `build/linux/arm64/release/crossdesk`。
|
||||
|
||||
启用 Wayland 和 DRM 的配置示例:
|
||||
|
||||
```bash
|
||||
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
|
||||
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||
--toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
@@ -136,6 +177,8 @@ xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
打包脚本会将 Slint 共享运行库安装到软件包私有目录 `/usr/lib/crossdesk`,无需用户另外安装 `libslint_cpp.so`。
|
||||
PipeWire 不属于强制运行时依赖:程序在运行时检测宿主系统的 PipeWire 0.3,
|
||||
没有该运行库时仍可使用 X11(以及构建时启用的 DRM)捕获。
|
||||
|
||||
### 通用编译选项
|
||||
|
||||
@@ -158,10 +201,11 @@ xmake r crossdesk
|
||||
|
||||
### 无 CUDA 环境下的开发支持
|
||||
|
||||
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
|
||||
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 20.04 兼容构建镜像](https://hub.docker.com/r/crossdesk/ubuntu20.04)。该镜像内置必要的构建依赖,可生成兼容 glibc 2.31 的单一 Linux 安装包。
|
||||
|
||||
进入容器,下载工程后执行:
|
||||
```
|
||||
|
||||
```bash
|
||||
export CUDA_PATH=/usr/local/cuda
|
||||
export XMAKE_GLOBALDIR=/data
|
||||
|
||||
@@ -170,50 +214,65 @@ xmake b --root -vy crossdesk
|
||||
```
|
||||
|
||||
对于**未安装 CUDA 环境的 Windows 开发者**,执行下面的命令安装 CUDA 编译环境:
|
||||
```
|
||||
|
||||
```powershell
|
||||
xmake require -vy "cuda 12.6.3"
|
||||
```
|
||||
|
||||
安装完成后执行:
|
||||
```
|
||||
|
||||
```powershell
|
||||
xmake require --info "cuda 12.6.3"
|
||||
```
|
||||
|
||||
输出如下:
|
||||
|
||||
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
||||
|
||||
根据上述输出获取到 CUDA 的安装目录,即 installdir 指向的位置。将 CUDA_PATH 加入系统环境变量,或在终端中输入:
|
||||
```
|
||||
|
||||
```powershell
|
||||
set CUDA_PATH=path_to_cuda_installdir
|
||||
```
|
||||
|
||||
重新执行:
|
||||
```
|
||||
|
||||
```powershell
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### 注意
|
||||
|
||||
运行时如果客户端状态栏显示 **未连接服务器**,请先在 [CrossDesk 官方网站](https://www.crossdesk.cn/) 安装客户端,以便在环境中安装所需的证书文件。
|
||||
|
||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||
|
||||
## 关于 Xmake
|
||||
### 关于 Xmake
|
||||
|
||||
#### 安装 Xmake
|
||||
|
||||
使用 curl:
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -fsSL https://xmake.io/shget.text | bash
|
||||
```
|
||||
|
||||
使用 wget:
|
||||
```
|
||||
|
||||
```bash
|
||||
wget https://xmake.io/shget.text -O - | bash
|
||||
```
|
||||
|
||||
使用 powershell:
|
||||
```
|
||||
|
||||
```powershell
|
||||
irm https://xmake.io/psget.text | iex
|
||||
```
|
||||
|
||||
#### 编译选项
|
||||
```
|
||||
|
||||
```text
|
||||
# 切换编译模式
|
||||
xmake f -m debug/release
|
||||
|
||||
@@ -227,14 +286,22 @@ xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### 运行选项
|
||||
```
|
||||
|
||||
```bash
|
||||
# 使用调试模式运行
|
||||
xmake r -d crossdesk
|
||||
```
|
||||
更多使用方法可参考 [Xmake官方文档](https://xmake.io/guide/quick-start.html) 。
|
||||
|
||||
更多使用方法可参考 [Xmake 官方文档](https://xmake.io/guide/quick-start.html)。
|
||||
|
||||
---
|
||||
|
||||
## 自托管服务器
|
||||
推荐使用Docker部署CrossDesk Server。
|
||||
|
||||
### 服务端
|
||||
|
||||
推荐使用 Docker 部署 CrossDesk Server。
|
||||
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
@@ -252,7 +319,8 @@ sudo docker run -d \
|
||||
|
||||
上述命令中,用户需注意的参数如下:
|
||||
|
||||
**参数**
|
||||
#### 配置参数
|
||||
|
||||
- EXTERNAL_IP:服务器公网 IP , 对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器地址**
|
||||
- INTERNAL_IP:服务器内网 IP
|
||||
- CROSSDESK_SERVER_PORT:自托管服务使用的端口,对应 CrossDesk 客户端**自托管服务器配置**中填写的**服务器端口**
|
||||
@@ -261,7 +329,8 @@ sudo docker run -d \
|
||||
- `-v /var/lib/crossdesk:/var/lib/crossdesk`:持久化数据库和证书文件到宿主机
|
||||
- `-v /var/log/crossdesk:/var/log/crossdesk`:持久化日志文件到宿主机
|
||||
|
||||
**示例**:
|
||||
#### 部署示例
|
||||
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
@@ -277,26 +346,31 @@ sudo docker run -d \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**注意**:
|
||||
#### 注意事项
|
||||
|
||||
- **服务器需开放端口:COTURN_PORT/udp,COTURN_PORT/tcp,MIN_PORT-MAX_PORT/udp,CROSSDESK_SERVER_PORT/tcp。**
|
||||
- 如果不挂载 volume,容器删除后数据会丢失
|
||||
- 证书文件会在首次启动时自动生成并持久化到宿主机的 `/var/lib/crossdesk/certs` 路径下。由于默认使用的是自签证书,无法保障安全性,建议在云服务商申请正式证书放到该目录下并重启服务。
|
||||
- 数据库文件会自动创建并持久化到宿主机的 `/var/lib/crossdesk/db/crossdesk-server.db` 路径下
|
||||
- 日志文件会自动创建并持久化到宿主机的 `/var/log/crossdesk/` 路径下
|
||||
|
||||
**权限注意**:如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
|
||||
- 证书生成失败,容器启动脚本会报错退出
|
||||
- 数据库目录创建失败,程序会抛出异常并崩溃
|
||||
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
|
||||
|
||||
**解决方案**:在启动容器前手动设置权限:
|
||||
#### 目录权限
|
||||
|
||||
如果 Docker 自动创建的目录权限不足(属于 root),容器内用户无法写入,会导致:
|
||||
|
||||
- 证书生成失败,容器启动脚本会报错退出
|
||||
- 数据库目录创建失败,程序会抛出异常并崩溃
|
||||
- 日志目录创建失败,日志文件无法写入(但程序可能继续运行)
|
||||
|
||||
在启动容器前手动设置权限:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
|
||||
### 客户端
|
||||
|
||||
1. 点击右上角设置进入设置页面。<br><br>
|
||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
||||
|
||||
@@ -304,32 +378,56 @@ sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
<img width="600" height="160" alt="image" src="https://github.com/user-attachments/assets/24c761a3-1985-4d7e-84be-787383c2afb8" /><br>
|
||||
|
||||
3. 输入`服务器地址`(**EXTERNAL_IP**)、`信令服务端口`(**CROSSDESK_SERVER_PORT**)、`中继服务端口`(**COTURN_PORT**),点击确认按钮。
|
||||
|
||||
|
||||
4. 勾选`自托管服务器配置`选项,点击确认按钮保存设置。如果服务端使用的是正式证书,则到此步骤为止,客户端即可显示已连接服务器。
|
||||
|
||||
5. 如果使用默认证书(正式证书忽略此步骤),则需要将服务端`/var/lib/crossdesk/certs/`目录下的`api.crossdesk.cn_root.crt`自签根证书下载到运行客户端的机器,并执行下述命令安装证书:
|
||||
|
||||
Windows 平台使用**管理员权限**打开 PowerShell 执行
|
||||
```
|
||||
**Windows:** 使用管理员权限打开 PowerShell 执行:
|
||||
|
||||
```powershell
|
||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||
```
|
||||
Linux
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
macOS
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||
```
|
||||
|
||||
### Web 客户端
|
||||
### 自托管 Web 客户端
|
||||
|
||||
详情见项目 [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
|
||||
# 常见问题
|
||||
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md) 。
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
见 [常见问题](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md)。
|
||||
|
||||
---
|
||||
|
||||
## 致谢
|
||||
|
||||
# 致谢
|
||||
- 感谢 [HelloGitHub](https://hellogithub.com/) 的推荐与关注。
|
||||
- 感谢 [阮一峰的科技爱好者周刊](https://github.com/ruanyf/weekly) 的收录与推荐。
|
||||
- 感谢 [LinuxDo](https://linux.do) 社区的关注、交流与支持,为 CrossDesk 项目的完善提供了帮助。
|
||||
|
||||
---
|
||||
|
||||
## Code signing policy
|
||||
|
||||
CrossDesk 使用 SignPath.io 为本仓库构建的官方 Windows 发布包提供代码签名。
|
||||
|
||||
**Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).**
|
||||
|
||||
- **Committers and reviewers(代码提交与审核):** [kunkundi](https://github.com/kunkundi)
|
||||
- **Approvers(签名审批):** [kunkundi](https://github.com/kunkundi)
|
||||
- **Privacy policy(隐私政策):** 除非用户或安装、运行 CrossDesk 的人员明确要求,否则 CrossDesk 不会向其他联网系统传输任何信息。
|
||||
|
||||
+160
-59
@@ -1,39 +1,51 @@
|
||||
# CrossDesk
|
||||
|
||||
<a href="https://hellogithub.com/repository/kunkundi/crossdesk" target="_blank"><img src="https://api.hellogithub.com/v1/widgets/recommend.svg?rid=55d41367570345f1838e02fd12be7961&claim_uid=cb0OpZRrBuGVAfL&theme=small" alt="Featured|HelloGitHub" /></a>
|
||||
[]()
|
||||
|
||||
[](https://www.crossdesk.cn/)
|
||||
[](https://www.gnu.org/licenses/gpl-3.0)
|
||||
[](https://github.com/kunkundi/crossdesk/commits/web-client)
|
||||
[](https://github.com/kunkundi/crossdesk/actions)
|
||||
[](https://hub.docker.com/r/crossdesk/crossdesk-server/tags)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
[](https://github.com/kunkundi/crossdesk/issues)
|
||||
[](https://github.com/kunkundi/crossdesk/stargazers)
|
||||
[](https://github.com/kunkundi/crossdesk/forks)
|
||||
|
||||
[ [中文](README.md) / English ]
|
||||
|
||||
PC Client
|
||||
---
|
||||
|
||||
## Preview
|
||||
|
||||
### PC Client
|
||||
|
||||

|
||||
|
||||
Web Client
|
||||
### Web Client
|
||||
|
||||
<p align="center">
|
||||
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
|
||||
</p>
|
||||
|
||||
# Intro
|
||||
---
|
||||
|
||||
## Introduction
|
||||
|
||||
CrossDesk is a lightweight cross-platform remote desktop software.
|
||||
|
||||
CrossDesk is an experimental application of [MiniRTC](https://github.com/kunkundi/minirtc.git), a lightweight cross-platform real-time audio and video transmission library. MiniRTC provides fundamental capabilities including network traversal ([RFC5245](https://datatracker.ietf.org/doc/html/rfc5245)), video software/hardware encoding and decoding (H264/AV1), audio encoding/decoding ([Opus](https://github.com/xiph/opus)), signaling interaction, network congestion control, and transmission encryption ([SRTP](https://tools.ietf.org/html/rfc3711)).
|
||||
|
||||
---
|
||||
|
||||
## System Requirements
|
||||
|
||||
| Platform | Minimum Version |
|
||||
|-----------|-----------------|
|
||||
| --- | --- |
|
||||
| **Windows** | Windows 10 or later (64-bit) |
|
||||
| **macOS** | macOS Intel 15.0 or later *(versions between 14.0 and 15.0 can be built manually for compatibility)*<br>macOS Apple Silicon 14.0 or later |
|
||||
| **Linux** | Ubuntu 22.04 or later *(older versions can be built manually for compatibility)* |
|
||||
| **Linux** | Ubuntu 20.04 or later |
|
||||
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -49,15 +61,17 @@ Before connecting, you can customize configuration options in the settings, such
|
||||
|
||||

|
||||
|
||||
### Web Client
|
||||
### Using the Web Client
|
||||
|
||||
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
|
||||
Visit [CrossDesk Web Client](https://web.crossdesk.cn/).
|
||||
Enter the **Remote Device ID** and **Password**, then click Connect to access the remote device. As shown, **iOS Safari remotely controlling Windows 11**:
|
||||
|
||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||
|
||||
### Windows Service (CrossDesk Service)
|
||||
|
||||
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
|
||||
|
||||
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
|
||||
- Remote `Ctrl+Alt+Del` (SAS) delivery.
|
||||
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
|
||||
@@ -65,7 +79,8 @@ CrossDesk provides a local helper service on Windows named **CrossDesk Service**
|
||||
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
|
||||
|
||||
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
|
||||
```
|
||||
|
||||
```powershell
|
||||
# install
|
||||
.\CrossDesk.exe --service-install
|
||||
# start
|
||||
@@ -81,20 +96,26 @@ For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_
|
||||
|
||||
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
|
||||
|
||||
---
|
||||
|
||||
## How to build
|
||||
|
||||
Requirements:
|
||||
### Build requirements
|
||||
|
||||
- [xmake](https://xmake.io/#/guide/installation)
|
||||
- [cmake](https://cmake.org/download/)
|
||||
- [cmake](https://cmake.org/download/) 3.21 or later (installed automatically by xmake when the system version is too old)
|
||||
|
||||
### Linux
|
||||
|
||||
Linux builds currently support Ubuntu 22.04 or later on amd64 and arm64. Install the base build dependencies first:
|
||||
Linux builds support Ubuntu 20.04 or later on amd64 and arm64. Release packages
|
||||
use Ubuntu 20.04/glibc 2.31 as their compatibility baseline. Install the base
|
||||
build dependencies first:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y \
|
||||
git curl unzip build-essential cmake pkg-config binutils dpkg-dev \
|
||||
git curl unzip build-essential gcc-10 g++-10 python3-pip \
|
||||
pkg-config binutils dpkg-dev \
|
||||
libx11-dev libxext-dev libxrender-dev libxft-dev libxrandr-dev \
|
||||
libxinerama-dev libxcursor-dev libxi-dev libxfixes-dev libxv-dev \
|
||||
libxtst-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev \
|
||||
@@ -102,10 +123,21 @@ sudo apt-get install -y \
|
||||
libasound2-dev libsndio-dev libpulse-dev
|
||||
```
|
||||
|
||||
When Ubuntu 20.04's bundled CMake 3.16 is too old, xmake automatically
|
||||
downloads and uses CMake 3.21 or later; replacing the system CMake manually is
|
||||
not required.
|
||||
|
||||
Ubuntu 20.04's default GCC 9 lacks parts of the C++20 standard library needed
|
||||
by Slint's C++ API, so the commands above install GCC 10 and the configuration
|
||||
below selects it explicitly.
|
||||
|
||||
Install the optional dependencies when Wayland capture or DRM capture is enabled:
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y libdbus-1-dev libpipewire-0.3-dev libdrm-dev
|
||||
sudo apt-get install -y libdbus-1-dev libdrm-dev
|
||||
|
||||
# Ubuntu 22.04 and later can alternatively use the distribution packages.
|
||||
# sudo apt-get install -y libpipewire-0.3-dev libspa-0.2-dev
|
||||
```
|
||||
|
||||
Clone the submodules and build a release binary:
|
||||
@@ -117,16 +149,28 @@ cd crossdesk
|
||||
# Run this when the repository has already been cloned
|
||||
git submodule update --init --recursive
|
||||
|
||||
xmake f -m release --USE_CUDA=false -y
|
||||
# Ubuntu 20.04 has no PipeWire 0.3 development package. Install the
|
||||
# header-only SDK used during compilation. Skip this when the distribution's
|
||||
# PipeWire 0.3 development packages are already installed.
|
||||
sudo ./docker/linux-build/install-pipewire-sdk.sh
|
||||
|
||||
xmake f -c -m release --USE_CUDA=false --toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
Use `--toolchain=gcc-10` so that both CrossDesk and third-party packages such
|
||||
as libyuv, which xmake builds during configuration, use GCC 10. Specifying only
|
||||
`--cc`/`--cxx` may still let dependency builds fall back to the system
|
||||
`/bin/cc` and `/bin/c++`. Keep `-c` when switching compilers to clear the old
|
||||
configuration.
|
||||
|
||||
The amd64 binary is written to `build/linux/x86_64/release/crossdesk`; the arm64 binary is written to `build/linux/arm64/release/crossdesk`.
|
||||
|
||||
Example configuration with Wayland and DRM capture enabled:
|
||||
|
||||
```bash
|
||||
xmake f -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false -y
|
||||
xmake f -c -m release --USE_WAYLAND=true --USE_DRM=true --USE_CUDA=false \
|
||||
--toolchain=gcc-10 -y
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
@@ -141,6 +185,9 @@ Build a Debian package after compiling a release binary on the matching architec
|
||||
```
|
||||
|
||||
The package scripts install the shared Slint runtime in the private `/usr/lib/crossdesk` directory, so users do not need to install `libslint_cpp.so` separately.
|
||||
PipeWire is not a mandatory runtime dependency. CrossDesk detects the host's
|
||||
PipeWire 0.3 runtime dynamically and can still use X11 capture (and DRM when
|
||||
enabled at build time) when it is unavailable.
|
||||
|
||||
### Common build options
|
||||
|
||||
@@ -161,13 +208,14 @@ Run:
|
||||
xmake r crossdesk
|
||||
```
|
||||
|
||||
#### Development Without CUDA Environment
|
||||
### Development Without CUDA Environment
|
||||
|
||||
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
|
||||
This image comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup.
|
||||
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 20.04 compatibility build image](https://hub.docker.com/r/crossdesk/ubuntu20.04) is provided.
|
||||
This image contains the required build dependencies and produces a single Linux package compatible with the glibc 2.31 baseline.
|
||||
|
||||
After entering the container, download the project and run:
|
||||
```
|
||||
|
||||
```bash
|
||||
export CUDA_PATH=/usr/local/cuda
|
||||
export XMAKE_GLOBALDIR=/data
|
||||
|
||||
@@ -176,53 +224,68 @@ xmake b --root -vy crossdesk
|
||||
```
|
||||
|
||||
For **Windows developers without a CUDA environment** installed, run the following command to install the CUDA build environment:
|
||||
```
|
||||
|
||||
```powershell
|
||||
xmake require -vy "cuda 12.6.3"
|
||||
```
|
||||
|
||||
After the installation is complete, execute:
|
||||
```
|
||||
|
||||
```powershell
|
||||
xmake require --info "cuda 12.6.3"
|
||||
```
|
||||
|
||||
The output will look like this:
|
||||
|
||||
<img width="860" height="226" alt="Image" src="https://github.com/user-attachments/assets/999ac365-581a-4b9a-806e-05eb3e4cf44d" />
|
||||
|
||||
From the output above, locate the CUDA installation directory — this is the path pointed to by installdir.
|
||||
Add this path to your system environment variable CUDA_PATH, or set it in the terminal using:
|
||||
|
||||
```powershell
|
||||
set CUDA_PATH=path_to_cuda_installdir
|
||||
```
|
||||
set CUDA_PATH=path_to_cuda_installdir:
|
||||
```
|
||||
|
||||
Then re-run:
|
||||
```
|
||||
|
||||
```powershell
|
||||
xmake f --USE_CUDA=true
|
||||
xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### Notice
|
||||
|
||||
If the client status bar shows **Disconnected** during runtime, please first install the client from the [CrossDesk official website](https://www.crossdesk.cn/) to ensure the required certificate files are available in the environment.
|
||||
|
||||
<img width="256" height="120" alt="image" src="https://github.com/user-attachments/assets/1812f7d6-516b-4b4f-8a3d-98bee505cc5a" />
|
||||
|
||||
## About Xmake
|
||||
### About Xmake
|
||||
|
||||
#### Installing Xmake
|
||||
|
||||
You can install Xmake using one of the following methods:
|
||||
|
||||
Using curl:
|
||||
```
|
||||
|
||||
```bash
|
||||
curl -fsSL https://xmake.io/shget.text | bash
|
||||
```
|
||||
|
||||
Using wget:
|
||||
```
|
||||
|
||||
```bash
|
||||
wget https://xmake.io/shget.text -O - | bash
|
||||
```
|
||||
|
||||
Using powershell:
|
||||
```
|
||||
|
||||
```powershell
|
||||
irm https://xmake.io/psget.text | iex
|
||||
```
|
||||
|
||||
#### Build Options
|
||||
```
|
||||
|
||||
```text
|
||||
# Switch build mode
|
||||
xmake f -m debug/release
|
||||
|
||||
@@ -236,16 +299,23 @@ xmake b -vy crossdesk
|
||||
```
|
||||
|
||||
#### Run Options
|
||||
```
|
||||
|
||||
```bash
|
||||
# Run in debug mode
|
||||
xmake r -d crossdesk
|
||||
```
|
||||
|
||||
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html) .
|
||||
For more information, please refer to the [official Xmake documentation](https://xmake.io/guide/quick-start.html).
|
||||
|
||||
---
|
||||
|
||||
## Self-Hosted Server
|
||||
|
||||
### Server
|
||||
|
||||
It is recommended to deploy CrossDesk Server using Docker.
|
||||
```
|
||||
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
--network host \
|
||||
@@ -262,7 +332,8 @@ sudo docker run -d \
|
||||
|
||||
The parameters you need to pay attention to are as follows:
|
||||
|
||||
**Parameters**
|
||||
#### Parameters
|
||||
|
||||
- **EXTERNAL_IP**: The server’s public IP. This corresponds to **Server Address** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||
- **INTERNAL_IP**: The server’s internal IP.
|
||||
- **CROSSDESK_SERVER_PORT**: The port used by the self-hosted service. This corresponds to **Server Port** in the CrossDesk client’s **Self-Hosted Server Configuration**.
|
||||
@@ -271,7 +342,8 @@ The parameters you need to pay attention to are as follows:
|
||||
- `-v /var/lib/crossdesk:/var/lib/crossdesk`: Persists database and certificate files on the host machine.
|
||||
- `-v /var/log/crossdesk:/var/log/crossdesk`: Persists log files on the host machine.
|
||||
|
||||
**Example**:
|
||||
#### Example
|
||||
|
||||
```bash
|
||||
sudo docker run -d \
|
||||
--name crossdesk_server \
|
||||
@@ -287,29 +359,35 @@ sudo docker run -d \
|
||||
crossdesk/crossdesk-server:v1.1.6
|
||||
```
|
||||
|
||||
**Notes**
|
||||
#### Notes
|
||||
|
||||
- **The server must open the following ports: COTURN_PORT/udp, COTURN_PORT/tcp, MIN_PORT–MAX_PORT/udp, and CROSSDESK_SERVER_PORT/tcp.**
|
||||
- If you don’t mount volumes, all data will be lost when the container is removed.
|
||||
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`.As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
|
||||
- Certificate files will be automatically generated on first startup and persisted to the host at `/var/lib/crossdesk/certs`. As the default certificates are self-signed and cannot guarantee security, it is strongly recommended to apply for a trusted certificate from a cloud provider, deploy it to this directory, and restart the service.
|
||||
- The database file will be automatically created and stored at `/var/lib/crossdesk/db/crossdesk-server.db`.
|
||||
- Log files will be created and stored at `/var/log/crossdesk/`.
|
||||
|
||||
**Permission Notice**
|
||||
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
|
||||
- Certificate generation failure, leading to startup script errors and container exit.
|
||||
- Database directory creation failure, causing the program to throw exceptions and crash.
|
||||
- Log directory creation failure, preventing logs from being written (though the program may continue running).
|
||||
#### Directory permissions
|
||||
|
||||
If the directories automatically created by Docker belong to root and have insufficient write permissions, the container user may not be able to write to them. This can cause:
|
||||
|
||||
- Certificate generation failure, leading to startup script errors and container exit.
|
||||
- Database directory creation failure, causing the program to throw exceptions and crash.
|
||||
- Log directory creation failure, preventing logs from being written (though the program may continue running).
|
||||
|
||||
Manually set permissions before starting the container:
|
||||
|
||||
**Solution:** Manually set permissions before starting the container:
|
||||
```bash
|
||||
sudo mkdir -p /var/lib/crossdesk /var/log/crossdesk
|
||||
sudo chown -R $(id -u):$(id -g) /var/lib/crossdesk /var/log/crossdesk
|
||||
```
|
||||
|
||||
### Server Side
|
||||
#### TLS certificate
|
||||
|
||||
Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/your/certs** directory.
|
||||
|
||||
### Client Side
|
||||
### Client
|
||||
|
||||
1. Click the settings icon in the top-right corner to enter the settings page.<br><br>
|
||||
<img width="600" height="210" alt="image" src="https://github.com/user-attachments/assets/6431131d-b32a-4726-8783-6788f47baa3b" /><br>
|
||||
|
||||
@@ -322,28 +400,51 @@ Place **crossdesk.cn.key** and **crossdesk.cn_bundle.crt** into the **/path/to/y
|
||||
|
||||
5. If the default certificate is used (skip this step if an official certificate is used), download the self-signed root certificate `api.crossdesk.cn_root.crt` from the server directory /var/lib/crossdesk/certs/ to the machine running the client, and install the certificate by executing the following command:
|
||||
|
||||
On Windows, open PowerShell with **administrator privileges** and execute:
|
||||
```
|
||||
**Windows:** Open PowerShell with administrator privileges and execute:
|
||||
|
||||
```powershell
|
||||
certutil -addstore "Root" "C:\path\to\api.crossdesk.cn_root.crt"
|
||||
```
|
||||
Linux
|
||||
```
|
||||
|
||||
**Linux:**
|
||||
|
||||
```bash
|
||||
sudo cp /path/to/api.crossdesk.cn_root.crt /usr/local/share/ca-certificates/api.crossdesk.cn_root.crt
|
||||
sudo update-ca-certificates
|
||||
```
|
||||
macOS
|
||||
```
|
||||
|
||||
**macOS:**
|
||||
|
||||
```bash
|
||||
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain path/to/api.crossdesk.cn_root.crt
|
||||
```
|
||||
|
||||
### Self-Hosted Web Client
|
||||
|
||||
### Web Client
|
||||
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client)。
|
||||
See [CrossDesk Web Client](https://github.com/kunkundi/crossdesk-web-client).
|
||||
|
||||
# FAQ
|
||||
See [FAQ](https://github.com/kunkundi/crosssesk/blob/self-hosted-server/docs/FAQ.md) .
|
||||
---
|
||||
|
||||
## FAQ
|
||||
|
||||
See [FAQ](https://github.com/kunkundi/crossdesk/blob/self-hosted-server/docs/FAQ.md).
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
# Acknowledgements
|
||||
- Thanks to [HelloGitHub](https://hellogithub.com/) for the recommendation and exposure.
|
||||
- Thanks to [Ruanyf Weekly](https://github.com/ruanyf/weekly) for featuring CrossDesk.
|
||||
- Thanks to the [LinuxDo](https://linux.do) community for the attention, discussions, and support that helped improve CrossDesk.
|
||||
|
||||
---
|
||||
|
||||
## Code signing policy
|
||||
|
||||
CrossDesk uses SignPath.io to code-sign official Windows releases built from this repository.
|
||||
|
||||
**Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/).**
|
||||
|
||||
- **Committers and reviewers:** [kunkundi](https://github.com/kunkundi)
|
||||
- **Approvers:** [kunkundi](https://github.com/kunkundi)
|
||||
- **Privacy policy:** CrossDesk will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
|
||||
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu22.04
|
||||
ARG BASE_IMAGE=nvidia/cuda:12.6.3-devel-ubuntu20.04
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG CMAKE_VERSION=3.31.6
|
||||
ARG XMAKE_VERSION=3.0.9
|
||||
ARG RUST_VERSION=1.92.0
|
||||
ARG RUSTUP_VERSION=1.28.2
|
||||
|
||||
LABEL org.opencontainers.image.title="CrossDesk Linux build environment" \
|
||||
org.opencontainers.image.description="Ubuntu 22.04 build environment for CrossDesk" \
|
||||
org.opencontainers.image.description="Ubuntu 20.04 compatibility build environment for CrossDesk" \
|
||||
org.opencontainers.image.source="https://github.com/kunkundi/crossdesk" \
|
||||
org.opencontainers.image.licenses="GPL-3.0-only"
|
||||
|
||||
@@ -22,6 +23,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
|
||||
XMAKE_GLOBALDIR=/data \
|
||||
XMAKE_ROOT=y \
|
||||
CUDA_PATH=/usr/local/cuda \
|
||||
CC=/usr/bin/gcc-10 \
|
||||
CXX=/usr/bin/g++-10 \
|
||||
PATH=/root/.local/bin:/opt/cargo/bin:/usr/local/bin:${PATH}
|
||||
|
||||
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
||||
@@ -35,13 +38,14 @@ RUN apt-get update \
|
||||
build-essential \
|
||||
ca-certificates \
|
||||
ccache \
|
||||
cmake \
|
||||
curl \
|
||||
dbus \
|
||||
dpkg-dev \
|
||||
file \
|
||||
flex \
|
||||
gettext \
|
||||
gcc-10 \
|
||||
g++-10 \
|
||||
git \
|
||||
libasound2-dev \
|
||||
libdbus-1-dev \
|
||||
@@ -52,11 +56,9 @@ RUN apt-get update \
|
||||
libfreetype6-dev \
|
||||
libgl1-mesa-dev \
|
||||
libmount-dev \
|
||||
libpipewire-0.3-dev \
|
||||
libpulse-dev \
|
||||
libreadline-dev \
|
||||
libsndio-dev \
|
||||
libspa-0.2-dev \
|
||||
libssl-dev \
|
||||
libtool \
|
||||
libx11-dev \
|
||||
@@ -95,6 +97,22 @@ RUN apt-get update \
|
||||
zlib1g-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Slint 1.17 requires CMake 3.21 or newer, while Ubuntu 20.04 provides 3.16.
|
||||
# PyPI publishes pinned manylinux2014 wheels for both amd64 and arm64.
|
||||
RUN python3 -m pip install --no-cache-dir "cmake==${CMAKE_VERSION}" \
|
||||
&& cmake --version | grep -F "cmake version ${CMAKE_VERSION}"
|
||||
|
||||
# Ubuntu 20.04 only ships PipeWire 0.2. Install the Ubuntu 22.04-era 0.3.48
|
||||
# headers as a build-only SDK. CrossDesk loads the host's PipeWire 0.3 runtime
|
||||
# with dlopen(), so no PipeWire shared library is copied into release packages.
|
||||
COPY docker/linux-build/install-pipewire-sdk.sh \
|
||||
/usr/local/bin/install-crossdesk-pipewire-sdk
|
||||
|
||||
RUN chmod 0755 /usr/local/bin/install-crossdesk-pipewire-sdk \
|
||||
&& install-crossdesk-pipewire-sdk \
|
||||
&& grep -F "#define PW_MICRO 48" \
|
||||
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h
|
||||
|
||||
RUN export GITHUB_ACTIONS=true \
|
||||
&& curl -fsSL https://xmake.io/shget.text \
|
||||
| bash -s -- "v${XMAKE_VERSION}" \
|
||||
@@ -136,7 +154,8 @@ COPY submodules/minirtc/thirdparty ./submodules/minirtc/thirdparty
|
||||
# CrossDesk's exact dependency graph. Source files are intentionally excluded
|
||||
# from the Docker context; source-only changes therefore reuse this layer.
|
||||
RUN xmake repo -u --root \
|
||||
&& xmake f -m release --USE_CUDA=true --USE_WAYLAND=true --root -y \
|
||||
&& xmake f -m release --USE_CUDA=true --USE_WAYLAND=true \
|
||||
--cc=gcc-10 --cxx=g++-10 --root -y \
|
||||
&& rm -rf /data/.xmake/cache /tmp/crossdesk-dependencies \
|
||||
&& mkdir -p /workspace
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
# CrossDesk Linux 构建镜像
|
||||
|
||||
这个目录维护 CrossDesk 专用的 Ubuntu 22.04 构建环境。镜像包含 xmake、
|
||||
Rust、Linux 开发库(包括 D-Bus、DRM、PipeWire 和 SPA)以及根据项目
|
||||
这个目录维护 CrossDesk 专用的 Ubuntu 20.04 兼容构建环境。镜像包含 GCC 10、
|
||||
CMake 3.31、xmake、Rust、Linux 开发库(包括 D-Bus、DRM,以及仅用于编译的
|
||||
PipeWire 0.3/SPA 头文件 SDK)以及根据项目
|
||||
`xmake.lua` 提前编译好的依赖。
|
||||
|
||||
## 发布方式
|
||||
@@ -11,14 +12,19 @@ Rust、Linux 开发库(包括 D-Bus、DRM、PipeWire 和 SPA)以及根据项
|
||||
全部通过后再合并为同一个 multi-arch 镜像:
|
||||
|
||||
```text
|
||||
crossdesk/ubuntu22.04:buildenv-cuda12.6.3-xmake3.0.9-rust1.92.0
|
||||
crossdesk/ubuntu20.04:buildenv-cuda12.6.3-gcc10-cmake3.31.6-pipewire0.3.48-xmake3.0.9-rust1.92.0
|
||||
```
|
||||
|
||||
Docker 会根据运行机器自动选择正确架构。amd64 变体以
|
||||
`nvidia/cuda:12.6.3-devel-ubuntu22.04` 为基础;arm64 变体不包含当前构建
|
||||
用不到的 CUDA SDK。两个变体都使用 Ubuntu 22.04/glibc 2.35。
|
||||
因此该镜像生成的 Linux 安装包最低支持 Ubuntu 22.04,不再保证兼容
|
||||
Ubuntu 20.04。
|
||||
`nvidia/cuda:12.6.3-devel-ubuntu20.04` 为基础;arm64 变体不包含当前构建
|
||||
用不到的 CUDA SDK。两个变体都使用 Ubuntu 20.04/glibc 2.31,因此生成的
|
||||
单一 Linux 安装包可运行在 Ubuntu 20.04 及更高版本。
|
||||
|
||||
Ubuntu 20.04 仓库只提供 PipeWire 0.2。镜像通过
|
||||
`install-pipewire-sdk.sh` 安装固定版本的 PipeWire 0.3.48/SPA 头文件,但不
|
||||
安装或打包 PipeWire 动态库。CrossDesk 在运行时通过 `dlopen()` 使用宿主系统
|
||||
提供的 PipeWire 0.3:Ubuntu 20.04 没有该运行库时仍可使用 X11(以及构建时
|
||||
启用的 DRM),Ubuntu 22.04 及更高版本则可使用 Wayland 捕获。
|
||||
|
||||
每次成功发布还会生成 `sha-<完整提交 SHA>`,用于精确回滚;`latest` 和上面的
|
||||
工具链版本 tag 只会在两个架构都验证成功后更新。
|
||||
@@ -27,7 +33,7 @@ Ubuntu 20.04。
|
||||
|
||||
在仓库的 Actions secrets 中配置:
|
||||
|
||||
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu22.04` 的 Docker Hub 用户名
|
||||
- `DOCKERHUB_USERNAME`:有权推送 `crossdesk/ubuntu20.04` 的 Docker Hub 用户名
|
||||
- `DOCKERHUB_TOKEN`:该用户的 Docker Hub access token
|
||||
|
||||
工作流文件进入默认分支后,也可以从 Actions 页面手动运行。为了避免首次发布
|
||||
|
||||
Executable
+55
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PIPEWIRE_VERSION="0.3.48"
|
||||
PIPEWIRE_ARCHIVE_SHA256="0d218be86b3d2b548c06259c47ad8d110ee1c09f071e17c4393eeef5c880fa6f"
|
||||
INSTALL_PREFIX="${1:-/opt/crossdesk-pipewire-sdk}"
|
||||
IFS='.' read -r PIPEWIRE_VERSION_MAJOR PIPEWIRE_VERSION_MINOR \
|
||||
PIPEWIRE_VERSION_MICRO <<< "${PIPEWIRE_VERSION}"
|
||||
|
||||
if [[ "${INSTALL_PREFIX}" != /* ]]; then
|
||||
echo "Install prefix must be an absolute path: ${INSTALL_PREFIX}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/crossdesk-pipewire-sdk.XXXXXX")"
|
||||
trap 'rm -rf "${WORK_DIR}"' EXIT
|
||||
|
||||
ARCHIVE_PATH="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
|
||||
SOURCE_DIR="${WORK_DIR}/pipewire-${PIPEWIRE_VERSION}"
|
||||
ARCHIVE_URL="https://gitlab.freedesktop.org/pipewire/pipewire/-/archive/${PIPEWIRE_VERSION}/pipewire-${PIPEWIRE_VERSION}.tar.gz"
|
||||
|
||||
curl --proto '=https' --tlsv1.2 --fail --silent --show-error --location \
|
||||
--retry 3 "${ARCHIVE_URL}" -o "${ARCHIVE_PATH}"
|
||||
echo "${PIPEWIRE_ARCHIVE_SHA256} ${ARCHIVE_PATH}" | sha256sum --check -
|
||||
tar -xzf "${ARCHIVE_PATH}" -C "${WORK_DIR}"
|
||||
|
||||
PIPEWIRE_INCLUDE_DIR="${INSTALL_PREFIX}/include/pipewire-0.3/pipewire"
|
||||
SPA_INCLUDE_DIR="${INSTALL_PREFIX}/include/spa-0.2/spa"
|
||||
|
||||
while IFS= read -r -d '' header_path; do
|
||||
relative_path="${header_path#${SOURCE_DIR}/src/pipewire/}"
|
||||
install -D -m 0644 "${header_path}" \
|
||||
"${PIPEWIRE_INCLUDE_DIR}/${relative_path}"
|
||||
done < <(find "${SOURCE_DIR}/src/pipewire" -type f -name '*.h' -print0)
|
||||
|
||||
while IFS= read -r -d '' header_path; do
|
||||
relative_path="${header_path#${SOURCE_DIR}/spa/include/spa/}"
|
||||
install -D -m 0644 "${header_path}" \
|
||||
"${SPA_INCLUDE_DIR}/${relative_path}"
|
||||
done < <(find "${SOURCE_DIR}/spa/include/spa" -type f -name '*.h' -print0)
|
||||
|
||||
sed \
|
||||
-e "s/@PIPEWIRE_VERSION_MAJOR@/${PIPEWIRE_VERSION_MAJOR}/g" \
|
||||
-e "s/@PIPEWIRE_VERSION_MINOR@/${PIPEWIRE_VERSION_MINOR}/g" \
|
||||
-e "s/@PIPEWIRE_VERSION_MICRO@/${PIPEWIRE_VERSION_MICRO}/g" \
|
||||
-e 's/@PIPEWIRE_API_VERSION@/"0.3"/g' \
|
||||
"${SOURCE_DIR}/src/pipewire/version.h.in" \
|
||||
> "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||
chmod 0644 "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||
|
||||
test -f "${PIPEWIRE_INCLUDE_DIR}/pipewire.h"
|
||||
test -f "${PIPEWIRE_INCLUDE_DIR}/version.h"
|
||||
test -f "${SPA_INCLUDE_DIR}/param/video/format-utils.h"
|
||||
|
||||
echo "Installed PipeWire ${PIPEWIRE_VERSION} header SDK under ${INSTALL_PREFIX}"
|
||||
@@ -2,19 +2,22 @@
|
||||
set -euo pipefail
|
||||
|
||||
EXPECTED_ARCH="${EXPECTED_ARCH:-}"
|
||||
EXPECTED_CMAKE_VERSION="${EXPECTED_CMAKE_VERSION:-3.31.6}"
|
||||
EXPECTED_XMAKE_VERSION="${EXPECTED_XMAKE_VERSION:-3.0.9}"
|
||||
EXPECTED_RUST_VERSION="${EXPECTED_RUST_VERSION:-1.92.0}"
|
||||
BUILD_CC="${CC:-/usr/bin/gcc-10}"
|
||||
BUILD_CXX="${CXX:-/usr/bin/g++-10}"
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /etc/os-release
|
||||
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "22.04" ]]; then
|
||||
echo "Expected Ubuntu 22.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
|
||||
if [[ "${ID}" != "ubuntu" || "${VERSION_ID}" != "20.04" ]]; then
|
||||
echo "Expected Ubuntu 20.04, found ${ID:-unknown} ${VERSION_ID:-unknown}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
GLIBC_VERSION="$(getconf GNU_LIBC_VERSION | awk '{print $2}')"
|
||||
if [[ "${GLIBC_VERSION}" != "2.35" ]]; then
|
||||
echo "Expected glibc 2.35, found ${GLIBC_VERSION}" >&2
|
||||
if [[ "${GLIBC_VERSION}" != "2.31" ]]; then
|
||||
echo "Expected glibc 2.31, found ${GLIBC_VERSION}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -28,13 +31,23 @@ XMAKE_VERSION_OUTPUT="$(xmake --version 2>&1)"
|
||||
grep -F "v${EXPECTED_XMAKE_VERSION}" <<<"${XMAKE_VERSION_OUTPUT}" >/dev/null
|
||||
rustc --version | grep -F "rustc ${EXPECTED_RUST_VERSION}" >/dev/null
|
||||
cargo --version >/dev/null
|
||||
cmake --version >/dev/null
|
||||
g++ --version >/dev/null
|
||||
cmake --version | grep -F "cmake version ${EXPECTED_CMAKE_VERSION}" >/dev/null
|
||||
"${BUILD_CC}" --version | grep -F 'gcc-10' >/dev/null
|
||||
"${BUILD_CXX}" --version | grep -F 'g++-10' >/dev/null
|
||||
printf '%s\n' \
|
||||
'#include <concepts>' \
|
||||
'#include <span>' \
|
||||
'template <typename T> concept TestConcept = true;' \
|
||||
'int main() {' \
|
||||
' int value = 0;' \
|
||||
' std::span<int> values(&value, 1);' \
|
||||
' static_assert(TestConcept<int>);' \
|
||||
' return values.empty();' \
|
||||
'}' \
|
||||
| "${BUILD_CXX}" -std=c++20 -fsyntax-only -x c++ -
|
||||
for pkg_config_package in \
|
||||
dbus-1 \
|
||||
libdrm \
|
||||
libpipewire-0.3 \
|
||||
libspa-0.2 \
|
||||
xft; do
|
||||
if ! pkg-config --exists "${pkg_config_package}"; then
|
||||
echo "Required pkg-config package is missing: ${pkg_config_package}" >&2
|
||||
@@ -42,6 +55,25 @@ for pkg_config_package in \
|
||||
fi
|
||||
done
|
||||
|
||||
for sdk_header in \
|
||||
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/pipewire.h \
|
||||
/opt/crossdesk-pipewire-sdk/include/pipewire-0.3/pipewire/version.h \
|
||||
/opt/crossdesk-pipewire-sdk/include/spa-0.2/spa/param/video/format-utils.h; do
|
||||
if [[ ! -f "${sdk_header}" ]]; then
|
||||
echo "Required PipeWire build SDK header is missing: ${sdk_header}" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
printf '%s\n' \
|
||||
'#include <pipewire/pipewire.h>' \
|
||||
'#include <spa/param/video/format-utils.h>' \
|
||||
'static_assert(PW_CHECK_VERSION(0, 3, 48));' \
|
||||
'int main() { return 0; }' \
|
||||
| "${BUILD_CXX}" -std=c++17 -fsyntax-only -x c++ \
|
||||
-I/opt/crossdesk-pipewire-sdk/include/pipewire-0.3 \
|
||||
-I/opt/crossdesk-pipewire-sdk/include/spa-0.2 -
|
||||
|
||||
for package_path in \
|
||||
"s/slint" \
|
||||
"l/libdatachannel" \
|
||||
|
||||
@@ -7,7 +7,7 @@ if [[ $# -lt 1 || $# -gt 2 ]]; then
|
||||
fi
|
||||
|
||||
INPUT_PATH="$1"
|
||||
MAXIMUM_VERSION="${2:-2.35}"
|
||||
MAXIMUM_VERSION="${2:-2.31}"
|
||||
|
||||
verify_elf() {
|
||||
local elf_file="$1"
|
||||
|
||||
@@ -104,6 +104,15 @@ OUTPUT_FILE="$PROJECT_ROOT/${PKG_NAME}-linux-${DEBIAN_ARCH}-${APP_VERSION}.deb"
|
||||
install -d "$DEBIAN_DIR" "$BIN_DIR" "$PRIVATE_DIR" "$DESKTOP_DIR"
|
||||
install -m 0755 "$BUILD_BINARY" "$PRIVATE_DIR/crossdesk-bin"
|
||||
|
||||
# PipeWire is deliberately optional. The Wayland capturer resolves PipeWire
|
||||
# 0.3 with dlopen() so the same package can run on Ubuntu 20.04 without the
|
||||
# runtime while using the host runtime on newer Ubuntu releases.
|
||||
if readelf -d "$BUILD_BINARY" \
|
||||
| grep -qE 'Shared library: \[libpipewire-0\.3\.so'; then
|
||||
echo "CrossDesk must not link directly to the optional PipeWire runtime" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Slint is intentionally built as a shared library. Keep that runtime private
|
||||
# to CrossDesk instead of relying on a non-existent distribution package.
|
||||
SLINT_NEEDED="$(readelf -d "$BUILD_BINARY" | sed -n \
|
||||
@@ -147,7 +156,7 @@ Version: $DEB_VERSION
|
||||
Architecture: $DEBIAN_ARCH
|
||||
Maintainer: $MAINTAINER
|
||||
Description: $DESCRIPTION
|
||||
Depends: libc6 (>= 2.35), libstdc++6 (>= 9), libx11-6, libxext6,
|
||||
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
|
||||
libxrender1, libxft2, libxrandr2, libxfixes3, libxcb1, libxcb-randr0,
|
||||
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
|
||||
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
||||
|
||||
@@ -9,3 +9,8 @@ CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
|
||||
@@ -6,3 +6,8 @@ IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Portable"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Service"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_service.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -0,0 +1,18 @@
|
||||
// Session helper icon and manifest resources.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
#ifdef CROSSDESK_PORTABLE
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
#elif defined(CROSSDESK_DEBUG)
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Session Helper"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_session_helper.exe"
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -4,6 +4,7 @@
|
||||
; Installer initial constants
|
||||
!define PRODUCT_NAME "CrossDesk"
|
||||
!define PRODUCT_VERSION "${VERSION}"
|
||||
!define PRODUCT_VERSION_NUMERIC "${VERSION_NUMERIC}"
|
||||
!define PRODUCT_PUBLISHER "CrossDesk"
|
||||
!define PRODUCT_WEB_SITE "https://www.crossdesk.cn/"
|
||||
!define APP_NAME "CrossDesk"
|
||||
@@ -41,6 +42,14 @@ RequestExecutionLevel admin
|
||||
|
||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||
OutFile "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||
VIProductVersion "${PRODUCT_VERSION_NUMERIC}"
|
||||
VIAddVersionKey /LANG=2052 "CompanyName" "CrossDesk"
|
||||
VIAddVersionKey /LANG=2052 "FileDescription" "CrossDesk Installer"
|
||||
VIAddVersionKey /LANG=2052 "FileVersion" "${PRODUCT_VERSION}"
|
||||
VIAddVersionKey /LANG=2052 "LegalCopyright" "Copyright (C) CrossDesk contributors"
|
||||
VIAddVersionKey /LANG=2052 "OriginalFilename" "crossdesk-win-x64-${PRODUCT_VERSION}.exe"
|
||||
VIAddVersionKey /LANG=2052 "ProductName" "${PRODUCT_NAME}"
|
||||
VIAddVersionKey /LANG=2052 "ProductVersion" "${PRODUCT_VERSION}"
|
||||
InstallDir "$PROGRAMFILES\CrossDesk"
|
||||
InstallDirRegKey HKCU "Software\${PRODUCT_NAME}" "InstallDir"
|
||||
ShowInstDetails show
|
||||
|
||||
@@ -26,12 +26,40 @@ function Normalize-AppVersion {
|
||||
return $InputVersion
|
||||
}
|
||||
|
||||
function Convert-To-WindowsVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InputVersion
|
||||
)
|
||||
|
||||
$body = $InputVersion -replace '^v', ''
|
||||
$baseMatch = [regex]::Match($body, '^[0-9]+(?:\.[0-9]+){0,3}')
|
||||
if (!$baseMatch.Success) {
|
||||
return "0.0.0.0"
|
||||
}
|
||||
|
||||
$parts = [System.Collections.Generic.List[int]]::new()
|
||||
foreach ($part in $baseMatch.Value.Split('.')) {
|
||||
$value = [int]$part
|
||||
if ($value -gt 65535) {
|
||||
$value = 0
|
||||
}
|
||||
$parts.Add($value)
|
||||
}
|
||||
while ($parts.Count -lt 4) {
|
||||
$parts.Add(0)
|
||||
}
|
||||
|
||||
return ($parts -join '.')
|
||||
}
|
||||
|
||||
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
|
||||
$windowsVersion = Convert-To-WindowsVersion -InputVersion $normalizedVersion
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Push-Location $scriptDir
|
||||
try {
|
||||
& makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi"
|
||||
& makensis "/DVERSION=$normalizedVersion" "/DVERSION_NUMERIC=$windowsVersion" "nsis_script.nsi"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<parameters>
|
||||
<parameter name="version" required="true" />
|
||||
</parameters>
|
||||
<pe-file path="crossdesk-win-x64-${version}.exe"
|
||||
product-name="CrossDesk"
|
||||
product-version="${version}"
|
||||
file-version="${version}"
|
||||
company-name="CrossDesk"
|
||||
copyright="Copyright (C) CrossDesk contributors"
|
||||
original-filename="crossdesk-win-x64-${version}.exe">
|
||||
<authenticode-sign />
|
||||
</pe-file>
|
||||
</artifact-configuration>
|
||||
@@ -0,0 +1,54 @@
|
||||
#include <winver.h>
|
||||
|
||||
#ifndef CROSSDESK_VERSION_STRING
|
||||
#define CROSSDESK_VERSION_STRING "0.0.0"
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_VERSION_NUMERIC
|
||||
#define CROSSDESK_VERSION_NUMERIC 0,0,0,0
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_FILE_DESCRIPTION
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk"
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_ORIGINAL_FILENAME
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "CrossDesk.exe"
|
||||
#endif
|
||||
|
||||
#ifndef CROSSDESK_FILE_TYPE
|
||||
#define CROSSDESK_FILE_TYPE VFT_APP
|
||||
#endif
|
||||
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION CROSSDESK_VERSION_NUMERIC
|
||||
PRODUCTVERSION CROSSDESK_VERSION_NUMERIC
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
FILEFLAGS VS_FF_DEBUG
|
||||
#else
|
||||
FILEFLAGS 0x0L
|
||||
#endif
|
||||
FILEOS VOS_NT_WINDOWS32
|
||||
FILETYPE CROSSDESK_FILE_TYPE
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "CrossDesk\0"
|
||||
VALUE "FileDescription", CROSSDESK_FILE_DESCRIPTION "\0"
|
||||
VALUE "FileVersion", CROSSDESK_VERSION_STRING "\0"
|
||||
VALUE "InternalName", CROSSDESK_ORIGINAL_FILENAME "\0"
|
||||
VALUE "LegalCopyright", "Copyright (C) CrossDesk contributors\0"
|
||||
VALUE "OriginalFilename", CROSSDESK_ORIGINAL_FILENAME "\0"
|
||||
VALUE "ProductName", "CrossDesk\0"
|
||||
VALUE "ProductVersion", CROSSDESK_VERSION_STRING "\0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x0409, 1200
|
||||
END
|
||||
END
|
||||
@@ -0,0 +1,5 @@
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Windows Graphics Capture Plugin"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "wgc_plugin.dll"
|
||||
#define CROSSDESK_FILE_TYPE VFT_DLL
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
#define CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// SendInput copies dwExtraInfo into KBDLLHOOKSTRUCT. Tag CrossDesk-generated
|
||||
// keyboard input so the controller hook can ignore only its own injections
|
||||
// while still accepting input from accessibility tools or remote sessions.
|
||||
inline constexpr std::uintptr_t kInjectedKeyboardInputMarker = 0x4353444B;
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // CROSSDESK_COMMON_WINDOWS_INPUT_MARKER_H_
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <poll.h>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "linux_evdev_keycode.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_key_metadata.h"
|
||||
@@ -17,6 +18,40 @@ namespace crossdesk {
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
static KeyCode ResolveX11Keycode(Display* display, int key_code,
|
||||
uint32_t scan_code, bool extended) {
|
||||
if (!display) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto key_it = vkCodeToX11KeySym.find(key_code);
|
||||
if (key_it != vkCodeToX11KeySym.end()) {
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display, static_cast<KeySym>(key_it->second));
|
||||
if (x11_keycode != 0) {
|
||||
return x11_keycode;
|
||||
}
|
||||
}
|
||||
|
||||
// Some controllers can preserve the physical Windows scan code even when
|
||||
// they cannot resolve a Windows virtual-key value. Xorg's evdev keycodes use
|
||||
// the Linux input code plus the protocol-reserved offset of 8.
|
||||
const int evdev_keycode =
|
||||
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
|
||||
if (evdev_keycode < 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int min_keycode = 0;
|
||||
int max_keycode = 0;
|
||||
XDisplayKeycodes(display, &min_keycode, &max_keycode);
|
||||
const int x11_keycode = evdev_keycode + 8;
|
||||
if (x11_keycode < min_keycode || x11_keycode > max_keycode) {
|
||||
return 0;
|
||||
}
|
||||
return static_cast<KeyCode>(x11_keycode);
|
||||
}
|
||||
|
||||
static KeySym NormalizeKeySym(KeySym key_sym) {
|
||||
if (key_sym >= XK_a && key_sym <= XK_z) {
|
||||
return key_sym - XK_a + XK_A;
|
||||
@@ -62,7 +97,16 @@ KeyboardCapturer::KeyboardCapturer()
|
||||
display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
LOG_ERROR("Failed to open X display.");
|
||||
return;
|
||||
}
|
||||
|
||||
int event_base = 0;
|
||||
int error_base = 0;
|
||||
int major_version = 0;
|
||||
int minor_version = 0;
|
||||
x11_xtest_available_ =
|
||||
XTestQueryExtension(display_, &event_base, &error_base, &major_version,
|
||||
&minor_version) != 0;
|
||||
}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {
|
||||
@@ -70,8 +114,10 @@ KeyboardCapturer::~KeyboardCapturer() {
|
||||
CleanupWaylandPortal();
|
||||
|
||||
if (display_) {
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
XCloseDisplay(display_);
|
||||
display_ = nullptr;
|
||||
x11_xtest_available_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,8 +202,6 @@ int KeyboardCapturer::Unhook() {
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
if (IsWaylandSession()) {
|
||||
if (!use_wayland_portal_ && !wayland_init_attempted_) {
|
||||
wayland_init_attempted_ = true;
|
||||
@@ -181,12 +225,33 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (vkCodeToX11KeySym.find(key_code) != vkCodeToX11KeySym.end()) {
|
||||
int x11_key_code = vkCodeToX11KeySym[key_code];
|
||||
KeyCode keycode = XKeysymToKeycode(display_, x11_key_code);
|
||||
XTestFakeKeyEvent(display_, keycode, is_down, CurrentTime);
|
||||
XFlush(display_);
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
if (!x11_xtest_available_) {
|
||||
LOG_ERROR("XTest extension not available for keyboard injection");
|
||||
return -2;
|
||||
}
|
||||
|
||||
const KeyCode x11_keycode =
|
||||
ResolveX11Keycode(display_, key_code, scan_code, extended);
|
||||
if (x11_keycode == 0) {
|
||||
LOG_WARN(
|
||||
"Cannot map remote keyboard event to X11 keycode, vk_code={}, "
|
||||
"scan_code={}, extended={}",
|
||||
key_code, scan_code, extended);
|
||||
return -3;
|
||||
}
|
||||
|
||||
if (!XTestFakeKeyEvent(display_, x11_keycode, is_down, CurrentTime)) {
|
||||
LOG_ERROR(
|
||||
"XTest keyboard injection failed, vk_code={}, scan_code={}, "
|
||||
"extended={}, x11_keycode={}, is_down={}",
|
||||
key_code, scan_code, extended, static_cast<int>(x11_keycode), is_down);
|
||||
return -4;
|
||||
}
|
||||
|
||||
// Complete the request before reporting success to the keyboard-state
|
||||
// reconciler. This also makes X11 protocol errors observable immediately.
|
||||
XSync(display_, False);
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
@@ -49,6 +50,8 @@ class KeyboardCapturer : public DeviceController {
|
||||
unsigned long root_;
|
||||
std::atomic<bool> running_;
|
||||
std::thread event_thread_;
|
||||
std::mutex x11_injection_mutex_;
|
||||
bool x11_xtest_available_ = false;
|
||||
bool use_wayland_portal_ = false;
|
||||
bool wayland_init_attempted_ = false;
|
||||
DBusConnection* dbus_connection_ = nullptr;
|
||||
|
||||
@@ -611,6 +611,7 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
// Prefer keycode injection to preserve physical-key semantics and avoid
|
||||
// implicit Shift interpretation for uppercase keysyms.
|
||||
if (display_) {
|
||||
std::lock_guard<std::mutex> lock(x11_injection_mutex_);
|
||||
const int keysym = key_it->second;
|
||||
const KeyCode x11_keycode =
|
||||
XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
|
||||
|
||||
@@ -1,36 +1,17 @@
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
#include <hidusage.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
constexpr wchar_t kRawInputWindowClassName[] =
|
||||
L"CrossDeskKeyboardRawInputWindow";
|
||||
|
||||
static int NormalizeModifierVkCode(const KBDLLHOOKSTRUCT* kb_data) {
|
||||
if (kb_data == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (kb_data->vkCode != VK_SHIFT && kb_data->vkCode != VK_CONTROL &&
|
||||
kb_data->vkCode != VK_MENU) {
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
UINT scan_code = static_cast<UINT>(kb_data->scanCode & 0xFF);
|
||||
if ((kb_data->flags & LLKHF_EXTENDED) != 0) {
|
||||
scan_code |= 0xE000;
|
||||
}
|
||||
|
||||
const UINT normalized_vk = MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX);
|
||||
if (normalized_vk != 0) {
|
||||
return static_cast<int>(normalized_vk);
|
||||
}
|
||||
|
||||
return static_cast<int>(kb_data->vkCode);
|
||||
}
|
||||
|
||||
static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
bool PreferSideSpecificVkInjection(int key_code) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
@@ -46,58 +27,247 @@ static bool PreferSideSpecificVkInjection(int key_code) {
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION && g_on_key_action) {
|
||||
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
if ((kbData->flags & LLKHF_INJECTED) != 0) {
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
const int key_code = NormalizeModifierVkCode(kbData);
|
||||
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
g_on_key_action(key_code, true, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
} else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) {
|
||||
g_on_key_action(key_code, false, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
return CallNextHookEx(NULL, nCode, wParam, lParam);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
KeyboardCapturer::KeyboardCapturer() {}
|
||||
|
||||
KeyboardCapturer::~KeyboardCapturer() {}
|
||||
KeyboardCapturer::~KeyboardCapturer() { Unhook(); }
|
||||
|
||||
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
|
||||
g_on_key_action = on_key_action;
|
||||
g_user_ptr = user_ptr;
|
||||
if (capture_thread_.joinable()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
keyboard_hook_ = SetWindowsHookEx(WH_KEYBOARD_LL, KeyboardProc, NULL, 0);
|
||||
if (!keyboard_hook_) {
|
||||
LOG_ERROR("Failed to install keyboard hook");
|
||||
on_key_action_ = on_key_action;
|
||||
user_ptr_ = user_ptr;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
capture_start_complete_ = false;
|
||||
capture_start_succeeded_ = false;
|
||||
}
|
||||
|
||||
capture_thread_ = std::thread(&KeyboardCapturer::RawInputThreadMain, this);
|
||||
|
||||
std::unique_lock<std::mutex> lock(capture_state_mutex_);
|
||||
capture_start_condition_.wait(
|
||||
lock, [this] { return capture_start_complete_; });
|
||||
const bool capture_started = capture_start_succeeded_;
|
||||
lock.unlock();
|
||||
|
||||
if (!capture_started) {
|
||||
if (capture_thread_.joinable()) {
|
||||
capture_thread_.join();
|
||||
}
|
||||
on_key_action_ = nullptr;
|
||||
user_ptr_ = nullptr;
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::Unhook() {
|
||||
if (keyboard_hook_) {
|
||||
g_on_key_action = nullptr;
|
||||
g_user_ptr = nullptr;
|
||||
UnhookWindowsHookEx(keyboard_hook_);
|
||||
keyboard_hook_ = nullptr;
|
||||
DWORD capture_thread_id = 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id = capture_thread_id_;
|
||||
}
|
||||
if (capture_thread_id != 0 &&
|
||||
!PostThreadMessageW(capture_thread_id, WM_QUIT, 0, 0)) {
|
||||
LOG_WARN("Failed to stop keyboard raw input thread, thread_id={}, error={}",
|
||||
capture_thread_id, GetLastError());
|
||||
}
|
||||
if (capture_thread_.joinable()) {
|
||||
capture_thread_.join();
|
||||
}
|
||||
|
||||
on_key_action_ = nullptr;
|
||||
user_ptr_ = nullptr;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// apply remote keyboard commands to the local machine
|
||||
void KeyboardCapturer::RawInputThreadMain() {
|
||||
const DWORD thread_id = GetCurrentThreadId();
|
||||
|
||||
MSG message{};
|
||||
PeekMessageW(&message, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
|
||||
const bool capture_started = CreateRawInputWindow();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = thread_id;
|
||||
capture_start_succeeded_ = capture_started;
|
||||
capture_start_complete_ = true;
|
||||
}
|
||||
capture_start_condition_.notify_one();
|
||||
|
||||
if (!capture_started) {
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Keyboard raw input capture started, thread_id={}", thread_id);
|
||||
while (true) {
|
||||
const BOOL get_message_result = GetMessageW(&message, nullptr, 0, 0);
|
||||
if (get_message_result <= 0) {
|
||||
if (get_message_result < 0) {
|
||||
LOG_WARN("Keyboard raw input message loop failed, thread_id={}, "
|
||||
"error={}",
|
||||
thread_id, GetLastError());
|
||||
}
|
||||
break;
|
||||
}
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
|
||||
DestroyRawInputWindow();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(capture_state_mutex_);
|
||||
capture_thread_id_ = 0;
|
||||
}
|
||||
LOG_INFO("Keyboard raw input capture stopped, thread_id={}", thread_id);
|
||||
}
|
||||
|
||||
bool KeyboardCapturer::CreateRawInputWindow() {
|
||||
const HINSTANCE instance = GetModuleHandleW(nullptr);
|
||||
WNDCLASSEXW window_class{};
|
||||
window_class.cbSize = sizeof(window_class);
|
||||
window_class.lpfnWndProc = &KeyboardCapturer::RawInputWindowProc;
|
||||
window_class.hInstance = instance;
|
||||
window_class.lpszClassName = kRawInputWindowClassName;
|
||||
|
||||
if (RegisterClassExW(&window_class) == 0) {
|
||||
const DWORD error = GetLastError();
|
||||
if (error != ERROR_CLASS_ALREADY_EXISTS) {
|
||||
LOG_WARN("Failed to register keyboard raw input window class, error={}",
|
||||
error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
raw_input_window_ = CreateWindowExW(
|
||||
0, kRawInputWindowClassName, L"", 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr,
|
||||
instance, this);
|
||||
if (!raw_input_window_) {
|
||||
LOG_WARN("Failed to create keyboard raw input window, error={}",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
RAWINPUTDEVICE keyboard_device{};
|
||||
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
|
||||
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
|
||||
keyboard_device.dwFlags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK;
|
||||
keyboard_device.hwndTarget = raw_input_window_;
|
||||
if (!RegisterRawInputDevices(&keyboard_device, 1,
|
||||
sizeof(keyboard_device))) {
|
||||
LOG_WARN("Failed to register keyboard raw input, error={}", GetLastError());
|
||||
DestroyWindow(raw_input_window_);
|
||||
raw_input_window_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
raw_input_registered_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
void KeyboardCapturer::DestroyRawInputWindow() {
|
||||
if (raw_input_registered_) {
|
||||
RAWINPUTDEVICE keyboard_device{};
|
||||
keyboard_device.usUsagePage = HID_USAGE_PAGE_GENERIC;
|
||||
keyboard_device.usUsage = HID_USAGE_GENERIC_KEYBOARD;
|
||||
keyboard_device.dwFlags = RIDEV_REMOVE;
|
||||
keyboard_device.hwndTarget = nullptr;
|
||||
if (!RegisterRawInputDevices(&keyboard_device, 1,
|
||||
sizeof(keyboard_device))) {
|
||||
LOG_WARN("Failed to unregister keyboard raw input, error={}",
|
||||
GetLastError());
|
||||
}
|
||||
raw_input_registered_ = false;
|
||||
}
|
||||
if (raw_input_window_) {
|
||||
DestroyWindow(raw_input_window_);
|
||||
raw_input_window_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardCapturer::RawInputWindowProc(
|
||||
HWND window, UINT message, WPARAM w_param, LPARAM l_param) {
|
||||
KeyboardCapturer* capturer = reinterpret_cast<KeyboardCapturer*>(
|
||||
GetWindowLongPtrW(window, GWLP_USERDATA));
|
||||
if (message == WM_NCCREATE) {
|
||||
auto* create = reinterpret_cast<CREATESTRUCTW*>(l_param);
|
||||
capturer = static_cast<KeyboardCapturer*>(create->lpCreateParams);
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(capturer));
|
||||
} else if (message == WM_INPUT && capturer) {
|
||||
capturer->HandleRawInput(reinterpret_cast<HRAWINPUT>(l_param));
|
||||
} else if (message == WM_NCDESTROY) {
|
||||
SetWindowLongPtrW(window, GWLP_USERDATA, 0);
|
||||
}
|
||||
return DefWindowProcW(window, message, w_param, l_param);
|
||||
}
|
||||
|
||||
void KeyboardCapturer::HandleRawInput(HRAWINPUT raw_input_handle) {
|
||||
RAWINPUT input{};
|
||||
UINT input_size = sizeof(input);
|
||||
const UINT bytes_read =
|
||||
GetRawInputData(raw_input_handle, RID_INPUT, &input, &input_size,
|
||||
sizeof(RAWINPUTHEADER));
|
||||
if (bytes_read == static_cast<UINT>(-1) ||
|
||||
bytes_read < sizeof(RAWINPUTHEADER) ||
|
||||
input.header.dwType != RIM_TYPEKEYBOARD) {
|
||||
return;
|
||||
}
|
||||
|
||||
const RAWKEYBOARD& keyboard = input.data.keyboard;
|
||||
if (keyboard.VKey == 0xFF ||
|
||||
keyboard.ExtraInformation ==
|
||||
static_cast<ULONG>(kInjectedKeyboardInputMarker)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bool is_down = false;
|
||||
if (keyboard.Message == WM_KEYDOWN || keyboard.Message == WM_SYSKEYDOWN) {
|
||||
is_down = true;
|
||||
} else if (keyboard.Message != WM_KEYUP &&
|
||||
keyboard.Message != WM_SYSKEYUP) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool extended = (keyboard.Flags & (RI_KEY_E0 | RI_KEY_E1)) != 0;
|
||||
UINT mapped_scan_code = keyboard.MakeCode;
|
||||
if ((keyboard.Flags & RI_KEY_E0) != 0) {
|
||||
mapped_scan_code |= 0xE000;
|
||||
} else if ((keyboard.Flags & RI_KEY_E1) != 0) {
|
||||
mapped_scan_code |= 0xE100;
|
||||
}
|
||||
if (mapped_scan_code == 0xE11D || mapped_scan_code == 0xE02A) {
|
||||
return;
|
||||
}
|
||||
|
||||
int key_code = static_cast<int>(keyboard.VKey);
|
||||
if (key_code == VK_SHIFT || key_code == VK_CONTROL || key_code == VK_MENU) {
|
||||
const UINT normalized =
|
||||
MapVirtualKeyW(mapped_scan_code, MAPVK_VSC_TO_VK_EX);
|
||||
if (normalized != 0) {
|
||||
key_code = static_cast<int>(normalized);
|
||||
}
|
||||
}
|
||||
|
||||
if (on_key_action_) {
|
||||
on_key_action_(key_code, is_down, keyboard.MakeCode, extended, user_ptr_);
|
||||
}
|
||||
}
|
||||
|
||||
// Apply remote keyboard commands to the local machine.
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo =
|
||||
static_cast<ULONG_PTR>(kInjectedKeyboardInputMarker);
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
@@ -113,7 +283,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else {
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
input.ki.wVk = static_cast<WORD>(key_code);
|
||||
|
||||
if (prefer_vk && resolved_scan_code != 0) {
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
@@ -134,7 +304,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
input.ki.dwFlags |= KEYEVENTF_KEYUP;
|
||||
}
|
||||
|
||||
UINT sent = SendInput(1, &input, sizeof(INPUT));
|
||||
const UINT sent = SendInput(1, &input, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN("SendInput failed for key_code={}, is_down={}, err={}", key_code,
|
||||
is_down, GetLastError());
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "device_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -26,8 +30,25 @@ class KeyboardCapturer : public DeviceController {
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
HHOOK keyboard_hook_ = nullptr;
|
||||
static LRESULT CALLBACK RawInputWindowProc(HWND window, UINT message,
|
||||
WPARAM w_param, LPARAM l_param);
|
||||
|
||||
void RawInputThreadMain();
|
||||
bool CreateRawInputWindow();
|
||||
void DestroyRawInputWindow();
|
||||
void HandleRawInput(HRAWINPUT raw_input_handle);
|
||||
|
||||
OnKeyAction on_key_action_ = nullptr;
|
||||
void* user_ptr_ = nullptr;
|
||||
HWND raw_input_window_ = nullptr;
|
||||
bool raw_input_registered_ = false;
|
||||
std::thread capture_thread_;
|
||||
DWORD capture_thread_id_ = 0;
|
||||
bool capture_start_complete_ = false;
|
||||
bool capture_start_succeeded_ = false;
|
||||
std::mutex capture_state_mutex_;
|
||||
std::condition_variable capture_start_condition_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -943,10 +943,6 @@ void GuiApplication::InitializeSettings() {
|
||||
|
||||
bool GuiApplication::InitializeSDL() {
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (!getenv("SDL_AUDIODRIVER")) {
|
||||
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
|
||||
}
|
||||
|
||||
// Standard Wayland top-level windows cannot choose an absolute screen
|
||||
// position. Use the session's XWayland compatibility server for the GUI so
|
||||
// auxiliary windows such as the controlled-side status window can be
|
||||
@@ -970,6 +966,14 @@ bool GuiApplication::InitializeSDL() {
|
||||
LOG_ERROR("SDL initialization failed: {}", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
const char* active_video_driver = SDL_GetCurrentVideoDriver();
|
||||
// X11 permits the manual move/position operations used by the frameless
|
||||
// main window. Keep native Wayland decorations as a compatibility fallback.
|
||||
use_x11_custom_titlebar_ =
|
||||
active_video_driver != nullptr &&
|
||||
std::strcmp(active_video_driver, "x11") == 0;
|
||||
#endif
|
||||
|
||||
if (const SDL_DisplayMode* mode = SDL_GetCurrentDisplayMode(0)) {
|
||||
screen_width_ = mode->w;
|
||||
@@ -1004,10 +1008,8 @@ void GuiApplication::InitializeUi() {
|
||||
#if _WIN32
|
||||
ui_->main->set_custom_titlebar(true);
|
||||
#elif defined(__linux__)
|
||||
if (use_xwayland_gui_) {
|
||||
ui_->main->set_custom_titlebar(true);
|
||||
ui_->main->set_wayland_titlebar(true);
|
||||
}
|
||||
ui_->main->set_custom_titlebar(use_x11_custom_titlebar_);
|
||||
ui_->main->set_wayland_titlebar(use_xwayland_gui_);
|
||||
#endif
|
||||
std::vector<ui::ReleaseNoteBlock> release_note_blocks;
|
||||
for (const auto& parsed : ParseReleaseNotesMarkdownForSlint(release_notes_)) {
|
||||
@@ -3040,9 +3042,8 @@ void GuiApplication::SendKeyInput(const std::string& text, bool pressed,
|
||||
controlled_remote_id_ = props->remote_id_;
|
||||
focused_remote_id_ = props->remote_id_;
|
||||
|
||||
// Native hooks see the same physical key before Slint does. When a native
|
||||
// hook is active, forwarding the FocusScope callback as well would duplicate
|
||||
// the event on platforms whose hook does not consume the local key.
|
||||
// Native capture forwards the event independently. Slint keyboard events
|
||||
// are used only by platforms whose native capture backend is unavailable.
|
||||
if (keyboard_capturer_is_started_ && !keyboard_capturer_uses_window_events_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ private:
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
bool use_xwayland_gui_ = false;
|
||||
bool use_x11_custom_titlebar_ = false;
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
constexpr size_t kMaxCapturedKeyboardInputs = 512;
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -256,18 +257,18 @@ int SessionDeviceManager::StartKeyboardCapturer() {
|
||||
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
void *user_ptr) {
|
||||
if (user_ptr) {
|
||||
auto *runtime = static_cast<GuiRuntime *>(user_ptr);
|
||||
runtime->keyboard_.SendKeyCommand(key_code, is_down, scan_code,
|
||||
extended);
|
||||
auto *devices = static_cast<SessionDeviceManager *>(user_ptr);
|
||||
devices->QueueCapturedKeyboardInput(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
},
|
||||
&owner_);
|
||||
this);
|
||||
if (hook_ret != 0) {
|
||||
owner_.keyboard_capturer_uses_window_events_ = true;
|
||||
LOG_WARN(
|
||||
"Start keyboard capturer failed, falling back to Slint keyboard events");
|
||||
} else {
|
||||
LOG_INFO("Start keyboard capturer with native hook");
|
||||
LOG_INFO("Start keyboard capturer with native input");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -281,6 +282,7 @@ int SessionDeviceManager::StopKeyboardCapturer() {
|
||||
|
||||
if (keyboard_capturer_) {
|
||||
keyboard_capturer_->Unhook();
|
||||
ClearCapturedKeyboardInput();
|
||||
LOG_INFO("Stop keyboard capturer");
|
||||
}
|
||||
return 0;
|
||||
@@ -410,6 +412,7 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.keyboard_capturer_is_started_ = true;
|
||||
}
|
||||
if (owner_.keyboard_capturer_is_started_) {
|
||||
DrainCapturedKeyboardInput();
|
||||
owner_.keyboard_.SendHeartbeat(false);
|
||||
}
|
||||
} else if (owner_.keyboard_capturer_is_started_) {
|
||||
@@ -421,6 +424,36 @@ void SessionDeviceManager::UpdateInteractions() {
|
||||
owner_.keyboard_.CheckRemoteTimeouts();
|
||||
}
|
||||
|
||||
void SessionDeviceManager::QueueCapturedKeyboardInput(int key_code,
|
||||
bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
if (captured_keyboard_inputs_.size() >= kMaxCapturedKeyboardInputs) {
|
||||
captured_keyboard_inputs_.pop_front();
|
||||
LOG_WARN("Captured keyboard input queue overflow, dropping oldest event");
|
||||
}
|
||||
captured_keyboard_inputs_.push_back(
|
||||
CapturedKeyboardInput{key_code, is_down, scan_code, extended});
|
||||
}
|
||||
|
||||
void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
||||
std::deque<CapturedKeyboardInput> inputs;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
inputs.swap(captured_keyboard_inputs_);
|
||||
}
|
||||
for (const CapturedKeyboardInput &input : inputs) {
|
||||
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
||||
input.scan_code, input.extended);
|
||||
}
|
||||
}
|
||||
|
||||
void SessionDeviceManager::ClearCapturedKeyboardInput() {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
captured_keyboard_inputs_.clear();
|
||||
}
|
||||
|
||||
bool SessionDeviceManager::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -53,6 +55,18 @@ public:
|
||||
const std::vector<DisplayInfo> &display_info_list() const;
|
||||
|
||||
private:
|
||||
struct CapturedKeyboardInput {
|
||||
int key_code = 0;
|
||||
bool is_down = false;
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
};
|
||||
|
||||
void QueueCapturedKeyboardInput(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended);
|
||||
void DrainCapturedKeyboardInput();
|
||||
void ClearCapturedKeyboardInput();
|
||||
|
||||
GuiRuntime &owner_;
|
||||
SDL_AudioStream *output_stream_ = nullptr;
|
||||
ScreenCapturerFactory *screen_capturer_factory_ = nullptr;
|
||||
@@ -63,6 +77,8 @@ private:
|
||||
MouseController *mouse_controller_ = nullptr;
|
||||
KeyboardCapturer *keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::deque<CapturedKeyboardInput> captured_keyboard_inputs_;
|
||||
std::mutex captured_keyboard_inputs_mutex_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
};
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#include "features/input/keyboard_controller.h"
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "minirtc.h"
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
@@ -45,8 +45,8 @@ int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
|
||||
#endif
|
||||
}
|
||||
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
||||
bool *extended_out) {
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (!scan_code_out || !extended_out) {
|
||||
return;
|
||||
}
|
||||
@@ -65,7 +65,7 @@ void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t *scan_code_out,
|
||||
#if _WIN32
|
||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||
|
||||
void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
||||
void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* stage) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (*last_tick != 0 && now - *last_tick < kSecureDesktopInputLogIntervalMs) {
|
||||
return;
|
||||
@@ -77,8 +77,8 @@ void LogSecureDesktopInputBlocked(uint32_t *last_tick, const char *stage) {
|
||||
stage ? stage : "");
|
||||
}
|
||||
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
||||
const RemoteAction &action) {
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||
const RemoteAction& action) {
|
||||
return response.is_object() &&
|
||||
response.value("error", std::string()) == "send_input_failed" &&
|
||||
response.value("code", 0u) == ERROR_ACCESS_DENIED &&
|
||||
@@ -87,9 +87,9 @@ bool IsTransientSecureDesktopInputFailure(const nlohmann::json &response,
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
} // namespace
|
||||
|
||||
KeyboardController::KeyboardController(GuiRuntime &owner) : owner_(owner) {}
|
||||
KeyboardController::KeyboardController(GuiRuntime& owner) : owner_(owner) {}
|
||||
|
||||
void KeyboardController::TrackPressedKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
@@ -106,13 +106,13 @@ void KeyboardController::ForceReleasePressedKeys() {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
pressed_keys.reserve(pressed_keys_.size());
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
for (const auto& [_, key] : pressed_keys_) {
|
||||
pressed_keys.push_back(key);
|
||||
}
|
||||
pressed_keys_.clear();
|
||||
}
|
||||
|
||||
for (const PressedKey &key : pressed_keys) {
|
||||
for (const PressedKey& key : pressed_keys) {
|
||||
SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
SendHeartbeat(true);
|
||||
@@ -130,7 +130,7 @@ void KeyboardController::SendHeartbeat(bool force) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(pressed_keys_mutex_);
|
||||
size_t index = 0;
|
||||
for (const auto &[_, key] : pressed_keys_) {
|
||||
for (const auto& [_, key] : pressed_keys_) {
|
||||
if (index >= kMaxKeyboardStateKeys) {
|
||||
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
|
||||
pressed_keys_.size());
|
||||
@@ -223,9 +223,10 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
|
||||
if (!json.is_discarded() &&
|
||||
IsTransientSecureDesktopInputFailure(json, action)) {
|
||||
LOG_INFO("Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
LOG_INFO(
|
||||
"Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -245,15 +246,15 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
extended);
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
void KeyboardController::ApplyRemoteEvent(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
const int key_code = static_cast<int>(action.k.key_value);
|
||||
const bool is_down = action.k.flag == KeyFlag::key_down;
|
||||
const bool injected =
|
||||
InjectRemoteKey(key_code, is_down, action.k.scan_code, action.k.extended);
|
||||
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
auto &state = remote_states_[remote_id];
|
||||
auto& state = remote_states_[remote_id];
|
||||
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (is_down && injected) {
|
||||
state.pressed_keys[key_code] =
|
||||
@@ -263,13 +264,13 @@ void KeyboardController::ApplyRemoteEvent(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
const RemoteAction &action) {
|
||||
void KeyboardController::ApplyRemoteState(const std::string& remote_id,
|
||||
const RemoteAction& action) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
std::vector<PressedKey> keys_to_press;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
auto &state = remote_states_[remote_id];
|
||||
auto& state = remote_states_[remote_id];
|
||||
if (action.ks.seq != 0 && state.last_seq != 0 &&
|
||||
static_cast<int32_t>(action.ks.seq - state.last_seq) <= 0) {
|
||||
return;
|
||||
@@ -283,24 +284,24 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
const size_t count =
|
||||
(std::min)(action.ks.pressed_count, kMaxKeyboardStateKeys);
|
||||
for (size_t index = 0; index < count; ++index) {
|
||||
const auto &key = action.ks.pressed_keys[index];
|
||||
const auto& key = action.ks.pressed_keys[index];
|
||||
const int key_code = static_cast<int>(key.key_value);
|
||||
desired_keys[key_code] =
|
||||
PressedKey{key_code, key.scan_code, key.extended};
|
||||
}
|
||||
for (const auto &[key_code, key] : state.pressed_keys) {
|
||||
for (const auto& [key_code, key] : state.pressed_keys) {
|
||||
if (desired_keys.find(key_code) == desired_keys.end()) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
}
|
||||
for (const auto &[key_code, key] : desired_keys) {
|
||||
for (const auto& [key_code, key] : desired_keys) {
|
||||
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
|
||||
keys_to_press.push_back(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const PressedKey &key : keys_to_release) {
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
if (InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
const auto state_it = remote_states_.find(remote_id);
|
||||
@@ -309,7 +310,7 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const PressedKey &key : keys_to_press) {
|
||||
for (const PressedKey& key : keys_to_press) {
|
||||
if (InjectRemoteKey(key.key_code, true, key.scan_code, key.extended)) {
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
remote_states_[remote_id].pressed_keys[key.key_code] = key;
|
||||
@@ -317,8 +318,8 @@ void KeyboardController::ApplyRemoteState(const std::string &remote_id,
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason) {
|
||||
void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
const char* reason) {
|
||||
std::vector<PressedKey> keys_to_release;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
@@ -326,7 +327,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
if (state_it == remote_states_.end()) {
|
||||
return;
|
||||
}
|
||||
for (const auto &[_, key] : state_it->second.pressed_keys) {
|
||||
for (const auto& [_, key] : state_it->second.pressed_keys) {
|
||||
keys_to_release.push_back(key);
|
||||
}
|
||||
remote_states_.erase(state_it);
|
||||
@@ -336,7 +337,7 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
|
||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
for (const PressedKey &key : keys_to_release) {
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
}
|
||||
@@ -346,7 +347,7 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
std::vector<std::string> timed_out_remotes;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(remote_states_mutex_);
|
||||
for (const auto &[remote_id, state] : remote_states_) {
|
||||
for (const auto& [remote_id, state] : remote_states_) {
|
||||
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
|
||||
state.last_seen_tick != 0 &&
|
||||
now - state.last_seen_tick > kRemoteReleaseTimeoutMs) {
|
||||
@@ -354,9 +355,9 @@ void KeyboardController::CheckRemoteTimeouts() {
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const std::string &remote_id : timed_out_remotes) {
|
||||
for (const std::string& remote_id : timed_out_remotes) {
|
||||
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -601,6 +601,15 @@ export component MainWindow inherits Window {
|
||||
border-width: 1px;
|
||||
border-color: ImGuiLineStyle.border;
|
||||
border-radius: 4px;
|
||||
// Measure the ID using the preferred field size.
|
||||
// The visible text can then scale only on systems
|
||||
// whose font metrics would otherwise clip it.
|
||||
local-id-width-probe := Text {
|
||||
visible: false;
|
||||
text: root.local-id;
|
||||
font-size: ImGuiFontStyle.field-value;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
Text {
|
||||
x: 8px;
|
||||
// Compensate for the font's ascender-heavy
|
||||
@@ -610,7 +619,9 @@ export component MainWindow inherits Window {
|
||||
height: parent.height - 4px;
|
||||
text: root.local-id;
|
||||
color: #20242a;
|
||||
font-size: ImGuiFontStyle.field-value;
|
||||
font-size: max(18px,
|
||||
ImGuiFontStyle.field-value *
|
||||
(self.width / max(self.width, local-id-width-probe.preferred-width)));
|
||||
letter-spacing: 1px;
|
||||
horizontal-alignment: left;
|
||||
vertical-alignment: center;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "path_manager.h"
|
||||
#include "rd_log.h"
|
||||
#include "session_helper_shared.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -828,6 +829,8 @@ InputInjectionResult InjectKeyboardInput(
|
||||
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.dwExtraInfo = static_cast<ULONG_PTR>(
|
||||
crossdesk::kInjectedKeyboardInputMarker);
|
||||
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: 00aad6aa37...50ca51ecc1
@@ -0,0 +1,52 @@
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <thread>
|
||||
|
||||
#include "keyboard_capturer.h"
|
||||
|
||||
int main() {
|
||||
Display* receiver = XOpenDisplay(nullptr);
|
||||
if (!receiver) {
|
||||
std::fprintf(stderr, "open display failed\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const int screen = DefaultScreen(receiver);
|
||||
Window window = XCreateSimpleWindow(receiver, RootWindow(receiver, screen),
|
||||
0, 0, 100, 100, 0, 0, 0);
|
||||
XSelectInput(receiver, window, KeyPressMask | KeyReleaseMask);
|
||||
XMapWindow(receiver, window);
|
||||
XSync(receiver, False);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
XSetInputFocus(receiver, window, RevertToParent, CurrentTime);
|
||||
XSync(receiver, False);
|
||||
|
||||
crossdesk::KeyboardCapturer keyboard;
|
||||
// Exercise the scan-code fallback: the controller supplied no usable VK,
|
||||
// but did preserve the set-1 scan code for the A key.
|
||||
const int down = keyboard.SendKeyboardCommand(0, true, 0x1E, false);
|
||||
const int up = keyboard.SendKeyboardCommand(0, false, 0x1E, false);
|
||||
|
||||
bool saw_down = false;
|
||||
bool saw_up = false;
|
||||
const auto deadline = std::chrono::steady_clock::now() +
|
||||
std::chrono::seconds(2);
|
||||
while (std::chrono::steady_clock::now() < deadline &&
|
||||
(!saw_down || !saw_up)) {
|
||||
while (XPending(receiver) > 0) {
|
||||
XEvent event{};
|
||||
XNextEvent(receiver, &event);
|
||||
saw_down = saw_down || event.type == KeyPress;
|
||||
saw_up = saw_up || event.type == KeyRelease;
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
XDestroyWindow(receiver, window);
|
||||
XCloseDisplay(receiver);
|
||||
std::printf("down_ret=%d up_ret=%d saw_down=%d saw_up=%d\n", down, up,
|
||||
saw_down, saw_up);
|
||||
return down == 0 && up == 0 && saw_down && saw_up ? 0 : 2;
|
||||
}
|
||||
@@ -84,6 +84,14 @@ int main() {
|
||||
const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc");
|
||||
const std::string portable_rc =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_portable.rc");
|
||||
const std::string service_rc =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_service.rc");
|
||||
const std::string session_helper_rc =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_session_helper.rc");
|
||||
const std::string wgc_plugin_rc =
|
||||
ReadFile(repo_root / "scripts/windows/wgc_plugin.rc");
|
||||
const std::string version_info =
|
||||
ReadFile(repo_root / "scripts/windows/version_info.rcinc");
|
||||
const std::string manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
const std::string debug_manifest =
|
||||
@@ -100,8 +108,31 @@ int main() {
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc,
|
||||
"crossdesk_portable.manifest");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "version_info.rcinc");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc,
|
||||
"version_info.rcinc");
|
||||
ok &= ExpectContains("crossdesk_service.rc", service_rc,
|
||||
"crossdesk_service.exe");
|
||||
ok &= ExpectContains("crossdesk_session_helper.rc", session_helper_rc,
|
||||
"crossdesk_session_helper.exe");
|
||||
ok &= ExpectContains("crossdesk_session_helper.rc", session_helper_rc,
|
||||
"CROSSDESK_PORTABLE");
|
||||
ok &= ExpectContains("crossdesk_session_helper.rc", session_helper_rc,
|
||||
"crossdesk_portable.manifest");
|
||||
ok &= ExpectContains("wgc_plugin.rc", wgc_plugin_rc, "wgc_plugin.dll");
|
||||
ok &= ExpectContains("version_info.rcinc", version_info, "ProductName");
|
||||
ok &= ExpectContains("version_info.rcinc", version_info,
|
||||
"ProductVersion");
|
||||
ok &= ExpectContains("version_info.rcinc", version_info,
|
||||
"OriginalFilename");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets,
|
||||
"scripts/windows/crossdesk_portable.rc");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets,
|
||||
"scripts/windows/crossdesk_service.rc");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets,
|
||||
"scripts/windows/crossdesk_session_helper.rc");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets,
|
||||
"scripts/windows/wgc_plugin.rc");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets, "CROSSDESK_PORTABLE");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
|
||||
Vendored
+17
-1
@@ -9,10 +9,13 @@ package("slint")
|
||||
|
||||
add_configs("shared", {description = "Build the Slint runtime as a shared library", default = true, type = "boolean", readonly = true})
|
||||
|
||||
add_deps("cmake")
|
||||
add_deps("cmake~slint >=3.21 <4.0", {host = true, private = true, system = false})
|
||||
add_deps("rust 1.92.0", {host = true, private = true, system = false})
|
||||
|
||||
on_load(function(package)
|
||||
if package:is_plat("linux") then
|
||||
package:add("deps", "fontconfig", {private = true, system = false})
|
||||
end
|
||||
package:add("includedirs", "include/slint")
|
||||
if package:is_plat("windows") then
|
||||
package:add("links", "slint_cpp.dll")
|
||||
@@ -22,6 +25,13 @@ package("slint")
|
||||
end)
|
||||
|
||||
on_install("windows", "linux", "macosx", function(package)
|
||||
for _, dep in ipairs(package:orderdeps()) do
|
||||
if dep:name() == "cmake" and dep:label() == "slint" then
|
||||
os.addenv("PATH", dep:installdir("bin"))
|
||||
import("core.cache.detectcache"):set2("find_program", "cmake", nil)
|
||||
break
|
||||
end
|
||||
end
|
||||
local configs = {
|
||||
"-DSLINT_BUILD_TESTING=OFF",
|
||||
"-DSLINT_BUILD_EXAMPLES=OFF",
|
||||
@@ -39,6 +49,12 @@ package("slint")
|
||||
"-DSLINT_STYLE=fluent",
|
||||
"-DBUILD_SHARED_LIBS=ON"
|
||||
}
|
||||
if package:is_plat("windows") then
|
||||
local rc = assert(package:build_getenv("mrc"),
|
||||
"failed to find the Windows resource compiler")
|
||||
rc = rc:gsub("\\", "/")
|
||||
table.insert(configs, "-DCMAKE_RC_COMPILER=" .. rc)
|
||||
end
|
||||
import("package.tools.cmake").install(package, configs)
|
||||
end)
|
||||
|
||||
|
||||
+23
-1
@@ -35,6 +35,28 @@ function setup_options_and_dependencies()
|
||||
set_languages("c++17")
|
||||
set_encodings("utf-8")
|
||||
|
||||
local crossdesk_version = get_config("CROSSDESK_VERSION") or "0.0.0"
|
||||
local version_base = crossdesk_version:gsub("^v", ""):match("^(%d[%d%.]*)") or "0.0.0"
|
||||
local version_parts = {}
|
||||
for part in version_base:gmatch("%d+") do
|
||||
local value = tonumber(part) or 0
|
||||
if value > 65535 then
|
||||
value = 0
|
||||
end
|
||||
table.insert(version_parts, value)
|
||||
if #version_parts == 4 then
|
||||
break
|
||||
end
|
||||
end
|
||||
while #version_parts < 4 do
|
||||
table.insert(version_parts, 0)
|
||||
end
|
||||
|
||||
add_defines("CROSSDESK_VERSION_STRING=\"" .. crossdesk_version .. "\"")
|
||||
if is_os("windows") then
|
||||
add_defines("CROSSDESK_VERSION_NUMERIC=" .. table.concat(version_parts, ","))
|
||||
end
|
||||
|
||||
-- set_policy("build.warning", true)
|
||||
-- set_warnings("all", "extra")
|
||||
-- add_cxxflags("/W4", "/WX")
|
||||
@@ -51,7 +73,7 @@ function setup_options_and_dependencies()
|
||||
add_defines("CROSSDESK_DEBUG")
|
||||
end
|
||||
|
||||
add_requireconfs("*.python", {version = "3.12", override = true, configs = {pgo = false}})
|
||||
add_requireconfs("**.python", {version = "3.12", override = true, configs = {pgo = false}})
|
||||
add_requires("spdlog 1.14.1", {system = false})
|
||||
add_requires("slint 1.17.1", {configs = {shared = true}})
|
||||
add_requires("libsdl3 3.2.26", {configs = {shared = false}})
|
||||
|
||||
+3
-1
@@ -64,10 +64,12 @@ function setup_platform_settings()
|
||||
"/usr/local/include/pipewire-0.3",
|
||||
"/usr/include/pipewire",
|
||||
"/usr/local/include/pipewire",
|
||||
"/opt/crossdesk-pipewire-sdk/include/pipewire-0.3",
|
||||
"/usr/include/spa-0.2",
|
||||
"/usr/local/include/spa-0.2",
|
||||
"/usr/include/spa",
|
||||
"/usr/local/include/spa"
|
||||
"/usr/local/include/spa",
|
||||
"/opt/crossdesk-pipewire-sdk/include/spa-0.2"
|
||||
}, {system = true})
|
||||
for _, include_dir in ipairs(collect_dbus_arch_include_dirs()) do
|
||||
add_includedirs(include_dir, {system = true})
|
||||
|
||||
+12
-2
@@ -197,6 +197,14 @@ function setup_targets()
|
||||
"src/device_controller/keyboard/linux", {public = true})
|
||||
end
|
||||
|
||||
if is_os("linux") then
|
||||
target("linux_keyboard_x11_integration_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_deps("device_controller")
|
||||
add_files("tests/linux_keyboard_x11_integration_test.cpp")
|
||||
end
|
||||
|
||||
target("thumbnail")
|
||||
set_kind("object")
|
||||
add_packages("libyuv", "openssl3")
|
||||
@@ -289,6 +297,7 @@ function setup_targets()
|
||||
add_files("src/screen_capturer/windows/screen_capturer_wgc.cpp",
|
||||
"src/screen_capturer/windows/wgc_session_impl.cpp",
|
||||
"src/screen_capturer/windows/wgc_plugin_entry.cpp")
|
||||
add_files("scripts/windows/wgc_plugin.rc")
|
||||
add_includedirs("src/common", "src/screen_capturer",
|
||||
"src/screen_capturer/windows")
|
||||
|
||||
@@ -298,6 +307,7 @@ function setup_targets()
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_files("src/service/windows/main.cpp",
|
||||
"src/service/windows/service_host.cpp")
|
||||
add_files("scripts/windows/crossdesk_service.rc")
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
|
||||
target("crossdesk_session_helper")
|
||||
@@ -306,8 +316,8 @@ function setup_targets()
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_files("src/service/windows/session_helper_main.cpp")
|
||||
add_files(crossdesk_windows_resource)
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
add_files("scripts/windows/crossdesk_session_helper.rc")
|
||||
add_includedirs("src/common", "src/service/windows", {public = true})
|
||||
end
|
||||
|
||||
target("crossdesk")
|
||||
|
||||
Reference in New Issue
Block a user