mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-06-10 01:14:53 +08:00
Compare commits
42 Commits
1d5d6f5121
...
v1.3.6
| Author | SHA1 | Date | |
|---|---|---|---|
| 5735f84008 | |||
| fe0cf42e5d | |||
| 04100584ce | |||
| 9d3a422916 | |||
| 65d8284fb8 | |||
| eea107db66 | |||
| 67812957db | |||
| 69d77e59cc | |||
| efcebfd82c | |||
| 32345f93bf | |||
| 193e4bd5bf | |||
| 53edf3d57e | |||
| 895e297771 | |||
| 8f3959e6c6 | |||
| 5ff6b601c7 | |||
| 4895ac9c23 | |||
| f121aa47f7 | |||
| 00a8d59284 | |||
| a30489e05b | |||
| dfbeb3ed20 | |||
| 2eed1c974e | |||
| 63a79a90ac | |||
| a61e74d97b | |||
| 54438a4aa1 | |||
| 7682ad63e4 | |||
| 06c53fdc9c | |||
| 665f4e684c | |||
| 52b894fe0e | |||
| 82c0cbbad4 | |||
| ce004af379 | |||
| 15bd9e9fdc | |||
| 37aabeaf72 | |||
| 473737ac9b | |||
| 1e29ec708f | |||
| 515d517a99 | |||
| a3aedcb624 | |||
| 98b7c6c966 | |||
| b1d956af2c | |||
| b7a031bb7f | |||
| 15cce07b6e | |||
| a8769dee06 | |||
| 855b15025c |
+176
-24
@@ -7,6 +7,11 @@ on:
|
||||
tags:
|
||||
- "*"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
patch:
|
||||
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -37,21 +42,53 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Extract version number
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
VERSION_NUM="${VERSION#v}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
BUILD_DATE_OVERRIDE=""
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV
|
||||
|
||||
- name: Set legal Debian version
|
||||
shell: bash
|
||||
run: |
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
BUILD_DATE="${BUILD_DATE_OVERRIDE}"
|
||||
if [[ -z "${BUILD_DATE}" ]]; then
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
fi
|
||||
|
||||
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
|
||||
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
|
||||
VERSION_BASE="0.0.0-${VERSION_BASE}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
|
||||
@@ -102,14 +139,43 @@ jobs:
|
||||
steps:
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
|
||||
echo "VERSION_NUM=${VERSION_NUM}"
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
@@ -163,10 +229,38 @@ jobs:
|
||||
$version = $ref -replace '^refs/(tags|heads)/', ''
|
||||
$version = $version -replace '^v', ''
|
||||
$version = $version -replace '/', '-'
|
||||
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
|
||||
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
|
||||
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV
|
||||
$PATCH_NUMBER = "${{ github.event.inputs.patch }}"
|
||||
|
||||
if ($PATCH_NUMBER -notmatch '^[0-9]+$') {
|
||||
$PATCH_NUMBER = "0"
|
||||
}
|
||||
|
||||
if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
$BUILD_DATE = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
$PATCH_NUMBER = $Matches[4]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') {
|
||||
$version = $Matches[1]
|
||||
$BUILD_DATE = $Matches[3]
|
||||
} elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") {
|
||||
$version = $Matches[1]
|
||||
$PATCH_NUMBER = $Matches[3]
|
||||
}
|
||||
|
||||
if ($PATCH_NUMBER -ne "0") {
|
||||
$VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE"
|
||||
} else {
|
||||
$VERSION_NUM = "v$version-$BUILD_DATE"
|
||||
}
|
||||
|
||||
echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV
|
||||
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
|
||||
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Cache xmake dependencies
|
||||
uses: actions/cache@v5
|
||||
@@ -239,16 +333,44 @@ jobs:
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
cd "${{ github.workspace }}\scripts\windows"
|
||||
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
|
||||
& "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Build Portable CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Package Portable
|
||||
shell: pwsh
|
||||
run: |
|
||||
$buildDir = "${{ github.workspace }}\build\windows\x64\release"
|
||||
$portableDir = "${{ github.workspace }}\portable"
|
||||
New-Item -ItemType Directory -Force -Path $portableDir
|
||||
Copy-Item "${{ github.workspace }}\build\windows\x64\release\crossdesk.exe" "$portableDir\CrossDesk.exe"
|
||||
Copy-Item "${{ github.workspace }}\build\windows\x64\release\*.dll" $portableDir -Force
|
||||
|
||||
$portableFiles = @(
|
||||
@("crossdesk.exe", "CrossDesk.exe"),
|
||||
@("crossdesk_service.exe", "crossdesk_service.exe"),
|
||||
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
|
||||
)
|
||||
|
||||
foreach ($file in $portableFiles) {
|
||||
$source = Join-Path $buildDir $file[0]
|
||||
$destination = Join-Path $portableDir $file[1]
|
||||
if (!(Test-Path $source)) {
|
||||
throw "Missing portable package file: $source"
|
||||
}
|
||||
Copy-Item $source $destination -Force
|
||||
}
|
||||
|
||||
Copy-Item (Join-Path $buildDir "*.dll") $portableDir -Force
|
||||
|
||||
foreach ($file in $portableFiles) {
|
||||
$packagedFile = Join-Path $portableDir $file[1]
|
||||
if (!(Test-Path $packagedFile)) {
|
||||
throw "Portable package is missing: $packagedFile"
|
||||
}
|
||||
}
|
||||
|
||||
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
|
||||
|
||||
- name: Upload artifact
|
||||
@@ -281,19 +403,47 @@ jobs:
|
||||
|
||||
- name: Extract version number
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
VERSION="${GITHUB_REF##*/}"
|
||||
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||
VERSION_REF="${GITHUB_REF##*/}"
|
||||
VERSION_BASE="${VERSION_REF#v}"
|
||||
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
|
||||
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d)
|
||||
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
|
||||
PATCH_NUMBER="${{ github.event.inputs.patch }}"
|
||||
|
||||
if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then
|
||||
PATCH_NUMBER=0
|
||||
fi
|
||||
|
||||
if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
BUILD_DATE="${BASH_REMATCH[3]}"
|
||||
elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
fi
|
||||
|
||||
BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}"
|
||||
if [[ "${PATCH_NUMBER}" != "0" ]]; then
|
||||
VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
|
||||
else
|
||||
VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}"
|
||||
fi
|
||||
|
||||
VERSION_WITH_V="v${VERSION_NUM}"
|
||||
VERSION_ONLY="${VERSION#v}"
|
||||
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Rename artifacts
|
||||
run: |
|
||||
@@ -351,8 +501,10 @@ jobs:
|
||||
run: |
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_NUM }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": "",
|
||||
"releaseNotes": "",
|
||||
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
|
||||
|
||||
@@ -20,21 +20,39 @@ jobs:
|
||||
|
||||
- name: Extract version from tag
|
||||
id: version
|
||||
shell: bash
|
||||
run: |
|
||||
TAG_NAME="${{ github.event.release.tag_name }}"
|
||||
VERSION_ONLY="${TAG_NAME#v}"
|
||||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT
|
||||
TAG_VERSION="${TAG_NAME#v}"
|
||||
VERSION_FULL="${TAG_VERSION}"
|
||||
VERSION_BASE="${TAG_VERSION}"
|
||||
PATCH_NUMBER=0
|
||||
|
||||
# Extract date from tag if available (format: v1.2.3-20251113-abc)
|
||||
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then
|
||||
DATE_STR="${BASH_REMATCH[1]}"
|
||||
# Extract date and patch from tags such as v1.2.3-1-20251113.
|
||||
if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[3]}"
|
||||
DATE_STR="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
PATCH_NUMBER="${BASH_REMATCH[4]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then
|
||||
VERSION_BASE="${BASH_REMATCH[1]}"
|
||||
DATE_STR="${BASH_REMATCH[3]}"
|
||||
BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
|
||||
else
|
||||
# Use release published date
|
||||
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
|
||||
fi
|
||||
|
||||
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT
|
||||
echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT
|
||||
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
|
||||
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Install jq
|
||||
run: sudo apt-get update && sudo apt-get install -y jq
|
||||
@@ -122,8 +140,10 @@ jobs:
|
||||
# Generate version.json using cat and heredoc
|
||||
cat > version.json << EOF
|
||||
{
|
||||
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
|
||||
"latest_version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"version": "${{ steps.version.outputs.VERSION_FULL }}",
|
||||
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
|
||||
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
|
||||
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
|
||||
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
|
||||
"tagName": "${{ steps.version.outputs.TAG_NAME }}",
|
||||
|
||||
@@ -53,6 +53,29 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|
||||
|
||||
<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);
|
||||
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
||||
|
||||
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
||||
|
||||
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
||||
```
|
||||
# 安装
|
||||
.\CrossDesk.exe --service-install
|
||||
# 启动
|
||||
.\CrossDesk.exe --service-start
|
||||
# 查看状态
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-stop
|
||||
# 卸载
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
||||
|
||||
## 如何编译
|
||||
|
||||
依赖:
|
||||
|
||||
@@ -56,6 +56,31 @@ Enter the **Remote Device ID** and **Password**, then click Connect to access th
|
||||
|
||||
<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.
|
||||
|
||||
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:
|
||||
```
|
||||
# install
|
||||
.\CrossDesk.exe --service-install
|
||||
# start
|
||||
.\CrossDesk.exe --service-start
|
||||
# check status
|
||||
.\CrossDesk.exe --service-status
|
||||
.\CrossDesk.exe --service-ping
|
||||
# stop
|
||||
.\CrossDesk.exe --service-stop
|
||||
# uninstall
|
||||
.\CrossDesk.exe --service-uninstall
|
||||
```
|
||||
|
||||
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:
|
||||
|
||||
@@ -4,12 +4,30 @@ set -e
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
|
||||
APP_VERSION="$1"
|
||||
ARCHITECTURE="amd64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PIPEWIRE_RUNTIME_DEP="libpipewire-0.3-0 | libpipewire-0.3-0t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
@@ -44,10 +62,8 @@ Description: $DESCRIPTION
|
||||
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
|
||||
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
|
||||
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
|
||||
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3,
|
||||
$PIPEWIRE_RUNTIME_DEP, xdg-desktop-portal,
|
||||
xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr
|
||||
Recommends: nvidia-cuda-toolkit
|
||||
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
|
||||
Recommends: $PORTAL_RUNTIME_RECOMMENDS, nvidia-cuda-toolkit
|
||||
Priority: optional
|
||||
Section: utils
|
||||
EOF
|
||||
|
||||
@@ -4,12 +4,30 @@ set -e
|
||||
PKG_NAME="crossdesk"
|
||||
APP_NAME="CrossDesk"
|
||||
|
||||
APP_VERSION="$1"
|
||||
ARCHITECTURE="arm64"
|
||||
MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
|
||||
DESCRIPTION="A simple cross-platform remote desktop client."
|
||||
ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
|
||||
PIPEWIRE_RUNTIME_DEP="libpipewire-0.3-0 | libpipewire-0.3-0t64"
|
||||
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
# Remove 'v' prefix from version for Debian package (Debian version must start with digit)
|
||||
DEB_VERSION="${APP_VERSION#v}"
|
||||
@@ -44,9 +62,8 @@ Description: $DESCRIPTION
|
||||
Depends: libc6 (>= 2.29), libstdc++6 (>= 9), libx11-6, libxcb1,
|
||||
libxcb-randr0, libxcb-xtest0, libxcb-xinerama0, libxcb-shape0,
|
||||
libxcb-xkb1, libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP,
|
||||
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3,
|
||||
$PIPEWIRE_RUNTIME_DEP, xdg-desktop-portal,
|
||||
xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr
|
||||
libsndio7.0, libxcb-shm0, libpulse0, libdrm2, libdbus-1-3
|
||||
Recommends: $PORTAL_RUNTIME_RECOMMENDS
|
||||
Priority: optional
|
||||
Section: utils
|
||||
EOF
|
||||
|
||||
+85
-51
@@ -4,17 +4,36 @@ set -e
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
|
||||
APP_VERSION="$1"
|
||||
PLATFORM="macos"
|
||||
ARCH="arm64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="10.12"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||
|
||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
@@ -73,67 +92,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||
find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
cat > build_pkg_resources/welcome.html <<EOF
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
echo "building pkg..."
|
||||
pkgbuild \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
|
||||
cat > build_pkg_scripts/postinstall <<'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
|
||||
# 获取当前登录用户
|
||||
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
|
||||
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
|
||||
|
||||
# 清除应用的权限授权,以便重新授权
|
||||
# 使用 tccutil 重置录屏权限和辅助功能权限
|
||||
if command -v tccutil >/dev/null 2>&1; then
|
||||
# 重置录屏权限
|
||||
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
|
||||
# 重置辅助功能权限
|
||||
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
|
||||
# 重置摄像头权限(如果需要)
|
||||
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
|
||||
# 重置麦克风权限(如果需要)
|
||||
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 为所有用户清除权限(可选,如果需要)
|
||||
# 遍历所有用户目录并清除权限
|
||||
for USER_DIR in /Users/*; do
|
||||
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
|
||||
USER_NAME=$(basename "$USER_DIR")
|
||||
# 跳过系统用户
|
||||
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
|
||||
# 删除 TCC 数据库中的相关条目(需要管理员权限)
|
||||
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
if [ -f "$TCC_DB" ]; then
|
||||
# 使用 sqlite3 删除相关权限记录(如果可用)
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>${INSTALLER_TITLE}</title>
|
||||
<welcome file="welcome.html" mime-type="text/html"/>
|
||||
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
|
||||
<choices-outline>
|
||||
<line choice="default">
|
||||
<line choice="${IDENTIFIER}"/>
|
||||
</line>
|
||||
</choices-outline>
|
||||
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||
<pkg-ref id="${IDENTIFIER}"/>
|
||||
</choice>
|
||||
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||
</installer-gui-script>
|
||||
EOF
|
||||
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
productbuild \
|
||||
--package build_pkg_temp/${APP_NAME}-component.pkg \
|
||||
--distribution build_pkg_temp/Distribution \
|
||||
--package-path build_pkg_temp \
|
||||
--resources build_pkg_resources \
|
||||
"${PKG_NAME}"
|
||||
|
||||
echo "PKG package created: ${PKG_NAME}"
|
||||
@@ -171,8 +205,8 @@ APPLESCRIPT
|
||||
fi
|
||||
echo "Set icon finished"
|
||||
|
||||
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
|
||||
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||
|
||||
echo "PKG package created successfully."
|
||||
echo "package ${APP_BUNDLE}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
|
||||
+85
-51
@@ -4,17 +4,36 @@ set -e
|
||||
APP_NAME="crossdesk"
|
||||
APP_NAME_UPPER="CrossDesk"
|
||||
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
|
||||
APP_VERSION="$1"
|
||||
PLATFORM="macos"
|
||||
ARCH="x64"
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
ICON_PATH="icons/macos/crossdesk.icns"
|
||||
MACOS_MIN_VERSION="10.12"
|
||||
|
||||
normalize_app_version() {
|
||||
local input="$1"
|
||||
local prefix=""
|
||||
local body="$input"
|
||||
|
||||
if [[ "$body" == v* ]]; then
|
||||
prefix="v"
|
||||
body="${body#v}"
|
||||
fi
|
||||
|
||||
if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then
|
||||
echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}"
|
||||
else
|
||||
echo "$input"
|
||||
fi
|
||||
}
|
||||
|
||||
APP_VERSION="$(normalize_app_version "$1")"
|
||||
|
||||
APP_BUNDLE="${APP_NAME_UPPER}.app"
|
||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||
|
||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||
@@ -73,67 +92,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
||||
</plist>
|
||||
EOF
|
||||
|
||||
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||
find "${APP_BUNDLE}" -name '._*' -delete
|
||||
|
||||
echo ".app created successfully."
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
mkdir -p build_pkg_resources
|
||||
cat > build_pkg_resources/welcome.html <<EOF
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #222;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
h1 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
p {
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||
</body>
|
||||
</html>
|
||||
EOF
|
||||
|
||||
echo "building pkg..."
|
||||
pkgbuild \
|
||||
--identifier "${IDENTIFIER}" \
|
||||
--version "${APP_VERSION}" \
|
||||
--install-location "/Applications" \
|
||||
--component "${APP_BUNDLE}" \
|
||||
--scripts build_pkg_scripts \
|
||||
build_pkg_temp/${APP_NAME}-component.pkg
|
||||
|
||||
mkdir -p build_pkg_scripts
|
||||
|
||||
cat > build_pkg_scripts/postinstall <<'EOF'
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
IDENTIFIER="cn.crossdesk.app"
|
||||
|
||||
# 获取当前登录用户
|
||||
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
|
||||
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
|
||||
|
||||
# 清除应用的权限授权,以便重新授权
|
||||
# 使用 tccutil 重置录屏权限和辅助功能权限
|
||||
if command -v tccutil >/dev/null 2>&1; then
|
||||
# 重置录屏权限
|
||||
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
|
||||
# 重置辅助功能权限
|
||||
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
|
||||
# 重置摄像头权限(如果需要)
|
||||
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
|
||||
# 重置麦克风权限(如果需要)
|
||||
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# 为所有用户清除权限(可选,如果需要)
|
||||
# 遍历所有用户目录并清除权限
|
||||
for USER_DIR in /Users/*; do
|
||||
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
|
||||
USER_NAME=$(basename "$USER_DIR")
|
||||
# 跳过系统用户
|
||||
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
|
||||
# 删除 TCC 数据库中的相关条目(需要管理员权限)
|
||||
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
if [ -f "$TCC_DB" ]; then
|
||||
# 使用 sqlite3 删除相关权限记录(如果可用)
|
||||
if command -v sqlite3 >/dev/null 2>&1; then
|
||||
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
exit 0
|
||||
cat > build_pkg_temp/Distribution <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<installer-gui-script minSpecVersion="1">
|
||||
<title>${INSTALLER_TITLE}</title>
|
||||
<welcome file="welcome.html" mime-type="text/html"/>
|
||||
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
|
||||
<choices-outline>
|
||||
<line choice="default">
|
||||
<line choice="${IDENTIFIER}"/>
|
||||
</line>
|
||||
</choices-outline>
|
||||
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||
<pkg-ref id="${IDENTIFIER}"/>
|
||||
</choice>
|
||||
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||
</installer-gui-script>
|
||||
EOF
|
||||
|
||||
chmod +x build_pkg_scripts/postinstall
|
||||
|
||||
productbuild \
|
||||
--package build_pkg_temp/${APP_NAME}-component.pkg \
|
||||
--distribution build_pkg_temp/Distribution \
|
||||
--package-path build_pkg_temp \
|
||||
--resources build_pkg_resources \
|
||||
"${PKG_NAME}"
|
||||
|
||||
echo "PKG package created: ${PKG_NAME}"
|
||||
@@ -171,8 +205,8 @@ APPLESCRIPT
|
||||
fi
|
||||
echo "Set icon finished"
|
||||
|
||||
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
|
||||
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||
|
||||
echo "PKG package created successfully."
|
||||
echo "package ${APP_BUNDLE}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
echo "installer ${PKG_NAME}"
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
APP_IDENTIFIER="cn.crossdesk.app"
|
||||
|
||||
# Keep known historical identifiers here. tccutil only resets identifiers that
|
||||
# Launch Services can currently resolve, so path/db cleanup below remains a
|
||||
# best-effort fallback for stale entries from unsigned or removed builds.
|
||||
BUNDLE_IDENTIFIERS=(
|
||||
"cn.crossdesk.app"
|
||||
"cn.crossdesk.CrossDesk"
|
||||
"com.crossdesk.app"
|
||||
"com.crossdesk.CrossDesk"
|
||||
"com.kunkundi.crossdesk"
|
||||
"com.kunkundi.CrossDesk"
|
||||
)
|
||||
|
||||
TCC_SERVICES=(
|
||||
"ScreenCapture"
|
||||
"Accessibility"
|
||||
"Microphone"
|
||||
"AudioCapture"
|
||||
)
|
||||
|
||||
run_tccutil() {
|
||||
local user_name="$1"
|
||||
local user_id="$2"
|
||||
local service="$3"
|
||||
local bundle_id="$4"
|
||||
|
||||
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
|
||||
/bin/launchctl asuser "$user_id" \
|
||||
/usr/bin/sudo -u "$user_name" \
|
||||
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||
else
|
||||
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
reset_bundle_tcc() {
|
||||
local user_name="$1"
|
||||
local user_id="$2"
|
||||
local bundle_id
|
||||
local service
|
||||
|
||||
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
|
||||
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for service in "${TCC_SERVICES[@]}"; do
|
||||
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
cleanup_tcc_db() {
|
||||
local db_path="$1"
|
||||
|
||||
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
||||
return
|
||||
fi
|
||||
|
||||
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
|
||||
DELETE FROM access
|
||||
WHERE service IN (
|
||||
'kTCCServiceScreenCapture',
|
||||
'kTCCServiceAccessibility',
|
||||
'kTCCServiceMicrophone',
|
||||
'kTCCServiceAudioCapture'
|
||||
)
|
||||
AND (
|
||||
client IN (
|
||||
'cn.crossdesk.app',
|
||||
'cn.crossdesk.CrossDesk',
|
||||
'com.crossdesk.app',
|
||||
'com.crossdesk.CrossDesk',
|
||||
'com.kunkundi.crossdesk',
|
||||
'com.kunkundi.CrossDesk'
|
||||
)
|
||||
OR lower(client) LIKE '%crossdesk%'
|
||||
);
|
||||
SQL
|
||||
}
|
||||
|
||||
cleanup_user_tcc_db() {
|
||||
local user_name="$1"
|
||||
local home_dir
|
||||
|
||||
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
|
||||
/usr/bin/awk '{print $2}')
|
||||
if [ -z "$home_dir" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
}
|
||||
|
||||
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
|
||||
if [ -n "$CONSOLE_USER" ] &&
|
||||
[ "$CONSOLE_USER" != "root" ] &&
|
||||
[ "$CONSOLE_USER" != "loginwindow" ]; then
|
||||
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
|
||||
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
|
||||
cleanup_user_tcc_db "$CONSOLE_USER"
|
||||
fi
|
||||
|
||||
# Also clear any system/root-scoped decisions as a harmless fallback.
|
||||
reset_bundle_tcc "" ""
|
||||
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
|
||||
|
||||
exit 0
|
||||
@@ -1,17 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<!-- 应用程序标识 -->
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
processorArchitecture="*"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<!-- 描述信息 -->
|
||||
<description>CrossDesk Application</description>
|
||||
|
||||
<!-- 权限:要求管理员运行 -->
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
@@ -20,24 +16,17 @@
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<!-- DPI 感知设置:支持高分屏 -->
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<!-- Windows Vista/7 风格 DPI 感知 -->
|
||||
<dpiAware>true/pm</dpiAware>
|
||||
<!-- Windows 10/11 高级 DPI 感知 -->
|
||||
<dpiAwareness>PerMonitorV2</dpiAwareness>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<!-- Windows 兼容性声明 -->
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<!-- 支持 Windows 10 -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
<!-- 支持 Windows 11(向下兼容 Win10 GUID) -->
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
</assembly>
|
||||
|
||||
@@ -1,2 +1,11 @@
|
||||
// Application icon resource; load by the resource name IDI_ICON1.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_debug.manifest"
|
||||
#else
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk.manifest"
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
|
||||
|
||||
<assemblyIdentity
|
||||
version="1.0.0.0"
|
||||
name="CrossDesk"
|
||||
type="win32" />
|
||||
|
||||
<description>CrossDesk Portable Application</description>
|
||||
|
||||
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<security>
|
||||
<requestedPrivileges>
|
||||
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
|
||||
</requestedPrivileges>
|
||||
</security>
|
||||
</trustInfo>
|
||||
|
||||
<application xmlns="urn:schemas-microsoft-com:asm.v3">
|
||||
<windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
||||
</windowsSettings>
|
||||
</application>
|
||||
|
||||
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
|
||||
<application>
|
||||
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
|
||||
</application>
|
||||
</compatibility>
|
||||
|
||||
</assembly>
|
||||
@@ -0,0 +1,8 @@
|
||||
// Portable build resource. The app itself runs as the current user; service
|
||||
// installation raises a separate UAC prompt only when the user chooses it.
|
||||
IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico"
|
||||
|
||||
#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1
|
||||
#define RT_MANIFEST 24
|
||||
|
||||
CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest"
|
||||
@@ -0,0 +1,40 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Version
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
function Normalize-AppVersion {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$InputVersion
|
||||
)
|
||||
|
||||
$prefix = ""
|
||||
$body = $InputVersion
|
||||
|
||||
if ($body.StartsWith("v")) {
|
||||
$prefix = "v"
|
||||
$body = $body.Substring(1)
|
||||
}
|
||||
|
||||
if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') {
|
||||
return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])"
|
||||
}
|
||||
|
||||
return $InputVersion
|
||||
}
|
||||
|
||||
$normalizedVersion = Normalize-AppVersion -InputVersion $Version
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Push-Location $scriptDir
|
||||
try {
|
||||
& makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
@@ -79,6 +79,9 @@ int ConfigCenter::Load() {
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
enable_minimize_to_tray_ = ini_.GetBoolValue(
|
||||
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
const char* file_transfer_save_path_value =
|
||||
ini_.GetValue(section_, "file_transfer_save_path", nullptr);
|
||||
@@ -118,6 +121,8 @@ int ConfigCenter::Save() {
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
|
||||
enable_minimize_to_tray_);
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
file_transfer_save_path_.c_str());
|
||||
@@ -325,6 +330,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
|
||||
portable_service_prompt_suppressed_ = suppressed;
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// getters
|
||||
|
||||
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
|
||||
@@ -377,6 +394,10 @@ bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
|
||||
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
|
||||
return portable_service_prompt_suppressed_;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
|
||||
file_transfer_save_path_ = path;
|
||||
ini_.SetValue(section_, "file_transfer_save_path",
|
||||
|
||||
@@ -39,6 +39,7 @@ class ConfigCenter {
|
||||
int SetMinimizeToTray(bool enable_minimize_to_tray);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
int SetFileTransferSavePath(const std::string& path);
|
||||
|
||||
// read config
|
||||
@@ -60,6 +61,7 @@ class ConfigCenter {
|
||||
bool IsMinimizeToTray() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
std::string GetFileTransferSavePath() const;
|
||||
|
||||
int Load();
|
||||
@@ -87,6 +89,7 @@ class ConfigCenter {
|
||||
bool enable_minimize_to_tray_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -7,9 +7,10 @@
|
||||
#ifndef _DEVICE_CONTROLLER_H_
|
||||
#define _DEVICE_CONTROLLER_H_
|
||||
|
||||
#include <cstring>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
|
||||
@@ -39,7 +40,7 @@ typedef enum {
|
||||
wheel_horizontal
|
||||
} MouseFlag;
|
||||
typedef enum { key_down = 0, key_up } KeyFlag;
|
||||
typedef enum { send_sas = 0 } ServiceCommandFlag;
|
||||
typedef enum { send_sas = 0, lock_workstation } ServiceCommandFlag;
|
||||
typedef struct {
|
||||
float x;
|
||||
float y;
|
||||
@@ -49,6 +50,8 @@ typedef struct {
|
||||
|
||||
typedef struct {
|
||||
size_t key_value;
|
||||
uint32_t scan_code;
|
||||
bool extended;
|
||||
KeyFlag flag;
|
||||
} Key;
|
||||
|
||||
@@ -103,7 +106,10 @@ struct RemoteAction {
|
||||
{"x", a.m.x}, {"y", a.m.y}, {"s", a.m.s}, {"flag", a.m.flag}};
|
||||
break;
|
||||
case ControlType::keyboard:
|
||||
j["keyboard"] = {{"key_value", a.k.key_value}, {"flag", a.k.flag}};
|
||||
j["keyboard"] = {{"key_value", a.k.key_value},
|
||||
{"scan_code", a.k.scan_code},
|
||||
{"extended", a.k.extended},
|
||||
{"flag", a.k.flag}};
|
||||
break;
|
||||
case ControlType::audio_capture:
|
||||
j["audio_capture"] = a.a;
|
||||
@@ -113,8 +119,7 @@ struct RemoteAction {
|
||||
break;
|
||||
case ControlType::service_status:
|
||||
j["service_status"] = {{"available", a.ss.available},
|
||||
{"interactive_stage",
|
||||
a.ss.interactive_stage}};
|
||||
{"interactive_stage", a.ss.interactive_stage}};
|
||||
break;
|
||||
case ControlType::service_command:
|
||||
j["service_command"] = {{"flag", a.c.flag}};
|
||||
@@ -152,6 +157,9 @@ struct RemoteAction {
|
||||
break;
|
||||
case ControlType::keyboard:
|
||||
out.k.key_value = j.at("keyboard").at("key_value").get<size_t>();
|
||||
out.k.scan_code =
|
||||
j.at("keyboard").value("scan_code", static_cast<uint32_t>(0));
|
||||
out.k.extended = j.at("keyboard").value("extended", false);
|
||||
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
|
||||
break;
|
||||
case ControlType::audio_capture:
|
||||
@@ -164,16 +172,15 @@ struct RemoteAction {
|
||||
const auto& service_status_json = j.at("service_status");
|
||||
out.ss.available = service_status_json.value("available", false);
|
||||
std::string interactive_stage =
|
||||
service_status_json.value("interactive_stage", std::string());
|
||||
service_status_json.value("interactive_stage", std::string());
|
||||
std::strncpy(out.ss.interactive_stage, interactive_stage.c_str(),
|
||||
sizeof(out.ss.interactive_stage) - 1);
|
||||
out.ss.interactive_stage[sizeof(out.ss.interactive_stage) - 1] =
|
||||
'\0';
|
||||
sizeof(out.ss.interactive_stage) - 1);
|
||||
out.ss.interactive_stage[sizeof(out.ss.interactive_stage) - 1] = '\0';
|
||||
break;
|
||||
}
|
||||
case ControlType::service_command:
|
||||
out.c.flag = static_cast<ServiceCommandFlag>(
|
||||
j.at("service_command").at("flag").get<int>());
|
||||
j.at("service_command").at("flag").get<int>());
|
||||
break;
|
||||
case ControlType::host_infomation: {
|
||||
std::string host_name =
|
||||
@@ -212,8 +219,8 @@ struct RemoteAction {
|
||||
}
|
||||
};
|
||||
|
||||
// int key_code, bool is_down
|
||||
typedef void (*OnKeyAction)(int, bool, void*);
|
||||
// int key_code, bool is_down, uint32_t scan_code, bool extended
|
||||
typedef void (*OnKeyAction)(int, bool, uint32_t, bool, void*);
|
||||
|
||||
class DeviceController {
|
||||
public:
|
||||
@@ -228,4 +235,4 @@ class DeviceController {
|
||||
// virtual int Unhook();
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "keyboard_converter.h"
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_key_metadata.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -35,9 +36,12 @@ static int KeyboardEventHandler(Display* display, XEvent* event) {
|
||||
|
||||
int key_code = key_it->second;
|
||||
bool is_key_down = (event->xkey.type == KeyPress);
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
LookupWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
|
||||
|
||||
if (g_on_key_action) {
|
||||
g_on_key_action(key_code, is_key_down, g_user_ptr);
|
||||
g_on_key_action(key_code, is_key_down, scan_code, extended, g_user_ptr);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
@@ -146,7 +150,10 @@ int KeyboardCapturer::Unhook() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
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;
|
||||
@@ -154,12 +161,14 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
use_wayland_portal_ = true;
|
||||
LOG_INFO("Keyboard controller initialized with Wayland portal backend");
|
||||
} else {
|
||||
LOG_WARN("Wayland keyboard control init failed, falling back to X11/XTest backend");
|
||||
LOG_WARN(
|
||||
"Wayland keyboard control init failed, falling back to X11/XTest "
|
||||
"backend");
|
||||
}
|
||||
}
|
||||
|
||||
if (use_wayland_portal_) {
|
||||
return SendWaylandKeyboardCommand(key_code, is_down);
|
||||
return SendWaylandKeyboardCommand(key_code, is_down, scan_code, extended);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -32,12 +32,15 @@ class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down);
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
bool InitWaylandPortal();
|
||||
void CleanupWaylandPortal();
|
||||
int SendWaylandKeyboardCommand(int key_code, bool is_down);
|
||||
int SendWaylandKeyboardCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended);
|
||||
bool NotifyWaylandKeyboardKeysym(int keysym, uint32_t state);
|
||||
bool NotifyWaylandKeyboardKeycode(int keycode, uint32_t state);
|
||||
bool SendWaylandPortalVoidCall(const char* method_name,
|
||||
|
||||
@@ -575,8 +575,12 @@ void KeyboardCapturer::CleanupWaylandPortal() {
|
||||
wayland_session_handle_.clear();
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down) {
|
||||
int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
if (!dbus_connection_ || wayland_session_handle_.empty()) {
|
||||
return -1;
|
||||
}
|
||||
@@ -613,6 +617,8 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down) {
|
||||
#else
|
||||
(void)key_code;
|
||||
(void)is_down;
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
|
||||
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode));
|
||||
int vk_code = ResolveVkCodeFromMacEvent(event, key_code, is_key_down);
|
||||
if (vk_code >= 0) {
|
||||
g_on_key_action(vk_code, is_key_down, g_user_ptr);
|
||||
g_on_key_action(vk_code, is_key_down, 0, false, g_user_ptr);
|
||||
}
|
||||
} else if (type == kCGEventFlagsChanged) {
|
||||
CGEventFlags current_flags = CGEventGetFlags(event);
|
||||
@@ -135,35 +135,40 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
|
||||
bool caps_lock_state = (current_flags & kCGEventFlagMaskAlphaShift) != 0;
|
||||
if (caps_lock_state != keyboard_capturer->caps_lock_flag_) {
|
||||
keyboard_capturer->caps_lock_flag_ = caps_lock_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->caps_lock_flag_, g_user_ptr);
|
||||
g_on_key_action(vk_code, keyboard_capturer->caps_lock_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// shift
|
||||
bool shift_state = (current_flags & kCGEventFlagMaskShift) != 0;
|
||||
if (shift_state != keyboard_capturer->shift_flag_) {
|
||||
keyboard_capturer->shift_flag_ = shift_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->shift_flag_, g_user_ptr);
|
||||
g_on_key_action(vk_code, keyboard_capturer->shift_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// control
|
||||
bool control_state = (current_flags & kCGEventFlagMaskControl) != 0;
|
||||
if (control_state != keyboard_capturer->control_flag_) {
|
||||
keyboard_capturer->control_flag_ = control_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->control_flag_, g_user_ptr);
|
||||
g_on_key_action(vk_code, keyboard_capturer->control_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// option
|
||||
bool option_state = (current_flags & kCGEventFlagMaskAlternate) != 0;
|
||||
if (option_state != keyboard_capturer->option_flag_) {
|
||||
keyboard_capturer->option_flag_ = option_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->option_flag_, g_user_ptr);
|
||||
g_on_key_action(vk_code, keyboard_capturer->option_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
|
||||
// command
|
||||
bool command_state = (current_flags & kCGEventFlagMaskCommand) != 0;
|
||||
if (command_state != keyboard_capturer->command_flag_) {
|
||||
keyboard_capturer->command_flag_ = command_state;
|
||||
g_on_key_action(vk_code, keyboard_capturer->command_flag_, g_user_ptr);
|
||||
g_on_key_action(vk_code, keyboard_capturer->command_flag_, 0, false,
|
||||
g_user_ptr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,7 +269,30 @@ inline bool IsFunctionKey(int key_code) {
|
||||
}
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
CGEventFlags ToCGEventFlags(uint32_t injected_flags) {
|
||||
CGEventFlags flags = 0;
|
||||
if ((injected_flags & kMacInjectedModifierShift) != 0) {
|
||||
flags |= kCGEventFlagMaskShift;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierControl) != 0) {
|
||||
flags |= kCGEventFlagMaskControl;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierOption) != 0) {
|
||||
flags |= kCGEventFlagMaskAlternate;
|
||||
}
|
||||
if ((injected_flags & kMacInjectedModifierCommand) != 0) {
|
||||
flags |= kCGEventFlagMaskCommand;
|
||||
}
|
||||
return flags;
|
||||
}
|
||||
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
const uint32_t injected_flags =
|
||||
injected_modifier_state_.Update(key_code, is_down);
|
||||
|
||||
if (vkCodeToCGKeyCode.find(key_code) != vkCodeToCGKeyCode.end()) {
|
||||
CGKeyCode cg_key_code = vkCodeToCGKeyCode[key_code];
|
||||
CGEventRef event = CGEventCreateKeyboardEvent(NULL, cg_key_code, is_down);
|
||||
@@ -273,7 +301,7 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGEventSetFlags(event, 0);
|
||||
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
|
||||
@@ -282,6 +310,10 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
if (IsFunctionKey(cg_key_code) && !is_down) {
|
||||
CGEventRef fn_release_event =
|
||||
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
|
||||
if (!fn_release_event) {
|
||||
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
|
||||
return -1;
|
||||
}
|
||||
CGEventPost(kCGHIDEventTap, fn_release_event);
|
||||
CFRelease(fn_release_event);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
#include "device_controller.h"
|
||||
#include "macos_keyboard_modifier_state.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -21,7 +22,9 @@ class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down);
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
CFMachPortRef event_tap_ = nullptr;
|
||||
@@ -34,6 +37,7 @@ class KeyboardCapturer : public DeviceController {
|
||||
bool option_flag_ = false;
|
||||
bool command_flag_ = false;
|
||||
int fn_key_code_ = 0x3F;
|
||||
MacKeyboardModifierState injected_modifier_state_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
@@ -7,14 +7,56 @@ namespace crossdesk {
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
|
||||
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) {
|
||||
switch (key_code) {
|
||||
case VK_LSHIFT:
|
||||
case VK_RSHIFT:
|
||||
case VK_LCONTROL:
|
||||
case VK_RCONTROL:
|
||||
case VK_LMENU:
|
||||
case VK_RMENU:
|
||||
case VK_LWIN:
|
||||
case VK_RWIN:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam) {
|
||||
if (nCode == HC_ACTION && g_on_key_action) {
|
||||
KBDLLHOOKSTRUCT* kbData = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
|
||||
const int key_code = NormalizeModifierVkCode(kbData);
|
||||
|
||||
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
|
||||
g_on_key_action(kbData->vkCode, true, g_user_ptr);
|
||||
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(kbData->vkCode, false, g_user_ptr);
|
||||
g_on_key_action(key_code, false, kbData->scanCode,
|
||||
(kbData->flags & LLKHF_EXTENDED) != 0, g_user_ptr);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
@@ -49,20 +91,40 @@ int KeyboardCapturer::Unhook() {
|
||||
}
|
||||
|
||||
// apply remote keyboard commands to the local machine
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down) {
|
||||
int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
|
||||
const UINT scan_code =
|
||||
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
|
||||
if (scan_code != 0) {
|
||||
const bool prefer_vk = PreferSideSpecificVkInjection(key_code);
|
||||
const UINT resolved_scan_code =
|
||||
scan_code != 0
|
||||
? static_cast<UINT>(scan_code & 0xFF) | (extended ? 0xE000u : 0u)
|
||||
: MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
|
||||
|
||||
if (scan_code != 0 && !prefer_vk) {
|
||||
input.ki.wVk = 0;
|
||||
input.ki.wScan = static_cast<WORD>(scan_code & 0xFF);
|
||||
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
|
||||
if ((scan_code & 0xFF00) != 0) {
|
||||
if (extended) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else {
|
||||
input.ki.wVk = (WORD)key_code;
|
||||
|
||||
if (prefer_vk && resolved_scan_code != 0) {
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
if ((resolved_scan_code & 0xFF00) != 0) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
} else if (resolved_scan_code != 0) {
|
||||
input.ki.wVk = 0;
|
||||
input.ki.wScan = static_cast<WORD>(resolved_scan_code & 0xFF);
|
||||
input.ki.dwFlags |= KEYEVENTF_SCANCODE;
|
||||
if ((resolved_scan_code & 0xFF00) != 0) {
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_down) {
|
||||
|
||||
@@ -21,7 +21,9 @@ class KeyboardCapturer : public DeviceController {
|
||||
public:
|
||||
virtual int Hook(OnKeyAction on_key_action, void* user_ptr);
|
||||
virtual int Unhook();
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down);
|
||||
virtual int SendKeyboardCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
|
||||
private:
|
||||
HHOOK keyboard_hook_ = nullptr;
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-05-21
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MACOS_KEYBOARD_MODIFIER_STATE_H_
|
||||
#define _MACOS_KEYBOARD_MODIFIER_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr uint32_t kMacInjectedModifierShift = 1u << 0;
|
||||
inline constexpr uint32_t kMacInjectedModifierControl = 1u << 1;
|
||||
inline constexpr uint32_t kMacInjectedModifierOption = 1u << 2;
|
||||
inline constexpr uint32_t kMacInjectedModifierCommand = 1u << 3;
|
||||
|
||||
class MacKeyboardModifierState {
|
||||
public:
|
||||
uint32_t Update(int key_code, bool is_down) {
|
||||
bool* state = MutableStateForVk(key_code);
|
||||
if (state != nullptr) {
|
||||
*state = is_down;
|
||||
}
|
||||
return flags();
|
||||
}
|
||||
|
||||
uint32_t flags() const {
|
||||
uint32_t result = 0;
|
||||
if (left_shift_down_ || right_shift_down_) {
|
||||
result |= kMacInjectedModifierShift;
|
||||
}
|
||||
if (left_control_down_ || right_control_down_) {
|
||||
result |= kMacInjectedModifierControl;
|
||||
}
|
||||
if (left_option_down_ || right_option_down_) {
|
||||
result |= kMacInjectedModifierOption;
|
||||
}
|
||||
if (left_command_down_ || right_command_down_) {
|
||||
result |= kMacInjectedModifierCommand;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void Clear() {
|
||||
left_shift_down_ = false;
|
||||
right_shift_down_ = false;
|
||||
left_control_down_ = false;
|
||||
right_control_down_ = false;
|
||||
left_option_down_ = false;
|
||||
right_option_down_ = false;
|
||||
left_command_down_ = false;
|
||||
right_command_down_ = false;
|
||||
}
|
||||
|
||||
private:
|
||||
bool* MutableStateForVk(int key_code) {
|
||||
switch (key_code) {
|
||||
case 0xA0: // VK_LSHIFT
|
||||
return &left_shift_down_;
|
||||
case 0xA1: // VK_RSHIFT
|
||||
return &right_shift_down_;
|
||||
case 0xA2: // VK_LCONTROL
|
||||
return &left_control_down_;
|
||||
case 0xA3: // VK_RCONTROL
|
||||
return &right_control_down_;
|
||||
case 0xA4: // VK_LMENU / left Option
|
||||
return &left_option_down_;
|
||||
case 0xA5: // VK_RMENU / right Option
|
||||
return &right_option_down_;
|
||||
case 0x5B: // VK_LWIN / left Command
|
||||
return &left_command_down_;
|
||||
case 0x5C: // VK_RWIN / right Command
|
||||
return &right_command_down_;
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool left_shift_down_ = false;
|
||||
bool right_shift_down_ = false;
|
||||
bool left_control_down_ = false;
|
||||
bool right_control_down_ = false;
|
||||
bool left_option_down_ = false;
|
||||
bool right_option_down_ = false;
|
||||
bool left_command_down_ = false;
|
||||
bool right_command_down_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#include <algorithm>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -20,85 +21,101 @@ int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
if (remote_action.type != ControlType::mouse) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||
display_index, display_info_list_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
const DisplayInfo& display_info = display_info_list_[display_index];
|
||||
if (display_info.width <= 0 || display_info.height <= 0) {
|
||||
LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}",
|
||||
display_info.width, display_info.height);
|
||||
return -1;
|
||||
}
|
||||
|
||||
const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
int mouse_pos_x =
|
||||
remote_action.m.x * display_info_list_[display_index].width +
|
||||
display_info_list_[display_index].left;
|
||||
normalized_x * display_info.width + display_info.left;
|
||||
int mouse_pos_y =
|
||||
remote_action.m.y * display_info_list_[display_index].height +
|
||||
display_info_list_[display_index].top;
|
||||
normalized_y * display_info.height + display_info.top;
|
||||
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
CGEventRef mouse_event = nullptr;
|
||||
CGEventType mouse_type;
|
||||
CGMouseButton mouse_button;
|
||||
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
||||
CGEventRef mouse_event = nullptr;
|
||||
CGEventType mouse_type;
|
||||
CGMouseButton mouse_button;
|
||||
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
|
||||
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
mouse_type = kCGEventLeftMouseDown;
|
||||
left_dragging_ = true;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
mouse_type = kCGEventLeftMouseUp;
|
||||
left_dragging_ = false;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
mouse_type = kCGEventRightMouseDown;
|
||||
right_dragging_ = true;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
mouse_type = kCGEventRightMouseUp;
|
||||
right_dragging_ = false;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
mouse_type = kCGEventOtherMouseDown;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
mouse_type = kCGEventOtherMouseUp;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0);
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s);
|
||||
break;
|
||||
default:
|
||||
if (left_dragging_) {
|
||||
mouse_type = kCGEventLeftMouseDragged;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
} else if (right_dragging_) {
|
||||
mouse_type = kCGEventRightMouseDragged;
|
||||
mouse_button = kCGMouseButtonRight;
|
||||
} else {
|
||||
mouse_type = kCGEventMouseMoved;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
}
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
mouse_type = kCGEventLeftMouseDown;
|
||||
left_dragging_ = true;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
mouse_type = kCGEventLeftMouseUp;
|
||||
left_dragging_ = false;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonLeft);
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
mouse_type = kCGEventRightMouseDown;
|
||||
right_dragging_ = true;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
mouse_type = kCGEventRightMouseUp;
|
||||
right_dragging_ = false;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonRight);
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
mouse_type = kCGEventOtherMouseDown;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
mouse_type = kCGEventOtherMouseUp;
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
kCGMouseButtonCenter);
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0);
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
mouse_event = CGEventCreateScrollWheelEvent(
|
||||
NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s);
|
||||
break;
|
||||
default:
|
||||
if (left_dragging_) {
|
||||
mouse_type = kCGEventLeftMouseDragged;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
} else if (right_dragging_) {
|
||||
mouse_type = kCGEventRightMouseDragged;
|
||||
mouse_button = kCGMouseButtonRight;
|
||||
} else {
|
||||
mouse_type = kCGEventMouseMoved;
|
||||
mouse_button = kCGMouseButtonLeft;
|
||||
}
|
||||
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
mouse_button);
|
||||
break;
|
||||
}
|
||||
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
|
||||
mouse_button);
|
||||
break;
|
||||
}
|
||||
|
||||
if (mouse_event) {
|
||||
CGEventPost(kCGHIDEventTap, mouse_event);
|
||||
CFRelease(mouse_event);
|
||||
}
|
||||
if (mouse_event) {
|
||||
CGEventPost(kCGHIDEventTap, mouse_event);
|
||||
CFRelease(mouse_event);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -18,7 +20,14 @@ int MouseController::Destroy() { return 0; }
|
||||
|
||||
int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
INPUT ip;
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||
display_index, display_info_list_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
INPUT ip = {0};
|
||||
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
ip.type = INPUT_MOUSE;
|
||||
@@ -63,13 +72,25 @@ int MouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
|
||||
ip.mi.time = 0;
|
||||
|
||||
SetCursorPos(ip.mi.dx, ip.mi.dy);
|
||||
if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) {
|
||||
LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, static_cast<int>(remote_action.m.flag),
|
||||
GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) {
|
||||
SendInput(1, &ip, sizeof(INPUT));
|
||||
UINT sent = SendInput(1, &ip, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN(
|
||||
"SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), GetLastError());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-05-07
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_KEY_METADATA_H_
|
||||
#define _WINDOWS_KEY_METADATA_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline bool LookupWindowsKeyMetadataFromVk(int key_code,
|
||||
uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (scan_code_out == nullptr || extended_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (key_code) {
|
||||
case 0x21: // Page Up
|
||||
*scan_code_out = 0x49;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x22: // Page Down
|
||||
*scan_code_out = 0x51;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x23: // End
|
||||
*scan_code_out = 0x4F;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x24: // Home
|
||||
*scan_code_out = 0x47;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x25: // Left Arrow
|
||||
*scan_code_out = 0x4B;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x26: // Up Arrow
|
||||
*scan_code_out = 0x48;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x27: // Right Arrow
|
||||
*scan_code_out = 0x4D;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x28: // Down Arrow
|
||||
*scan_code_out = 0x50;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x2D: // Insert
|
||||
*scan_code_out = 0x52;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x2E: // Delete
|
||||
*scan_code_out = 0x53;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x6F: // Numpad /
|
||||
*scan_code_out = 0x35;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0xA3: // Right Ctrl
|
||||
*scan_code_out = 0x1D;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0xA5: // Right Alt
|
||||
*scan_code_out = 0x38;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x5B: // Left Win
|
||||
*scan_code_out = 0x5B;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
case 0x5C: // Right Win
|
||||
*scan_code_out = 0x5C;
|
||||
*extended_out = true;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -33,10 +33,15 @@ struct TranslationRow {
|
||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||
u8"Недавние подключения") \
|
||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||
u8"Развернуть панель управления") \
|
||||
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||
u8"Свернуть панель управления") \
|
||||
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
|
||||
X(show_net_traffic_stats, u8"显示流量统计", "Show Net Traffic Stats", \
|
||||
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
|
||||
u8"Показать статистику трафика") \
|
||||
X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \
|
||||
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
|
||||
u8"Скрыть статистику трафика") \
|
||||
X(video, u8"视频", "Video", u8"Видео") \
|
||||
X(audio, u8"音频", "Audio", u8"Аудио") \
|
||||
@@ -47,26 +52,70 @@ struct TranslationRow {
|
||||
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
|
||||
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
|
||||
u8"Выйти из полноэкранного режима") \
|
||||
X(control_mouse, u8"控制", "Control", u8"Управление") \
|
||||
X(release_mouse, u8"释放", "Release", u8"Освободить") \
|
||||
X(audio_capture, u8"声音", "Audio", u8"Звук") \
|
||||
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
|
||||
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
|
||||
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
||||
"Remote lock screen visible, send SAS", \
|
||||
u8"Видна блокировка, отправьте SAS") \
|
||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||
"Remote secure desktop active", \
|
||||
u8"Активен защищенный рабочий стол") \
|
||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||
"Remote Windows service unavailable", \
|
||||
u8"Служба Windows на удаленной стороне недоступна") \
|
||||
X(remote_unlock_requires_secure_desktop, \
|
||||
u8"当前仍需要安全桌面专用采集/输入", \
|
||||
"Secure desktop capture/input is still required", \
|
||||
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего стола") \
|
||||
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
||||
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
||||
"Remote lock screen visible, send SAS", \
|
||||
u8"Видна блокировка, отправьте SAS") \
|
||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||
"Remote Windows service unavailable", \
|
||||
u8"Служба Windows на удаленной стороне недоступна") \
|
||||
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
|
||||
X(windows_service_setup_message, \
|
||||
u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
|
||||
u8"Service。\n未安装该服务不影响 CrossDesk " \
|
||||
u8"正常使用,仅无法在锁屏状态下控制本机。", \
|
||||
"To support remote control of this device while it is locked, CrossDesk " \
|
||||
"Service must be installed with administrator permission.\nWithout this " \
|
||||
"service, CrossDesk still works normally; only lock-screen control of " \
|
||||
"this computer is unavailable.", \
|
||||
u8"Чтобы поддерживать удаленное управление этим устройством на экране " \
|
||||
u8"блокировки, необходимо установить CrossDesk Service с правами " \
|
||||
u8"администратора.\nБез этой службы CrossDesk продолжит работать " \
|
||||
u8"нормально; будет недоступно только управление этим компьютером на " \
|
||||
u8"экране блокировки.") \
|
||||
X(install_windows_service, u8"安装", "Install", u8"Установить") \
|
||||
X(windows_service_settings_label, u8"锁屏控制服务:", \
|
||||
"Lock Screen Service:", u8"Служба блокировки экрана:") \
|
||||
X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \
|
||||
X(do_not_remind_again, u8"不再提醒", "Do not remind again", \
|
||||
u8"Больше не напоминать") \
|
||||
X(windows_service_prompt_suppressed_message, \
|
||||
u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \
|
||||
"You will not be reminded again. To enable remote control while locked " \
|
||||
"later, click Install in Settings.", \
|
||||
u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \
|
||||
u8"экране блокировки, нажмите «Установить» в настройках.") \
|
||||
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
|
||||
u8"Установка службы...") \
|
||||
X(windows_service_install_success, u8"服务已安装并启动", \
|
||||
"Service installed and started", u8"Служба установлена и запущена") \
|
||||
X(windows_service_install_failed, \
|
||||
u8"服务安装失败。请确认 " \
|
||||
u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \
|
||||
u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \
|
||||
"Service installation failed. Make sure CrossDesk.exe, " \
|
||||
"crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \
|
||||
"portable folder, then approve the administrator prompt.", \
|
||||
u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \
|
||||
u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \
|
||||
u8"одной папке портативной версии, затем подтвердите запрос прав " \
|
||||
u8"администратора.") \
|
||||
X(remote_unlock_requires_secure_desktop, \
|
||||
u8"当前仍需要安全桌面专用采集/输入", \
|
||||
"Secure desktop capture/input is still required", \
|
||||
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
|
||||
u8"стола") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||
@@ -134,7 +183,8 @@ struct TranslationRow {
|
||||
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
||||
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
||||
u8"Вступит в силу после перезапуска") \
|
||||
X(select_file, u8"选择文件", "Select File", u8"Выбрать файл") \
|
||||
X(select_file, u8"选择文件发送", "Select File to Send", \
|
||||
u8"Выбрать файл для отправки") \
|
||||
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
|
||||
u8"Прогресс передачи файлов") \
|
||||
X(queued, u8"队列中", "Queued", u8"В очереди") \
|
||||
|
||||
@@ -204,11 +204,11 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
props->params_.user_id = props->local_id_.c_str();
|
||||
props->peer_ = CreatePeer(&props->params_);
|
||||
|
||||
props->control_window_width_ = title_bar_height_ * 9.0f;
|
||||
props->control_window_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_min_width_ = title_bar_height_ * 0.65f;
|
||||
props->control_window_min_height_ = title_bar_height_ * 1.3f;
|
||||
props->control_window_max_width_ = title_bar_height_ * 9.0f;
|
||||
props->control_window_max_width_ = title_bar_height_ * 10.0f;
|
||||
props->control_window_max_height_ = title_bar_height_ * 7.0f;
|
||||
|
||||
props->connection_status_ = ConnectionStatus::Connecting;
|
||||
@@ -272,4 +272,4 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
+163
-34
@@ -42,6 +42,8 @@
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||
|
||||
const ImWchar* GetMultilingualGlyphRanges() {
|
||||
static std::vector<ImWchar> glyph_ranges;
|
||||
if (glyph_ranges.empty()) {
|
||||
@@ -82,15 +84,22 @@ HICON LoadTrayIcon() {
|
||||
|
||||
struct WindowsServiceInteractiveStatus {
|
||||
bool available = false;
|
||||
bool sas_secure_desktop_grace_active = false;
|
||||
unsigned int error_code = 0;
|
||||
std::string interactive_stage;
|
||||
std::string error;
|
||||
};
|
||||
|
||||
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
|
||||
constexpr DWORD kWindowsServiceQueryTimeoutMs = 100;
|
||||
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
|
||||
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
|
||||
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
|
||||
|
||||
bool IsTransientWindowsServiceStatusError(const std::string& error) {
|
||||
return error == "pipe_unavailable" || error == "pipe_connect_failed" ||
|
||||
error == "pipe_read_failed";
|
||||
}
|
||||
|
||||
RemoteAction BuildWindowsServiceStatusAction(
|
||||
const WindowsServiceInteractiveStatus& status) {
|
||||
RemoteAction action{};
|
||||
@@ -125,6 +134,8 @@ bool QueryWindowsServiceInteractiveStatus(
|
||||
}
|
||||
|
||||
status->interactive_stage = json.value("interactive_stage", std::string());
|
||||
status->sas_secure_desktop_grace_active =
|
||||
json.value("sas_secure_desktop_grace_active", false);
|
||||
|
||||
if (ShouldNormalizeUnlockToUserDesktop(
|
||||
json.value("interactive_lock_screen_visible", false),
|
||||
@@ -598,6 +609,11 @@ int Render::LoadSettingsFromCacheFile() {
|
||||
enable_autostart_ = config_center_->IsEnableAutostart();
|
||||
enable_daemon_ = config_center_->IsEnableDaemon();
|
||||
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
portable_service_prompt_suppressed_ =
|
||||
config_center_->IsPortableServicePromptSuppressed();
|
||||
portable_service_do_not_remind_ = portable_service_prompt_suppressed_;
|
||||
#endif
|
||||
|
||||
// File transfer save path
|
||||
{
|
||||
@@ -625,6 +641,12 @@ int Render::LoadSettingsFromCacheFile() {
|
||||
}
|
||||
|
||||
int Render::ScreenCapturerInit() {
|
||||
#ifdef __APPLE__
|
||||
if (!EnsureMacScreenRecordingPermission()) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!screen_capturer_) {
|
||||
screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create();
|
||||
}
|
||||
@@ -642,18 +664,37 @@ int Render::ScreenCapturerInit() {
|
||||
fps,
|
||||
[this, fps](unsigned char* data, int size, int width, int height,
|
||||
const char* display_name) -> void {
|
||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch())
|
||||
.count();
|
||||
const auto now_time =
|
||||
static_cast<uint64_t>(std::chrono::duration_cast<
|
||||
std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now()
|
||||
.time_since_epoch())
|
||||
.count());
|
||||
auto duration = now_time - last_frame_time_;
|
||||
if (duration * fps >= 1000) { // ~60 FPS
|
||||
const std::string stream_id = display_name ? display_name : "";
|
||||
const bool resumed_after_gap =
|
||||
last_frame_time_ != 0 &&
|
||||
duration >= kCaptureResumeKeyFrameGapMs;
|
||||
const bool stream_changed =
|
||||
!last_video_frame_stream_id_.empty() &&
|
||||
last_video_frame_stream_id_ != stream_id;
|
||||
if (resumed_after_gap || stream_changed) {
|
||||
if (RequestVideoKeyFrame(peer_, stream_id.c_str()) == 0) {
|
||||
LOG_INFO(
|
||||
"Request video key frame before sending captured frame, "
|
||||
"stream='{}', gap_ms={}, stream_changed={}",
|
||||
stream_id, duration, stream_changed);
|
||||
}
|
||||
}
|
||||
XVideoFrame frame;
|
||||
frame.data = (const char*)data;
|
||||
frame.size = size;
|
||||
frame.width = width;
|
||||
frame.height = height;
|
||||
frame.captured_timestamp = GetSystemTimeMicros(peer_);
|
||||
SendVideoFrame(peer_, &frame, display_name);
|
||||
SendVideoFrame(peer_, &frame, stream_id.c_str());
|
||||
last_video_frame_stream_id_ = stream_id;
|
||||
last_frame_time_ = now_time;
|
||||
}
|
||||
});
|
||||
@@ -675,6 +716,12 @@ int Render::ScreenCapturerInit() {
|
||||
}
|
||||
|
||||
int Render::StartScreenCapturer() {
|
||||
#ifdef __APPLE__
|
||||
if (!EnsureMacScreenRecordingPermission()) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!screen_capturer_) {
|
||||
LOG_INFO("Screen capturer instance missing, recreating before start");
|
||||
if (0 != ScreenCapturerInit()) {
|
||||
@@ -722,11 +769,16 @@ int Render::StartSpeakerCapturer() {
|
||||
}
|
||||
|
||||
if (speaker_capturer_) {
|
||||
speaker_capturer_->Start();
|
||||
const int ret = speaker_capturer_->Start();
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Start speaker capturer failed: {}", ret);
|
||||
return ret;
|
||||
}
|
||||
start_speaker_capturer_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int Render::StopSpeakerCapturer() {
|
||||
@@ -739,6 +791,12 @@ int Render::StopSpeakerCapturer() {
|
||||
}
|
||||
|
||||
int Render::StartMouseController() {
|
||||
#ifdef __APPLE__
|
||||
if (!EnsureMacAccessibilityPermission()) {
|
||||
return -1;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!device_controller_factory_) {
|
||||
LOG_INFO("Device controller factory is nullptr");
|
||||
return -1;
|
||||
@@ -796,6 +854,13 @@ int Render::StopMouseController() {
|
||||
int Render::StartKeyboardCapturer() {
|
||||
keyboard_capturer_uses_sdl_events_ = false;
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (!EnsureMacAccessibilityPermission()) {
|
||||
keyboard_capturer_uses_sdl_events_ = true;
|
||||
return 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
if (IsWaylandSession()) {
|
||||
keyboard_capturer_uses_sdl_events_ = true;
|
||||
@@ -812,10 +877,11 @@ int Render::StartKeyboardCapturer() {
|
||||
}
|
||||
|
||||
int keyboard_capturer_init_ret = keyboard_capturer_->Hook(
|
||||
[](int key_code, bool is_down, void* user_ptr) {
|
||||
[](int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
void* user_ptr) {
|
||||
if (user_ptr) {
|
||||
Render* render = (Render*)user_ptr;
|
||||
render->SendKeyCommand(key_code, is_down);
|
||||
render->SendKeyCommand(key_code, is_down, scan_code, extended);
|
||||
}
|
||||
},
|
||||
this);
|
||||
@@ -1093,8 +1159,9 @@ void Render::UpdateInteractions() {
|
||||
}
|
||||
|
||||
if (start_speaker_capturer_ && !speaker_capturer_is_started_) {
|
||||
StartSpeakerCapturer();
|
||||
speaker_capturer_is_started_ = true;
|
||||
if (0 == StartSpeakerCapturer()) {
|
||||
speaker_capturer_is_started_ = true;
|
||||
}
|
||||
} else if (!start_speaker_capturer_ && speaker_capturer_is_started_) {
|
||||
StopSpeakerCapturer();
|
||||
speaker_capturer_is_started_ = false;
|
||||
@@ -1539,6 +1606,10 @@ int Render::DrawMainWindow() {
|
||||
|
||||
UpdateNotificationWindow();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
PortableServiceInstallWindow();
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
if (show_request_permission_window_) {
|
||||
RequestPermissionWindow();
|
||||
@@ -1648,26 +1719,6 @@ int Render::DrawServerWindow() {
|
||||
}
|
||||
|
||||
int Render::Run() {
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty() &&
|
||||
latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
latest_version_ = 'v' + latest_version_info_["version"].get<std::string>();
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
} else {
|
||||
release_notes_ = "";
|
||||
}
|
||||
update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
if (update_available_) {
|
||||
show_update_notification_window_ = true;
|
||||
}
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
update_available_ = false;
|
||||
}
|
||||
|
||||
path_manager_ = std::make_unique<PathManager>("CrossDesk");
|
||||
if (path_manager_) {
|
||||
exec_log_path_ = path_manager_->GetLogPath().string();
|
||||
@@ -1696,11 +1747,50 @@ int Render::Run() {
|
||||
InitializeLogger();
|
||||
LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION);
|
||||
|
||||
latest_version_info_ = CheckUpdate();
|
||||
if (!latest_version_info_.empty()) {
|
||||
std::string version;
|
||||
if (latest_version_info_.contains("latest_version") &&
|
||||
latest_version_info_["latest_version"].is_string()) {
|
||||
version = latest_version_info_["latest_version"].get<std::string>();
|
||||
} else if (latest_version_info_.contains("version") &&
|
||||
latest_version_info_["version"].is_string()) {
|
||||
version = latest_version_info_["version"].get<std::string>();
|
||||
}
|
||||
|
||||
if (!version.empty()) {
|
||||
latest_version_ = 'v' + version;
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
}
|
||||
if (latest_version_info_.contains("releaseNotes") &&
|
||||
latest_version_info_["releaseNotes"].is_string()) {
|
||||
release_notes_ = latest_version_info_["releaseNotes"].get<std::string>();
|
||||
} else {
|
||||
release_notes_ = "";
|
||||
}
|
||||
update_available_ =
|
||||
!version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_);
|
||||
LOG_INFO("Update check: current={}, latest={}, available={}",
|
||||
CROSSDESK_VERSION, latest_version_, update_available_);
|
||||
if (update_available_) {
|
||||
show_update_notification_window_ = true;
|
||||
}
|
||||
} else {
|
||||
latest_version_ = "";
|
||||
update_available_ = false;
|
||||
LOG_WARN("Update check skipped: version.json is empty or missing latest_version");
|
||||
}
|
||||
|
||||
InitializeSettings();
|
||||
InitializeSDL();
|
||||
InitializeModules();
|
||||
InitializeMainWindow();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
CheckPortableWindowsService();
|
||||
#endif
|
||||
|
||||
const int scaled_video_width_ = 160;
|
||||
const int scaled_video_height_ = 90;
|
||||
|
||||
@@ -1922,6 +2012,12 @@ void Render::HandleWindowsServiceIntegration() {
|
||||
LOG_WARN("Remote SAS request failed: {}", response);
|
||||
} else {
|
||||
LOG_INFO("Remote SAS request forwarded to local Windows service");
|
||||
optimistic_windows_secure_desktop_until_tick_ =
|
||||
static_cast<uint32_t>(SDL_GetTicks()) +
|
||||
kWindowsServiceSasSecureDesktopGraceMs;
|
||||
local_service_status_received_ = true;
|
||||
local_service_available_ = true;
|
||||
local_interactive_stage_ = "secure-desktop";
|
||||
}
|
||||
last_windows_service_status_tick_ = 0;
|
||||
force_broadcast = true;
|
||||
@@ -1937,9 +2033,32 @@ void Render::HandleWindowsServiceIntegration() {
|
||||
|
||||
WindowsServiceInteractiveStatus status;
|
||||
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
|
||||
local_service_status_received_ = status_ok;
|
||||
WindowsServiceInteractiveStatus broadcast_status = status;
|
||||
const bool previous_secure_desktop_interaction =
|
||||
IsSecureDesktopInteractionRequired(local_interactive_stage_);
|
||||
const bool optimistic_secure_desktop_active =
|
||||
optimistic_windows_secure_desktop_until_tick_ != 0 &&
|
||||
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
|
||||
now) > 0;
|
||||
const bool keep_optimistic_secure_desktop =
|
||||
status_ok && status.available && optimistic_secure_desktop_active &&
|
||||
status.sas_secure_desktop_grace_active &&
|
||||
status.interactive_stage == "user-desktop";
|
||||
local_service_status_received_ =
|
||||
status_ok || previous_secure_desktop_interaction;
|
||||
local_service_available_ = status.available;
|
||||
local_interactive_stage_ = status.available ? status.interactive_stage : "";
|
||||
if (status.available) {
|
||||
if (keep_optimistic_secure_desktop) {
|
||||
local_interactive_stage_ = "secure-desktop";
|
||||
broadcast_status.interactive_stage = local_interactive_stage_;
|
||||
} else {
|
||||
local_interactive_stage_ = status.interactive_stage;
|
||||
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
}
|
||||
} else if (!previous_secure_desktop_interaction) {
|
||||
local_interactive_stage_.clear();
|
||||
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
}
|
||||
|
||||
if (status_ok) {
|
||||
const bool availability_changed =
|
||||
@@ -1952,6 +2071,11 @@ void Render::HandleWindowsServiceIntegration() {
|
||||
if (status.available) {
|
||||
LOG_INFO(
|
||||
"Local Windows service available for secure desktop integration");
|
||||
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||
LOG_INFO(
|
||||
"Local Windows service temporarily unavailable, keeping last "
|
||||
"secure desktop state: error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Local Windows service unavailable, secure desktop integration "
|
||||
@@ -1972,7 +2096,7 @@ void Render::HandleWindowsServiceIntegration() {
|
||||
last_logged_service_error_code = 0;
|
||||
}
|
||||
|
||||
RemoteAction remote_action = BuildWindowsServiceStatusAction(status);
|
||||
RemoteAction remote_action = BuildWindowsServiceStatusAction(broadcast_status);
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
|
||||
control_data_label_.c_str());
|
||||
@@ -1991,6 +2115,7 @@ void Render::ResetLocalWindowsServiceState(bool clear_pending_sas) {
|
||||
local_service_status_received_ = false;
|
||||
local_service_available_ = false;
|
||||
local_interactive_stage_.clear();
|
||||
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2181,6 +2306,10 @@ void Render::Cleanup() {
|
||||
CleanupFactories();
|
||||
CleanupPeers();
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
|
||||
WaitForThumbnailSaveTasks();
|
||||
|
||||
AudioDeviceDestroy();
|
||||
|
||||
+45
-8
@@ -62,6 +62,7 @@ class Render {
|
||||
std::chrono::steady_clock::time_point file_send_last_update_time_;
|
||||
uint64_t file_send_last_bytes_ = 0;
|
||||
bool file_transfer_window_visible_ = false;
|
||||
bool file_transfer_window_hovered_ = false;
|
||||
std::atomic<uint32_t> current_file_id_{0};
|
||||
|
||||
struct QueuedFile {
|
||||
@@ -114,6 +115,7 @@ class Render {
|
||||
bool is_control_bar_in_left_ = true;
|
||||
bool control_bar_hovered_ = false;
|
||||
bool display_selectable_hovered_ = false;
|
||||
bool shortcut_selectable_hovered_ = false;
|
||||
bool control_bar_expand_ = true;
|
||||
bool reset_control_bar_pos_ = false;
|
||||
bool control_window_width_is_changing_ = false;
|
||||
@@ -124,10 +126,10 @@ class Render {
|
||||
float sub_stream_window_width_ = 1280;
|
||||
float sub_stream_window_height_ = 720;
|
||||
float control_window_min_width_ = 20;
|
||||
float control_window_max_width_ = 230;
|
||||
float control_window_max_width_ = 300;
|
||||
float control_window_min_height_ = 38;
|
||||
float control_window_max_height_ = 180;
|
||||
float control_window_width_ = 230;
|
||||
float control_window_width_ = 300;
|
||||
float control_window_height_ = 38;
|
||||
float control_bar_pos_x_ = 0;
|
||||
float control_bar_pos_y_ = 30;
|
||||
@@ -294,6 +296,9 @@ class Render {
|
||||
void OpenScreenRecordingPreferences();
|
||||
void OpenAccessibilityPreferences();
|
||||
bool DrawToggleSwitch(const char* id, bool active, bool enabled);
|
||||
void RefreshMacPermissionStatus(bool force);
|
||||
bool EnsureMacScreenRecordingPermission();
|
||||
bool EnsureMacAccessibilityPermission();
|
||||
#endif
|
||||
|
||||
public:
|
||||
@@ -338,7 +343,8 @@ class Render {
|
||||
static void FreeRemoteAction(RemoteAction& action);
|
||||
|
||||
private:
|
||||
int SendKeyCommand(int key_code, bool is_down);
|
||||
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
static bool IsModifierVkKey(int key_code);
|
||||
void TrackPressedKeyState(int key_code, bool is_down);
|
||||
void ForceReleasePressedKeys();
|
||||
@@ -379,6 +385,19 @@ class Render {
|
||||
void HandleWindowsServiceIntegration();
|
||||
#if _WIN32
|
||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||
#if CROSSDESK_PORTABLE
|
||||
enum class PortableServiceInstallState {
|
||||
idle,
|
||||
installing,
|
||||
succeeded,
|
||||
failed,
|
||||
};
|
||||
|
||||
void CheckPortableWindowsService();
|
||||
int PortableServiceInstallWindow();
|
||||
void StartPortableWindowsServiceInstall();
|
||||
void JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
#endif
|
||||
|
||||
private:
|
||||
@@ -534,8 +553,8 @@ class Render {
|
||||
std::string remote_client_id_ = "";
|
||||
std::unordered_set<int> pressed_keyboard_keys_;
|
||||
std::mutex pressed_keyboard_keys_mutex_;
|
||||
SDL_Event last_mouse_event;
|
||||
SDL_AudioStream* output_stream_;
|
||||
SDL_Event last_mouse_event{};
|
||||
SDL_AudioStream* output_stream_ = nullptr;
|
||||
uint32_t STREAM_REFRESH_EVENT = 0;
|
||||
#if _WIN32
|
||||
std::atomic<bool> pending_windows_service_sas_{false};
|
||||
@@ -544,6 +563,17 @@ class Render {
|
||||
std::string local_interactive_stage_;
|
||||
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
||||
uint32_t last_windows_service_status_tick_ = 0;
|
||||
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||
#if CROSSDESK_PORTABLE
|
||||
bool portable_service_prompt_checked_ = false;
|
||||
bool show_portable_service_install_window_ = false;
|
||||
bool show_portable_service_prompt_suppressed_window_ = false;
|
||||
bool portable_service_do_not_remind_ = false;
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||
PortableServiceInstallState::idle};
|
||||
std::thread portable_service_install_thread_;
|
||||
#endif
|
||||
#endif
|
||||
|
||||
// stream window render
|
||||
@@ -657,8 +687,8 @@ class Render {
|
||||
// Map file_id to FileTransferState for global file transfer (props == null)
|
||||
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
|
||||
std::shared_mutex file_id_to_transfer_state_mutex_;
|
||||
SDL_AudioDeviceID input_dev_;
|
||||
SDL_AudioDeviceID output_dev_;
|
||||
SDL_AudioDeviceID input_dev_ = 0;
|
||||
SDL_AudioDeviceID output_dev_ = 0;
|
||||
ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
|
||||
ScreenCapturer* screen_capturer_ = nullptr;
|
||||
SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr;
|
||||
@@ -667,13 +697,20 @@ class Render {
|
||||
MouseController* mouse_controller_ = nullptr;
|
||||
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
uint64_t last_frame_time_;
|
||||
uint64_t last_frame_time_ = 0;
|
||||
std::string last_video_frame_stream_id_;
|
||||
bool show_new_version_icon_ = false;
|
||||
bool show_new_version_icon_in_menu_ = true;
|
||||
double new_version_icon_last_trigger_time_ = 0.0;
|
||||
double new_version_icon_render_start_time_ = 0.0;
|
||||
#ifdef __APPLE__
|
||||
bool show_request_permission_window_ = true;
|
||||
bool mac_permission_status_initialized_ = false;
|
||||
uint32_t mac_permission_last_check_tick_ = 0;
|
||||
bool mac_screen_recording_permission_granted_ = false;
|
||||
bool mac_accessibility_permission_granted_ = false;
|
||||
bool mac_screen_recording_permission_requested_ = false;
|
||||
bool mac_accessibility_permission_requested_ = false;
|
||||
#endif
|
||||
char client_id_[10] = "";
|
||||
char client_id_display_[12] = "";
|
||||
|
||||
+142
-14
@@ -17,6 +17,7 @@
|
||||
#include "platform.h"
|
||||
#include "rd_log.h"
|
||||
#include "render.h"
|
||||
#include "windows_key_metadata.h"
|
||||
#if _WIN32
|
||||
#include "interactive_state.h"
|
||||
#include "service_host.h"
|
||||
@@ -28,34 +29,66 @@ namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
int TranslateSdlKeypadScancodeToVk(SDL_Scancode scancode) {
|
||||
switch (scancode) {
|
||||
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) {
|
||||
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
|
||||
|
||||
switch (event.scancode) {
|
||||
case SDL_SCANCODE_NUMLOCKCLEAR:
|
||||
return 0x90;
|
||||
case SDL_SCANCODE_KP_ENTER:
|
||||
return 0x0D;
|
||||
case SDL_SCANCODE_KP_0:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2D;
|
||||
}
|
||||
return 0x60;
|
||||
case SDL_SCANCODE_KP_1:
|
||||
if (!numlock_enabled) {
|
||||
return 0x23;
|
||||
}
|
||||
return 0x61;
|
||||
case SDL_SCANCODE_KP_2:
|
||||
if (!numlock_enabled) {
|
||||
return 0x28;
|
||||
}
|
||||
return 0x62;
|
||||
case SDL_SCANCODE_KP_3:
|
||||
if (!numlock_enabled) {
|
||||
return 0x22;
|
||||
}
|
||||
return 0x63;
|
||||
case SDL_SCANCODE_KP_4:
|
||||
if (!numlock_enabled) {
|
||||
return 0x25;
|
||||
}
|
||||
return 0x64;
|
||||
case SDL_SCANCODE_KP_5:
|
||||
return 0x65;
|
||||
case SDL_SCANCODE_KP_6:
|
||||
if (!numlock_enabled) {
|
||||
return 0x27;
|
||||
}
|
||||
return 0x66;
|
||||
case SDL_SCANCODE_KP_7:
|
||||
if (!numlock_enabled) {
|
||||
return 0x24;
|
||||
}
|
||||
return 0x67;
|
||||
case SDL_SCANCODE_KP_8:
|
||||
if (!numlock_enabled) {
|
||||
return 0x26;
|
||||
}
|
||||
return 0x68;
|
||||
case SDL_SCANCODE_KP_9:
|
||||
if (!numlock_enabled) {
|
||||
return 0x21;
|
||||
}
|
||||
return 0x69;
|
||||
case SDL_SCANCODE_KP_PERIOD:
|
||||
case SDL_SCANCODE_KP_COMMA:
|
||||
if (!numlock_enabled) {
|
||||
return 0x2E;
|
||||
}
|
||||
return 0x6E;
|
||||
case SDL_SCANCODE_KP_DIVIDE:
|
||||
return 0x6F;
|
||||
@@ -73,7 +106,7 @@ int TranslateSdlKeypadScancodeToVk(SDL_Scancode scancode) {
|
||||
}
|
||||
|
||||
int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent& event) {
|
||||
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event.scancode);
|
||||
const int keypad_key_code = TranslateSdlKeypadScancodeToVk(event);
|
||||
if (keypad_key_code >= 0) {
|
||||
return keypad_key_code;
|
||||
}
|
||||
@@ -200,6 +233,49 @@ int TranslateSdlKeyboardEventToVk(const SDL_KeyboardEvent& event) {
|
||||
}
|
||||
}
|
||||
|
||||
int NormalizeWindowsModifierVk(int key_code, uint32_t scan_code,
|
||||
bool extended) {
|
||||
#if _WIN32
|
||||
if (key_code != 0x10 && key_code != 0x11 && key_code != 0x12) {
|
||||
return key_code;
|
||||
}
|
||||
|
||||
UINT scan_code_with_prefix = static_cast<UINT>(scan_code & 0xFF);
|
||||
if (extended) {
|
||||
scan_code_with_prefix |= 0xE000;
|
||||
}
|
||||
|
||||
const UINT normalized_vk =
|
||||
MapVirtualKeyW(scan_code_with_prefix, MAPVK_VSC_TO_VK_EX);
|
||||
return normalized_vk != 0 ? static_cast<int>(normalized_vk) : key_code;
|
||||
#else
|
||||
(void)scan_code;
|
||||
(void)extended;
|
||||
return key_code;
|
||||
#endif
|
||||
}
|
||||
|
||||
void PopulateWindowsKeyMetadataFromVk(int key_code, uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (scan_code_out == nullptr || extended_out == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
const UINT scan_code =
|
||||
MapVirtualKeyW(static_cast<UINT>(key_code), MAPVK_VK_TO_VSC_EX);
|
||||
if (scan_code == 0) {
|
||||
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
|
||||
return;
|
||||
}
|
||||
|
||||
*scan_code_out = static_cast<uint32_t>(scan_code & 0xFF);
|
||||
*extended_out = (scan_code & 0xFF00) != 0;
|
||||
#else
|
||||
LookupWindowsKeyMetadataFromVk(key_code, scan_code_out, extended_out);
|
||||
#endif
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
constexpr uint32_t kSecureDesktopInputLogIntervalMs = 2000;
|
||||
|
||||
@@ -241,6 +317,22 @@ void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* side,
|
||||
"cannot drive the Windows password UI",
|
||||
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
|
||||
}
|
||||
|
||||
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||
const RemoteAction& action) {
|
||||
if (!response.is_object()) {
|
||||
return false;
|
||||
}
|
||||
if (response.value("error", std::string()) != "send_input_failed") {
|
||||
return false;
|
||||
}
|
||||
if (response.value("code", 0u) != ERROR_ACCESS_DENIED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return action.type == ControlType::keyboard &&
|
||||
action.k.flag == KeyFlag::key_up;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
@@ -353,15 +445,26 @@ void Render::ForceReleasePressedKeys() {
|
||||
}
|
||||
}
|
||||
|
||||
int Render::SendKeyCommand(int key_code, bool is_down) {
|
||||
RemoteAction remote_action;
|
||||
int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
|
||||
bool extended) {
|
||||
RemoteAction remote_action{};
|
||||
remote_action.type = ControlType::keyboard;
|
||||
if (is_down) {
|
||||
remote_action.k.flag = KeyFlag::key_down;
|
||||
} else {
|
||||
remote_action.k.flag = KeyFlag::key_up;
|
||||
}
|
||||
|
||||
if (scan_code == 0) {
|
||||
PopulateWindowsKeyMetadataFromVk(key_code, &scan_code, &extended);
|
||||
}
|
||||
#if _WIN32
|
||||
key_code = NormalizeWindowsModifierVk(key_code, scan_code, extended);
|
||||
#endif
|
||||
|
||||
remote_action.k.key_value = key_code;
|
||||
remote_action.k.scan_code = scan_code;
|
||||
remote_action.k.extended = extended;
|
||||
|
||||
std::string target_id = controlled_remote_id_.empty() ? focused_remote_id_
|
||||
: controlled_remote_id_;
|
||||
@@ -405,7 +508,7 @@ int Render::ProcessKeyboardEvent(const SDL_Event& event) {
|
||||
|
||||
int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
controlled_remote_id_ = "";
|
||||
RemoteAction remote_action;
|
||||
RemoteAction remote_action{};
|
||||
float cursor_x = last_mouse_event.motion.x;
|
||||
float cursor_y = last_mouse_event.motion.y;
|
||||
|
||||
@@ -474,6 +577,12 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool file_transfer_window_hovered =
|
||||
props->file_transfer_.file_transfer_window_hovered_;
|
||||
const bool overlay_hovered =
|
||||
props->control_bar_hovered_ || props->display_selectable_hovered_ ||
|
||||
props->shortcut_selectable_hovered_ || file_transfer_window_hovered;
|
||||
|
||||
const SDL_FRect render_rect = props->stream_render_rect_f_;
|
||||
if (render_rect.w <= 1.0f || render_rect.h <= 1.0f) {
|
||||
continue;
|
||||
@@ -517,7 +626,7 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
remote_action.m.flag = MouseFlag::move;
|
||||
}
|
||||
|
||||
if (props->control_bar_hovered_ || props->display_selectable_hovered_) {
|
||||
if (overlay_hovered) {
|
||||
break;
|
||||
}
|
||||
if (props->peer_) {
|
||||
@@ -563,7 +672,7 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||
remote_action.m.x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
|
||||
remote_action.m.y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
|
||||
|
||||
if (props->control_bar_hovered_) {
|
||||
if (overlay_hovered) {
|
||||
continue;
|
||||
}
|
||||
if (props->peer_) {
|
||||
@@ -970,6 +1079,11 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
|
||||
render->pending_windows_service_sas_.store(true,
|
||||
std::memory_order_relaxed);
|
||||
} else if (remote_action.c.flag == ServiceCommandFlag::lock_workstation) {
|
||||
if (!LockWorkStation()) {
|
||||
LOG_WARN("Remote lock workstation request failed, error={}",
|
||||
GetLastError());
|
||||
}
|
||||
}
|
||||
#endif
|
||||
return;
|
||||
@@ -1006,7 +1120,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
// remote
|
||||
#if _WIN32
|
||||
if (render->local_service_status_received_ &&
|
||||
render->local_service_available_ &&
|
||||
IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) {
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
int absolute_x = 0;
|
||||
@@ -1042,10 +1155,19 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
if (remote_action.type == ControlType::keyboard) {
|
||||
const int key_code = static_cast<int>(remote_action.k.key_value);
|
||||
const bool is_down = remote_action.k.flag == KeyFlag::key_down;
|
||||
const std::string response =
|
||||
SendCrossDeskSecureDesktopKeyInput(key_code, is_down, 1000);
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
key_code, is_down, remote_action.k.scan_code,
|
||||
remote_action.k.extended, 1000);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
if (!json.is_discarded() &&
|
||||
IsTransientSecureDesktopInputFailure(json, remote_action)) {
|
||||
LOG_INFO(
|
||||
"Secure desktop keyboard injection transient failure, "
|
||||
"key_code={}, is_down={}, response={}",
|
||||
key_code, is_down, response);
|
||||
return;
|
||||
}
|
||||
LogSecureDesktopInputBlocked(
|
||||
&render->last_local_secure_input_block_log_tick_, "local",
|
||||
render->local_interactive_stage_.c_str());
|
||||
@@ -1070,11 +1192,17 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
||||
render->keyboard_capturer_) {
|
||||
render->keyboard_capturer_->SendKeyboardCommand(
|
||||
(int)remote_action.k.key_value,
|
||||
remote_action.k.flag == KeyFlag::key_down);
|
||||
remote_action.k.flag == KeyFlag::key_down, remote_action.k.scan_code,
|
||||
remote_action.k.extended);
|
||||
} else if (remote_action.type == ControlType::display_id &&
|
||||
render->screen_capturer_) {
|
||||
render->selected_display_ = remote_action.d;
|
||||
render->screen_capturer_->SwitchTo(remote_action.d);
|
||||
const int ret = render->screen_capturer_->SwitchTo(remote_action.d);
|
||||
if (ret == 0) {
|
||||
render->selected_display_ = remote_action.d;
|
||||
} else {
|
||||
LOG_WARN("Display switch skipped, invalid display_id={}",
|
||||
remote_action.d);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,22 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
void ShowControlBarTooltip(const std::string& text) {
|
||||
if (!ImGui::IsItemHovered() || text.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int CountDigits(int number) {
|
||||
if (number == 0) return 1;
|
||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||
@@ -162,7 +178,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
ImVec2 btn_min = ImGui::GetItemRectMin();
|
||||
ImVec2 btn_size_actual = ImGui::GetItemRectSize();
|
||||
ShowControlBarTooltip(
|
||||
localization::select_display[localization_language_index_]);
|
||||
|
||||
props->display_selectable_hovered_ = false;
|
||||
if (ImGui::BeginPopup("display")) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < props->display_info_list_.size(); i++) {
|
||||
@@ -178,21 +197,66 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
props->display_selectable_hovered_ = ImGui::IsWindowHovered();
|
||||
}
|
||||
props->display_selectable_hovered_ =
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::SetWindowFontScale(0.35f);
|
||||
ImVec2 text_size = ImGui::CalcTextSize(
|
||||
std::to_string(props->selected_display_ + 1).c_str());
|
||||
ImVec2 text_pos =
|
||||
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.5f,
|
||||
btn_min.y + (btn_size_actual.y - text_size.y) * 0.35f);
|
||||
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.55f,
|
||||
btn_min.y + (btn_size_actual.y - text_size.y) * 0.33f);
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
text_pos, IM_COL32(0, 0, 0, 255),
|
||||
std::to_string(props->selected_display_ + 1).c_str());
|
||||
|
||||
auto send_service_command = [&](ServiceCommandFlag flag,
|
||||
const char* log_action) {
|
||||
if (props->connection_status_ == ConnectionStatus::Connected &&
|
||||
props->peer_) {
|
||||
RemoteAction remote_action;
|
||||
remote_action.type = ControlType::service_command;
|
||||
remote_action.c.flag = flag;
|
||||
std::string msg = remote_action.to_json();
|
||||
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
|
||||
props->control_data_label_.c_str());
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Send {} command failed, remote_id={}, ret={}", log_action,
|
||||
props->remote_id_, ret);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
ImGui::SameLine();
|
||||
std::string shortcut = ICON_FA_KEYBOARD;
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) {
|
||||
ImGui::OpenPopup("shortcut");
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::send_shortcut[localization_language_index_]);
|
||||
|
||||
props->shortcut_selectable_hovered_ = false;
|
||||
if (ImGui::BeginPopup("shortcut")) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
std::string sas_label = "Ctrl+Alt+Del";
|
||||
std::string lock_label = "Win+L";
|
||||
if (ImGui::Selectable(sas_label.c_str())) {
|
||||
send_service_command(ServiceCommandFlag::send_sas, "SAS");
|
||||
}
|
||||
if (ImGui::Selectable(lock_label.c_str())) {
|
||||
send_service_command(ServiceCommandFlag::lock_workstation,
|
||||
"remote lock");
|
||||
}
|
||||
props->shortcut_selectable_hovered_ =
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
float mouse_x = ImGui::GetCursorScreenPos().x;
|
||||
float mouse_y = ImGui::GetCursorScreenPos().y;
|
||||
@@ -211,6 +275,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
: localization::control_mouse[localization_language_index_];
|
||||
}
|
||||
}
|
||||
const bool mouse_button_hovered = ImGui::IsItemHovered();
|
||||
const std::string mouse_tooltip =
|
||||
props->enable_mouse_control_
|
||||
? localization::release_mouse[localization_language_index_]
|
||||
: localization::control_mouse[localization_language_index_];
|
||||
ShowControlBarTooltip(mouse_tooltip);
|
||||
|
||||
if (!props->enable_mouse_control_) {
|
||||
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
|
||||
@@ -223,8 +293,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImVec2(
|
||||
mouse_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
mouse_y + button_height - line_padding + line_thickness * 0.7f),
|
||||
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
line_thickness);
|
||||
}
|
||||
|
||||
@@ -256,6 +326,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
props->control_data_label_.c_str());
|
||||
}
|
||||
}
|
||||
const bool audio_button_hovered = ImGui::IsItemHovered();
|
||||
const std::string audio_tooltip =
|
||||
props->audio_capture_button_pressed_
|
||||
? localization::mute[localization_language_index_]
|
||||
: localization::audio_capture[localization_language_index_];
|
||||
ShowControlBarTooltip(audio_tooltip);
|
||||
|
||||
if (!props->audio_capture_button_pressed_) {
|
||||
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
|
||||
@@ -268,8 +344,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImVec2(
|
||||
audio_x + button_width - line_padding - line_thickness * 0.7f,
|
||||
audio_y + button_height - line_padding + line_thickness * 0.7f),
|
||||
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
audio_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||
: IM_COL32(179, 213, 253, 255),
|
||||
line_thickness);
|
||||
}
|
||||
|
||||
@@ -282,6 +358,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
std::string path = OpenFileDialog(title);
|
||||
ProcessSelectedFile(path, props, file_label_);
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::select_file[localization_language_index_]);
|
||||
|
||||
ImGui::SameLine();
|
||||
// net traffic stats button
|
||||
@@ -306,6 +384,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
: localization::show_net_traffic_stats
|
||||
[localization_language_index_];
|
||||
}
|
||||
const std::string net_traffic_stats_tooltip =
|
||||
props->net_traffic_stats_button_pressed_
|
||||
? localization::hide_net_traffic_stats[localization_language_index_]
|
||||
: localization::show_net_traffic_stats
|
||||
[localization_language_index_];
|
||||
ShowControlBarTooltip(net_traffic_stats_tooltip);
|
||||
|
||||
if (button_color_style_pushed) {
|
||||
ImGui::PopStyleColor();
|
||||
@@ -332,6 +416,11 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
}
|
||||
props->reset_control_bar_pos_ = true;
|
||||
}
|
||||
const std::string fullscreen_tooltip =
|
||||
fullscreen_button_pressed_
|
||||
? localization::exit_fullscreen[localization_language_index_]
|
||||
: localization::fullscreen[localization_language_index_];
|
||||
ShowControlBarTooltip(fullscreen_tooltip);
|
||||
|
||||
ImGui::SameLine();
|
||||
// close button
|
||||
@@ -341,6 +430,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
ImVec2(button_width, button_height))) {
|
||||
CleanupPeer(props);
|
||||
}
|
||||
ShowControlBarTooltip(
|
||||
localization::disconnect[localization_language_index_]);
|
||||
|
||||
ImGui::SameLine();
|
||||
|
||||
@@ -370,6 +461,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
: ICON_FA_ANGLE_RIGHT)
|
||||
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
|
||||
: ICON_FA_ANGLE_LEFT);
|
||||
const std::string control_bar_tooltip =
|
||||
props->control_bar_expand_
|
||||
? localization::collapse_control_bar[localization_language_index_]
|
||||
: localization::expand_control_bar[localization_language_index_];
|
||||
if (ImGui::Button(control_bar.c_str(),
|
||||
ImVec2(button_height * 0.6f, button_height))) {
|
||||
props->control_bar_expand_ = !props->control_bar_expand_;
|
||||
@@ -381,6 +476,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
props->net_traffic_stats_button_pressed_ = false;
|
||||
}
|
||||
}
|
||||
ShowControlBarTooltip(control_bar_tooltip);
|
||||
|
||||
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
|
||||
NetTrafficStats(props);
|
||||
@@ -486,4 +582,4 @@ int Render::NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -104,7 +104,7 @@ int Render::AboutWindow() {
|
||||
ImGui::SetCursorPosX(about_window_width * 0.1f);
|
||||
ImGui::Text("%s", text.c_str());
|
||||
|
||||
if (0) {
|
||||
if (update_available_ && show_new_version_icon_in_menu_) {
|
||||
std::string new_version_available =
|
||||
localization::new_version_available[localization_language_index_] +
|
||||
": ";
|
||||
|
||||
@@ -31,6 +31,7 @@ int BitrateDisplay(int bitrate) {
|
||||
int Render::FileTransferWindow(
|
||||
std::shared_ptr<SubStreamWindowProperties>& props) {
|
||||
FileTransferState* state = props ? &props->file_transfer_ : &file_transfer_;
|
||||
state->file_transfer_window_hovered_ = false;
|
||||
|
||||
// Only show window if there are files in transfer list or currently
|
||||
// transferring
|
||||
@@ -72,8 +73,6 @@ int Render::FileTransferWindow(
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
|
||||
// Position window at bottom-left of stream window
|
||||
// Adjust window size based on number of files
|
||||
float file_transfer_window_width = main_window_width_ * 0.6f;
|
||||
@@ -82,15 +81,25 @@ int Render::FileTransferWindow(
|
||||
float pos_x = file_transfer_window_width * 0.05f;
|
||||
float pos_y = stream_window_height_ - file_transfer_window_height -
|
||||
file_transfer_window_width * 0.05;
|
||||
float same_line_width = file_transfer_window_width * 0.1f;
|
||||
|
||||
const ImVec2 mouse_pos = ImGui::GetMousePos();
|
||||
const bool mouse_in_window_rect =
|
||||
mouse_pos.x >= pos_x &&
|
||||
mouse_pos.x <= pos_x + file_transfer_window_width &&
|
||||
mouse_pos.y >= pos_y &&
|
||||
mouse_pos.y <= pos_y + file_transfer_window_height;
|
||||
|
||||
ImGui::SetNextWindowPos(ImVec2(pos_x, pos_y), ImGuiCond_Always);
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(file_transfer_window_width, file_transfer_window_height),
|
||||
ImGuiCond_Always);
|
||||
if (mouse_in_window_rect) {
|
||||
ImGui::SetNextWindowFocus();
|
||||
}
|
||||
|
||||
// Set Chinese font for proper display
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
const bool has_chinese_font = stream_windows_system_chinese_font_ != nullptr;
|
||||
if (has_chinese_font) {
|
||||
ImGui::PushFont(stream_windows_system_chinese_font_);
|
||||
}
|
||||
|
||||
@@ -103,24 +112,27 @@ int Render::FileTransferWindow(
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
bool window_opened = true;
|
||||
if (ImGui::Begin(
|
||||
localization::file_transfer_progress[localization_language_index_]
|
||||
.c_str(),
|
||||
&window_opened,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoScrollbar)) {
|
||||
const bool show_contents = ImGui::Begin(
|
||||
localization::file_transfer_progress[localization_language_index_]
|
||||
.c_str(),
|
||||
&window_opened,
|
||||
ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoResize |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoScrollbar);
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
state->file_transfer_window_hovered_ =
|
||||
mouse_in_window_rect ||
|
||||
ImGui::IsWindowHovered(ImGuiHoveredFlags_RootAndChildWindows);
|
||||
|
||||
if (!window_opened) {
|
||||
state->file_transfer_window_visible_ = false;
|
||||
}
|
||||
|
||||
if (show_contents && window_opened) {
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
|
||||
// Close button handling
|
||||
if (!window_opened) {
|
||||
state->file_transfer_window_visible_ = false;
|
||||
ImGui::End();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Display file list
|
||||
if (file_list.empty()) {
|
||||
@@ -225,21 +237,16 @@ int Render::FileTransferWindow(
|
||||
}
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
// Pop Chinese font if it was pushed
|
||||
if (stream_windows_system_chinese_font_) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
} else {
|
||||
ImGui::PopStyleColor(4);
|
||||
ImGui::PopStyleVar(2);
|
||||
}
|
||||
|
||||
ImGui::End();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
|
||||
if (has_chinese_font) {
|
||||
ImGui::PopFont();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
|
||||
@@ -4,10 +4,19 @@
|
||||
#include "render.h"
|
||||
#include "tinyfiledialogs.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
#include "service_host.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
int Render::SettingWindow() {
|
||||
ImGuiIO& io = ImGui::GetIO();
|
||||
float portable_y_padding = 0.0f;
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
portable_y_padding = 0.05f;
|
||||
#endif
|
||||
|
||||
if (show_settings_window_) {
|
||||
if (settings_window_pos_reset_) {
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
@@ -18,12 +27,14 @@ int Render::SettingWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f));
|
||||
ImVec2(io.DisplaySize.x * 0.315f,
|
||||
io.DisplaySize.y * (0.9f + portable_y_padding)));
|
||||
#else
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f));
|
||||
ImVec2(io.DisplaySize.x * 0.315f,
|
||||
io.DisplaySize.y * (0.85f + portable_y_padding)));
|
||||
#endif
|
||||
} else {
|
||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||
@@ -32,12 +43,14 @@ int Render::SettingWindow() {
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f));
|
||||
ImVec2(io.DisplaySize.x * 0.42f,
|
||||
io.DisplaySize.y * (0.9f + portable_y_padding)));
|
||||
#else
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
|
||||
ImGui::SetNextWindowSize(
|
||||
ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f));
|
||||
ImVec2(io.DisplaySize.x * 0.42f,
|
||||
io.DisplaySize.y * (0.85f + portable_y_padding)));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -73,23 +86,21 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
if (ImGui::BeginCombo(
|
||||
"##language",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_button_value_)]
|
||||
.display_name
|
||||
.c_str())) {
|
||||
if (ImGui::BeginCombo("##language",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
language_button_value_)]
|
||||
.display_name.c_str())) {
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
for (int i = 0; i < static_cast<int>(supported_languages.size());
|
||||
++i) {
|
||||
bool selected = (i == language_button_value_);
|
||||
if (ImGui::Selectable(
|
||||
supported_languages[i].display_name.c_str(), selected))
|
||||
if (ImGui::Selectable(supported_languages[i].display_name.c_str(),
|
||||
selected))
|
||||
language_button_value_ = i;
|
||||
if (selected) {
|
||||
ImGui::SetItemDefaultFocus();
|
||||
@@ -125,7 +136,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -158,7 +169,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -194,7 +205,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f);
|
||||
}
|
||||
|
||||
ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
|
||||
@@ -228,7 +239,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_hardware_video_codec",
|
||||
@@ -249,7 +260,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_turn", &enable_turn_);
|
||||
@@ -268,7 +279,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_srtp", &enable_srtp_);
|
||||
@@ -289,7 +300,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
|
||||
@@ -308,7 +319,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
|
||||
@@ -327,7 +338,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
|
||||
@@ -359,7 +370,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_minimize_to_tray_",
|
||||
@@ -384,7 +395,7 @@ int Render::SettingWindow() {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f);
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
|
||||
}
|
||||
|
||||
std::string display_path =
|
||||
@@ -429,6 +440,80 @@ int Render::SettingWindow() {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text("%s", localization::windows_service_settings_label
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::SameLine();
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.0f);
|
||||
} else if (ConfigCenter::LANGUAGE::ENGLISH == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.42f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
|
||||
}
|
||||
|
||||
const PortableServiceInstallState state =
|
||||
portable_service_install_state_.load(std::memory_order_acquire);
|
||||
const bool service_installed =
|
||||
IsCrossDeskServiceInstalled() ||
|
||||
state == PortableServiceInstallState::succeeded;
|
||||
if (service_installed) {
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 3.9f);
|
||||
} else if (ConfigCenter::LANGUAGE::ENGLISH ==
|
||||
localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.32f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f);
|
||||
}
|
||||
ImGui::Text("%s", localization::windows_service_installed
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
} else {
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(localization::install_windows_service
|
||||
[localization_language_index_]
|
||||
.c_str())) {
|
||||
StartPortableWindowsServiceInstall();
|
||||
}
|
||||
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
ImGui::Text("%s", localization::installing_windows_service
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
} else if (state == PortableServiceInstallState::failed) {
|
||||
ImGui::SameLine();
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::failed[localization_language_index_].c_str());
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::BeginTooltip();
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::PushTextWrapPos(title_bar_button_width_ * 10.0f);
|
||||
ImGui::TextWrapped("%s",
|
||||
localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::EndTooltip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
|
||||
} else {
|
||||
@@ -436,7 +521,7 @@ int Render::SettingWindow() {
|
||||
}
|
||||
|
||||
settings_items_offset +=
|
||||
settings_items_padding + title_bar_button_width_ * 0.3f;
|
||||
settings_items_padding + title_bar_button_width_ * 0.15f;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
|
||||
ImGui::PopStyleVar();
|
||||
@@ -463,9 +548,8 @@ int Render::SettingWindow() {
|
||||
LOG_INFO("Set localization language: {}",
|
||||
localization::GetSupportedLanguages()
|
||||
[localization::detail::ClampLanguageIndex(
|
||||
localization_language_index_)]
|
||||
.code
|
||||
.c_str());
|
||||
localization_language_index_)]
|
||||
.code.c_str());
|
||||
|
||||
// Video quality
|
||||
if (video_quality_button_value_ == 0) {
|
||||
|
||||
@@ -0,0 +1,360 @@
|
||||
#include "render.h"
|
||||
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
|
||||
#include <shellapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "localization.h"
|
||||
#include "rd_log.h"
|
||||
#include "service_host.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetCurrentExecutablePath() {
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
DWORD length = GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
if (length < buffer.size()) {
|
||||
return std::filesystem::path(buffer.data(), buffer.data() + length);
|
||||
}
|
||||
if (buffer.size() >= 32768) {
|
||||
return {};
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
}
|
||||
|
||||
bool InstallServiceWithElevation() {
|
||||
const std::filesystem::path executable_path = GetCurrentExecutablePath();
|
||||
if (executable_path.empty()) {
|
||||
LOG_ERROR("Portable service install failed: current executable not found");
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::filesystem::path service_path =
|
||||
executable_path.parent_path() / L"crossdesk_service.exe";
|
||||
const std::filesystem::path helper_path =
|
||||
executable_path.parent_path() / L"crossdesk_session_helper.exe";
|
||||
if (!std::filesystem::exists(service_path) ||
|
||||
!std::filesystem::exists(helper_path)) {
|
||||
LOG_ERROR(
|
||||
"Portable service install failed: service binaries missing, "
|
||||
"service={}, "
|
||||
"helper={}",
|
||||
service_path.string(), helper_path.string());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring executable = executable_path.wstring();
|
||||
std::wstring working_dir = executable_path.parent_path().wstring();
|
||||
std::wstring parameters = L"--service-install";
|
||||
|
||||
SHELLEXECUTEINFOW execute_info{};
|
||||
execute_info.cbSize = sizeof(execute_info);
|
||||
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||
execute_info.hwnd = nullptr;
|
||||
execute_info.lpVerb = L"runas";
|
||||
execute_info.lpFile = executable.c_str();
|
||||
execute_info.lpParameters = parameters.c_str();
|
||||
execute_info.lpDirectory = working_dir.c_str();
|
||||
execute_info.nShow = SW_HIDE;
|
||||
|
||||
if (!ShellExecuteExW(&execute_info)) {
|
||||
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
DWORD wait_result = WaitForSingleObject(execute_info.hProcess, INFINITE);
|
||||
DWORD exit_code = 1;
|
||||
if (wait_result == WAIT_OBJECT_0) {
|
||||
GetExitCodeProcess(execute_info.hProcess, &exit_code);
|
||||
} else {
|
||||
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
|
||||
}
|
||||
CloseHandle(execute_info.hProcess);
|
||||
|
||||
if (exit_code != 0) {
|
||||
LOG_ERROR("Portable service install command failed, exit_code={}",
|
||||
exit_code);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool started = StartCrossDeskService();
|
||||
if (!started) {
|
||||
LOG_WARN("Portable service installed but start failed");
|
||||
}
|
||||
return IsCrossDeskServiceInstalled() && started;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Render::CheckPortableWindowsService() {
|
||||
if (portable_service_prompt_checked_) {
|
||||
return;
|
||||
}
|
||||
portable_service_prompt_checked_ = true;
|
||||
|
||||
if (IsCrossDeskServiceInstalled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (portable_service_prompt_suppressed_) {
|
||||
return;
|
||||
}
|
||||
|
||||
portable_service_install_state_.store(PortableServiceInstallState::idle,
|
||||
std::memory_order_relaxed);
|
||||
show_portable_service_install_window_ = true;
|
||||
}
|
||||
|
||||
void Render::StartPortableWindowsServiceInstall() {
|
||||
portable_service_do_not_remind_ = false;
|
||||
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||
if (!portable_service_install_state_.compare_exchange_strong(
|
||||
expected, PortableServiceInstallState::installing,
|
||||
std::memory_order_acq_rel)) {
|
||||
if (expected != PortableServiceInstallState::failed) {
|
||||
return;
|
||||
}
|
||||
portable_service_install_state_.store(
|
||||
PortableServiceInstallState::installing, std::memory_order_release);
|
||||
}
|
||||
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
portable_service_install_thread_ = std::thread([this]() {
|
||||
const bool installed = InstallServiceWithElevation();
|
||||
portable_service_install_state_.store(
|
||||
installed ? PortableServiceInstallState::succeeded
|
||||
: PortableServiceInstallState::failed,
|
||||
std::memory_order_release);
|
||||
});
|
||||
}
|
||||
|
||||
void Render::JoinPortableWindowsServiceInstallThread() {
|
||||
if (portable_service_install_thread_.joinable()) {
|
||||
portable_service_install_thread_.join();
|
||||
}
|
||||
}
|
||||
|
||||
int Render::PortableServiceInstallWindow() {
|
||||
if (!show_portable_service_install_window_ &&
|
||||
!show_portable_service_prompt_suppressed_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||
const float window_width =
|
||||
(std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f);
|
||||
const float window_height =
|
||||
(std::min)(viewport->WorkSize.y * 0.5f, title_bar_button_width_ * 8.0f);
|
||||
|
||||
if (show_portable_service_prompt_suppressed_window_) {
|
||||
const float notice_width = window_width;
|
||||
const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f,
|
||||
title_bar_button_width_ * 4.6f);
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - notice_width) / 2.0f,
|
||||
viewport->WorkPos.y +
|
||||
(viewport->WorkSize.y - notice_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(notice_width, notice_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::Begin(
|
||||
localization::notification[localization_language_index_].c_str(),
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoTitleBar);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SetWindowFontScale(0.55f);
|
||||
ImGui::SetCursorPosX(notice_width * 0.08f);
|
||||
ImGui::Text(
|
||||
"%s", localization::notification[localization_language_index_].c_str());
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::SetCursorPosX(notice_width * 0.06f);
|
||||
ImGui::SetCursorPosY(notice_height * 0.28f);
|
||||
ImGui::PushTextWrapPos(notice_width * 0.88f);
|
||||
ImGui::TextWrapped("%s",
|
||||
localization::windows_service_prompt_suppressed_message
|
||||
[localization_language_index_]
|
||||
.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
const std::string ok_label = localization::ok[localization_language_index_];
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float ok_width =
|
||||
ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||
ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(notice_height * 0.75f);
|
||||
if (ImGui::Button(ok_label.c_str())) {
|
||||
show_portable_service_prompt_suppressed_window_ = false;
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor();
|
||||
}
|
||||
|
||||
if (!show_portable_service_install_window_) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
ImGui::SetNextWindowPos(
|
||||
ImVec2(
|
||||
viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f,
|
||||
viewport->WorkPos.y + (viewport->WorkSize.y - window_height) / 2.0f),
|
||||
ImGuiCond_Appearing);
|
||||
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||
ImGuiCond_Always);
|
||||
|
||||
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||
|
||||
ImGui::Begin(
|
||||
localization::windows_service_setup_title[localization_language_index_]
|
||||
.c_str(),
|
||||
nullptr,
|
||||
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||
ImGuiWindowFlags_NoTitleBar);
|
||||
|
||||
ImGui::Spacing();
|
||||
ImGui::SetWindowFontScale(0.55f);
|
||||
ImGui::SetCursorPosX(window_width * 0.08f);
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::windows_service_setup_title[localization_language_index_]
|
||||
.c_str());
|
||||
|
||||
const PortableServiceInstallState state =
|
||||
portable_service_install_state_.load(std::memory_order_acquire);
|
||||
const char* status_text = nullptr;
|
||||
if (state == PortableServiceInstallState::installing ||
|
||||
state == PortableServiceInstallState::succeeded ||
|
||||
state == PortableServiceInstallState::failed) {
|
||||
status_text =
|
||||
localization::installing_windows_service[localization_language_index_]
|
||||
.c_str();
|
||||
if (state == PortableServiceInstallState::succeeded) {
|
||||
status_text = localization::windows_service_install_success
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
} else if (state == PortableServiceInstallState::failed) {
|
||||
status_text = localization::windows_service_install_failed
|
||||
[localization_language_index_]
|
||||
.c_str();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(0.45f);
|
||||
ImGui::SetCursorPosX(window_width * 0.04f);
|
||||
ImGui::SetCursorPosY(window_height * 0.22f);
|
||||
ImGui::BeginChild("PortableServiceInstallContent",
|
||||
ImVec2(window_width * 0.92f, window_height * 0.45f),
|
||||
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
const float wrap_pos = ImGui::GetContentRegionAvail().x;
|
||||
ImGui::PushTextWrapPos(wrap_pos);
|
||||
ImGui::TextWrapped(
|
||||
"%s",
|
||||
localization::windows_service_setup_message[localization_language_index_]
|
||||
.c_str());
|
||||
if (status_text != nullptr) {
|
||||
ImGui::Spacing();
|
||||
ImGui::TextWrapped("%s", status_text);
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::EndChild();
|
||||
|
||||
ImGui::SetWindowFontScale(0.5f);
|
||||
ImGui::SetCursorPosX(window_width * 0.08f);
|
||||
ImGui::SetCursorPosY(window_height * 0.71f);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
|
||||
ImGui::Checkbox(
|
||||
localization::do_not_remind_again[localization_language_index_].c_str(),
|
||||
&portable_service_do_not_remind_);
|
||||
ImGui::PopStyleVar();
|
||||
|
||||
const float button_y = window_height * 0.84f;
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const auto default_button_width = [&style](const std::string& label) {
|
||||
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||
};
|
||||
const std::string install_label =
|
||||
localization::install_windows_service[localization_language_index_];
|
||||
const std::string cancel_label =
|
||||
localization::cancel[localization_language_index_];
|
||||
const std::string ok_label = localization::ok[localization_language_index_];
|
||||
const float buttons_width = state == PortableServiceInstallState::succeeded
|
||||
? default_button_width(ok_label)
|
||||
: default_button_width(install_label) +
|
||||
style.ItemSpacing.x +
|
||||
default_button_width(cancel_label);
|
||||
ImGui::SetCursorPosX((window_width - buttons_width) * 0.5f);
|
||||
ImGui::SetCursorPosY(button_y);
|
||||
|
||||
if (state == PortableServiceInstallState::succeeded) {
|
||||
if (ImGui::Button(ok_label.c_str())) {
|
||||
show_portable_service_install_window_ = false;
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
}
|
||||
} else {
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(install_label.c_str())) {
|
||||
StartPortableWindowsServiceInstall();
|
||||
}
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::SameLine();
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::BeginDisabled();
|
||||
}
|
||||
if (ImGui::Button(cancel_label.c_str())) {
|
||||
if (portable_service_do_not_remind_) {
|
||||
portable_service_prompt_suppressed_ = true;
|
||||
config_center_->SetPortableServicePromptSuppressed(true);
|
||||
show_portable_service_prompt_suppressed_window_ = true;
|
||||
}
|
||||
show_portable_service_install_window_ = false;
|
||||
}
|
||||
if (state == PortableServiceInstallState::installing) {
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetWindowFontScale(1.0f);
|
||||
ImGui::End();
|
||||
ImGui::PopStyleVar(3);
|
||||
ImGui::PopStyleColor();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -6,11 +6,27 @@
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#include <unistd.h>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500;
|
||||
|
||||
void OpenPrivacyPreferences(const char* pane) {
|
||||
if (pane == nullptr || pane[0] == '\0') {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string command =
|
||||
"open \"x-apple.systempreferences:com.apple.preference.security?";
|
||||
command += pane;
|
||||
command += "\"";
|
||||
system(command.c_str());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
||||
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
|
||||
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
|
||||
@@ -35,16 +51,19 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
||||
bool hovered = ImGui::IsItemHovered();
|
||||
bool clicked = ImGui::IsItemClicked() && enabled;
|
||||
|
||||
ImVec4 track_color = active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
||||
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
||||
ImVec4 track_color =
|
||||
active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
||||
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
||||
|
||||
if (!enabled) {
|
||||
track_color.w *= DISABLED_ALPHA;
|
||||
}
|
||||
|
||||
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
|
||||
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH, track_pos.y + TRACK_HEIGHT - 0.5f);
|
||||
draw_list->AddRectFilled(track_min, track_max, ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
||||
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH,
|
||||
track_pos.y + TRACK_HEIGHT - 0.5f);
|
||||
draw_list->AddRectFilled(track_min, track_max,
|
||||
ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
||||
|
||||
float knob_position = active ? 1.0f : 0.0f;
|
||||
float knob_min_x = track_pos.x + KNOB_PADDING;
|
||||
@@ -59,7 +78,8 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
||||
|
||||
ImVec2 knob_min = ImVec2(knob_x, knob_y);
|
||||
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
|
||||
draw_list->AddRectFilled(knob_min, knob_max, ImGui::GetColorU32(knob_color), KNOB_RADIUS);
|
||||
draw_list->AddRectFilled(knob_min, knob_max,
|
||||
ImGui::GetColorU32(knob_color), KNOB_RADIUS);
|
||||
|
||||
return clicked;
|
||||
}
|
||||
@@ -81,29 +101,82 @@ bool Render::CheckAccessibilityPermission() {
|
||||
}
|
||||
|
||||
void Render::OpenAccessibilityPreferences() {
|
||||
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
|
||||
system("open "
|
||||
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
|
||||
"Accessibility\"");
|
||||
if (!mac_accessibility_permission_requested_) {
|
||||
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_Accessibility");
|
||||
}
|
||||
}
|
||||
|
||||
void Render::OpenScreenRecordingPreferences() {
|
||||
if (@available(macOS 10.15, *)) {
|
||||
CGRequestScreenCaptureAccess();
|
||||
if (!mac_screen_recording_permission_requested_) {
|
||||
CGRequestScreenCaptureAccess();
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||
}
|
||||
} else {
|
||||
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||
}
|
||||
}
|
||||
|
||||
void Render::RefreshMacPermissionStatus(bool force) {
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
if (!force && mac_permission_status_initialized_ &&
|
||||
now - mac_permission_last_check_tick_ <
|
||||
kPermissionRefreshIntervalVisibleMs) {
|
||||
return;
|
||||
}
|
||||
|
||||
system("open "
|
||||
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
|
||||
"ScreenCapture\"");
|
||||
const bool old_screen_recording_granted =
|
||||
mac_screen_recording_permission_granted_;
|
||||
const bool old_accessibility_granted = mac_accessibility_permission_granted_;
|
||||
|
||||
mac_screen_recording_permission_granted_ =
|
||||
CheckScreenRecordingPermission();
|
||||
mac_accessibility_permission_granted_ = CheckAccessibilityPermission();
|
||||
mac_permission_last_check_tick_ = now;
|
||||
mac_permission_status_initialized_ = true;
|
||||
|
||||
if (old_screen_recording_granted !=
|
||||
mac_screen_recording_permission_granted_ ||
|
||||
old_accessibility_granted != mac_accessibility_permission_granted_) {
|
||||
LOG_INFO("macOS permission status: screen_recording={}, accessibility={}",
|
||||
mac_screen_recording_permission_granted_,
|
||||
mac_accessibility_permission_granted_);
|
||||
}
|
||||
}
|
||||
|
||||
bool Render::EnsureMacScreenRecordingPermission() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
if (mac_screen_recording_permission_granted_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
show_request_permission_window_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Render::EnsureMacAccessibilityPermission() {
|
||||
RefreshMacPermissionStatus(false);
|
||||
if (mac_accessibility_permission_granted_) {
|
||||
return true;
|
||||
}
|
||||
|
||||
show_request_permission_window_ = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
int Render::RequestPermissionWindow() {
|
||||
bool screen_recording_granted = CheckScreenRecordingPermission();
|
||||
bool accessibility_granted = CheckAccessibilityPermission();
|
||||
RefreshMacPermissionStatus(false);
|
||||
|
||||
show_request_permission_window_ = !screen_recording_granted || !accessibility_granted;
|
||||
const bool screen_recording_granted =
|
||||
mac_screen_recording_permission_granted_;
|
||||
const bool accessibility_granted = mac_accessibility_permission_granted_;
|
||||
|
||||
show_request_permission_window_ =
|
||||
!screen_recording_granted || !accessibility_granted;
|
||||
|
||||
if (!show_request_permission_window_) {
|
||||
return 0;
|
||||
@@ -162,8 +235,10 @@ int Render::RequestPermissionWindow() {
|
||||
if (accessibility_granted) {
|
||||
DrawToggleSwitch("accessibility_toggle_on", true, false);
|
||||
} else {
|
||||
if (DrawToggleSwitch("accessibility_toggle", accessibility_granted, !accessibility_granted)) {
|
||||
if (DrawToggleSwitch("accessibility_toggle", false, true)) {
|
||||
OpenAccessibilityPreferences();
|
||||
mac_accessibility_permission_requested_ = true;
|
||||
RefreshMacPermissionStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,12 +253,12 @@ int Render::RequestPermissionWindow() {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::SetCursorPosX(checkbox_padding);
|
||||
if (screen_recording_granted) {
|
||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.0f);
|
||||
DrawToggleSwitch("screen_recording_toggle_on", true, false);
|
||||
} else {
|
||||
if (DrawToggleSwitch("screen_recording_toggle", screen_recording_granted,
|
||||
!screen_recording_granted)) {
|
||||
if (DrawToggleSwitch("screen_recording_toggle", false, true)) {
|
||||
OpenScreenRecordingPreferences();
|
||||
mac_screen_recording_permission_requested_ = true;
|
||||
RefreshMacPermissionStatus(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,4 +277,4 @@ int Render::RequestPermissionWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -92,7 +92,7 @@ int Render::UpdateNotificationWindow() {
|
||||
ImGui::SetWindowFontScale(0.55f);
|
||||
std::string title =
|
||||
localization::new_version_available[localization_language_index_] +
|
||||
": v" + latest_version_;
|
||||
": " + latest_version_;
|
||||
ImGui::Text("%s", title.c_str());
|
||||
ImGui::SetWindowFontScale(0.1f);
|
||||
|
||||
|
||||
@@ -1,12 +1,98 @@
|
||||
#include "path_manager.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <vector>
|
||||
|
||||
#ifndef CROSSDESK_PORTABLE
|
||||
#define CROSSDESK_PORTABLE 0
|
||||
#endif
|
||||
|
||||
#if CROSSDESK_PORTABLE
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#elif !defined(_WIN32)
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
#if CROSSDESK_PORTABLE
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
#ifdef _WIN32
|
||||
std::vector<wchar_t> buffer(MAX_PATH);
|
||||
while (true) {
|
||||
DWORD length =
|
||||
GetModuleFileNameW(nullptr, buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()));
|
||||
if (length == 0) {
|
||||
return {};
|
||||
}
|
||||
if (length < buffer.size()) {
|
||||
return std::filesystem::path(buffer.data(), buffer.data() + length)
|
||||
.parent_path();
|
||||
}
|
||||
if (buffer.size() >= 32768) {
|
||||
return {};
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
uint32_t size = 0;
|
||||
_NSGetExecutablePath(nullptr, &size);
|
||||
std::vector<char> buffer(size + 1);
|
||||
if (_NSGetExecutablePath(buffer.data(), &size) != 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::path executable =
|
||||
std::filesystem::weakly_canonical(buffer.data(), ec);
|
||||
if (ec) {
|
||||
executable = buffer.data();
|
||||
}
|
||||
return executable.parent_path();
|
||||
#else
|
||||
std::vector<char> buffer(PATH_MAX);
|
||||
while (true) {
|
||||
ssize_t length = readlink("/proc/self/exe", buffer.data(),
|
||||
buffer.size() - 1);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
if (static_cast<size_t>(length) < buffer.size() - 1) {
|
||||
buffer[static_cast<size_t>(length)] = '\0';
|
||||
return std::filesystem::path(buffer.data()).parent_path();
|
||||
}
|
||||
buffer.resize(buffer.size() * 2);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path GetPortableRootPath() {
|
||||
std::filesystem::path executable_dir = GetExecutableDirectory();
|
||||
if (!executable_dir.empty()) {
|
||||
return executable_dir;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
std::filesystem::path current = std::filesystem::current_path(ec);
|
||||
return ec ? std::filesystem::path(".") : current;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
PathManager::PathManager(const std::string& app_name) : app_name_(app_name) {}
|
||||
|
||||
std::filesystem::path PathManager::GetConfigPath() {
|
||||
#if CROSSDESK_PORTABLE
|
||||
return GetPortableRootPath() / "data";
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
return GetKnownFolder(FOLDERID_RoamingAppData) / app_name_;
|
||||
#elif __APPLE__
|
||||
@@ -14,9 +100,13 @@ std::filesystem::path PathManager::GetConfigPath() {
|
||||
#else
|
||||
return GetEnvOrDefault("XDG_CONFIG_HOME", GetHome() + "/.config") / app_name_;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path PathManager::GetCachePath() {
|
||||
#if CROSSDESK_PORTABLE
|
||||
return GetPortableRootPath() / "data";
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
#ifdef CROSSDESK_DEBUG
|
||||
return "cache";
|
||||
@@ -28,9 +118,13 @@ std::filesystem::path PathManager::GetCachePath() {
|
||||
#else
|
||||
return GetEnvOrDefault("XDG_CACHE_HOME", GetHome() + "/.cache") / app_name_;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
std::filesystem::path PathManager::GetLogPath() {
|
||||
#if CROSSDESK_PORTABLE
|
||||
return GetPortableRootPath() / "logs";
|
||||
#else
|
||||
#ifdef _WIN32
|
||||
return GetKnownFolder(FOLDERID_LocalAppData) / app_name_ / "logs";
|
||||
#elif __APPLE__
|
||||
@@ -38,6 +132,7 @@ std::filesystem::path PathManager::GetLogPath() {
|
||||
#else
|
||||
return GetCachePath() / "logs";
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PathManager::CreateDirectories(const std::filesystem::path& p) {
|
||||
@@ -93,4 +188,4 @@ std::filesystem::path PathManager::GetEnvOrDefault(const char* env_var,
|
||||
|
||||
return std::filesystem::path(def);
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -57,6 +57,11 @@ int ScreenCapturerWayland::Init(const int fps, cb_desktop_data cb) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (!EnsurePipeWireRuntimeAvailable()) {
|
||||
LOG_ERROR("Wayland screen capturer requires PipeWire 0.3 runtime");
|
||||
return -1;
|
||||
}
|
||||
|
||||
fps_ = fps;
|
||||
callback_ = cb;
|
||||
pointer_granted_ = false;
|
||||
|
||||
@@ -52,6 +52,7 @@ class ScreenCapturerWayland : public ScreenCapturer {
|
||||
bool SelectPortalDevices();
|
||||
bool SelectPortalSource();
|
||||
bool StartPortalSession();
|
||||
bool EnsurePipeWireRuntimeAvailable() const;
|
||||
bool OpenPipeWireRemote();
|
||||
bool SetupPipeWireStream(bool relaxed_connect, PipeWireConnectMode mode);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
#if CROSSDESK_WAYLAND_BUILD_ENABLED
|
||||
|
||||
#include <dlfcn.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
@@ -10,6 +11,7 @@
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
@@ -20,6 +22,143 @@ namespace crossdesk {
|
||||
|
||||
namespace {
|
||||
|
||||
struct PipeWireDynamicApi {
|
||||
void* library = nullptr;
|
||||
bool available = false;
|
||||
|
||||
decltype(&::pw_init) init = nullptr;
|
||||
decltype(&::pw_deinit) deinit = nullptr;
|
||||
decltype(&::pw_thread_loop_new) thread_loop_new = nullptr;
|
||||
decltype(&::pw_thread_loop_destroy) thread_loop_destroy = nullptr;
|
||||
decltype(&::pw_thread_loop_get_loop) thread_loop_get_loop = nullptr;
|
||||
decltype(&::pw_thread_loop_start) thread_loop_start = nullptr;
|
||||
decltype(&::pw_thread_loop_stop) thread_loop_stop = nullptr;
|
||||
decltype(&::pw_thread_loop_lock) thread_loop_lock = nullptr;
|
||||
decltype(&::pw_thread_loop_unlock) thread_loop_unlock = nullptr;
|
||||
decltype(&::pw_thread_loop_wait) thread_loop_wait = nullptr;
|
||||
decltype(&::pw_thread_loop_signal) thread_loop_signal = nullptr;
|
||||
decltype(&::pw_context_new) context_new = nullptr;
|
||||
decltype(&::pw_context_destroy) context_destroy = nullptr;
|
||||
decltype(&::pw_context_connect_fd) context_connect_fd = nullptr;
|
||||
decltype(&::pw_properties_new) properties_new = nullptr;
|
||||
decltype(&::pw_properties_set) properties_set = nullptr;
|
||||
decltype(&::pw_stream_new) stream_new = nullptr;
|
||||
decltype(&::pw_stream_add_listener) stream_add_listener = nullptr;
|
||||
decltype(&::pw_stream_state_as_string) stream_state_as_string = nullptr;
|
||||
decltype(&::pw_stream_connect) stream_connect = nullptr;
|
||||
decltype(&::pw_stream_update_params) stream_update_params = nullptr;
|
||||
decltype(&::pw_stream_set_active) stream_set_active = nullptr;
|
||||
decltype(&::pw_stream_disconnect) stream_disconnect = nullptr;
|
||||
decltype(&::pw_stream_destroy) stream_destroy = nullptr;
|
||||
decltype(&::pw_stream_dequeue_buffer) stream_dequeue_buffer = nullptr;
|
||||
decltype(&::pw_stream_queue_buffer) stream_queue_buffer = nullptr;
|
||||
decltype(&::pw_core_disconnect) core_disconnect = nullptr;
|
||||
decltype(&::pw_proxy_destroy) proxy_destroy = nullptr;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
bool LoadPipeWireSymbol(void* library, T* function, const char* symbol_name) {
|
||||
*function = reinterpret_cast<T>(dlsym(library, symbol_name));
|
||||
if (*function != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_ERROR("Unable to find PipeWire symbol {}", symbol_name);
|
||||
return false;
|
||||
}
|
||||
|
||||
void UnloadPipeWireApi(PipeWireDynamicApi* api) {
|
||||
if (api->library != nullptr) {
|
||||
dlclose(api->library);
|
||||
}
|
||||
*api = PipeWireDynamicApi{};
|
||||
}
|
||||
|
||||
bool LoadPipeWireApi(PipeWireDynamicApi* api) {
|
||||
static constexpr const char* kPipeWireLibraries[] = {
|
||||
"libpipewire-0.3.so.0",
|
||||
"libpipewire-0.3.so",
|
||||
};
|
||||
|
||||
for (const char* library_name : kPipeWireLibraries) {
|
||||
api->library = dlopen(library_name, RTLD_LAZY | RTLD_LOCAL);
|
||||
if (api->library != nullptr) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (api->library == nullptr) {
|
||||
LOG_WARN("PipeWire 0.3 runtime library is unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!LoadPipeWireSymbol(api->library, &api->init, "pw_init") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->deinit, "pw_deinit") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_new,
|
||||
"pw_thread_loop_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_destroy,
|
||||
"pw_thread_loop_destroy") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_get_loop,
|
||||
"pw_thread_loop_get_loop") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_start,
|
||||
"pw_thread_loop_start") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_stop,
|
||||
"pw_thread_loop_stop") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_lock,
|
||||
"pw_thread_loop_lock") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_unlock,
|
||||
"pw_thread_loop_unlock") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_wait,
|
||||
"pw_thread_loop_wait") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->thread_loop_signal,
|
||||
"pw_thread_loop_signal") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->context_new, "pw_context_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->context_destroy,
|
||||
"pw_context_destroy") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->context_connect_fd,
|
||||
"pw_context_connect_fd") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->properties_new,
|
||||
"pw_properties_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->properties_set,
|
||||
"pw_properties_set") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_new, "pw_stream_new") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_add_listener,
|
||||
"pw_stream_add_listener") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_state_as_string,
|
||||
"pw_stream_state_as_string") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_connect,
|
||||
"pw_stream_connect") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_update_params,
|
||||
"pw_stream_update_params") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_set_active,
|
||||
"pw_stream_set_active") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_disconnect,
|
||||
"pw_stream_disconnect") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_destroy,
|
||||
"pw_stream_destroy") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_dequeue_buffer,
|
||||
"pw_stream_dequeue_buffer") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->stream_queue_buffer,
|
||||
"pw_stream_queue_buffer") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->core_disconnect,
|
||||
"pw_core_disconnect") ||
|
||||
!LoadPipeWireSymbol(api->library, &api->proxy_destroy,
|
||||
"pw_proxy_destroy")) {
|
||||
UnloadPipeWireApi(api);
|
||||
return false;
|
||||
}
|
||||
|
||||
api->available = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
const PipeWireDynamicApi* GetPipeWireApi() {
|
||||
static PipeWireDynamicApi api;
|
||||
static std::once_flag once;
|
||||
std::call_once(once, []() { LoadPipeWireApi(&api); });
|
||||
return api.available ? &api : nullptr;
|
||||
}
|
||||
|
||||
const char* PipeWireFormatName(uint32_t spa_format) {
|
||||
switch (spa_format) {
|
||||
case SPA_VIDEO_FORMAT_BGRx:
|
||||
@@ -76,6 +215,7 @@ double SnapLikelyFractionalScale(double observed_scale) {
|
||||
}
|
||||
|
||||
struct PipeWireTargetLookupState {
|
||||
const PipeWireDynamicApi* pipewire = nullptr;
|
||||
pw_thread_loop* loop = nullptr;
|
||||
uint32_t target_node_id = 0;
|
||||
int sync_seq = -1;
|
||||
@@ -87,11 +227,13 @@ struct PipeWireTargetLookupState {
|
||||
std::string LookupPipeWireTargetObjectSerial(pw_core* core,
|
||||
pw_thread_loop* loop,
|
||||
uint32_t node_id) {
|
||||
if (!core || !loop || node_id == 0) {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (!pipewire || !core || !loop || node_id == 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
PipeWireTargetLookupState state;
|
||||
state.pipewire = pipewire;
|
||||
state.loop = loop;
|
||||
state.target_node_id = node_id;
|
||||
|
||||
@@ -138,7 +280,7 @@ std::string LookupPipeWireTargetObjectSerial(pw_core* core,
|
||||
return;
|
||||
}
|
||||
state->done = true;
|
||||
pw_thread_loop_signal(state->loop, false);
|
||||
state->pipewire->thread_loop_signal(state->loop, false);
|
||||
};
|
||||
core_events.error = [](void* userdata, uint32_t id, int seq, int res,
|
||||
const char* message) {
|
||||
@@ -152,7 +294,7 @@ std::string LookupPipeWireTargetObjectSerial(pw_core* core,
|
||||
LOG_WARN("PipeWire registry lookup error: {}",
|
||||
message ? message : "unknown");
|
||||
state->done = true;
|
||||
pw_thread_loop_signal(state->loop, false);
|
||||
state->pipewire->thread_loop_signal(state->loop, false);
|
||||
};
|
||||
|
||||
pw_registry_add_listener(registry, ®istry_listener, ®istry_events,
|
||||
@@ -161,12 +303,12 @@ std::string LookupPipeWireTargetObjectSerial(pw_core* core,
|
||||
state.sync_seq = pw_core_sync(core, PW_ID_CORE, 0);
|
||||
|
||||
while (!state.done) {
|
||||
pw_thread_loop_wait(loop);
|
||||
pipewire->thread_loop_wait(loop);
|
||||
}
|
||||
|
||||
spa_hook_remove(®istry_listener);
|
||||
spa_hook_remove(&core_listener);
|
||||
pw_proxy_destroy(reinterpret_cast<pw_proxy*>(registry));
|
||||
pipewire->proxy_destroy(reinterpret_cast<pw_proxy*>(registry));
|
||||
return state.found ? state.object_serial : "";
|
||||
}
|
||||
|
||||
@@ -188,56 +330,68 @@ int BytesPerPixel(uint32_t spa_format) {
|
||||
|
||||
} // namespace
|
||||
|
||||
bool ScreenCapturerWayland::EnsurePipeWireRuntimeAvailable() const {
|
||||
return GetPipeWireApi() != nullptr;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
PipeWireConnectMode mode) {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (!pipewire) {
|
||||
LOG_ERROR("PipeWire 0.3 runtime library is unavailable");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pipewire_fd_ < 0 || pipewire_node_id_ == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!pipewire_initialized_) {
|
||||
pw_init(nullptr, nullptr);
|
||||
pipewire->init(nullptr, nullptr);
|
||||
pipewire_initialized_ = true;
|
||||
}
|
||||
|
||||
pw_thread_loop_ = pw_thread_loop_new("crossdesk-wayland-capture", nullptr);
|
||||
pw_thread_loop_ =
|
||||
pipewire->thread_loop_new("crossdesk-wayland-capture", nullptr);
|
||||
if (!pw_thread_loop_) {
|
||||
LOG_ERROR("Failed to create PipeWire thread loop");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pw_thread_loop_start(pw_thread_loop_) < 0) {
|
||||
if (pipewire->thread_loop_start(pw_thread_loop_) < 0) {
|
||||
LOG_ERROR("Failed to start PipeWire thread loop");
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
pipewire_thread_loop_started_ = true;
|
||||
|
||||
pw_thread_loop_lock(pw_thread_loop_);
|
||||
pipewire->thread_loop_lock(pw_thread_loop_);
|
||||
|
||||
pw_context_ =
|
||||
pw_context_new(pw_thread_loop_get_loop(pw_thread_loop_), nullptr, 0);
|
||||
pw_context_ = pipewire->context_new(
|
||||
pipewire->thread_loop_get_loop(pw_thread_loop_), nullptr, 0);
|
||||
if (!pw_context_) {
|
||||
LOG_ERROR("Failed to create PipeWire context");
|
||||
pw_thread_loop_unlock(pw_thread_loop_);
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
|
||||
pw_core_ = pw_context_connect_fd(pw_context_, pipewire_fd_, nullptr, 0);
|
||||
pw_core_ =
|
||||
pipewire->context_connect_fd(pw_context_, pipewire_fd_, nullptr, 0);
|
||||
if (!pw_core_) {
|
||||
LOG_ERROR("Failed to connect to PipeWire remote");
|
||||
pw_thread_loop_unlock(pw_thread_loop_);
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
pipewire_fd_ = -1;
|
||||
|
||||
pw_properties* stream_props =
|
||||
pw_properties_new(PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY,
|
||||
"Capture", PW_KEY_MEDIA_ROLE, "Screen", nullptr);
|
||||
pw_properties* stream_props = pipewire->properties_new(
|
||||
PW_KEY_MEDIA_TYPE, "Video", PW_KEY_MEDIA_CATEGORY, "Capture",
|
||||
PW_KEY_MEDIA_ROLE, "Screen", nullptr);
|
||||
if (!stream_props) {
|
||||
LOG_ERROR("Failed to allocate PipeWire stream properties");
|
||||
pw_thread_loop_unlock(pw_thread_loop_);
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
@@ -247,8 +401,8 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
target_object_serial = LookupPipeWireTargetObjectSerial(
|
||||
pw_core_, pw_thread_loop_, pipewire_node_id_);
|
||||
if (!target_object_serial.empty()) {
|
||||
pw_properties_set(stream_props, PW_KEY_TARGET_OBJECT,
|
||||
target_object_serial.c_str());
|
||||
pipewire->properties_set(stream_props, PW_KEY_TARGET_OBJECT,
|
||||
target_object_serial.c_str());
|
||||
LOG_INFO("PipeWire target object serial for node {} is {}",
|
||||
pipewire_node_id_, target_object_serial);
|
||||
} else {
|
||||
@@ -260,10 +414,10 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
}
|
||||
|
||||
pw_stream_ =
|
||||
pw_stream_new(pw_core_, "CrossDesk Wayland Capture", stream_props);
|
||||
pipewire->stream_new(pw_core_, "CrossDesk Wayland Capture", stream_props);
|
||||
if (!pw_stream_) {
|
||||
LOG_ERROR("Failed to create PipeWire stream");
|
||||
pw_thread_loop_unlock(pw_thread_loop_);
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
@@ -289,9 +443,11 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("PipeWire stream state: {} -> {}",
|
||||
pw_stream_state_as_string(old_state),
|
||||
pw_stream_state_as_string(state));
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
LOG_INFO(
|
||||
"PipeWire stream state: {} -> {}",
|
||||
pipewire ? pipewire->stream_state_as_string(old_state) : "unknown",
|
||||
pipewire ? pipewire->stream_state_as_string(state) : "unknown");
|
||||
};
|
||||
events.param_changed = [](void* userdata, uint32_t id,
|
||||
const struct spa_pod* param) {
|
||||
@@ -369,7 +525,10 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
SPA_POD_Int(sizeof(struct spa_meta_header))));
|
||||
|
||||
if (self->pw_stream_) {
|
||||
pw_stream_update_params(self->pw_stream_, params, param_count);
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (pipewire) {
|
||||
pipewire->stream_update_params(self->pw_stream_, params, param_count);
|
||||
}
|
||||
}
|
||||
self->pipewire_format_ready_.store(true);
|
||||
|
||||
@@ -457,7 +616,7 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
return events;
|
||||
}();
|
||||
|
||||
pw_stream_add_listener(pw_stream_, listener, &stream_events, this);
|
||||
pipewire->stream_add_listener(pw_stream_, listener, &stream_events, this);
|
||||
pipewire_format_ready_.store(false);
|
||||
pipewire_stream_start_ms_.store(NowMs());
|
||||
pipewire_last_frame_ms_.store(0);
|
||||
@@ -501,7 +660,7 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
|
||||
if (param_count == 0) {
|
||||
LOG_ERROR("No valid PipeWire format params were built");
|
||||
pw_thread_loop_unlock(pw_thread_loop_);
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
CleanupPipeWire();
|
||||
return false;
|
||||
}
|
||||
@@ -522,12 +681,12 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
PipeWireConnectModeName(mode), pipewire_node_id_, target_id,
|
||||
target_object_serial.empty() ? "none" : target_object_serial.c_str(),
|
||||
relaxed_connect, param_count, fixed_size.width, fixed_size.height);
|
||||
const int ret = pw_stream_connect(
|
||||
const int ret = pipewire->stream_connect(
|
||||
pw_stream_, PW_DIRECTION_INPUT, target_id,
|
||||
static_cast<pw_stream_flags>(PW_STREAM_FLAG_AUTOCONNECT |
|
||||
PW_STREAM_FLAG_MAP_BUFFERS),
|
||||
param_count > 0 ? params : nullptr, static_cast<uint32_t>(param_count));
|
||||
pw_thread_loop_unlock(pw_thread_loop_);
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
|
||||
if (ret < 0) {
|
||||
LOG_ERROR("pw_stream_connect failed: {}", spa_strerror(ret));
|
||||
@@ -539,16 +698,17 @@ bool ScreenCapturerWayland::SetupPipeWireStream(bool relaxed_connect,
|
||||
}
|
||||
|
||||
void ScreenCapturerWayland::CleanupPipeWire() {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
const bool need_lock =
|
||||
pw_thread_loop_ &&
|
||||
pipewire && pw_thread_loop_ &&
|
||||
(pw_stream_ != nullptr || pw_core_ != nullptr || pw_context_ != nullptr);
|
||||
if (need_lock) {
|
||||
pw_thread_loop_lock(pw_thread_loop_);
|
||||
pipewire->thread_loop_lock(pw_thread_loop_);
|
||||
}
|
||||
|
||||
if (pw_stream_) {
|
||||
pw_stream_set_active(pw_stream_, false);
|
||||
pw_stream_disconnect(pw_stream_);
|
||||
if (pw_stream_ && pipewire) {
|
||||
pipewire->stream_set_active(pw_stream_, false);
|
||||
pipewire->stream_disconnect(pw_stream_);
|
||||
}
|
||||
|
||||
if (stream_listener_) {
|
||||
@@ -557,33 +717,34 @@ void ScreenCapturerWayland::CleanupPipeWire() {
|
||||
stream_listener_ = nullptr;
|
||||
}
|
||||
|
||||
if (pw_stream_) {
|
||||
pw_stream_destroy(pw_stream_);
|
||||
pw_stream_ = nullptr;
|
||||
if (pw_stream_ && pipewire) {
|
||||
pipewire->stream_destroy(pw_stream_);
|
||||
}
|
||||
pw_stream_ = nullptr;
|
||||
|
||||
if (pw_core_) {
|
||||
pw_core_disconnect(pw_core_);
|
||||
pw_core_ = nullptr;
|
||||
if (pw_core_ && pipewire) {
|
||||
pipewire->core_disconnect(pw_core_);
|
||||
}
|
||||
pw_core_ = nullptr;
|
||||
|
||||
if (pw_context_) {
|
||||
pw_context_destroy(pw_context_);
|
||||
pw_context_ = nullptr;
|
||||
if (pw_context_ && pipewire) {
|
||||
pipewire->context_destroy(pw_context_);
|
||||
}
|
||||
pw_context_ = nullptr;
|
||||
|
||||
if (need_lock) {
|
||||
pw_thread_loop_unlock(pw_thread_loop_);
|
||||
pipewire->thread_loop_unlock(pw_thread_loop_);
|
||||
}
|
||||
|
||||
if (pw_thread_loop_) {
|
||||
if (pw_thread_loop_ && pipewire) {
|
||||
if (pipewire_thread_loop_started_) {
|
||||
pw_thread_loop_stop(pw_thread_loop_);
|
||||
pipewire->thread_loop_stop(pw_thread_loop_);
|
||||
pipewire_thread_loop_started_ = false;
|
||||
}
|
||||
pw_thread_loop_destroy(pw_thread_loop_);
|
||||
pw_thread_loop_ = nullptr;
|
||||
pipewire->thread_loop_destroy(pw_thread_loop_);
|
||||
}
|
||||
pw_thread_loop_ = nullptr;
|
||||
pipewire_thread_loop_started_ = false;
|
||||
|
||||
if (pipewire_fd_ >= 0) {
|
||||
close(pipewire_fd_);
|
||||
@@ -594,23 +755,24 @@ void ScreenCapturerWayland::CleanupPipeWire() {
|
||||
pipewire_stream_start_ms_.store(0);
|
||||
pipewire_last_frame_ms_.store(0);
|
||||
|
||||
if (pipewire_initialized_) {
|
||||
pw_deinit();
|
||||
pipewire_initialized_ = false;
|
||||
if (pipewire_initialized_ && pipewire) {
|
||||
pipewire->deinit();
|
||||
}
|
||||
pipewire_initialized_ = false;
|
||||
}
|
||||
|
||||
void ScreenCapturerWayland::HandlePipeWireBuffer() {
|
||||
if (!pw_stream_) {
|
||||
const PipeWireDynamicApi* pipewire = GetPipeWireApi();
|
||||
if (!pw_stream_ || !pipewire) {
|
||||
return;
|
||||
}
|
||||
|
||||
pw_buffer* buffer = pw_stream_dequeue_buffer(pw_stream_);
|
||||
pw_buffer* buffer = pipewire->stream_dequeue_buffer(pw_stream_);
|
||||
if (!buffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto requeue = [&]() { pw_stream_queue_buffer(pw_stream_, buffer); };
|
||||
auto requeue = [&]() { pipewire->stream_queue_buffer(pw_stream_, buffer); };
|
||||
|
||||
if (paused_) {
|
||||
requeue();
|
||||
|
||||
@@ -16,7 +16,11 @@ int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
|
||||
screen_capturer_sck_impl_ = CreateScreenCapturerSck();
|
||||
screen_capturer_sck_impl_->Init(fps, on_data_);
|
||||
const int ret = screen_capturer_sck_impl_->Init(fps, on_data_);
|
||||
if (ret != 0) {
|
||||
screen_capturer_sck_impl_.reset();
|
||||
return ret;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -29,8 +33,11 @@ int ScreenCapturerSck::Destroy() {
|
||||
}
|
||||
|
||||
int ScreenCapturerSck::Start(bool show_cursor) {
|
||||
screen_capturer_sck_impl_->Start(show_cursor);
|
||||
return 0;
|
||||
if (!screen_capturer_sck_impl_) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return screen_capturer_sck_impl_->Start(show_cursor);
|
||||
}
|
||||
|
||||
int ScreenCapturerSck::Stop() {
|
||||
@@ -80,4 +87,4 @@ std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
|
||||
void ScreenCapturerSck::OnFrame() {}
|
||||
|
||||
void ScreenCapturerSck::CleanUp() {}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -16,7 +16,12 @@
|
||||
#include <IOKit/graphics/IOGraphicsLib.h>
|
||||
#include <IOSurface/IOSurface.h>
|
||||
#include <ScreenCaptureKit/ScreenCaptureKit.h>
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <vector>
|
||||
#include "display_info.h"
|
||||
@@ -28,6 +33,15 @@ class ScreenCapturerSckImpl;
|
||||
|
||||
static const int kFullDesktopScreenId = -1;
|
||||
|
||||
static std::string NSErrorToString(NSError *error) {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *description = [error.localizedDescription UTF8String];
|
||||
return description ? description : "";
|
||||
}
|
||||
|
||||
// The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture
|
||||
// was reported to be broken before macOS 13 - see http://crbug.com/40234870.
|
||||
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were
|
||||
@@ -78,6 +92,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
std::map<CGDirectDisplayID, int> display_id_map_reverse_;
|
||||
std::map<CGDirectDisplayID, std::string> display_id_name_map_;
|
||||
unsigned char *nv12_frame_ = nullptr;
|
||||
size_t nv12_frame_size_ = 0;
|
||||
int width_ = 0;
|
||||
int height_ = 0;
|
||||
int fps_ = 60;
|
||||
@@ -100,7 +115,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// Helper object to receive Objective-C callbacks from ScreenCaptureKit and call into this C++
|
||||
// object. The helper may outlive this C++ instance, if a completion-handler is passed to
|
||||
// ScreenCaptureKit APIs and the C++ object is deleted before the handler executes.
|
||||
SckHelper *__strong helper_;
|
||||
SckHelper *__strong helper_ = nil;
|
||||
// Callback for returning captured frames, or errors, to the caller. Only used on the caller's
|
||||
// thread.
|
||||
cb_desktop_data _on_data = nullptr;
|
||||
@@ -110,7 +125,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
// Guards some variables that may be accessed on different threads.
|
||||
std::mutex lock_;
|
||||
// Provides captured desktop frames.
|
||||
SCStream *__strong stream_;
|
||||
SCStream *__strong stream_ = nil;
|
||||
// Currently selected display, or 0 if the full desktop is selected. This capturer does not
|
||||
// support full-desktop capture, and will fall back to the first display.
|
||||
CGDirectDisplayID current_display_ = 0;
|
||||
@@ -182,6 +197,19 @@ ScreenCapturerSckImpl::ScreenCapturerSckImpl() {
|
||||
}
|
||||
|
||||
ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||
SckHelper *helper_to_release = nil;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (stream_) {
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
stream_ = nil;
|
||||
}
|
||||
_on_data = nullptr;
|
||||
helper_to_release = helper_;
|
||||
helper_ = nil;
|
||||
}
|
||||
[helper_to_release releaseCapturer];
|
||||
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
display_id_map_reverse_.clear();
|
||||
@@ -190,15 +218,22 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
|
||||
if (nv12_frame_) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = nullptr;
|
||||
nv12_frame_size_ = 0;
|
||||
}
|
||||
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
[helper_ releaseCapturer];
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
if (!cb) {
|
||||
LOG_ERROR("Screen capturer callback is null");
|
||||
return -1;
|
||||
}
|
||||
|
||||
_on_data = cb;
|
||||
fps_ = fps;
|
||||
fps_ = fps > 0 ? fps : 60;
|
||||
display_info_list_.clear();
|
||||
display_id_map_.clear();
|
||||
display_id_map_reverse_.clear();
|
||||
display_id_name_map_.clear();
|
||||
|
||||
if (@available(macOS 10.15, *)) {
|
||||
bool has_permission = CGPreflightScreenCaptureAccess();
|
||||
@@ -216,8 +251,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) {
|
||||
if (error) {
|
||||
capture_error = error;
|
||||
LOG_ERROR("Failed to get shareable content: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
LOG_ERROR("Failed to get shareable content: {}", NSErrorToString(error));
|
||||
} else {
|
||||
content = result;
|
||||
}
|
||||
@@ -227,7 +261,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
|
||||
|
||||
if (capture_error || !content || content.displays.count == 0) {
|
||||
LOG_ERROR("Failed to get display info, error: {}",
|
||||
std::string([capture_error.localizedDescription UTF8String]));
|
||||
NSErrorToString(capture_error));
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -284,51 +318,58 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
|
||||
if (stream_) {
|
||||
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
stream_ = nil;
|
||||
current_display_ = display_id_map_[monitor_index];
|
||||
StartOrReconfigureCapturer();
|
||||
}];
|
||||
} else {
|
||||
current_display_ = display_id_map_[monitor_index];
|
||||
StartOrReconfigureCapturer();
|
||||
auto display_it = display_id_map_.find(monitor_index);
|
||||
if (display_it == display_id_map_.end()) {
|
||||
LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
|
||||
monitor_index, display_id_map_.size());
|
||||
return -1;
|
||||
}
|
||||
|
||||
const CGDirectDisplayID target_display = display_it->second;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
current_display_ = target_display;
|
||||
}
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
||||
int target = initial_monitor_index_;
|
||||
if (display_info_list_.empty()) return -1;
|
||||
CGDirectDisplayID target_display = display_id_map_[target];
|
||||
if (current_display_ == target_display) return 0;
|
||||
if (stream_) {
|
||||
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
stream_ = nil;
|
||||
current_display_ = target_display;
|
||||
StartOrReconfigureCapturer();
|
||||
}];
|
||||
} else {
|
||||
current_display_ = target_display;
|
||||
StartOrReconfigureCapturer();
|
||||
auto display_it = display_id_map_.find(target);
|
||||
if (display_it == display_id_map_.end()) {
|
||||
LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
|
||||
return -1;
|
||||
}
|
||||
|
||||
CGDirectDisplayID target_display = display_it->second;
|
||||
if (current_display_ == target_display) return 0;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
current_display_ = target_display;
|
||||
}
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Destroy() {
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (stream_) {
|
||||
LOG_INFO("Destroying stream");
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
stream_ = nil;
|
||||
SckHelper *helper_to_release = nil;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (stream_) {
|
||||
LOG_INFO("Destroying stream");
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
stream_ = nil;
|
||||
}
|
||||
current_display_ = 0;
|
||||
permanent_error_ = false;
|
||||
_on_data = nullptr;
|
||||
helper_to_release = helper_;
|
||||
helper_ = nil;
|
||||
}
|
||||
current_display_ = 0;
|
||||
permanent_error_ = false;
|
||||
_on_data = nullptr;
|
||||
[helper_ releaseCapturer];
|
||||
helper_ = nil;
|
||||
|
||||
[helper_to_release releaseCapturer];
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -416,7 +457,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
|
||||
// TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for
|
||||
// best performance.
|
||||
NSError *add_stream_output_error;
|
||||
NSError *add_stream_output_error = nil;
|
||||
dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL);
|
||||
bool add_stream_output_result = [stream_ addStreamOutput:helper_
|
||||
type:SCStreamOutputTypeScreen
|
||||
@@ -425,7 +466,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
|
||||
if (!add_stream_output_result) {
|
||||
stream_ = nil;
|
||||
LOG_ERROR("addStreamOutput failed");
|
||||
LOG_ERROR("addStreamOutput failed: {}", NSErrorToString(add_stream_output_error));
|
||||
permanent_error_ = true;
|
||||
return;
|
||||
}
|
||||
@@ -436,7 +477,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
// calls stopCaptureWithCompletionHandler on the stream, which cancels
|
||||
// this handler.
|
||||
permanent_error_ = true;
|
||||
LOG_ERROR("startCaptureWithCompletionHandler failed");
|
||||
LOG_ERROR("startCaptureWithCompletionHandler failed: {}", NSErrorToString(error));
|
||||
} else {
|
||||
LOG_INFO("Capture started");
|
||||
}
|
||||
@@ -448,8 +489,18 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
|
||||
void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
CFDictionaryRef attachment) {
|
||||
(void)attachment;
|
||||
if (!pixelBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t width = CVPixelBufferGetWidth(pixelBuffer);
|
||||
size_t height = CVPixelBufferGetHeight(pixelBuffer);
|
||||
if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height,
|
||||
CVPixelBufferGetPlaneCount(pixelBuffer));
|
||||
return;
|
||||
}
|
||||
|
||||
CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
if (status != kCVReturnSuccess) {
|
||||
@@ -458,18 +509,37 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
}
|
||||
|
||||
size_t required_size = width * height * 3 / 2;
|
||||
if (!nv12_frame_ || (width_ * height_ * 3 / 2 < required_size)) {
|
||||
if (required_size > static_cast<size_t>((std::numeric_limits<int>::max)())) {
|
||||
LOG_ERROR("Captured frame is too large: {} bytes", required_size);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!_on_data) {
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!nv12_frame_ || nv12_frame_size_ < required_size) {
|
||||
delete[] nv12_frame_;
|
||||
nv12_frame_ = new unsigned char[required_size];
|
||||
width_ = width;
|
||||
height_ = height;
|
||||
nv12_frame_size_ = required_size;
|
||||
}
|
||||
width_ = static_cast<int>(width);
|
||||
height_ = static_cast<int>(height);
|
||||
|
||||
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
|
||||
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
|
||||
|
||||
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
|
||||
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1);
|
||||
if (!base_y || !base_uv || stride_y < width || stride_uv < width) {
|
||||
LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}",
|
||||
base_y != nullptr, base_uv != nullptr, stride_y, stride_uv);
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
return;
|
||||
}
|
||||
|
||||
unsigned char *dst_y = nv12_frame_;
|
||||
for (size_t row = 0; row < height; ++row) {
|
||||
@@ -481,7 +551,8 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
memcpy(dst_uv + row * width, static_cast<unsigned char *>(base_uv) + row * stride_uv, width);
|
||||
}
|
||||
|
||||
_on_data(nv12_frame_, width * height * 3 / 2, width, height,
|
||||
_on_data(nv12_frame_, static_cast<int>(required_size), static_cast<int>(width),
|
||||
static_cast<int>(height),
|
||||
display_id_name_map_[current_display_].c_str());
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
|
||||
@@ -503,10 +574,14 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
}
|
||||
|
||||
SckHelper *local_helper = helper_;
|
||||
if (!local_helper) {
|
||||
LOG_ERROR("Cannot reconfigure capturer: helper is null");
|
||||
return;
|
||||
}
|
||||
|
||||
auto handler = ^(SCShareableContent *content, NSError *error) {
|
||||
if (error) {
|
||||
LOG_ERROR("getShareableContent failed: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
LOG_ERROR("getShareableContent failed: {}", NSErrorToString(error));
|
||||
[local_helper onShareableContentCreated:nil];
|
||||
return;
|
||||
}
|
||||
@@ -576,4 +651,4 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
|
||||
std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() {
|
||||
return std::make_unique<ScreenCapturerSckImpl>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,6 +244,31 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||
ReleaseDuplication();
|
||||
int current_monitor = monitor_index_.load();
|
||||
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
EnumerateDisplays();
|
||||
if (display_info_list_.empty()) {
|
||||
LOG_ERROR("DXGI: no displays found while recreating duplication");
|
||||
return false;
|
||||
}
|
||||
if (current_monitor < 0 ||
|
||||
current_monitor >= static_cast<int>(display_info_list_.size())) {
|
||||
current_monitor = 0;
|
||||
monitor_index_ = 0;
|
||||
}
|
||||
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||
LOG_INFO("DXGI: recreated duplication for monitor {}",
|
||||
monitor_index_.load());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
staging_.Reset();
|
||||
if (duplication_) {
|
||||
@@ -254,6 +279,8 @@ void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
|
||||
void ScreenCapturerDxgi::CaptureLoop() {
|
||||
const int timeout_ms = 33;
|
||||
auto last_duplication_retry =
|
||||
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
@@ -261,6 +288,11 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
|
||||
if (!duplication_) {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_duplication_retry >= std::chrono::milliseconds(500)) {
|
||||
last_duplication_retry = now;
|
||||
RecreateDuplicationForCurrentMonitor();
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
@@ -274,9 +306,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||
// attempt to recreate duplication
|
||||
ReleaseDuplication();
|
||||
CreateDuplicationForMonitor(monitor_index_);
|
||||
RecreateDuplicationForCurrentMonitor();
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -353,4 +383,4 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -50,6 +50,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
bool InitializeDxgi();
|
||||
void EnumerateDisplays();
|
||||
bool CreateDuplicationForMonitor(int monitor_index);
|
||||
bool RecreateDuplicationForCurrentMonitor();
|
||||
void CaptureLoop();
|
||||
void ReleaseDuplication();
|
||||
|
||||
@@ -78,4 +79,4 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
};
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -100,8 +100,7 @@ bool ScreenCapturerWgc::IsWgcSupported() {
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
int error = 0;
|
||||
if (inited_ == true) return error;
|
||||
if (inited_ == true) return 0;
|
||||
|
||||
// nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2];
|
||||
// nv12_frame_scaled_ = new unsigned char[1280 * 720 * 3 / 2];
|
||||
@@ -112,8 +111,18 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
|
||||
if (!IsWgcSupported()) {
|
||||
LOG_ERROR("WGC not supported");
|
||||
error = 2;
|
||||
return error;
|
||||
return 2;
|
||||
}
|
||||
|
||||
return RebuildSessions(monitor_index_);
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::RebuildSessions(int preferred_monitor_index) {
|
||||
CleanUp();
|
||||
|
||||
if (!IsWgcSupported()) {
|
||||
LOG_ERROR("WGC not supported");
|
||||
return 2;
|
||||
}
|
||||
|
||||
monitor_ = GetPrimaryMonitor();
|
||||
@@ -125,6 +134,13 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (preferred_monitor_index < 0 ||
|
||||
preferred_monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||
preferred_monitor_index = 0;
|
||||
}
|
||||
monitor_index_ = preferred_monitor_index;
|
||||
|
||||
int error = 0;
|
||||
for (int i = 0; i < display_info_list_.size(); i++) {
|
||||
const auto& display = display_info_list_[i];
|
||||
LOG_INFO(
|
||||
@@ -138,20 +154,28 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||
sessions_.back().session_->RegisterObserver(this);
|
||||
error = sessions_.back().session_->Initialize((HMONITOR)display.handle);
|
||||
if (error != 0) {
|
||||
LOG_ERROR("WGC: initialize session {} failed, ret={}", i, error);
|
||||
CleanUp();
|
||||
return error;
|
||||
}
|
||||
sessions_[i].inited_ = true;
|
||||
inited_ = true;
|
||||
}
|
||||
|
||||
LOG_INFO("Default on monitor {}:{}", monitor_index_,
|
||||
display_info_list_[monitor_index_].name);
|
||||
|
||||
initial_monitor_index_ = monitor_index_;
|
||||
if (initial_monitor_index_ < 0 ||
|
||||
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
|
||||
initial_monitor_index_ = monitor_index_;
|
||||
}
|
||||
inited_ = true;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::Destroy() { return 0; }
|
||||
int ScreenCapturerWgc::Destroy() {
|
||||
CleanUp();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::Start(bool show_cursor) {
|
||||
if (running_ == true) {
|
||||
@@ -160,13 +184,37 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
|
||||
}
|
||||
|
||||
if (inited_ == false) {
|
||||
LOG_ERROR("Screen capturer not inited");
|
||||
return 4;
|
||||
const int ret = RebuildSessions(monitor_index_);
|
||||
if (ret != 0) {
|
||||
LOG_ERROR("Screen capturer not inited");
|
||||
return ret;
|
||||
}
|
||||
}
|
||||
|
||||
int ret = StartSessions(show_cursor);
|
||||
if (ret == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_WARN("WGC: start failed, rebuilding sessions");
|
||||
ret = RebuildSessions(monitor_index_);
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
return StartSessions(show_cursor);
|
||||
}
|
||||
|
||||
int ScreenCapturerWgc::StartSessions(bool show_cursor) {
|
||||
bool any_started = false;
|
||||
bool active_started = false;
|
||||
int last_error = 0;
|
||||
for (int i = 0; i < sessions_.size(); i++) {
|
||||
int active_monitor = monitor_index_;
|
||||
if (active_monitor < 0 ||
|
||||
active_monitor >= static_cast<int>(sessions_.size())) {
|
||||
active_monitor = 0;
|
||||
monitor_index_ = 0;
|
||||
}
|
||||
for (int i = 0; i < static_cast<int>(sessions_.size()); i++) {
|
||||
if (sessions_[i].inited_ == false) {
|
||||
LOG_ERROR("Session {} not inited", i);
|
||||
continue;
|
||||
@@ -182,16 +230,27 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i != 0) {
|
||||
if (i != active_monitor) {
|
||||
sessions_[i].session_->Pause();
|
||||
sessions_[i].paused_ = true;
|
||||
} else {
|
||||
sessions_[i].session_->Resume();
|
||||
sessions_[i].paused_ = false;
|
||||
}
|
||||
sessions_[i].running_ = true;
|
||||
any_started = true;
|
||||
if (i == active_monitor) {
|
||||
active_started = true;
|
||||
}
|
||||
}
|
||||
running_ = running_ || any_started;
|
||||
}
|
||||
|
||||
running_ = active_started;
|
||||
if (!active_started) {
|
||||
LOG_ERROR("WGC: active session did not start successfully");
|
||||
Stop();
|
||||
return last_error != 0 ? last_error : -1;
|
||||
}
|
||||
if (!any_started) {
|
||||
LOG_ERROR("WGC: no session started successfully");
|
||||
return last_error != 0 ? last_error : -1;
|
||||
@@ -349,13 +408,16 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
||||
}
|
||||
|
||||
void ScreenCapturerWgc::CleanUp() {
|
||||
if (inited_) {
|
||||
for (auto& session : sessions_) {
|
||||
if (session.session_) {
|
||||
session.session_->Stop();
|
||||
}
|
||||
running_ = false;
|
||||
for (auto& session : sessions_) {
|
||||
if (session.session_) {
|
||||
session.session_->Stop();
|
||||
}
|
||||
sessions_.clear();
|
||||
}
|
||||
sessions_.clear();
|
||||
display_info_list_.clear();
|
||||
gs_display_list.clear();
|
||||
monitor_ = nullptr;
|
||||
inited_ = false;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -40,6 +40,8 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
||||
|
||||
protected:
|
||||
void CleanUp();
|
||||
int RebuildSessions(int preferred_monitor_index);
|
||||
int StartSessions(bool show_cursor);
|
||||
|
||||
private:
|
||||
HMONITOR monitor_;
|
||||
@@ -74,4 +76,4 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
||||
std::mutex frame_mutex_;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -29,11 +29,15 @@ namespace {
|
||||
using Json = nlohmann::json;
|
||||
|
||||
constexpr DWORD kSecureDesktopStatusIntervalMs = 250;
|
||||
constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 150;
|
||||
constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500;
|
||||
constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120;
|
||||
constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500;
|
||||
constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000;
|
||||
constexpr int kSecureDesktopCaptureMinIntervalMs = 100;
|
||||
constexpr DWORD kPostSecureDesktopRestartRetryMs = 500;
|
||||
constexpr DWORD kPostSecureDesktopRestartTimeoutMs = 10000;
|
||||
constexpr int kSecureDesktopCaptureMinFps = 30;
|
||||
constexpr int kSecureDesktopCaptureMaxIntervalMs =
|
||||
1000 / kSecureDesktopCaptureMinFps;
|
||||
|
||||
struct SecureDesktopServiceStatus {
|
||||
bool service_available = false;
|
||||
@@ -129,10 +133,28 @@ class WgcPluginCapturer final : public ScreenCapturer {
|
||||
};
|
||||
|
||||
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
|
||||
bool show_cursor) {
|
||||
bool show_cursor,
|
||||
const std::string& stage) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top
|
||||
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0);
|
||||
if (!stage.empty()) {
|
||||
stream << ":" << stage;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string BuildSecureCaptureStartCommand(int left, int top, int width,
|
||||
int height, bool show_cursor,
|
||||
int fps,
|
||||
const std::string& stage) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputCaptureStartCommandPrefix << left << ":" << top
|
||||
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0)
|
||||
<< ":" << fps;
|
||||
if (!stage.empty()) {
|
||||
stream << ":" << stage;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
@@ -148,6 +170,11 @@ bool IsTransientSecureDesktopFrameError(const std::string& error_message) {
|
||||
error_message.find("\"error\":\"bitblt_failed\"") != std::string::npos;
|
||||
}
|
||||
|
||||
bool IsTransientWindowsServiceStatusError(const std::string& error) {
|
||||
return error == "pipe_unavailable" || error == "pipe_connect_failed" ||
|
||||
error == "pipe_read_failed";
|
||||
}
|
||||
|
||||
bool ReadPipeMessage(HANDLE pipe, std::vector<uint8_t>* response_out,
|
||||
DWORD* error_code_out = nullptr) {
|
||||
if (response_out == nullptr) {
|
||||
@@ -274,17 +301,15 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
int width, int height, bool show_cursor,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* captured_width_out,
|
||||
int* captured_height_out,
|
||||
std::string* error_out) {
|
||||
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
|
||||
captured_height_out == nullptr) {
|
||||
bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||
const std::string& command,
|
||||
std::vector<uint8_t>* response_out,
|
||||
std::string* error_out) {
|
||||
if (response_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
response_out->clear();
|
||||
const std::wstring pipe_name =
|
||||
GetCrossDeskSecureInputHelperPipeName(session_id);
|
||||
if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) {
|
||||
@@ -306,8 +331,6 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
||||
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
||||
|
||||
const std::string command =
|
||||
BuildSecureCaptureCommand(left, top, width, height, show_cursor);
|
||||
DWORD bytes_written = 0;
|
||||
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
|
||||
&bytes_written, nullptr)) {
|
||||
@@ -319,9 +342,8 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<uint8_t> response;
|
||||
DWORD read_error = 0;
|
||||
const bool read_ok = ReadPipeMessage(pipe, &response, &read_error);
|
||||
const bool read_ok = ReadPipeMessage(pipe, response_out, &read_error);
|
||||
CloseHandle(pipe);
|
||||
if (!read_ok) {
|
||||
if (error_out != nullptr) {
|
||||
@@ -330,6 +352,29 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||
int width, int height, bool show_cursor,
|
||||
const std::string& stage,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* captured_width_out,
|
||||
int* captured_height_out,
|
||||
std::string* error_out) {
|
||||
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
|
||||
captured_height_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string command =
|
||||
BuildSecureCaptureCommand(left, top, width, height, show_cursor, stage);
|
||||
std::vector<uint8_t> response;
|
||||
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||
error_out)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ParseSecureDesktopFrameResponse(response, nv12_frame_out,
|
||||
captured_width_out,
|
||||
captured_height_out, error_out);
|
||||
@@ -349,21 +394,46 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
return;
|
||||
}
|
||||
|
||||
const char* raw_display_name = display_name ? display_name : "";
|
||||
std::string mapped_name;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
auto it = label_alias_.find(display_name);
|
||||
auto it = label_alias_.find(raw_display_name);
|
||||
if (it != label_alias_.end())
|
||||
mapped_name = it->second;
|
||||
else
|
||||
mapped_name = display_name;
|
||||
mapped_name = raw_display_name;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
|
||||
if (post_secure_desktop_waiting_for_frame_.load(
|
||||
std::memory_order_relaxed) &&
|
||||
!post_secure_desktop_drop_logged_.exchange(
|
||||
true, std::memory_order_relaxed)) {
|
||||
LOG_WARN(
|
||||
"Windows capturer dropping post-secure-desktop frame from "
|
||||
"unknown display: display='{}', mapped='{}', size={}x{}, "
|
||||
"bytes={}",
|
||||
raw_display_name, mapped_name, w, h, size);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (post_secure_desktop_waiting_for_frame_.exchange(
|
||||
false, std::memory_order_relaxed)) {
|
||||
const ULONGLONG start_tick =
|
||||
post_secure_desktop_started_tick_.exchange(
|
||||
0, std::memory_order_relaxed);
|
||||
const ULONGLONG elapsed_ms =
|
||||
start_tick == 0 ? 0 : GetTickCount64() - start_tick;
|
||||
post_secure_desktop_drop_logged_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
LOG_INFO(
|
||||
"Windows capturer first normal frame after secure desktop: "
|
||||
"display='{}', mapped='{}', size={}x{}, bytes={}, elapsed_ms={}",
|
||||
raw_display_name, mapped_name, w, h, size, elapsed_ms);
|
||||
}
|
||||
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
|
||||
};
|
||||
|
||||
@@ -481,6 +551,10 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
|
||||
running_.store(true, std::memory_order_relaxed);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
if (!secure_capture_thread_.joinable()) {
|
||||
secure_capture_thread_ =
|
||||
std::thread([this]() { SecureDesktopCaptureLoop(); });
|
||||
@@ -491,11 +565,16 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
int ScreenCapturerWin::Stop() {
|
||||
running_.store(false, std::memory_order_relaxed);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||
int ret = 0;
|
||||
if (impl_) {
|
||||
ret = impl_->Stop();
|
||||
}
|
||||
StopSecureCaptureThread();
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@@ -582,6 +661,93 @@ void ScreenCapturerWin::StopSecureCaptureThread() {
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
||||
if (!impl_ || !running_.load(std::memory_order_relaxed)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
|
||||
auto restore_monitor = [&]() {
|
||||
RebuildAliasesFromImpl();
|
||||
if (current_monitor > 0 && impl_->SwitchTo(current_monitor) != 0) {
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
auto try_started_backend = [&](std::unique_ptr<ScreenCapturer> cand,
|
||||
const char* name,
|
||||
bool is_wgc_plugin) -> bool {
|
||||
if (!cand) {
|
||||
return false;
|
||||
}
|
||||
const int init_ret = cand->Init(fps_, cb_);
|
||||
if (init_ret != 0) {
|
||||
LOG_WARN("Windows capturer: {} init after secure desktop failed (ret={})",
|
||||
name, init_ret);
|
||||
return false;
|
||||
}
|
||||
const int start_ret = cand->Start(show_cursor);
|
||||
if (start_ret != 0) {
|
||||
LOG_WARN(
|
||||
"Windows capturer: {} start after secure desktop failed (ret={})",
|
||||
name, start_ret);
|
||||
cand->Destroy();
|
||||
return false;
|
||||
}
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
}
|
||||
impl_ = std::move(cand);
|
||||
impl_is_wgc_plugin_ = is_wgc_plugin;
|
||||
restore_monitor();
|
||||
LOG_INFO("Windows capturer: restarted {} after secure desktop", name);
|
||||
return true;
|
||||
};
|
||||
|
||||
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
|
||||
impl_->Stop();
|
||||
int ret = impl_->Start(show_cursor);
|
||||
if (ret == 0) {
|
||||
restore_monitor();
|
||||
return true;
|
||||
}
|
||||
|
||||
LOG_WARN(
|
||||
"Windows capturer: capture backend restart after secure desktop failed "
|
||||
"(ret={}), rebuilding backend",
|
||||
ret);
|
||||
impl_->Destroy();
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
ret = impl_->Start(show_cursor);
|
||||
}
|
||||
if (ret == 0) {
|
||||
restore_monitor();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (impl_is_wgc_plugin_ &&
|
||||
try_started_backend(WgcPluginCapturer::Create(), "WGC plugin", true)) {
|
||||
return true;
|
||||
}
|
||||
if (try_started_backend(std::make_unique<ScreenCapturerDxgi>(), "DXGI",
|
||||
false)) {
|
||||
return true;
|
||||
}
|
||||
if (try_started_backend(std::make_unique<ScreenCapturerGdi>(), "GDI",
|
||||
false)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (impl_) {
|
||||
LOG_WARN(
|
||||
"Windows capturer: all backend restart attempts after secure desktop "
|
||||
"failed (last_ret={})",
|
||||
ret);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
|
||||
int* height,
|
||||
std::string* display_name) {
|
||||
@@ -616,10 +782,239 @@ bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::CloseSecureDesktopSharedFrame() {
|
||||
if (secure_frame_view_ != nullptr) {
|
||||
UnmapViewOfFile(secure_frame_view_);
|
||||
secure_frame_view_ = nullptr;
|
||||
}
|
||||
if (secure_frame_ready_event_ != nullptr) {
|
||||
CloseHandle(secure_frame_ready_event_);
|
||||
secure_frame_ready_event_ = nullptr;
|
||||
}
|
||||
if (secure_frame_mapping_ != nullptr) {
|
||||
CloseHandle(secure_frame_mapping_);
|
||||
secure_frame_mapping_ = nullptr;
|
||||
}
|
||||
secure_frame_view_size_ = 0;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
||||
DWORD target_session_id = session_id;
|
||||
if (target_session_id == 0xFFFFFFFF) {
|
||||
target_session_id = secure_shared_session_id_;
|
||||
}
|
||||
|
||||
if (secure_shared_capture_started_ &&
|
||||
target_session_id != 0xFFFFFFFF) {
|
||||
std::vector<uint8_t> response;
|
||||
std::string error_message;
|
||||
QuerySecureDesktopHelperCommand(
|
||||
target_session_id, kCrossDeskSecureInputCaptureStopCommand, &response,
|
||||
&error_message);
|
||||
}
|
||||
|
||||
CloseSecureDesktopSharedFrame();
|
||||
secure_shared_capture_started_ = false;
|
||||
secure_shared_session_id_ = 0xFFFFFFFF;
|
||||
secure_shared_left_ = 0;
|
||||
secure_shared_top_ = 0;
|
||||
secure_shared_width_ = 0;
|
||||
secure_shared_height_ = 0;
|
||||
secure_shared_fps_ = 0;
|
||||
secure_shared_show_cursor_ = true;
|
||||
secure_shared_stage_.clear();
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
|
||||
size_t min_size,
|
||||
std::string* error_out) {
|
||||
if (secure_frame_view_ != nullptr &&
|
||||
secure_shared_session_id_ == session_id &&
|
||||
secure_frame_view_size_ >= min_size) {
|
||||
return true;
|
||||
}
|
||||
|
||||
CloseSecureDesktopSharedFrame();
|
||||
|
||||
const std::wstring mapping_name =
|
||||
GetCrossDeskSecureDesktopFrameMappingName(session_id);
|
||||
HANDLE frame_mapping =
|
||||
OpenFileMappingW(FILE_MAP_READ, FALSE, mapping_name.c_str());
|
||||
if (frame_mapping == nullptr) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "open_frame_mapping_failed:" +
|
||||
std::to_string(GetLastError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* frame_view =
|
||||
static_cast<uint8_t*>(MapViewOfFile(frame_mapping, FILE_MAP_READ, 0, 0, 0));
|
||||
if (frame_view == nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
CloseHandle(frame_mapping);
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "map_frame_view_failed:" + std::to_string(error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::wstring event_name =
|
||||
GetCrossDeskSecureDesktopFrameReadyEventName(session_id);
|
||||
HANDLE frame_ready_event =
|
||||
OpenEventW(SYNCHRONIZE, FALSE, event_name.c_str());
|
||||
if (frame_ready_event == nullptr) {
|
||||
const DWORD error = GetLastError();
|
||||
UnmapViewOfFile(frame_view);
|
||||
CloseHandle(frame_mapping);
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "open_frame_event_failed:" + std::to_string(error);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
secure_frame_mapping_ = frame_mapping;
|
||||
secure_frame_ready_event_ = frame_ready_event;
|
||||
secure_frame_view_ = frame_view;
|
||||
secure_frame_view_size_ = min_size;
|
||||
secure_shared_session_id_ = session_id;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
|
||||
DWORD wait_ms, std::vector<uint8_t>* nv12_frame_out, int* width_out,
|
||||
int* height_out, std::string* error_out) {
|
||||
if (nv12_frame_out == nullptr || width_out == nullptr ||
|
||||
height_out == nullptr || secure_frame_view_ == nullptr ||
|
||||
secure_frame_ready_event_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const DWORD wait_result = WaitForSingleObject(secure_frame_ready_event_,
|
||||
wait_ms);
|
||||
if (wait_result == WAIT_TIMEOUT) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "frame_wait_timeout";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (wait_result != WAIT_OBJECT_0) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "frame_wait_failed:" + std::to_string(GetLastError());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
auto* header =
|
||||
reinterpret_cast<CrossDeskSecureDesktopSharedFrameHeader*>(
|
||||
secure_frame_view_);
|
||||
if (header->magic != kCrossDeskSecureDesktopFrameMagic ||
|
||||
header->version != kCrossDeskSecureDesktopFrameVersion) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "invalid_shared_frame_header";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
if (header->writing != 0) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "shared_frame_write_in_progress";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint32_t sequence = header->sequence;
|
||||
const uint32_t payload_size = header->payload_size;
|
||||
const uint32_t buffer_size = header->buffer_size;
|
||||
if (payload_size == 0 || payload_size > buffer_size ||
|
||||
sizeof(*header) + static_cast<size_t>(payload_size) >
|
||||
secure_frame_view_size_) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "invalid_shared_frame_size";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
nv12_frame_out->resize(payload_size);
|
||||
std::memcpy(nv12_frame_out->data(), secure_frame_view_ + sizeof(*header),
|
||||
payload_size);
|
||||
MemoryBarrier();
|
||||
if (header->writing != 0 || header->sequence != sequence) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "shared_frame_changed_during_read";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
*width_out = static_cast<int>(header->width);
|
||||
*height_out = static_cast<int>(header->height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||
DWORD session_id, int left, int top, int width, int height,
|
||||
const std::string& stage, bool show_cursor, int fps,
|
||||
std::string* error_out) {
|
||||
const size_t payload_size = static_cast<size_t>(width) * height * 3 / 2;
|
||||
const size_t mapping_size =
|
||||
sizeof(CrossDeskSecureDesktopSharedFrameHeader) + payload_size;
|
||||
if (payload_size == 0) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = "invalid_capture_size";
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (secure_shared_capture_started_ &&
|
||||
secure_shared_session_id_ == session_id &&
|
||||
secure_shared_left_ == left && secure_shared_top_ == top &&
|
||||
secure_shared_width_ == width && secure_shared_height_ == height &&
|
||||
secure_shared_stage_ == stage &&
|
||||
secure_shared_show_cursor_ == show_cursor && secure_shared_fps_ == fps &&
|
||||
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
|
||||
const std::string command =
|
||||
BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps,
|
||||
stage);
|
||||
std::vector<uint8_t> response;
|
||||
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||
error_out)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||
if (json.is_discarded() || !json.value("ok", false)) {
|
||||
if (error_out != nullptr) {
|
||||
*error_out = ExtractPipeTextResponse(response);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
secure_shared_capture_started_ = true;
|
||||
secure_shared_session_id_ = session_id;
|
||||
secure_shared_left_ = left;
|
||||
secure_shared_top_ = top;
|
||||
secure_shared_width_ = width;
|
||||
secure_shared_height_ = height;
|
||||
secure_shared_show_cursor_ = show_cursor;
|
||||
secure_shared_fps_ = fps;
|
||||
secure_shared_stage_ = stage;
|
||||
|
||||
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||
StopSecureDesktopSharedCapture(session_id);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
const int frame_interval_ms =
|
||||
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinIntervalMs, 1000 / fps_)
|
||||
: kSecureDesktopCaptureMinIntervalMs;
|
||||
fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_)
|
||||
: kSecureDesktopCaptureMaxIntervalMs;
|
||||
ULONGLONG last_status_tick = 0;
|
||||
ULONGLONG last_error_tick = 0;
|
||||
bool last_capture_active = false;
|
||||
@@ -627,6 +1022,9 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
std::string last_stage;
|
||||
std::string last_service_error;
|
||||
ULONGLONG capture_stage_started_tick = 0;
|
||||
bool post_secure_restart_pending = false;
|
||||
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||
ULONGLONG last_post_secure_restart_tick = 0;
|
||||
SecureDesktopServiceStatus status;
|
||||
std::vector<uint8_t> secure_frame;
|
||||
|
||||
@@ -653,6 +1051,11 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
"Windows capturer secure desktop service available, polling "
|
||||
"session_id={}",
|
||||
status.active_session_id);
|
||||
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop service temporarily unavailable: "
|
||||
"error={}, code={}",
|
||||
status.error, status.error_code);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure desktop service unavailable: "
|
||||
@@ -673,6 +1076,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
std::memory_order_relaxed);
|
||||
if (status.capture_active != last_capture_active ||
|
||||
status.interactive_stage != last_stage) {
|
||||
const bool secure_capture_started =
|
||||
!last_capture_active && status.capture_active;
|
||||
const bool secure_capture_ended =
|
||||
last_capture_active && !status.capture_active;
|
||||
capture_stage_started_tick = now;
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop state: active={}, stage='{}', "
|
||||
@@ -681,17 +1088,53 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
status.active_session_id);
|
||||
last_capture_active = status.capture_active;
|
||||
last_stage = status.interactive_stage;
|
||||
if (secure_capture_started) {
|
||||
post_secure_restart_pending = false;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(
|
||||
0, std::memory_order_relaxed);
|
||||
} else if (secure_capture_ended) {
|
||||
post_secure_restart_pending = true;
|
||||
post_secure_restart_deadline_tick =
|
||||
now + kPostSecureDesktopRestartTimeoutMs;
|
||||
last_post_secure_restart_tick = 0;
|
||||
post_secure_desktop_waiting_for_frame_.store(
|
||||
true, std::memory_order_relaxed);
|
||||
post_secure_desktop_drop_logged_.store(
|
||||
false, std::memory_order_relaxed);
|
||||
post_secure_desktop_started_tick_.store(
|
||||
now, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
last_status_tick = now;
|
||||
}
|
||||
|
||||
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
if (post_secure_restart_pending) {
|
||||
if (now >= post_secure_restart_deadline_tick) {
|
||||
LOG_WARN(
|
||||
"Windows capturer: capture backend restart after secure desktop "
|
||||
"timed out");
|
||||
post_secure_restart_pending = false;
|
||||
} else if (last_post_secure_restart_tick == 0 ||
|
||||
now - last_post_secure_restart_tick >=
|
||||
kPostSecureDesktopRestartRetryMs) {
|
||||
last_post_secure_restart_tick = now;
|
||||
post_secure_restart_pending =
|
||||
!RestartCaptureBackendAfterSecureDesktop();
|
||||
}
|
||||
}
|
||||
std::this_thread::sleep_for(
|
||||
std::chrono::milliseconds(status.service_available ? 50 : 200));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!status.helper_running) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||
continue;
|
||||
}
|
||||
@@ -702,6 +1145,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
int height = 0;
|
||||
std::string display_name;
|
||||
if (!GetCurrentCaptureRegion(&left, &top, &width, &height, &display_name)) {
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
continue;
|
||||
}
|
||||
@@ -709,15 +1153,40 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
int captured_width = 0;
|
||||
int captured_height = 0;
|
||||
std::string error_message;
|
||||
if (QuerySecureDesktopHelperFrame(
|
||||
status.active_session_id, left, top, width, height,
|
||||
show_cursor_.load(std::memory_order_relaxed), &secure_frame,
|
||||
bool frame_delivered = false;
|
||||
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||
const int shared_fps =
|
||||
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinFps, fps_)
|
||||
: kSecureDesktopCaptureMinFps;
|
||||
|
||||
if (StartSecureDesktopSharedCapture(status.active_session_id, left, top,
|
||||
width, height,
|
||||
status.interactive_stage, show_cursor,
|
||||
shared_fps, &error_message) &&
|
||||
ReadSecureDesktopSharedFrame(
|
||||
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
|
||||
&captured_width, &captured_height, &error_message)) {
|
||||
if (cb_orig_ && !secure_frame.empty()) {
|
||||
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||
captured_width, captured_height, display_name.c_str());
|
||||
}
|
||||
} else {
|
||||
frame_delivered = true;
|
||||
}
|
||||
|
||||
if (!frame_delivered &&
|
||||
QuerySecureDesktopHelperFrame(status.active_session_id, left, top,
|
||||
width, height, show_cursor,
|
||||
status.interactive_stage,
|
||||
&secure_frame, &captured_width,
|
||||
&captured_height, &error_message)) {
|
||||
if (cb_orig_ && !secure_frame.empty()) {
|
||||
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||
captured_width, captured_height, display_name.c_str());
|
||||
}
|
||||
frame_delivered = true;
|
||||
}
|
||||
|
||||
if (!frame_delivered) {
|
||||
const bool transient_error =
|
||||
IsTransientSecureDesktopFrameError(error_message);
|
||||
const bool in_grace_period = capture_stage_started_tick != 0 &&
|
||||
@@ -731,10 +1200,19 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
if (now - last_error_tick >= log_interval) {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure desktop frame query failed, stage='{}', "
|
||||
"session_id={}, error={}",
|
||||
status.interactive_stage, status.active_session_id, error_message);
|
||||
if (transient_error) {
|
||||
LOG_INFO(
|
||||
"Windows capturer secure desktop transient frame query failed, "
|
||||
"stage='{}', session_id={}, error={}",
|
||||
status.interactive_stage, status.active_session_id,
|
||||
error_message);
|
||||
} else {
|
||||
LOG_WARN(
|
||||
"Windows capturer secure desktop frame query failed, stage='{}', "
|
||||
"session_id={}, error={}",
|
||||
status.interactive_stage, status.active_session_id,
|
||||
error_message);
|
||||
}
|
||||
last_error_tick = now;
|
||||
}
|
||||
}
|
||||
@@ -742,7 +1220,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms));
|
||||
}
|
||||
|
||||
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <Windows.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
@@ -58,14 +59,44 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
std::atomic<int> monitor_index_{0};
|
||||
int initial_monitor_index_ = 0;
|
||||
std::atomic<bool> secure_desktop_capture_active_{false};
|
||||
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
|
||||
std::atomic<bool> post_secure_desktop_drop_logged_{false};
|
||||
std::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
|
||||
std::thread secure_capture_thread_;
|
||||
HANDLE secure_frame_mapping_ = nullptr;
|
||||
HANDLE secure_frame_ready_event_ = nullptr;
|
||||
uint8_t* secure_frame_view_ = nullptr;
|
||||
size_t secure_frame_view_size_ = 0;
|
||||
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
|
||||
int secure_shared_left_ = 0;
|
||||
int secure_shared_top_ = 0;
|
||||
int secure_shared_width_ = 0;
|
||||
int secure_shared_height_ = 0;
|
||||
int secure_shared_fps_ = 0;
|
||||
bool secure_shared_show_cursor_ = true;
|
||||
std::string secure_shared_stage_;
|
||||
bool secure_shared_capture_started_ = false;
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl();
|
||||
void StopSecureCaptureThread();
|
||||
bool RestartCaptureBackendAfterSecureDesktop();
|
||||
void SecureDesktopCaptureLoop();
|
||||
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
|
||||
std::string* display_name);
|
||||
bool StartSecureDesktopSharedCapture(DWORD session_id, int left, int top,
|
||||
int width, int height,
|
||||
const std::string& stage,
|
||||
bool show_cursor, int fps,
|
||||
std::string* error_out);
|
||||
void StopSecureDesktopSharedCapture(DWORD session_id);
|
||||
bool OpenSecureDesktopSharedFrame(DWORD session_id, size_t min_size,
|
||||
std::string* error_out);
|
||||
bool ReadSecureDesktopSharedFrame(DWORD wait_ms,
|
||||
std::vector<uint8_t>* nv12_frame_out,
|
||||
int* width_out, int* height_out,
|
||||
std::string* error_out);
|
||||
void CloseSecureDesktopSharedFrame();
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -2,23 +2,10 @@
|
||||
|
||||
#include <Windows.Graphics.Capture.Interop.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#define CHECK_INIT \
|
||||
if (!is_initialized_) { \
|
||||
LOG_ERROR("AE_NEED_INIT"); \
|
||||
return 4; \
|
||||
}
|
||||
|
||||
#define CHECK_CLOSED \
|
||||
if (cleaned_.load() == true) { \
|
||||
throw winrt::hresult_error(RO_E_CLOSED); \
|
||||
}
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
extern "C" {
|
||||
@@ -40,7 +27,7 @@ int WgcSessionImpl::Initialize(HWND hwnd) {
|
||||
|
||||
target_.hwnd = hwnd;
|
||||
target_.is_window = true;
|
||||
return Initialize();
|
||||
return InitializeLocked();
|
||||
}
|
||||
|
||||
int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
|
||||
@@ -48,7 +35,7 @@ int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
|
||||
|
||||
target_.hmonitor = hmonitor;
|
||||
target_.is_window = false;
|
||||
return Initialize();
|
||||
return InitializeLocked();
|
||||
}
|
||||
|
||||
void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
|
||||
@@ -59,68 +46,13 @@ void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
|
||||
int WgcSessionImpl::Start(bool show_cursor) {
|
||||
std::lock_guard locker(lock_);
|
||||
|
||||
if (is_running_) return 0;
|
||||
|
||||
int error = 1;
|
||||
|
||||
CHECK_INIT;
|
||||
try {
|
||||
last_show_cursor_ = show_cursor;
|
||||
if (!capture_session_) {
|
||||
auto current_size = capture_item_.Size();
|
||||
capture_framepool_ =
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||
CreateFreeThreaded(d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, current_size);
|
||||
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
|
||||
capture_frame_size_ = current_size;
|
||||
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
|
||||
capture_close_trigger_ = capture_item_.Closed(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
|
||||
}
|
||||
|
||||
if (!capture_framepool_) throw std::exception();
|
||||
|
||||
is_running_ = true;
|
||||
|
||||
// we do not need to crate a thread to enter a message loop coz we use
|
||||
// CreateFreeThreaded instead of Create to create a capture frame pool,
|
||||
// we need to test the performance later
|
||||
// loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this));
|
||||
|
||||
capture_session_.IsCursorCaptureEnabled(show_cursor);
|
||||
capture_session_.StartCapture();
|
||||
|
||||
error = 0;
|
||||
} catch (winrt::hresult_error) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
return 86;
|
||||
} catch (...) {
|
||||
return 86;
|
||||
}
|
||||
|
||||
return error;
|
||||
return StartLocked(show_cursor);
|
||||
}
|
||||
|
||||
int WgcSessionImpl::Stop() {
|
||||
std::lock_guard locker(lock_);
|
||||
|
||||
CHECK_INIT;
|
||||
|
||||
is_running_ = false;
|
||||
|
||||
// if (loop_.joinable()) loop_.join();
|
||||
|
||||
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
|
||||
|
||||
if (capture_session_) {
|
||||
capture_session_.Close();
|
||||
capture_session_ = nullptr;
|
||||
}
|
||||
|
||||
CleanUpLocked();
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -129,7 +61,10 @@ int WgcSessionImpl::Pause() {
|
||||
|
||||
is_paused_ = true;
|
||||
|
||||
CHECK_INIT;
|
||||
if (!is_initialized_) {
|
||||
LOG_ERROR("AE_NEED_INIT");
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -138,7 +73,10 @@ int WgcSessionImpl::Resume() {
|
||||
|
||||
is_paused_ = false;
|
||||
|
||||
CHECK_INIT;
|
||||
if (!is_initialized_) {
|
||||
LOG_ERROR("AE_NEED_INIT");
|
||||
return 4;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -175,10 +113,10 @@ auto WgcSessionImpl::CreateCaptureItemForWindow(HWND hwnd) {
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
||||
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
||||
interop_factory->CreateForWindow(
|
||||
winrt::check_hresult(interop_factory->CreateForWindow(
|
||||
hwnd,
|
||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||
reinterpret_cast<void**>(winrt::put_abi(item)));
|
||||
reinterpret_cast<void**>(winrt::put_abi(item))));
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -187,10 +125,10 @@ auto WgcSessionImpl::CreateCaptureItemForMonitor(HMONITOR hmonitor) {
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
||||
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
||||
interop_factory->CreateForMonitor(
|
||||
winrt::check_hresult(interop_factory->CreateForMonitor(
|
||||
hmonitor,
|
||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||
reinterpret_cast<void**>(winrt::put_abi(item)));
|
||||
reinterpret_cast<void**>(winrt::put_abi(item))));
|
||||
return item;
|
||||
}
|
||||
|
||||
@@ -218,6 +156,104 @@ HRESULT WgcSessionImpl::CreateMappedTexture(
|
||||
d3d11_texture_mapped_.put());
|
||||
}
|
||||
|
||||
int WgcSessionImpl::StartCaptureLocked(bool show_cursor) {
|
||||
if (!is_initialized_) {
|
||||
LOG_ERROR("AE_NEED_INIT");
|
||||
return 4;
|
||||
}
|
||||
if (!capture_item_) {
|
||||
LOG_ERROR("WGC: capture item is null");
|
||||
return 4;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!capture_session_) {
|
||||
auto current_size = capture_item_.Size();
|
||||
capture_framepool_ =
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||
CreateFreeThreaded(d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, current_size);
|
||||
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
|
||||
capture_frame_size_ = current_size;
|
||||
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
|
||||
capture_close_trigger_ = capture_item_.Closed(
|
||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
|
||||
}
|
||||
|
||||
if (!capture_framepool_ || !capture_session_) {
|
||||
throw std::exception();
|
||||
}
|
||||
|
||||
capture_session_.IsCursorCaptureEnabled(show_cursor);
|
||||
capture_session_.StartCapture();
|
||||
is_running_ = true;
|
||||
return 0;
|
||||
} catch (const winrt::hresult_error&) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
return 86;
|
||||
} catch (...) {
|
||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||
return 86;
|
||||
}
|
||||
}
|
||||
|
||||
int WgcSessionImpl::StartLocked(bool show_cursor) {
|
||||
if (is_running_) return 0;
|
||||
|
||||
last_show_cursor_ = show_cursor;
|
||||
if (!is_initialized_) {
|
||||
const int init_ret = InitializeLocked();
|
||||
if (init_ret != 0) {
|
||||
return init_ret;
|
||||
}
|
||||
}
|
||||
|
||||
int ret = StartCaptureLocked(show_cursor);
|
||||
if (ret == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_WARN("WGC: start capture failed, rebuilding capture item");
|
||||
CleanUpLocked();
|
||||
ret = InitializeLocked();
|
||||
if (ret != 0) {
|
||||
return ret;
|
||||
}
|
||||
return StartCaptureLocked(show_cursor);
|
||||
}
|
||||
|
||||
void WgcSessionImpl::StopLocked() {
|
||||
is_running_ = false;
|
||||
|
||||
// if (loop_.joinable()) loop_.join();
|
||||
|
||||
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
|
||||
if (capture_close_trigger_) capture_close_trigger_.revoke();
|
||||
|
||||
if (capture_session_) {
|
||||
try {
|
||||
capture_session_.Close();
|
||||
} catch (...) {
|
||||
LOG_WARN("WGC: capture session close failed");
|
||||
}
|
||||
capture_session_ = nullptr;
|
||||
}
|
||||
|
||||
if (capture_framepool_) {
|
||||
try {
|
||||
capture_framepool_.Close();
|
||||
} catch (...) {
|
||||
LOG_WARN("WGC: frame pool close failed");
|
||||
}
|
||||
capture_framepool_ = nullptr;
|
||||
}
|
||||
|
||||
d3d11_texture_mapped_ = nullptr;
|
||||
}
|
||||
|
||||
void WgcSessionImpl::OnFrame(
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
|
||||
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
|
||||
@@ -225,7 +261,7 @@ void WgcSessionImpl::OnFrame(
|
||||
|
||||
auto is_new_size = false;
|
||||
|
||||
{
|
||||
try {
|
||||
auto frame = sender.TryGetNextFrame();
|
||||
auto frame_size = frame.ContentSize();
|
||||
|
||||
@@ -239,60 +275,63 @@ void WgcSessionImpl::OnFrame(
|
||||
}
|
||||
|
||||
// copy to mapped texture
|
||||
{
|
||||
if (is_paused_) {
|
||||
return;
|
||||
}
|
||||
|
||||
auto frame_captured =
|
||||
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
|
||||
|
||||
if (!d3d11_texture_mapped_ || is_new_size) {
|
||||
HRESULT tex_hr = CreateMappedTexture(frame_captured);
|
||||
if (FAILED(tex_hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
|
||||
.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
|
||||
frame_captured.get());
|
||||
|
||||
D3D11_MAPPED_SUBRESOURCE map_result;
|
||||
HRESULT hr = d3d11_device_context_->Map(
|
||||
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
|
||||
0 /*coz we use CreateFreeThreaded, so we cant use flags
|
||||
D3D11_MAP_FLAG_DO_NOT_WAIT*/
|
||||
,
|
||||
&map_result);
|
||||
if (FAILED(hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"map resource failed: " + std::to_wstring(hr)).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// copy data from map_result.pData
|
||||
if (map_result.pData && observer_) {
|
||||
observer_->OnFrame(
|
||||
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
||||
static_cast<unsigned int>(frame_size.Height),
|
||||
map_result.RowPitch,
|
||||
const_cast<const unsigned char*>(
|
||||
(unsigned char*)map_result.pData)},
|
||||
id_);
|
||||
}
|
||||
|
||||
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
|
||||
if (is_paused_) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (is_new_size) {
|
||||
capture_framepool_.Recreate(d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, capture_frame_size_);
|
||||
auto frame_captured =
|
||||
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
|
||||
|
||||
if (!d3d11_texture_mapped_ || is_new_size) {
|
||||
HRESULT tex_hr = CreateMappedTexture(frame_captured);
|
||||
if (FAILED(tex_hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
|
||||
.c_str());
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
|
||||
frame_captured.get());
|
||||
|
||||
D3D11_MAPPED_SUBRESOURCE map_result;
|
||||
HRESULT hr = d3d11_device_context_->Map(
|
||||
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
|
||||
0 /*coz we use CreateFreeThreaded, so we cant use flags
|
||||
D3D11_MAP_FLAG_DO_NOT_WAIT*/
|
||||
,
|
||||
&map_result);
|
||||
if (FAILED(hr)) {
|
||||
OutputDebugStringW(
|
||||
(L"map resource failed: " + std::to_wstring(hr)).c_str());
|
||||
return;
|
||||
}
|
||||
|
||||
// copy data from map_result.pData
|
||||
if (map_result.pData && observer_) {
|
||||
observer_->OnFrame(
|
||||
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
||||
static_cast<unsigned int>(frame_size.Height),
|
||||
map_result.RowPitch,
|
||||
const_cast<const unsigned char*>(
|
||||
(unsigned char*)map_result.pData)},
|
||||
id_);
|
||||
}
|
||||
|
||||
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
|
||||
|
||||
if (is_new_size) {
|
||||
capture_framepool_.Recreate(
|
||||
d3d11_direct_device_,
|
||||
winrt::Windows::Graphics::DirectX::
|
||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||
2, capture_frame_size_);
|
||||
}
|
||||
} catch (const winrt::hresult_error&) {
|
||||
LOG_WARN("WGC: frame processing failed");
|
||||
} catch (...) {
|
||||
LOG_WARN("WGC: frame processing failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -300,11 +339,13 @@ void WgcSessionImpl::OnClosed(
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||
winrt::Windows::Foundation::IInspectable const&) {
|
||||
std::lock_guard locker(lock_);
|
||||
const bool was_running = is_running_;
|
||||
const bool was_paused = is_paused_;
|
||||
try {
|
||||
CleanUp();
|
||||
is_initialized_ = false;
|
||||
if (Initialize() == 0) {
|
||||
int ret = Start(last_show_cursor_);
|
||||
CleanUpLocked();
|
||||
is_paused_ = was_paused;
|
||||
if (InitializeLocked() == 0) {
|
||||
int ret = was_running ? StartCaptureLocked(last_show_cursor_) : 0;
|
||||
if (ret == 0) {
|
||||
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
|
||||
} else {
|
||||
@@ -319,9 +360,14 @@ void WgcSessionImpl::OnClosed(
|
||||
}
|
||||
}
|
||||
|
||||
int WgcSessionImpl::Initialize() {
|
||||
int WgcSessionImpl::InitializeLocked() {
|
||||
if (is_initialized_) return 0;
|
||||
|
||||
d3d11_texture_mapped_ = nullptr;
|
||||
d3d11_device_context_ = nullptr;
|
||||
d3d11_direct_device_ = nullptr;
|
||||
capture_frame_size_ = {};
|
||||
|
||||
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
|
||||
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
|
||||
return 1;
|
||||
@@ -332,6 +378,10 @@ int WgcSessionImpl::Initialize() {
|
||||
capture_item_ = CreateCaptureItemForWindow(target_.hwnd);
|
||||
else
|
||||
capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor);
|
||||
if (!capture_item_) {
|
||||
LOG_ERROR("WGC: create capture item returned null");
|
||||
return 86;
|
||||
}
|
||||
|
||||
// Set up
|
||||
auto d3d11_device =
|
||||
@@ -353,21 +403,18 @@ int WgcSessionImpl::Initialize() {
|
||||
void WgcSessionImpl::CleanUp() {
|
||||
std::lock_guard locker(lock_);
|
||||
|
||||
auto expected = false;
|
||||
if (cleaned_.compare_exchange_strong(expected, true)) {
|
||||
capture_close_trigger_.revoke();
|
||||
capture_framepool_trigger_.revoke();
|
||||
CleanUpLocked();
|
||||
}
|
||||
|
||||
if (capture_framepool_) capture_framepool_.Close();
|
||||
void WgcSessionImpl::CleanUpLocked() {
|
||||
StopLocked();
|
||||
|
||||
if (capture_session_) capture_session_.Close();
|
||||
|
||||
capture_framepool_ = nullptr;
|
||||
capture_session_ = nullptr;
|
||||
capture_item_ = nullptr;
|
||||
|
||||
is_initialized_ = false;
|
||||
}
|
||||
capture_item_ = nullptr;
|
||||
d3d11_device_context_ = nullptr;
|
||||
d3d11_direct_device_ = nullptr;
|
||||
capture_frame_size_ = {};
|
||||
is_initialized_ = false;
|
||||
is_paused_ = false;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
|
||||
|
||||
@@ -68,8 +68,12 @@ class WgcSessionImpl : public WgcSession {
|
||||
void OnClosed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||
winrt::Windows::Foundation::IInspectable const&);
|
||||
|
||||
int Initialize();
|
||||
int InitializeLocked();
|
||||
int StartLocked(bool show_cursor);
|
||||
int StartCaptureLocked(bool show_cursor);
|
||||
void StopLocked();
|
||||
void CleanUp();
|
||||
void CleanUpLocked();
|
||||
|
||||
// void message_func();
|
||||
|
||||
@@ -94,7 +98,6 @@ class WgcSessionImpl : public WgcSession {
|
||||
winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
|
||||
winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};
|
||||
|
||||
std::atomic<bool> cleaned_ = false;
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
|
||||
capture_framepool_{nullptr};
|
||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||
|
||||
@@ -13,7 +13,8 @@ namespace crossdesk {
|
||||
|
||||
inline bool IsSecureDesktopInteractionRequired(
|
||||
const std::string& interactive_stage) {
|
||||
return interactive_stage == "credential-ui" ||
|
||||
return interactive_stage == "lock-screen" ||
|
||||
interactive_stage == "credential-ui" ||
|
||||
interactive_stage == "secure-desktop";
|
||||
}
|
||||
|
||||
@@ -38,4 +39,4 @@ inline bool ShouldNormalizeUnlockToUserDesktop(
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -31,6 +31,7 @@ constexpr char kSecureDesktopMouseIpcCommandPrefix[] = "secure-input-mouse:";
|
||||
constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe";
|
||||
constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000;
|
||||
constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000;
|
||||
constexpr ULONGLONG kSasSecureDesktopGraceMs = 15000;
|
||||
|
||||
using SendSasFunction = VOID(WINAPI*)(BOOL);
|
||||
|
||||
@@ -46,6 +47,13 @@ struct InputDesktopInfo {
|
||||
std::string name;
|
||||
};
|
||||
|
||||
struct SecureDesktopMouseRequest {
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
int wheel = 0;
|
||||
int flag = 0;
|
||||
};
|
||||
|
||||
struct ScopedEnvironmentBlock {
|
||||
~ScopedEnvironmentBlock() {
|
||||
if (environment != nullptr) {
|
||||
@@ -255,8 +263,8 @@ bool GrantCrossDeskServiceStartAccessToAuthenticatedUsers(SC_HANDLE service) {
|
||||
std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
||||
const std::string& command,
|
||||
DWORD timeout_ms) {
|
||||
constexpr int kPipeConnectRetryCount = 3;
|
||||
constexpr DWORD kPipeConnectRetryDelayMs = 15;
|
||||
const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms;
|
||||
|
||||
auto is_transient_pipe_error = [](DWORD error) {
|
||||
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY ||
|
||||
@@ -264,12 +272,23 @@ std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
||||
};
|
||||
|
||||
HANDLE pipe = INVALID_HANDLE_VALUE;
|
||||
for (int attempt = 0; attempt < kPipeConnectRetryCount; ++attempt) {
|
||||
if (!WaitNamedPipeW(pipe_name.c_str(), timeout_ms)) {
|
||||
DWORD last_error = ERROR_SEM_TIMEOUT;
|
||||
while (GetTickCount64() <= deadline_tick) {
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
const DWORD wait_timeout =
|
||||
deadline_tick > now
|
||||
? static_cast<DWORD>((std::min)(
|
||||
deadline_tick - now, static_cast<ULONGLONG>(MAXDWORD)))
|
||||
: 0;
|
||||
|
||||
if (!WaitNamedPipeW(pipe_name.c_str(), wait_timeout)) {
|
||||
const DWORD error = GetLastError();
|
||||
if (attempt + 1 < kPipeConnectRetryCount &&
|
||||
is_transient_pipe_error(error)) {
|
||||
Sleep(kPipeConnectRetryDelayMs);
|
||||
last_error = error;
|
||||
const ULONGLONG retry_tick = GetTickCount64();
|
||||
if (is_transient_pipe_error(error) && retry_tick < deadline_tick) {
|
||||
Sleep(static_cast<DWORD>((std::min)(
|
||||
static_cast<ULONGLONG>(kPipeConnectRetryDelayMs),
|
||||
deadline_tick - retry_tick)));
|
||||
continue;
|
||||
}
|
||||
return BuildErrorJson("pipe_unavailable", error);
|
||||
@@ -282,14 +301,21 @@ std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
||||
}
|
||||
|
||||
const DWORD error = GetLastError();
|
||||
if (attempt + 1 < kPipeConnectRetryCount &&
|
||||
is_transient_pipe_error(error)) {
|
||||
Sleep(kPipeConnectRetryDelayMs);
|
||||
last_error = error;
|
||||
const ULONGLONG retry_tick = GetTickCount64();
|
||||
if (is_transient_pipe_error(error) && retry_tick < deadline_tick) {
|
||||
Sleep(static_cast<DWORD>((std::min)(
|
||||
static_cast<ULONGLONG>(kPipeConnectRetryDelayMs),
|
||||
deadline_tick - retry_tick)));
|
||||
continue;
|
||||
}
|
||||
return BuildErrorJson("pipe_connect_failed", error);
|
||||
}
|
||||
|
||||
if (pipe == INVALID_HANDLE_VALUE) {
|
||||
return BuildErrorJson("pipe_unavailable", last_error);
|
||||
}
|
||||
|
||||
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
||||
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
||||
|
||||
@@ -313,10 +339,12 @@ std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
||||
return std::string(buffer, buffer + bytes_read);
|
||||
}
|
||||
|
||||
std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down) {
|
||||
std::string BuildSecureDesktopKeyboardIpcCommand(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended) {
|
||||
std::ostringstream stream;
|
||||
stream << kSecureDesktopKeyboardIpcCommandPrefix << key_code << ":"
|
||||
<< (is_down ? 1 : 0);
|
||||
<< (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0);
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
@@ -328,20 +356,42 @@ std::string BuildSecureDesktopMouseIpcCommand(int x, int y, int wheel,
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string BuildSecureInputHelperKeyboardCommand(int key_code, bool is_down) {
|
||||
std::string BuildSecureInputHelperKeyboardCommand(
|
||||
int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||
const std::string& interactive_stage) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":"
|
||||
<< (is_down ? 1 : 0);
|
||||
<< (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0);
|
||||
if (!interactive_stage.empty()) {
|
||||
stream << ":" << interactive_stage;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::string BuildSecureInputHelperMouseCommand(
|
||||
int x, int y, int wheel, int flag,
|
||||
const std::string& interactive_stage) {
|
||||
std::ostringstream stream;
|
||||
stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":"
|
||||
<< wheel << ":" << flag;
|
||||
if (!interactive_stage.empty()) {
|
||||
stream << ":" << interactive_stage;
|
||||
}
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ParseSecureDesktopKeyboardIpcCommand(const std::string& command,
|
||||
int* key_code_out,
|
||||
bool* is_down_out) {
|
||||
if (key_code_out == nullptr || is_down_out == nullptr) {
|
||||
int* key_code_out, bool* is_down_out,
|
||||
uint32_t* scan_code_out,
|
||||
bool* extended_out) {
|
||||
if (key_code_out == nullptr || is_down_out == nullptr ||
|
||||
scan_code_out == nullptr || extended_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*scan_code_out = 0;
|
||||
*extended_out = false;
|
||||
|
||||
if (command.rfind(kSecureDesktopKeyboardIpcCommandPrefix, 0) != 0) {
|
||||
return false;
|
||||
}
|
||||
@@ -358,18 +408,102 @@ bool ParseSecureDesktopKeyboardIpcCommand(const std::string& command,
|
||||
return false;
|
||||
}
|
||||
|
||||
const std::string state = command.substr(separator + 1);
|
||||
const size_t scan_separator = command.find(':', separator + 1);
|
||||
const std::string state =
|
||||
scan_separator == std::string::npos
|
||||
? command.substr(separator + 1)
|
||||
: command.substr(separator + 1, scan_separator - separator - 1);
|
||||
if (state == "1" || state == "down") {
|
||||
*is_down_out = true;
|
||||
} else if (state == "0" || state == "up") {
|
||||
*is_down_out = false;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (scan_separator == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
if (state == "0" || state == "up") {
|
||||
*is_down_out = false;
|
||||
|
||||
const size_t extended_separator = command.find(':', scan_separator + 1);
|
||||
const std::string scan_code_str =
|
||||
extended_separator == std::string::npos
|
||||
? command.substr(scan_separator + 1)
|
||||
: command.substr(scan_separator + 1,
|
||||
extended_separator - scan_separator - 1);
|
||||
try {
|
||||
*scan_code_out = static_cast<uint32_t>(std::stoul(scan_code_str));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (extended_separator == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::string extended_str = command.substr(extended_separator + 1);
|
||||
if (extended_str == "1" || extended_str == "true") {
|
||||
*extended_out = true;
|
||||
return true;
|
||||
}
|
||||
if (extended_str == "0" || extended_str == "false") {
|
||||
*extended_out = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ParseSecureDesktopMouseIpcCommand(const std::string& command,
|
||||
SecureDesktopMouseRequest* request_out) {
|
||||
if (request_out == nullptr) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (command.rfind(kSecureDesktopMouseIpcCommandPrefix, 0) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t x_begin = sizeof(kSecureDesktopMouseIpcCommandPrefix) - 1;
|
||||
size_t separator = command.find(':', x_begin);
|
||||
if (separator == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
request_out->x = std::stoi(command.substr(x_begin, separator - x_begin));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t y_begin = separator + 1;
|
||||
separator = command.find(':', y_begin);
|
||||
if (separator == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
request_out->y = std::stoi(command.substr(y_begin, separator - y_begin));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const size_t wheel_begin = separator + 1;
|
||||
separator = command.find(':', wheel_begin);
|
||||
if (separator == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
request_out->wheel =
|
||||
std::stoi(command.substr(wheel_begin, separator - wheel_begin));
|
||||
request_out->flag = std::stoi(command.substr(separator + 1));
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CreateSessionSystemToken(DWORD session_id, HANDLE* token_out,
|
||||
DWORD* error_code_out = nullptr) {
|
||||
if (token_out == nullptr) {
|
||||
@@ -457,6 +591,23 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
|
||||
return "user-desktop";
|
||||
}
|
||||
|
||||
bool IsCredentialUiVisible(bool prelogin, bool session_locked,
|
||||
bool logon_ui_running,
|
||||
bool input_desktop_available,
|
||||
bool secure_desktop_active) {
|
||||
return (prelogin || session_locked || secure_desktop_active) &&
|
||||
(logon_ui_running || !input_desktop_available);
|
||||
}
|
||||
|
||||
std::wstring SecureInputHelperDesktopForStage(
|
||||
const std::string& interactive_stage) {
|
||||
if (interactive_stage == "credential-ui" ||
|
||||
interactive_stage == "secure-desktop") {
|
||||
return L"winsta0\\Winlogon";
|
||||
}
|
||||
return L"winsta0\\default";
|
||||
}
|
||||
|
||||
bool GetSessionUserName(DWORD session_id, std::wstring* username_out) {
|
||||
if (username_out == nullptr) {
|
||||
return false;
|
||||
@@ -885,12 +1036,14 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
||||
session_helper_report_credential_ui_visible_ = false;
|
||||
session_helper_report_unlock_ui_visible_ = false;
|
||||
secure_input_helper_running_ = false;
|
||||
sas_secure_desktop_seen_ = false;
|
||||
last_sas_error_code_ = 0;
|
||||
last_sas_success_ = false;
|
||||
session_helper_started_at_tick_ = 0;
|
||||
session_helper_report_state_age_ms_ = 0;
|
||||
session_helper_report_uptime_ms_ = 0;
|
||||
secure_input_helper_started_at_tick_ = 0;
|
||||
sas_secure_desktop_until_tick_ = 0;
|
||||
session_helper_process_handle_ = nullptr;
|
||||
session_helper_stop_event_ = nullptr;
|
||||
secure_input_helper_process_handle_ = nullptr;
|
||||
@@ -902,6 +1055,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
||||
session_helper_report_input_desktop_.clear();
|
||||
session_helper_report_interactive_stage_.clear();
|
||||
secure_input_helper_last_error_.clear();
|
||||
secure_input_helper_interactive_stage_.clear();
|
||||
last_session_event_type_ = 0;
|
||||
last_session_event_session_id_ = active_session_id_;
|
||||
RefreshSessionState();
|
||||
@@ -1177,7 +1331,13 @@ bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
|
||||
}
|
||||
|
||||
bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
|
||||
return prelogin_ || secure_desktop_active_ || logon_ui_visible_ ||
|
||||
const bool service_host_credential_ui_visible =
|
||||
!session_helper_status_ok_ &&
|
||||
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||
input_desktop_available_,
|
||||
secure_desktop_active_);
|
||||
return IsSasSecureDesktopGraceActiveLocked() || prelogin_ ||
|
||||
secure_desktop_active_ || service_host_credential_ui_visible ||
|
||||
session_helper_report_credential_ui_visible_ ||
|
||||
session_helper_report_secure_desktop_active_ ||
|
||||
session_helper_report_unlock_ui_visible_ ||
|
||||
@@ -1185,6 +1345,30 @@ bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
|
||||
session_helper_report_interactive_stage_ == "secure-desktop";
|
||||
}
|
||||
|
||||
void CrossDeskServiceHost::UpdateSasSecureDesktopGraceLocked(
|
||||
const std::string& observed_stage) {
|
||||
if (sas_secure_desktop_until_tick_ == 0) {
|
||||
sas_secure_desktop_seen_ = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (observed_stage == "credential-ui" || observed_stage == "secure-desktop" ||
|
||||
observed_stage == "lock-screen") {
|
||||
sas_secure_desktop_seen_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (sas_secure_desktop_seen_ && observed_stage == "user-desktop") {
|
||||
sas_secure_desktop_until_tick_ = 0;
|
||||
sas_secure_desktop_seen_ = false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CrossDeskServiceHost::IsSasSecureDesktopGraceActiveLocked() const {
|
||||
return last_sas_success_ && sas_secure_desktop_until_tick_ != 0 &&
|
||||
GetTickCount64() < sas_secure_desktop_until_tick_;
|
||||
}
|
||||
|
||||
bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
|
||||
DWORD target_session_id) const {
|
||||
if (target_session_id == 0xFFFFFFFF) {
|
||||
@@ -1195,6 +1379,28 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
|
||||
IsHelperReportingLockScreenLocked());
|
||||
}
|
||||
|
||||
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||
if (IsSasSecureDesktopGraceActiveLocked() &&
|
||||
(session_helper_report_interactive_stage_.empty() ||
|
||||
session_helper_report_interactive_stage_ == "user-desktop")) {
|
||||
return "secure-desktop";
|
||||
}
|
||||
|
||||
if (!session_helper_report_interactive_stage_.empty()) {
|
||||
return session_helper_report_interactive_stage_;
|
||||
}
|
||||
|
||||
const bool service_host_credential_ui_visible =
|
||||
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||
input_desktop_available_,
|
||||
secure_desktop_active_);
|
||||
return DetermineInteractiveStage(
|
||||
IsHelperReportingLockScreenLocked(),
|
||||
session_helper_report_credential_ui_visible_ ||
|
||||
service_host_credential_ui_visible,
|
||||
session_helper_report_secure_desktop_active_ || secure_desktop_active_);
|
||||
}
|
||||
|
||||
std::wstring CrossDeskServiceHost::GetSessionHelperPath() const {
|
||||
std::wstring current_executable = GetCurrentExecutablePathW();
|
||||
if (current_executable.empty()) {
|
||||
@@ -1284,6 +1490,7 @@ void CrossDeskServiceHost::ReapSecureInputHelper() {
|
||||
secure_input_helper_process_id_ = 0;
|
||||
secure_input_helper_exit_code_ = exit_code;
|
||||
secure_input_helper_started_at_tick_ = 0;
|
||||
secure_input_helper_interactive_stage_.clear();
|
||||
}
|
||||
|
||||
if (process_handle != nullptr) {
|
||||
@@ -1342,6 +1549,7 @@ void CrossDeskServiceHost::StopSecureInputHelper() {
|
||||
secure_input_helper_running_ = false;
|
||||
secure_input_helper_process_id_ = 0;
|
||||
secure_input_helper_started_at_tick_ = 0;
|
||||
secure_input_helper_interactive_stage_.clear();
|
||||
}
|
||||
|
||||
if (stop_event_handle != nullptr) {
|
||||
@@ -1469,7 +1677,8 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
|
||||
bool CrossDeskServiceHost::LaunchSecureInputHelper(
|
||||
DWORD session_id, const std::string& interactive_stage) {
|
||||
std::wstring helper_path = GetSecureInputHelperPath();
|
||||
if (helper_path.empty() || !std::filesystem::exists(helper_path)) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
@@ -1503,7 +1712,10 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
|
||||
|
||||
STARTUPINFOW startup_info{};
|
||||
startup_info.cb = sizeof(startup_info);
|
||||
startup_info.lpDesktop = const_cast<LPWSTR>(L"winsta0\\Winlogon");
|
||||
std::wstring secure_input_helper_desktop =
|
||||
SecureInputHelperDesktopForStage(interactive_stage);
|
||||
startup_info.lpDesktop =
|
||||
const_cast<LPWSTR>(secure_input_helper_desktop.c_str());
|
||||
PROCESS_INFORMATION process_info{};
|
||||
BOOL created = FALSE;
|
||||
|
||||
@@ -1552,10 +1764,14 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
|
||||
secure_input_helper_last_error_.clear();
|
||||
secure_input_helper_running_ = true;
|
||||
secure_input_helper_started_at_tick_ = GetTickCount64();
|
||||
secure_input_helper_interactive_stage_ = interactive_stage;
|
||||
}
|
||||
|
||||
LOG_INFO("Secure input helper started: session_id={}, pid={}", session_id,
|
||||
process_info.dwProcessId);
|
||||
LOG_INFO(
|
||||
"Secure input helper started: session_id={}, pid={}, stage='{}', "
|
||||
"desktop='{}'",
|
||||
session_id, process_info.dwProcessId, interactive_stage,
|
||||
WideToUtf8(secure_input_helper_desktop));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1654,6 +1870,7 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
||||
json.value("interactive_stage", std::string());
|
||||
session_helper_report_state_age_ms_ = json.value("state_age_ms", 0ull);
|
||||
session_helper_report_uptime_ms_ = json.value("uptime_ms", 0ull);
|
||||
UpdateSasSecureDesktopGraceLocked(session_helper_report_interactive_stage_);
|
||||
}
|
||||
|
||||
void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
||||
@@ -1712,8 +1929,18 @@ std::string CrossDeskServiceHost::HandleIpcCommand(const std::string& command) {
|
||||
}
|
||||
int key_code = 0;
|
||||
bool is_down = false;
|
||||
if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, &is_down)) {
|
||||
return SendSecureDesktopKeyboardInput(key_code, is_down);
|
||||
uint32_t scan_code = 0;
|
||||
bool extended = false;
|
||||
if (ParseSecureDesktopKeyboardIpcCommand(normalized, &key_code, &is_down,
|
||||
&scan_code, &extended)) {
|
||||
return SendSecureDesktopKeyboardInput(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
SecureDesktopMouseRequest mouse_request;
|
||||
if (ParseSecureDesktopMouseIpcCommand(normalized, &mouse_request)) {
|
||||
return SendSecureDesktopMouseInput(mouse_request.x, mouse_request.y,
|
||||
mouse_request.wheel,
|
||||
mouse_request.flag);
|
||||
}
|
||||
return BuildErrorJson("unknown_command");
|
||||
}
|
||||
@@ -1727,21 +1954,26 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
bool keep_secure_input_helper = false;
|
||||
bool launch_secure_input_helper = false;
|
||||
DWORD secure_input_target_session_id = 0xFFFFFFFF;
|
||||
std::string secure_input_interactive_stage;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
secure_input_target_session_id = active_session_id_;
|
||||
secure_input_interactive_stage = ResolveInteractiveStageLocked();
|
||||
keep_secure_input_helper =
|
||||
ShouldKeepSecureInputHelperLocked(secure_input_target_session_id);
|
||||
launch_secure_input_helper =
|
||||
keep_secure_input_helper &&
|
||||
(!secure_input_helper_running_ ||
|
||||
secure_input_helper_session_id_ != secure_input_target_session_id);
|
||||
secure_input_helper_session_id_ != secure_input_target_session_id ||
|
||||
secure_input_helper_interactive_stage_ !=
|
||||
secure_input_interactive_stage);
|
||||
}
|
||||
|
||||
if (keep_secure_input_helper) {
|
||||
if (launch_secure_input_helper) {
|
||||
StopSecureInputHelper();
|
||||
LaunchSecureInputHelper(secure_input_target_session_id);
|
||||
LaunchSecureInputHelper(secure_input_target_session_id,
|
||||
secure_input_interactive_stage);
|
||||
}
|
||||
} else {
|
||||
StopSecureInputHelper();
|
||||
@@ -1765,7 +1997,11 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
EscapeJsonString(session_helper_report_input_desktop_);
|
||||
std::string secure_input_helper_last_error =
|
||||
EscapeJsonString(secure_input_helper_last_error_);
|
||||
std::string secure_input_helper_interactive_stage =
|
||||
EscapeJsonString(secure_input_helper_interactive_stage_);
|
||||
bool interactive_state_ready = session_helper_status_ok_;
|
||||
const bool sas_secure_desktop_grace_active =
|
||||
IsSasSecureDesktopGraceActiveLocked();
|
||||
const char* interactive_state_source =
|
||||
interactive_state_ready ? "session-helper" : "service-host";
|
||||
const bool effective_session_locked = GetEffectiveSessionLockedLocked();
|
||||
@@ -1773,27 +2009,34 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
interactive_state_ready
|
||||
? (effective_session_locked && IsHelperReportingLockScreenLocked())
|
||||
: false;
|
||||
const bool service_host_credential_ui_visible =
|
||||
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||
input_desktop_available_,
|
||||
secure_desktop_active_);
|
||||
bool credential_ui_visible =
|
||||
interactive_state_ready ? session_helper_report_credential_ui_visible_
|
||||
: logon_ui_visible_;
|
||||
: service_host_credential_ui_visible;
|
||||
bool unlock_ui_visible = interactive_state_ready
|
||||
? session_helper_report_unlock_ui_visible_
|
||||
: (logon_ui_visible_ || secure_desktop_active_);
|
||||
: (credential_ui_visible ||
|
||||
secure_desktop_active_);
|
||||
unlock_ui_visible = unlock_ui_visible || sas_secure_desktop_grace_active;
|
||||
bool interactive_secure_desktop_active =
|
||||
interactive_state_ready ? session_helper_report_secure_desktop_active_
|
||||
: secure_desktop_active_;
|
||||
interactive_secure_desktop_active =
|
||||
interactive_secure_desktop_active || sas_secure_desktop_grace_active;
|
||||
bool interactive_logon_ui_visible =
|
||||
interactive_state_ready ? session_helper_report_logon_ui_visible_
|
||||
: logon_ui_visible_;
|
||||
credential_ui_visible;
|
||||
bool interactive_session_locked = effective_session_locked ||
|
||||
interactive_lock_screen_visible ||
|
||||
unlock_ui_visible;
|
||||
unlock_ui_visible ||
|
||||
sas_secure_desktop_grace_active;
|
||||
std::string interactive_input_desktop = EscapeJsonString(
|
||||
interactive_state_ready ? session_helper_report_input_desktop_
|
||||
: input_desktop_name_);
|
||||
std::string interactive_stage = EscapeJsonString(DetermineInteractiveStage(
|
||||
interactive_lock_screen_visible, credential_ui_visible,
|
||||
interactive_secure_desktop_active));
|
||||
std::string raw_interactive_stage = ResolveInteractiveStageLocked();
|
||||
std::string interactive_stage = EscapeJsonString(raw_interactive_stage);
|
||||
std::ostringstream stream;
|
||||
stream << "{\"ok\":true,\"service\":\"CrossDeskService\""
|
||||
<< ",\"active_session_id\":" << active_session_id_
|
||||
@@ -1814,6 +2057,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
<< (interactive_logon_ui_visible ? "true" : "false")
|
||||
<< ",\"interactive_secure_desktop_active\":"
|
||||
<< (interactive_secure_desktop_active ? "true" : "false")
|
||||
<< ",\"sas_secure_desktop_grace_active\":"
|
||||
<< (sas_secure_desktop_grace_active ? "true" : "false")
|
||||
<< ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false")
|
||||
<< ",\"credential_ui_visible\":"
|
||||
<< (credential_ui_visible ? "true" : "false")
|
||||
@@ -1887,6 +2132,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
||||
<< secure_input_helper_last_error << "\""
|
||||
<< ",\"secure_input_helper_last_error_code\":"
|
||||
<< secure_input_helper_last_error_code_
|
||||
<< ",\"secure_input_helper_stage\":\""
|
||||
<< secure_input_helper_interactive_stage << "\""
|
||||
<< ",\"secure_input_helper_uptime_ms\":"
|
||||
<< (secure_input_helper_started_at_tick_ >= started_at_tick_
|
||||
? (GetTickCount64() - secure_input_helper_started_at_tick_)
|
||||
@@ -1916,10 +2163,14 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence() {
|
||||
SasResult result = SendSasNow();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
last_sas_tick_ = GetTickCount64();
|
||||
const ULONGLONG now = GetTickCount64();
|
||||
last_sas_tick_ = now;
|
||||
last_sas_success_ = result.success;
|
||||
last_sas_error_code_ = result.error_code;
|
||||
last_sas_error_ = result.error;
|
||||
sas_secure_desktop_until_tick_ =
|
||||
result.success ? now + kSasSecureDesktopGraceMs : 0;
|
||||
sas_secure_desktop_seen_ = false;
|
||||
}
|
||||
|
||||
if (!result.success) {
|
||||
@@ -1928,20 +2179,26 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence() {
|
||||
return "{\"ok\":true,\"sent\":\"sas\"}";
|
||||
}
|
||||
|
||||
std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(int key_code,
|
||||
bool is_down) {
|
||||
std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
||||
int key_code, bool is_down, uint32_t scan_code, bool extended) {
|
||||
RefreshSessionState();
|
||||
ReapSecureInputHelper();
|
||||
EnsureSessionHelper();
|
||||
RefreshSessionHelperReportedState();
|
||||
|
||||
DWORD target_session_id = 0xFFFFFFFF;
|
||||
bool helper_running = false;
|
||||
bool can_inject = false;
|
||||
std::string interactive_stage;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
target_session_id = active_session_id_;
|
||||
interactive_stage = ResolveInteractiveStageLocked();
|
||||
const bool helper_stage_matches =
|
||||
secure_input_helper_interactive_stage_ == interactive_stage;
|
||||
helper_running = secure_input_helper_running_ &&
|
||||
secure_input_helper_session_id_ == target_session_id;
|
||||
secure_input_helper_session_id_ == target_session_id &&
|
||||
helper_stage_matches;
|
||||
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
|
||||
}
|
||||
|
||||
@@ -1954,7 +2211,7 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(int key_code,
|
||||
|
||||
if (!helper_running) {
|
||||
StopSecureInputHelper();
|
||||
if (!LaunchSecureInputHelper(target_session_id)) {
|
||||
if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
||||
secure_input_helper_last_error_code_);
|
||||
@@ -1963,7 +2220,55 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(int key_code,
|
||||
|
||||
return QueryNamedPipeMessage(
|
||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
||||
BuildSecureInputHelperKeyboardCommand(key_code, is_down), 1000);
|
||||
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
|
||||
extended, interactive_stage),
|
||||
1000);
|
||||
}
|
||||
|
||||
std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
|
||||
int wheel,
|
||||
int flag) {
|
||||
RefreshSessionState();
|
||||
ReapSecureInputHelper();
|
||||
EnsureSessionHelper();
|
||||
RefreshSessionHelperReportedState();
|
||||
|
||||
DWORD target_session_id = 0xFFFFFFFF;
|
||||
bool helper_running = false;
|
||||
bool can_inject = false;
|
||||
std::string interactive_stage;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
target_session_id = active_session_id_;
|
||||
interactive_stage = ResolveInteractiveStageLocked();
|
||||
const bool helper_stage_matches =
|
||||
secure_input_helper_interactive_stage_ == interactive_stage;
|
||||
helper_running = secure_input_helper_running_ &&
|
||||
secure_input_helper_session_id_ == target_session_id &&
|
||||
helper_stage_matches;
|
||||
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
|
||||
}
|
||||
|
||||
if (target_session_id == 0xFFFFFFFF) {
|
||||
return BuildErrorJson("no_active_console_session");
|
||||
}
|
||||
if (!can_inject) {
|
||||
return BuildErrorJson("secure_input_not_active");
|
||||
}
|
||||
|
||||
if (!helper_running) {
|
||||
StopSecureInputHelper();
|
||||
if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
||||
secure_input_helper_last_error_code_);
|
||||
}
|
||||
}
|
||||
|
||||
return QueryNamedPipeMessage(
|
||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
||||
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage),
|
||||
1000);
|
||||
}
|
||||
|
||||
bool InstallCrossDeskService(const std::wstring& binary_path) {
|
||||
@@ -2176,9 +2481,12 @@ std::string QueryCrossDeskService(const std::string& command,
|
||||
}
|
||||
|
||||
std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
|
||||
uint32_t scan_code,
|
||||
bool extended,
|
||||
DWORD timeout_ms) {
|
||||
return QueryCrossDeskService(
|
||||
BuildSecureDesktopKeyboardIpcCommand(key_code, is_down), timeout_ms);
|
||||
return QueryCrossDeskService(BuildSecureDesktopKeyboardIpcCommand(
|
||||
key_code, is_down, scan_code, extended),
|
||||
timeout_ms);
|
||||
}
|
||||
|
||||
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||
@@ -2187,4 +2495,4 @@ std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||
BuildSecureDesktopMouseIpcCommand(x, y, wheel, flag), timeout_ms);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
@@ -44,7 +45,8 @@ class CrossDeskServiceHost {
|
||||
bool LaunchSessionHelper(DWORD session_id);
|
||||
void ReapSecureInputHelper();
|
||||
void StopSecureInputHelper();
|
||||
bool LaunchSecureInputHelper(DWORD session_id);
|
||||
bool LaunchSecureInputHelper(DWORD session_id,
|
||||
const std::string& interactive_stage);
|
||||
std::wstring GetSessionHelperPath() const;
|
||||
std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
|
||||
std::wstring GetSecureInputHelperPath() const;
|
||||
@@ -54,13 +56,19 @@ class CrossDeskServiceHost {
|
||||
bool GetEffectiveSessionLockedLocked() const;
|
||||
bool IsHelperReportingLockScreenLocked() const;
|
||||
bool HasSecureInputUiLocked() const;
|
||||
void UpdateSasSecureDesktopGraceLocked(const std::string& observed_stage);
|
||||
bool IsSasSecureDesktopGraceActiveLocked() const;
|
||||
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
|
||||
std::string ResolveInteractiveStageLocked() const;
|
||||
void RefreshSessionHelperReportedState();
|
||||
void RecordSessionEvent(DWORD event_type, DWORD session_id);
|
||||
std::string HandleIpcCommand(const std::string& command);
|
||||
std::string BuildStatusResponse();
|
||||
std::string SendSecureAttentionSequence();
|
||||
std::string SendSecureDesktopKeyboardInput(int key_code, bool is_down);
|
||||
std::string SendSecureDesktopKeyboardInput(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false);
|
||||
std::string SendSecureDesktopMouseInput(int x, int y, int wheel, int flag);
|
||||
|
||||
static void WINAPI ServiceMain(DWORD argc, LPWSTR* argv);
|
||||
static BOOL WINAPI ConsoleControlHandler(DWORD control_type);
|
||||
@@ -97,6 +105,7 @@ class CrossDeskServiceHost {
|
||||
ULONGLONG session_helper_report_state_age_ms_ = 0;
|
||||
ULONGLONG session_helper_report_uptime_ms_ = 0;
|
||||
ULONGLONG secure_input_helper_started_at_tick_ = 0;
|
||||
ULONGLONG sas_secure_desktop_until_tick_ = 0;
|
||||
bool session_locked_ = false;
|
||||
bool logon_ui_visible_ = false;
|
||||
bool prelogin_ = false;
|
||||
@@ -113,6 +122,7 @@ class CrossDeskServiceHost {
|
||||
bool session_helper_report_unlock_ui_visible_ = false;
|
||||
bool secure_input_helper_running_ = false;
|
||||
bool console_mode_ = false;
|
||||
bool sas_secure_desktop_seen_ = false;
|
||||
DWORD last_sas_error_code_ = 0;
|
||||
bool last_sas_success_ = false;
|
||||
HANDLE session_helper_process_handle_ = nullptr;
|
||||
@@ -126,6 +136,7 @@ class CrossDeskServiceHost {
|
||||
std::string session_helper_report_input_desktop_;
|
||||
std::string session_helper_report_interactive_stage_;
|
||||
std::string secure_input_helper_last_error_;
|
||||
std::string secure_input_helper_interactive_stage_;
|
||||
|
||||
static CrossDeskServiceHost* instance_;
|
||||
};
|
||||
@@ -138,6 +149,8 @@ bool StopCrossDeskService(DWORD timeout_ms = 5000);
|
||||
std::string QueryCrossDeskService(const std::string& command,
|
||||
DWORD timeout_ms = 1000);
|
||||
std::string SendCrossDeskSecureDesktopKeyInput(int key_code, bool is_down,
|
||||
uint32_t scan_code = 0,
|
||||
bool extended = false,
|
||||
DWORD timeout_ms = 1000);
|
||||
std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||
int flag,
|
||||
@@ -145,4 +158,4 @@ std::string SendCrossDeskSecureDesktopMouseInput(int x, int y, int wheel,
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,15 @@ inline constexpr char kCrossDeskSecureInputKeyboardCommandPrefix[] =
|
||||
"keyboard:";
|
||||
inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = "mouse:";
|
||||
inline constexpr char kCrossDeskSecureInputCaptureCommandPrefix[] = "capture:";
|
||||
inline constexpr char kCrossDeskSecureInputCaptureStartCommandPrefix[] =
|
||||
"capture-start:";
|
||||
inline constexpr char kCrossDeskSecureInputCaptureStopCommand[] =
|
||||
"capture-stop";
|
||||
inline constexpr DWORD kCrossDeskSecureInputPipeBufferBytes = 16 * 1024 * 1024;
|
||||
inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
|
||||
L"Global\\CrossDeskSecureDesktopFrame-";
|
||||
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
|
||||
L"Global\\CrossDeskSecureDesktopFrameReady-";
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
|
||||
|
||||
@@ -37,6 +45,19 @@ struct CrossDeskSecureDesktopFrameHeader {
|
||||
uint32_t height;
|
||||
uint32_t payload_size;
|
||||
};
|
||||
|
||||
struct CrossDeskSecureDesktopSharedFrameHeader {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
volatile uint32_t writing;
|
||||
uint32_t sequence;
|
||||
int32_t left;
|
||||
int32_t top;
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
uint32_t payload_size;
|
||||
uint32_t buffer_size;
|
||||
};
|
||||
#pragma pack(pop)
|
||||
|
||||
inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) {
|
||||
@@ -49,6 +70,18 @@ inline std::wstring GetCrossDeskSecureInputHelperPipeName(DWORD session_id) {
|
||||
std::to_wstring(session_id);
|
||||
}
|
||||
|
||||
inline std::wstring GetCrossDeskSecureDesktopFrameMappingName(
|
||||
DWORD session_id) {
|
||||
return std::wstring(kCrossDeskSecureDesktopFrameMappingPrefix) +
|
||||
std::to_wstring(session_id);
|
||||
}
|
||||
|
||||
inline std::wstring GetCrossDeskSecureDesktopFrameReadyEventName(
|
||||
DWORD session_id) {
|
||||
return std::wstring(kCrossDeskSecureDesktopFrameReadyEventPrefix) +
|
||||
std::to_wstring(session_id);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -9,6 +9,17 @@ namespace crossdesk {
|
||||
class SpeakerCapturerMacosx;
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string NSErrorToString(NSError* error) {
|
||||
if (!error) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const char* description = [error.localizedDescription UTF8String];
|
||||
return description ? description : "";
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
|
||||
@property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner;
|
||||
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner;
|
||||
@@ -28,15 +39,36 @@ class SpeakerCapturerMacosx;
|
||||
ofType:(SCStreamOutputType)type {
|
||||
if (type != SCStreamOutputTypeAudio) return;
|
||||
|
||||
crossdesk::SpeakerCapturerMacosx* owner = _owner;
|
||||
if (!owner || !owner->cb_) {
|
||||
return;
|
||||
}
|
||||
|
||||
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
|
||||
if (!blockBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t length = CMBlockBufferGetDataLength(blockBuffer);
|
||||
char* dataPtr = NULL;
|
||||
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
||||
OSStatus dataStatus =
|
||||
CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr);
|
||||
if (dataStatus != noErr || dataPtr == nullptr || length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
|
||||
if (!formatDesc) {
|
||||
return;
|
||||
}
|
||||
|
||||
const AudioStreamBasicDescription* asbd =
|
||||
CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
|
||||
if (!asbd || asbd->mChannelsPerFrame == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_owner->cb_ && dataPtr && length > 0 && asbd) {
|
||||
if (owner->cb_) {
|
||||
std::vector<short> out_pcm16;
|
||||
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
|
||||
int channels = asbd->mChannelsPerFrame;
|
||||
@@ -86,7 +118,10 @@ class SpeakerCapturerMacosx;
|
||||
size_t total_bytes = out_pcm16.size() * sizeof(short);
|
||||
unsigned char* p = (unsigned char*)out_pcm16.data();
|
||||
for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) {
|
||||
_owner->cb_(p + offset, frame_bytes, "audio");
|
||||
if (!owner->cb_) {
|
||||
return;
|
||||
}
|
||||
owner->cb_(p + offset, frame_bytes, "audio");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -155,7 +190,7 @@ int SpeakerCapturerMacosx::Init(speaker_data_cb cb) {
|
||||
|
||||
if (error || !impl_->content) {
|
||||
LOG_ERROR("Failed to get shareable content: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
NSErrorToString(error));
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -209,7 +244,7 @@ int SpeakerCapturerMacosx::Start() {
|
||||
error:&addOutputError];
|
||||
if (!ok || addOutputError) {
|
||||
LOG_ERROR("addStreamOutput error: {}",
|
||||
std::string([addOutputError.localizedDescription UTF8String]));
|
||||
NSErrorToString(addOutputError));
|
||||
impl_->stream = nil;
|
||||
impl_->delegate = nil;
|
||||
return -1;
|
||||
@@ -220,7 +255,7 @@ int SpeakerCapturerMacosx::Start() {
|
||||
[impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) {
|
||||
if (error) {
|
||||
LOG_ERROR("startCaptureWithCompletionHandler error: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
NSErrorToString(error));
|
||||
ret = -1;
|
||||
}
|
||||
dispatch_semaphore_signal(semaStart);
|
||||
@@ -238,13 +273,14 @@ int SpeakerCapturerMacosx::Stop() {
|
||||
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
|
||||
if (error) {
|
||||
LOG_ERROR("stopCaptureWithCompletionHandler error: {}",
|
||||
std::string([error.localizedDescription UTF8String]));
|
||||
NSErrorToString(error));
|
||||
}
|
||||
dispatch_semaphore_signal(sema);
|
||||
}];
|
||||
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
|
||||
|
||||
impl_->stream = nil;
|
||||
impl_->delegate.owner = nullptr;
|
||||
impl_->delegate = nil;
|
||||
|
||||
return 0;
|
||||
@@ -269,4 +305,4 @@ int SpeakerCapturerMacosx::Destroy() {
|
||||
int SpeakerCapturerMacosx::Pause() { return 0; }
|
||||
|
||||
int SpeakerCapturerMacosx::Resume() { return Start(); }
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -8,7 +8,15 @@
|
||||
|
||||
#include <httplib.h>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
@@ -16,12 +24,296 @@
|
||||
namespace crossdesk {
|
||||
|
||||
static std::string latest_release_date_ = "";
|
||||
static bool latest_patch_available_ = false;
|
||||
static int latest_patch_ = 0;
|
||||
|
||||
std::vector<int> SplitVersion(const std::string& ver);
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr size_t kMaxInlinePatchDigits = 4;
|
||||
|
||||
struct ParsedVersion {
|
||||
std::vector<int> numbers;
|
||||
std::string date;
|
||||
bool has_patch = false;
|
||||
int patch = 0;
|
||||
};
|
||||
|
||||
bool IsDigit(char c) {
|
||||
return std::isdigit(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAlphaNumeric(char c) {
|
||||
return std::isalnum(static_cast<unsigned char>(c)) != 0;
|
||||
}
|
||||
|
||||
bool IsAllDigits(const std::string& value) {
|
||||
if (value.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (char c : value) {
|
||||
if (!IsDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool TryParseNonNegativeInt(const std::string& value, int* result) {
|
||||
if (!IsAllDigits(value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const long long parsed = std::stoll(value);
|
||||
if (parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*result = static_cast<int>(parsed);
|
||||
return true;
|
||||
} catch (...) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool TryParseInlinePatch(const std::string& value, int* result) {
|
||||
if (value.size() > kMaxInlinePatchDigits) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return TryParseNonNegativeInt(value, result);
|
||||
}
|
||||
|
||||
size_t FindNumericStart(const std::string& version) {
|
||||
size_t start = 0;
|
||||
while (start < version.size() && !IsDigit(version[start])) {
|
||||
start++;
|
||||
}
|
||||
return start;
|
||||
}
|
||||
|
||||
size_t FindNumericEnd(const std::string& version, size_t start) {
|
||||
size_t end = start;
|
||||
while (end < version.size() &&
|
||||
(IsDigit(version[end]) || version[end] == '.')) {
|
||||
end++;
|
||||
}
|
||||
return end;
|
||||
}
|
||||
|
||||
bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) {
|
||||
const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]);
|
||||
const size_t end = pos + len;
|
||||
const bool after_ok = end >= value.size() || !IsDigit(value[end]);
|
||||
return before_ok && after_ok;
|
||||
}
|
||||
|
||||
bool IsCompactDateAt(const std::string& value, size_t pos) {
|
||||
if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 8; ++i) {
|
||||
if (!IsDigit(value[pos + i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string CompactDateToIso(const std::string& compact_date) {
|
||||
return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" +
|
||||
compact_date.substr(6, 2);
|
||||
}
|
||||
|
||||
bool ExtractDateFromText(const std::string& value,
|
||||
std::string* date,
|
||||
size_t* date_end) {
|
||||
for (size_t i = 0; i < value.size(); ++i) {
|
||||
if (IsCompactDateAt(value, i)) {
|
||||
*date = CompactDateToIso(value.substr(i, 8));
|
||||
*date_end = i + 8;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
ParsedVersion ParseVersion(const std::string& version) {
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
|
||||
ParsedVersion parsed;
|
||||
parsed.numbers = SplitVersion(version.substr(numeric_start,
|
||||
numeric_end - numeric_start));
|
||||
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
size_t pos = 0;
|
||||
while (pos < suffix.size()) {
|
||||
while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
const size_t token_start = pos;
|
||||
while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) {
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (token_start == pos) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string token = suffix.substr(token_start, pos - token_start);
|
||||
if (parsed.date.empty() && IsCompactDateAt(token, 0)) {
|
||||
parsed.date = CompactDateToIso(token);
|
||||
continue;
|
||||
}
|
||||
|
||||
int patch = 0;
|
||||
if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) {
|
||||
parsed.has_patch = true;
|
||||
parsed.patch = patch;
|
||||
}
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
int CompareNumericVersion(const std::vector<int>& current,
|
||||
const std::vector<int>& latest) {
|
||||
std::vector<int> current_parts = current;
|
||||
std::vector<int> latest_parts = latest;
|
||||
const size_t len = std::max(current_parts.size(), latest_parts.size());
|
||||
current_parts.resize(len, 0);
|
||||
latest_parts.resize(len, 0);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (latest_parts[i] > current_parts[i]) {
|
||||
return 1;
|
||||
}
|
||||
if (latest_parts[i] < current_parts[i]) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ResetLatestMetadata() {
|
||||
latest_release_date_ = "";
|
||||
latest_patch_available_ = false;
|
||||
latest_patch_ = 0;
|
||||
}
|
||||
|
||||
bool ReadPatchField(const nlohmann::json& json, int* patch) {
|
||||
if (!json.contains("patch")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto& patch_value = json["patch"];
|
||||
if (patch_value.is_number_integer()) {
|
||||
const long long parsed = patch_value.get<long long>();
|
||||
if (parsed < 0 || parsed > std::numeric_limits<int>::max()) {
|
||||
return false;
|
||||
}
|
||||
*patch = static_cast<int>(parsed);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (patch_value.is_string()) {
|
||||
return TryParseNonNegativeInt(patch_value.get<std::string>(), patch);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void LogHttpError(const httplib::Result& result) {
|
||||
LOG_WARN("Failed to fetch version.json: error={}, message={}",
|
||||
static_cast<int>(result.error()), httplib::to_string(result.error()));
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
LOG_WARN("version.json SSL error={}, OpenSSL error={}", result.ssl_error(),
|
||||
result.ssl_openssl_error());
|
||||
#endif
|
||||
}
|
||||
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
bool PathExists(const std::string& path) {
|
||||
if (path.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::error_code ec;
|
||||
return std::filesystem::exists(path, ec);
|
||||
}
|
||||
|
||||
std::string GetEnvPathIfExists(const char* key) {
|
||||
const char* value = std::getenv(key);
|
||||
if (!value) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const std::string path = value;
|
||||
return PathExists(path) ? path : "";
|
||||
}
|
||||
|
||||
std::string FindFirstExistingPath(
|
||||
const std::vector<std::string>& candidates) {
|
||||
for (const auto& candidate : candidates) {
|
||||
if (PathExists(candidate)) {
|
||||
return candidate;
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
void ConfigureLinuxCaCerts(httplib::Client* cli) {
|
||||
const std::string ca_file = [&]() {
|
||||
const std::string env_path = GetEnvPathIfExists("SSL_CERT_FILE");
|
||||
if (!env_path.empty()) {
|
||||
return env_path;
|
||||
}
|
||||
|
||||
return FindFirstExistingPath({
|
||||
"/etc/ssl/certs/ca-certificates.crt",
|
||||
"/etc/pki/tls/certs/ca-bundle.crt",
|
||||
"/etc/ssl/cert.pem",
|
||||
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
||||
});
|
||||
}();
|
||||
|
||||
const std::string ca_dir = [&]() {
|
||||
const std::string env_path = GetEnvPathIfExists("SSL_CERT_DIR");
|
||||
if (!env_path.empty()) {
|
||||
return env_path;
|
||||
}
|
||||
|
||||
return FindFirstExistingPath({
|
||||
"/etc/ssl/certs",
|
||||
"/etc/pki/tls/certs",
|
||||
"/etc/openssl/certs",
|
||||
});
|
||||
}();
|
||||
|
||||
if (ca_file.empty() && ca_dir.empty()) {
|
||||
LOG_WARN("No Linux CA bundle found for version.json request; relying on OpenSSL defaults");
|
||||
return;
|
||||
}
|
||||
|
||||
cli->set_ca_cert_path(ca_file, ca_dir);
|
||||
LOG_INFO("Configured version.json TLS CA bundle: file={}, dir={}",
|
||||
ca_file.empty() ? "<none>" : ca_file,
|
||||
ca_dir.empty() ? "<none>" : ca_dir);
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
std::string ExtractNumericPart(const std::string& ver) {
|
||||
size_t start = 0;
|
||||
while (start < ver.size() && !std::isdigit(ver[start])) start++;
|
||||
size_t end = start;
|
||||
while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++;
|
||||
const size_t start = FindNumericStart(ver);
|
||||
const size_t end = FindNumericEnd(ver, start);
|
||||
return ver.substr(start, end - start);
|
||||
}
|
||||
|
||||
@@ -42,25 +334,13 @@ std::vector<int> SplitVersion(const std::string& ver) {
|
||||
// extract date from version string (format: v1.2.3-20251113-abc
|
||||
// or 1.2.3-20251113-abc)
|
||||
std::string ExtractDateFromVersion(const std::string& version) {
|
||||
size_t dash1 = version.find('-');
|
||||
if (dash1 != std::string::npos) {
|
||||
size_t dash2 = version.find('-', dash1 + 1);
|
||||
if (dash2 != std::string::npos) {
|
||||
std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1);
|
||||
|
||||
bool is_date = true;
|
||||
for (char c : date_part) {
|
||||
if (!std::isdigit(c)) {
|
||||
is_date = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (is_date) {
|
||||
// convert YYYYMMDD to YYYY-MM-DD
|
||||
return date_part.substr(0, 4) + "-" + date_part.substr(4, 2) + "-" +
|
||||
date_part.substr(6, 2);
|
||||
}
|
||||
}
|
||||
const size_t numeric_start = FindNumericStart(version);
|
||||
const size_t numeric_end = FindNumericEnd(version, numeric_start);
|
||||
const std::string suffix = version.substr(numeric_end);
|
||||
std::string date;
|
||||
size_t date_end = 0;
|
||||
if (ExtractDateFromText(suffix, &date, &date_end)) {
|
||||
return date;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
@@ -73,55 +353,41 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) {
|
||||
}
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest) {
|
||||
auto v1 = SplitVersion(ExtractNumericPart(current));
|
||||
auto v2 = SplitVersion(ExtractNumericPart(latest));
|
||||
|
||||
size_t len = std::max(v1.size(), v2.size());
|
||||
v1.resize(len, 0);
|
||||
v2.resize(len, 0);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (v2[i] > v1[i]) return true;
|
||||
if (v2[i] < v1[i]) return false;
|
||||
}
|
||||
|
||||
// if versions are equal, compare by release date
|
||||
if (!latest_release_date_.empty()) {
|
||||
// try to extract date from current version string
|
||||
std::string current_date = ExtractDateFromVersion(current);
|
||||
if (!current_date.empty()) {
|
||||
return IsNewerDate(current_date, latest_release_date_);
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
return IsNewerVersionWithMetadata(
|
||||
current, latest, latest_release_date_,
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
}
|
||||
|
||||
bool IsNewerVersionWithDate(const std::string& current_version,
|
||||
const std::string& current_date,
|
||||
const std::string& latest_version,
|
||||
const std::string& latest_date) {
|
||||
// compare versions
|
||||
auto v1 = SplitVersion(ExtractNumericPart(current_version));
|
||||
auto v2 = SplitVersion(ExtractNumericPart(latest_version));
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch) {
|
||||
(void)latest_date;
|
||||
|
||||
size_t len = std::max(v1.size(), v2.size());
|
||||
v1.resize(len, 0);
|
||||
v2.resize(len, 0);
|
||||
const ParsedVersion current_version = ParseVersion(current);
|
||||
const ParsedVersion latest_version = ParseVersion(latest);
|
||||
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
if (v2[i] > v1[i]) return true;
|
||||
if (v2[i] < v1[i]) return false;
|
||||
const int numeric_compare =
|
||||
CompareNumericVersion(current_version.numbers, latest_version.numbers);
|
||||
if (numeric_compare > 0) {
|
||||
return true;
|
||||
}
|
||||
if (numeric_compare < 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if versions are equal, compare by release date
|
||||
if (!current_date.empty() && !latest_date.empty()) {
|
||||
return IsNewerDate(current_date, latest_date);
|
||||
const bool metadata_has_patch = latest_patch >= 0;
|
||||
const bool latest_has_patch = metadata_has_patch || latest_version.has_patch;
|
||||
if (latest_has_patch || current_version.has_patch) {
|
||||
const int resolved_latest_patch =
|
||||
metadata_has_patch ? latest_patch
|
||||
: (latest_version.has_patch ? latest_version.patch
|
||||
: 0);
|
||||
const int resolved_current_patch =
|
||||
current_version.has_patch ? current_version.patch : 0;
|
||||
return resolved_latest_patch > resolved_current_patch;
|
||||
}
|
||||
|
||||
// if dates are not available, versions are equal
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -130,8 +396,14 @@ nlohmann::json CheckUpdate() {
|
||||
|
||||
cli.set_connection_timeout(5);
|
||||
cli.set_read_timeout(5);
|
||||
cli.set_follow_location(true);
|
||||
|
||||
if (auto res = cli.Get("/version.json")) {
|
||||
#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__)
|
||||
ConfigureLinuxCaCerts(&cli);
|
||||
#endif
|
||||
|
||||
auto res = cli.Get("/version.json");
|
||||
if (res) {
|
||||
if (res->status == 200) {
|
||||
try {
|
||||
auto j = nlohmann::json::parse(res->body);
|
||||
@@ -140,19 +412,28 @@ nlohmann::json CheckUpdate() {
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
}
|
||||
latest_patch_ = 0;
|
||||
latest_patch_available_ = ReadPatchField(j, &latest_patch_);
|
||||
LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}",
|
||||
j.value("latest_version", j.value("version", "")),
|
||||
j.value("releaseDate", ""),
|
||||
latest_patch_available_ ? latest_patch_ : -1);
|
||||
return j;
|
||||
} catch (std::exception&) {
|
||||
latest_release_date_ = "";
|
||||
} catch (const std::exception& e) {
|
||||
LOG_WARN("Failed to parse version.json: {}", e.what());
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
} else {
|
||||
latest_release_date_ = "";
|
||||
LogHttpError(res);
|
||||
ResetLatestMetadata();
|
||||
return nlohmann::json{};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -16,6 +16,12 @@ nlohmann::json CheckUpdate();
|
||||
|
||||
bool IsNewerVersion(const std::string& current, const std::string& latest);
|
||||
|
||||
// Pass latest_patch < 0 when patch metadata is unavailable.
|
||||
bool IsNewerVersionWithMetadata(const std::string& current,
|
||||
const std::string& latest,
|
||||
const std::string& latest_date,
|
||||
int latest_patch);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+1
-1
Submodule submodules/minirtc updated: a9259b3be3...bb0fae0617
@@ -0,0 +1,183 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "src/gui/toolbars/control_bar.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectContainsAtLeast(const char* name, const std::string& value,
|
||||
const std::string& expected, size_t min_count) {
|
||||
size_t count = 0;
|
||||
size_t pos = 0;
|
||||
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
||||
++count;
|
||||
pos += expected.size();
|
||||
}
|
||||
|
||||
if (count >= min_count) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " expected at least " << min_count
|
||||
<< " occurrences of: " << expected << ", found " << count
|
||||
<< "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectResetBeforeDisplayPopup(const std::string& value) {
|
||||
const std::string reset = "props->display_selectable_hovered_ = false;";
|
||||
const std::string popup = "ImGui::BeginPopup(\"display\")";
|
||||
const size_t reset_pos = value.find(reset);
|
||||
const size_t popup_pos = value.find(popup);
|
||||
|
||||
if (reset_pos != std::string::npos && popup_pos != std::string::npos &&
|
||||
reset_pos < popup_pos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << "control_bar.cpp must clear display_selectable_hovered_ before "
|
||||
"checking the display popup\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectResetBeforeShortcutPopup(const std::string& value) {
|
||||
const std::string reset = "props->shortcut_selectable_hovered_ = false;";
|
||||
const std::string popup = "ImGui::BeginPopup(\"shortcut\")";
|
||||
const size_t reset_pos = value.find(reset);
|
||||
const size_t popup_pos = value.find(popup);
|
||||
|
||||
if (reset_pos != std::string::npos && popup_pos != std::string::npos &&
|
||||
reset_pos < popup_pos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << "control_bar.cpp must clear shortcut_selectable_hovered_ before "
|
||||
"checking the shortcut popup\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string control_bar =
|
||||
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"props->display_selectable_hovered_ = false;");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"ImGui::IsWindowHovered("
|
||||
"ImGuiHoveredFlags_RootAndChildWindows)");
|
||||
ok &= ExpectResetBeforeDisplayPopup(control_bar);
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"props->shortcut_selectable_hovered_ =");
|
||||
ok &= ExpectResetBeforeShortcutPopup(control_bar);
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"void ShowControlBarTooltip(const std::string& text)");
|
||||
ok &= ExpectContainsAtLeast("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip(", 10);
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::select_display"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::send_shortcut"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->mouse_control_button_label_)");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->audio_capture_button_label_)");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::select_file"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->net_traffic_stats_button_label_)");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ShowControlBarTooltip("
|
||||
"props->fullscreen_button_label_)");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::release_mouse"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::control_mouse"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::audio_capture"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::mute[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::hide_net_traffic_stats"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::show_net_traffic_stats"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::exit_fullscreen"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::fullscreen"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::disconnect"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::expand_control_bar"
|
||||
"[localization_language_index_]");
|
||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||
"localization::collapse_control_bar"
|
||||
"[localization_language_index_]");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
#include "macos_keyboard_modifier_state.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <iostream>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ExpectEqual(const char* name, uint32_t actual, uint32_t expected) {
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " mismatch\n"
|
||||
<< " expected: " << expected << "\n"
|
||||
<< " actual: " << actual << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
crossdesk::MacKeyboardModifierState state;
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectEqual("initial flags", state.flags(), 0);
|
||||
ok &= ExpectEqual("left shift down", state.Update(0xA0, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("shifted semicolon keeps shift",
|
||||
state.Update(0xBA, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("semicolon up keeps shift", state.Update(0xBA, false),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("right shift down while left held",
|
||||
state.Update(0xA1, true),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("left shift up while right held", state.Update(0xA0, false),
|
||||
crossdesk::kMacInjectedModifierShift);
|
||||
ok &= ExpectEqual("right shift up clears shift", state.Update(0xA1, false),
|
||||
0);
|
||||
|
||||
ok &= ExpectEqual("left control down", state.Update(0xA2, true),
|
||||
crossdesk::kMacInjectedModifierControl);
|
||||
ok &= ExpectEqual("right alt adds option", state.Update(0xA5, true),
|
||||
crossdesk::kMacInjectedModifierControl |
|
||||
crossdesk::kMacInjectedModifierOption);
|
||||
ok &= ExpectEqual("left command adds command", state.Update(0x5B, true),
|
||||
crossdesk::kMacInjectedModifierControl |
|
||||
crossdesk::kMacInjectedModifierOption |
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("left control up leaves option command",
|
||||
state.Update(0xA2, false),
|
||||
crossdesk::kMacInjectedModifierOption |
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("right alt up leaves command", state.Update(0xA5, false),
|
||||
crossdesk::kMacInjectedModifierCommand);
|
||||
ok &= ExpectEqual("left command up clears all", state.Update(0x5B, false),
|
||||
0);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
#include "path_manager.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <limits.h>
|
||||
#else
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path GetExecutableDirectory() {
|
||||
#ifdef _WIN32
|
||||
wchar_t buffer[MAX_PATH] = {};
|
||||
DWORD length = GetModuleFileNameW(nullptr, buffer, MAX_PATH);
|
||||
if (length == 0 || length == MAX_PATH) {
|
||||
return {};
|
||||
}
|
||||
return std::filesystem::path(buffer).parent_path();
|
||||
#elif defined(__APPLE__)
|
||||
char buffer[PATH_MAX] = {};
|
||||
uint32_t size = sizeof(buffer);
|
||||
if (_NSGetExecutablePath(buffer, &size) != 0) {
|
||||
return {};
|
||||
}
|
||||
return std::filesystem::weakly_canonical(buffer).parent_path();
|
||||
#else
|
||||
char buffer[PATH_MAX] = {};
|
||||
ssize_t length = readlink("/proc/self/exe", buffer, sizeof(buffer) - 1);
|
||||
if (length <= 0) {
|
||||
return {};
|
||||
}
|
||||
buffer[length] = '\0';
|
||||
return std::filesystem::path(buffer).parent_path();
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ExpectEqual(const char* name,
|
||||
const std::filesystem::path& actual,
|
||||
const std::filesystem::path& expected) {
|
||||
if (actual.lexically_normal() == expected.lexically_normal()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " mismatch\n"
|
||||
<< " expected: " << expected.string() << "\n"
|
||||
<< " actual: " << actual.string() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path exe_dir = GetExecutableDirectory();
|
||||
if (exe_dir.empty()) {
|
||||
std::cerr << "failed to resolve executable directory\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
crossdesk::PathManager path_manager("CrossDesk");
|
||||
const std::filesystem::path expected_data = exe_dir / "data";
|
||||
const std::filesystem::path expected_logs = exe_dir / "logs";
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectEqual("config path", path_manager.GetConfigPath(), expected_data);
|
||||
ok &= ExpectEqual("cache path", path_manager.GetCachePath(), expected_data);
|
||||
ok &= ExpectEqual("log path", path_manager.GetLogPath(), expected_logs);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
#include "version_checker.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
bool ExpectEqual(const std::string& name, bool actual, bool expected) {
|
||||
if (actual == expected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " mismatch\n"
|
||||
<< " expected: " << expected << "\n"
|
||||
<< " actual: " << actual << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
bool ok = true;
|
||||
|
||||
ok &= ExpectEqual("new patch-before-date is newer",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-20260529", "v1.3.5-1-20260529", "", -1),
|
||||
true);
|
||||
ok &= ExpectEqual("larger patch wins regardless of date",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-2-20260530", "v1.3.5-3-20260529", "", -1),
|
||||
true);
|
||||
ok &= ExpectEqual("smaller patch loses regardless of date",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-3-20260529", "v1.3.5-2-20260530", "", -1),
|
||||
false);
|
||||
ok &= ExpectEqual("old date-before-patch remains supported",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-20260529-1", "v1.3.5-20260529-2", "", -1),
|
||||
true);
|
||||
ok &= ExpectEqual("metadata patch overrides date",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-9-20260530", "v1.3.5", "2026-05-31", 10),
|
||||
true);
|
||||
ok &= ExpectEqual("date alone does not update same version",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-20260529", "v1.3.5-20260530", "", -1),
|
||||
false);
|
||||
ok &= ExpectEqual("numeric version still wins",
|
||||
crossdesk::IsNewerVersionWithMetadata(
|
||||
"v1.3.5-9-20260529", "v1.3.6-1-20260529", "", -1),
|
||||
true);
|
||||
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "scripts/windows/crossdesk.rc")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
bool ExpectActivationContext(const std::filesystem::path& manifest_path) {
|
||||
ACTCTXW context = {};
|
||||
context.cbSize = sizeof(context);
|
||||
std::wstring source = manifest_path.wstring();
|
||||
context.lpSource = source.c_str();
|
||||
|
||||
HANDLE activation_context = CreateActCtxW(&context);
|
||||
if (activation_context == INVALID_HANDLE_VALUE) {
|
||||
std::cerr << "CreateActCtxW failed for " << manifest_path.string()
|
||||
<< ", error=" << GetLastError() << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
ReleaseActCtx(activation_context);
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
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 manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
const std::string debug_manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
const std::string portable_manifest =
|
||||
ReadFile(repo_root / "scripts/windows/crossdesk_portable.manifest");
|
||||
const std::string targets = ReadFile(repo_root / "xmake/targets.lua");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG");
|
||||
ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc,
|
||||
"crossdesk_portable.manifest");
|
||||
ok &= ExpectContains("crossdesk_portable.rc", portable_rc, "RT_MANIFEST");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets,
|
||||
"scripts/windows/crossdesk_portable.rc");
|
||||
ok &= ExpectContains("xmake/targets.lua", targets, "CROSSDESK_PORTABLE");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
|
||||
ok &= ExpectContains("crossdesk.manifest", manifest,
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk.manifest", manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"level=\"asInvoker\"");
|
||||
ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
|
||||
ok &= ExpectContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"level=\"asInvoker\"");
|
||||
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"level=\"requireAdministrator\"");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2005/WindowsSettings");
|
||||
ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"http://schemas.microsoft.com/SMI/2016/WindowsSettings");
|
||||
ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest,
|
||||
"processorArchitecture=\"*\"");
|
||||
#ifdef _WIN32
|
||||
ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_debug.manifest");
|
||||
ok &= ExpectActivationContext(
|
||||
repo_root / "scripts/windows/crossdesk_portable.manifest");
|
||||
#endif
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(
|
||||
current / "src/device_controller/mouse/windows/mouse_controller.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string mouse_controller = ReadFile(
|
||||
repo_root / "src/device_controller/mouse/windows/mouse_controller.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"INPUT ip = {0};");
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"SetCursorPos failed");
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"SendInput failed for mouse");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "interactive_state.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(
|
||||
current / "src/service/windows/service_host.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectTrue(const char* name, bool value) {
|
||||
if (value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " expected true\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string control_bar =
|
||||
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
|
||||
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||
const std::string render_h = ReadFile(repo_root / "src/gui/render.h");
|
||||
const std::string service_host =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
||||
const std::string service_host_h =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.h");
|
||||
const std::string session_helper =
|
||||
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectTrue("secure desktop input routing",
|
||||
crossdesk::IsSecureDesktopInteractionRequired(
|
||||
"secure-desktop"));
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"CanSendSecureAttentionSequence("
|
||||
"props->remote_interactive_stage_)");
|
||||
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||
"ImGui::BeginDisabled();\n"
|
||||
" }\n"
|
||||
" if (ImGui::Selectable(sas_label.c_str()))");
|
||||
ok &= ExpectNotContains("render.cpp", render, "sas_requires_lock_screen");
|
||||
ok &= ExpectContains("render.h", render_h,
|
||||
"optimistic_windows_secure_desktop_until_tick_");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"kWindowsServiceSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"status->sas_secure_desktop_grace_active");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"json.value(\"sas_secure_desktop_grace_active\", false)");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"status.sas_secure_desktop_grace_active");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"local_interactive_stage_ = \"secure-desktop\"");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"sas_secure_desktop_until_tick_");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"sas_secure_desktop_seen_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"kSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"IsSasSecureDesktopGraceActiveLocked()");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"UpdateSasSecureDesktopGraceLocked("
|
||||
"session_helper_report_interactive_stage_)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"sas_secure_desktop_seen_ = true");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"sas_secure_desktop_until_tick_ = 0");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"sas_secure_desktop_until_tick_ =");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"now + kSasSecureDesktopGraceMs");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"\\\"sas_secure_desktop_grace_active\\\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"raw_interactive_stage = ResolveInteractiveStageLocked()");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"kSessionHelperStatePollMs = 1000");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EVENT_SYSTEM_DESKTOPSWITCH");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"SetWinEventHook(");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"MsgWaitForMultipleObjects");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"WaitForSessionHelperStateChange(stop_event, "
|
||||
"desktop_switch_event)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"inaccessible_secure_input_desktop");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"desktop_info.error_code == ERROR_ACCESS_DENIED");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"secure_desktop_active = input_desktop_is_winlogon ||");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
@@ -0,0 +1,225 @@
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
std::filesystem::path FindRepoRoot() {
|
||||
std::filesystem::path current = std::filesystem::current_path();
|
||||
while (!current.empty()) {
|
||||
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||
std::filesystem::exists(current / "src/service/windows/service_host.cpp")) {
|
||||
return current;
|
||||
}
|
||||
current = current.parent_path();
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string ReadFile(const std::filesystem::path& path) {
|
||||
std::ifstream file(path, std::ios::binary);
|
||||
if (!file) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::ostringstream stream;
|
||||
stream << file.rdbuf();
|
||||
return stream.str();
|
||||
}
|
||||
|
||||
bool ExpectContains(const char* name, const std::string& value,
|
||||
const std::string& expected) {
|
||||
if (value.find(expected) != std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ExpectNotContains(const char* name, const std::string& value,
|
||||
const std::string& unexpected) {
|
||||
if (value.find(unexpected) == std::string::npos) {
|
||||
return true;
|
||||
}
|
||||
|
||||
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
const std::filesystem::path repo_root = FindRepoRoot();
|
||||
if (repo_root.empty()) {
|
||||
std::cerr << "failed to locate repository root\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::string service_host =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
||||
const std::string service_host_h =
|
||||
ReadFile(repo_root / "src/service/windows/service_host.h");
|
||||
const std::string session_helper =
|
||||
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||
const std::string targets =
|
||||
ReadFile(repo_root / "xmake/targets.lua");
|
||||
const std::string interactive_state =
|
||||
ReadFile(repo_root / "src/service/windows/interactive_state.h");
|
||||
const std::string render_callback =
|
||||
ReadFile(repo_root / "src/gui/render_callback.cpp");
|
||||
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||
const std::string screen_capturer_h =
|
||||
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.h");
|
||||
const std::string screen_capturer_cpp =
|
||||
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.cpp");
|
||||
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"ParseSecureDesktopMouseIpcCommand");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"BuildSecureInputHelperMouseCommand");
|
||||
ok &= ExpectContains("targets.lua", targets,
|
||||
"target(\"crossdesk_session_helper\")");
|
||||
ok &= ExpectContains("targets.lua", targets,
|
||||
"add_files(\"scripts/windows/crossdesk.rc\")");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnablePerMonitorDpiAwareness");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"SetProcessDpiAwarenessContext(\n"
|
||||
" DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnablePerMonitorDpiAwareness();\n\n"
|
||||
" InitializeHelperLogger();");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"while (GetTickCount64() <= deadline_tick)");
|
||||
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||
"constexpr int kPipeConnectRetryCount = 3");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"BuildSecureInputHelperKeyboardCommand(");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"const std::string& interactive_stage");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"bool LaunchSecureInputHelper(DWORD session_id,\n"
|
||||
" const std::string& interactive_stage)");
|
||||
ok &= ExpectContains("service_host.h", service_host_h,
|
||||
"std::string secure_input_helper_interactive_stage_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"SecureInputHelperDesktopForStage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return L\"winsta0\\\\Winlogon\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return L\"winsta0\\\\default\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_ == interactive_stage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_ = interactive_stage");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"secure_input_helper_interactive_stage_.clear()");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"LaunchSecureInputHelper(target_session_id, interactive_stage)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"\\\"secure_input_helper_stage\\\":\\\"");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"session_helper_report_interactive_stage_");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"return SendSecureDesktopMouseInput");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"constexpr DWORD kWindowsServiceQueryTimeoutMs = 500");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"IsTransientWindowsServiceStatusError(status.error)");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"IsTransientWindowsServiceStatusError(status.error)");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"Local Windows service temporarily unavailable");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"Windows capturer secure desktop service temporarily unavailable");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"Windows capturer secure desktop transient frame query failed");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"if (transient_error) {\n"
|
||||
" LOG_INFO(");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
"IsTransientSecureDesktopInputFailure");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
"Secure desktop keyboard injection transient failure");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"MOUSEEVENTF_VIRTUALDESK");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"std::vector<INPUT> inputs");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"SendInput(static_cast<UINT>(inputs.size())");
|
||||
ok &= ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||
"SetCursorPos(request.x, request.y)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"NormalizeAbsoluteMouseCoordinate");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnsureThreadInteractiveDesktop");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"OpenInputDesktop");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"DesktopNameForInteractiveStage");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"interactive_stage == \"credential-ui\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"return L\"Winlogon\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"interactive_stage == \"lock-screen\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"return L\"Default\"");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"EnsureThreadInteractiveDesktopForStage");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"switch_interactive_desktop_failed");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"Json BuildInputFailureJson");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"json[\"target_desktop\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"json[\"current_desktop\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"json[\"stage\"]");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code,\n"
|
||||
" &extended, &interactive_stage)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"InjectKeyboardInput(key_code, is_down, scan_code, extended,\n"
|
||||
" interactive_stage)");
|
||||
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||
"InjectMouseInput(mouse_request)");
|
||||
ok &= ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||
"EnsureThreadDesktop(L\"Winlogon\", &secure_desktop)");
|
||||
ok &= ExpectContains("service_host.cpp", service_host,
|
||||
"winsta0\\\\default");
|
||||
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||
"startup_info.lpDesktop = const_cast<LPWSTR>(L\"winsta0\\\\Winlogon\")");
|
||||
ok &= ExpectContains("interactive_state.h", interactive_state,
|
||||
"interactive_stage == \"lock-screen\"");
|
||||
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||
"RemoteAction remote_action{};");
|
||||
ok &= ExpectContains("render.cpp", render,
|
||||
"previous_secure_desktop_interaction");
|
||||
ok &= ExpectNotContains(
|
||||
"render_callback.cpp", render_callback,
|
||||
"render->local_service_available_ &&\n"
|
||||
" IsSecureDesktopInteractionRequired(render->local_interactive_stage_)");
|
||||
ok &= ExpectContains("screen_capturer_win.h", screen_capturer_h,
|
||||
"std::string secure_shared_stage_;");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"const std::string& stage");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"secure_shared_stage_ == stage");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"secure_shared_stage_ = stage");
|
||||
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||
"secure_shared_stage_.clear()");
|
||||
return ok ? 0 : 1;
|
||||
}
|
||||
+8
-1
@@ -23,6 +23,12 @@ function setup_options_and_dependencies()
|
||||
set_description("Enable DRM capture on Linux (assumes dependencies are installed)")
|
||||
option_end()
|
||||
|
||||
option("CROSSDESK_PORTABLE")
|
||||
set_default(false)
|
||||
set_showmenu(true)
|
||||
set_description("Build CrossDesk as a portable package that stores data beside the executable")
|
||||
option_end()
|
||||
|
||||
add_rules("mode.release", "mode.debug")
|
||||
set_languages("c++17")
|
||||
set_encodings("utf-8")
|
||||
@@ -35,6 +41,7 @@ function setup_options_and_dependencies()
|
||||
add_defines("USE_CUDA=" .. (is_config("USE_CUDA", true) and "1" or "0"))
|
||||
add_defines("USE_WAYLAND=" .. (is_config("USE_WAYLAND", true) and "1" or "0"))
|
||||
add_defines("USE_DRM=" .. (is_config("USE_DRM", true) and "1" or "0"))
|
||||
add_defines("CROSSDESK_PORTABLE=" .. (is_config("CROSSDESK_PORTABLE", true) and "1" or "0"))
|
||||
|
||||
if is_mode("debug") then
|
||||
add_defines("CROSSDESK_DEBUG")
|
||||
@@ -47,4 +54,4 @@ function setup_options_and_dependencies()
|
||||
add_requires("nlohmann_json 3.11.3")
|
||||
add_requires("cpp-httplib v0.26.0", {configs = {ssl = true}})
|
||||
add_requires("tinyfiledialogs 3.15.1")
|
||||
end
|
||||
end
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ function setup_platform_settings()
|
||||
end
|
||||
|
||||
if is_config("USE_WAYLAND", true) then
|
||||
add_links("dbus-1", "pipewire-0.3")
|
||||
add_links("dbus-1")
|
||||
add_defines("CROSSDESK_HAS_WAYLAND_CAPTURER=1")
|
||||
add_existing_include_dirs({
|
||||
"/usr/include/dbus-1.0",
|
||||
|
||||
+68
-2
@@ -3,6 +3,11 @@ function setup_targets()
|
||||
|
||||
includes("submodules", "thirdparty")
|
||||
|
||||
local crossdesk_windows_resource = "scripts/windows/crossdesk.rc"
|
||||
if is_config("CROSSDESK_PORTABLE", true) then
|
||||
crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc"
|
||||
end
|
||||
|
||||
target("rd_log")
|
||||
set_kind("object")
|
||||
add_packages("spdlog")
|
||||
@@ -25,6 +30,59 @@ function setup_targets()
|
||||
add_files("src/path_manager/*.cpp")
|
||||
add_includedirs("src/path_manager", {public = true})
|
||||
|
||||
target("path_manager_portable_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_defines("CROSSDESK_PORTABLE=1")
|
||||
add_includedirs("src/path_manager")
|
||||
add_files("tests/path_manager_portable_test.cpp",
|
||||
"src/path_manager/path_manager.cpp")
|
||||
|
||||
target("macos_keyboard_modifier_state_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_includedirs("src/device_controller")
|
||||
add_files("tests/macos_keyboard_modifier_state_test.cpp")
|
||||
|
||||
target("windows_manifest_resource_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/windows_manifest_resource_test.cpp")
|
||||
|
||||
target("windows_service_mouse_ipc_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/windows_service_mouse_ipc_test.cpp")
|
||||
|
||||
target("windows_mouse_controller_safety_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/windows_mouse_controller_safety_test.cpp")
|
||||
|
||||
target("windows_sas_guard_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_includedirs("src/service/windows")
|
||||
add_files("tests/windows_sas_guard_test.cpp")
|
||||
|
||||
target("display_popup_hover_state_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_files("tests/display_popup_hover_state_test.cpp")
|
||||
|
||||
target("version_checker_test")
|
||||
set_kind("binary")
|
||||
set_default(false)
|
||||
add_packages("cpp-httplib")
|
||||
add_deps("rd_log")
|
||||
add_includedirs("src/version_checker")
|
||||
add_files("tests/version_checker_test.cpp",
|
||||
"src/version_checker/version_checker.cpp")
|
||||
if is_os("macosx") then
|
||||
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("screen_capturer")
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "common")
|
||||
@@ -124,6 +182,10 @@ function setup_targets()
|
||||
add_deps("rd_log")
|
||||
add_files("src/version_checker/*.cpp")
|
||||
add_includedirs("src/version_checker", {public = true})
|
||||
if is_os("macosx") then
|
||||
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("tools")
|
||||
set_kind("object")
|
||||
@@ -159,6 +221,9 @@ function setup_targets()
|
||||
add_packages("libyuv")
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_defines("CROSSDESK_WGC_PLUGIN_BUILD=1")
|
||||
-- Keep the project on C++17 while C++/WinRT still falls back to
|
||||
-- MSVC's deprecated experimental coroutine header.
|
||||
add_defines("_SILENCE_EXPERIMENTAL_COROUTINE_DEPRECATION_WARNINGS")
|
||||
add_links("windowsapp")
|
||||
add_files("src/screen_capturer/windows/screen_capturer_wgc.cpp",
|
||||
"src/screen_capturer/windows/wgc_session_impl.cpp",
|
||||
@@ -180,6 +245,7 @@ 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})
|
||||
end
|
||||
|
||||
@@ -193,6 +259,6 @@ function setup_targets()
|
||||
add_includedirs("src/service/windows", {public = true})
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
||||
add_files("scripts/windows/crossdesk.rc")
|
||||
add_files(crossdesk_windows_resource)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user