Compare commits

...

47 Commits

Author SHA1 Message Date
dijunkun 0681f6540d [fix] fix persistent scrollbar in recent connections area when empty, refs #85 2026-06-26 17:29:04 +08:00
dijunkun d843901550 [fix] fix intermittent crash on incoming connection by guarding shared connection status state across threads 2026-06-26 17:21:09 +08:00
dijunkun b41630ebb4 [fix] fix blank connection status popup by handling Gathering state 2026-06-26 16:23:15 +08:00
dijunkun a815eecc73 [fix] fix close-issue workflow env indentation, pagination, and activity time base 2026-06-26 16:14:09 +08:00
dijunkun 7e0984fe9c [chore] refine online status indicator with gradient glow and remove status text 2026-06-26 16:09:21 +08:00
dijunkun 9c28cd2ab2 [fix] fix crash when switching monitors due to race condition and missing bounds checks in screen capturer 2026-06-26 14:44:30 +08:00
dijunkun 3c4000bdbb [fix] add missing Xft development dependency for Linux tray build 2026-06-25 00:35:25 +08:00
dijunkun d8e9fa5bba [feat] support minimizing to the system tray on Linux when closing 2026-06-23 17:11:42 +08:00
dijunkun e026491b9f [feat] support minimizing to the system tray on macOS when closing, refs #87 2026-06-23 17:11:42 +08:00
dijunkun 009699b375 [chore] optimize online status indicator icon 2026-06-22 16:52:51 +08:00
dijunkun 3677588a3d [fix] align recent connection name footer 2026-06-18 10:53:31 +08:00
dijunkun 3d280053a7 [feat] add custom names for recent connection devices and improve panel display 2026-06-17 17:48:17 +08:00
dijunkun fbde3f6a47 [fix] prevent remote keyboard keys from getting stuck 2026-06-15 17:28:34 +08:00
dijunkun 1c1a33fdce [fix] use evdev keycodes for Wayland keyboard input 2026-06-12 11:25:00 +08:00
kunkundi fea238722d [fix] fix macOS remote double-click and triple-click handling, refs #86 2026-06-05 00:25:08 +08:00
kunkundi 178d958c08 [fix] show self-hosted TLS certificate errors in status bar 2026-06-01 00:31:45 +08:00
kunkundi f9633f366b [fix] update MiniRTC: support loading macOS self-hosted TLS roots, refs #79 2026-06-01 00:31:27 +08:00
dijunkun 7a81f3e767 Merge branch 'file-transfer' of https://github.com/kunkundi/crossdesk into file-transfer 2026-05-31 23:21:42 +08:00
dijunkun bbbbbf7927 [fix] update MiniRTC: support loading Windows self-signed root certificates from intermediate stores, refs #79 2026-05-31 23:04:13 +08:00
dijunkun 5735f84008 [ci] include version alias in version.json for compatibility 2026-05-29 11:28:09 +08:00
kunkundi fe0cf42e5d [fix] use Linux system CA paths for update checks 2026-05-29 10:51:22 +08:00
kunkundi 04100584ce [fix] restore latest_version update metadata 2026-05-29 02:11:46 +08:00
kunkundi 9d3a422916 [fix] log update check failures and use macOS trust store 2026-05-29 02:00:13 +08:00
kunkundi 65d8284fb8 [ci] standardize hotfix package version ordering across platforms 2026-05-29 00:52:07 +08:00
kunkundi eea107db66 [chore] adjust settings window button vertical spacing 2026-05-28 23:36:25 +08:00
kunkundi 67812957db [ci] fix GitHub Actions Linux build version parsing 2026-05-28 23:30:42 +08:00
dijunkun 69d77e59cc [fix] make portable Windows Service setup optional, refs #84 2026-05-28 18:59:27 +08:00
dijunkun efcebfd82c [feat] add explicit hotfix patch version support 2026-05-28 15:22:35 +08:00
dijunkun 32345f93bf [fix] validate macOS input display state before injecting events 2026-05-28 04:22:14 +08:00
dijunkun 193e4bd5bf [fix] guard macOS speaker capture callbacks against invalid buffers 2026-05-28 04:22:07 +08:00
dijunkun 53edf3d57e [fix] harden macOS screen capture lifecycle against late callbacks 2026-05-28 04:21:59 +08:00
dijunkun 895e297771 [fix] initialize render runtime state to avoid invalid SDL handles 2026-05-28 04:21:52 +08:00
dijunkun 8f3959e6c6 [fix] reset stale macOS permissions during install 2026-05-28 01:34:02 +08:00
dijunkun 5ff6b601c7 [fix] improve macOS permission request flow 2026-05-28 01:33:50 +08:00
kunkundi 4895ac9c23 [fix] bundle Windows Service binaries in Windows portable package 2026-05-27 19:37:06 +08:00
kunkundi f121aa47f7 [feat] add portable Windows Service install prompt with one-click setup 2026-05-27 19:32:58 +08:00
dijunkun 00a8d59284 [chore] update README 2026-05-27 17:22:51 +08:00
dijunkun a30489e05b [feat] update MiniRTC: report relay traversal when either ICE candidate is relayed 2026-05-27 16:37:08 +08:00
dijunkun dfbeb3ed20 [fix] request stream keyframes when capture resumes 2026-05-27 16:05:42 +08:00
dijunkun 2eed1c974e [fix] recover Windows capture backends after secure desktop exit 2026-05-27 16:05:00 +08:00
dijunkun 63a79a90ac [fix] refine Windows credential UI state detection 2026-05-27 16:04:38 +08:00
dijunkun a61e74d97b [feat] add video keyframe request APIs 2026-05-27 15:58:32 +08:00
kunkundi 54438a4aa1 [feat] refine control bar display index label sizing and alignment 2026-05-27 10:24:53 +08:00
dijunkun 7682ad63e4 [feat] add localized tooltips for control bar buttons 2026-05-27 00:37:34 +08:00
dijunkun 06c53fdc9c [fix] handle SAS secure desktop transitions and restore desktop capture promptly, refs #77 2026-05-26 04:38:07 +08:00
dijunkun 665f4e684c [feat] improve Windows secure desktop capture and input handling, refs #77 2026-05-26 03:26:37 +08:00
dijunkun 52b894fe0e [feat] improve secure desktop capture by streaming latest frames through shared memory 2026-05-26 01:28:12 +08:00
70 changed files with 7757 additions and 1142 deletions
+172 -24
View File
@@ -7,6 +7,11 @@ on:
tags: tags:
- "*" - "*"
workflow_dispatch: workflow_dispatch:
inputs:
patch:
description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build."
required: false
default: "0"
permissions: permissions:
contents: write contents: write
@@ -37,21 +42,53 @@ jobs:
steps: steps:
- name: Extract version number - name: Extract version number
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
VERSION_NUM="${VERSION#v}" VERSION_BASE="${VERSION_REF#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV 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 - name: Set legal Debian version
shell: bash shell: bash
run: | run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) BUILD_DATE="${BUILD_DATE_OVERRIDE}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) if [[ -z "${BUILD_DATE}" ]]; then
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
fi
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" VERSION_BASE="0.0.0-${VERSION_BASE}"
fi
if [[ "${PATCH_NUMBER}" != "0" ]]; then
LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}"
else else
LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}"
fi fi
echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV
@@ -67,6 +104,7 @@ jobs:
CUDA_PATH: /usr/local/cuda CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data XMAKE_GLOBALDIR: /data
run: | run: |
apt install -y libxft-dev
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk xmake b -vy --root crossdesk
@@ -102,14 +140,43 @@ jobs:
steps: steps:
- name: Extract version number - name: Extract version number
id: version id: version
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) 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}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}" echo "VERSION_NUM=${VERSION_NUM}"
echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
@@ -163,10 +230,38 @@ jobs:
$version = $ref -replace '^refs/(tags|heads)/', '' $version = $ref -replace '^refs/(tags|heads)/', ''
$version = $version -replace '^v', '' $version = $version -replace '^v', ''
$version = $version -replace '/', '-' $version = $version -replace '/', '-'
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd") $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 "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV
- name: Cache xmake dependencies - name: Cache xmake dependencies
uses: actions/cache@v5 uses: actions/cache@v5
@@ -239,8 +334,7 @@ jobs:
- name: Package - name: Package
shell: pwsh shell: pwsh
run: | run: |
cd "${{ github.workspace }}\scripts\windows" & "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi
- name: Build Portable CrossDesk - name: Build Portable CrossDesk
run: | run: |
@@ -250,10 +344,34 @@ jobs:
- name: Package Portable - name: Package Portable
shell: pwsh shell: pwsh
run: | run: |
$buildDir = "${{ github.workspace }}\build\windows\x64\release"
$portableDir = "${{ github.workspace }}\portable" $portableDir = "${{ github.workspace }}\portable"
New-Item -ItemType Directory -Force -Path $portableDir 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" Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
- name: Upload artifact - name: Upload artifact
@@ -286,19 +404,47 @@ jobs:
- name: Extract version number - name: Extract version number
id: version id: version
shell: bash
run: | run: |
VERSION="${GITHUB_REF##*/}" VERSION_REF="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) VERSION_BASE="${VERSION_REF#v}"
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d) PATCH_NUMBER="${{ github.event.inputs.patch }}"
VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
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_WITH_V="v${VERSION_NUM}"
VERSION_ONLY="${VERSION#v}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT
echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $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=${BUILD_DATE}" >> $GITHUB_OUTPUT
echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Rename artifacts - name: Rename artifacts
run: | run: |
@@ -356,8 +502,10 @@ jobs:
run: | run: |
cat > version.json << EOF 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 }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": "", "releaseName": "",
"releaseNotes": "", "releaseNotes": "",
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}", "tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
+14 -25
View File
@@ -4,6 +4,7 @@ on:
schedule: schedule:
# run every day at midnight # run every day at midnight
- cron: "0 0 * * *" - cron: "0 0 * * *"
workflow_dispatch:
permissions: permissions:
issues: write issues: write
@@ -15,19 +16,21 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check inactive issues and close them - name: Check inactive issues and close them
uses: actions/github-script@v6 uses: actions/github-script@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with: with:
script: | script: |
const { data: issues } = await github.rest.issues.listForRepo({ const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
const now = Date.now();
// paginate through all open issues (listForRepo also returns PRs)
const issues = await github.paginate(github.rest.issues.listForRepo, {
owner: context.repo.owner, owner: context.repo.owner,
repo: context.repo.repo, repo: context.repo.repo,
state: 'open', state: 'open',
per_page: 100,
}); });
const now = new Date().getTime();
const inactivePeriod = 7 * 24 * 60 * 60 * 1000; // 7 days
for (const issue of issues) { for (const issue of issues) {
// skip pull requests (they are also returned by listForRepo) // skip pull requests (they are also returned by listForRepo)
if (issue.pull_request) continue; if (issue.pull_request) continue;
@@ -38,26 +41,14 @@ jobs:
continue; continue;
} }
// fetch comments for this issue // last activity time = the issue's own updated_at, which is
const { data: comments } = await github.rest.issues.listComments({ // refreshed on comments, labels, etc. This avoids relying on
owner: context.repo.owner, // fetching comments and is accurate even when comments are edited.
repo: context.repo.repo, const lastActivityTime = new Date(issue.updated_at).getTime();
issue_number: issue.number,
per_page: 100,
});
// determine the "last activity" time
let lastActivityTime;
if (comments.length > 0) {
const lastComment = comments[comments.length - 1];
lastActivityTime = new Date(lastComment.updated_at).getTime();
} else {
lastActivityTime = new Date(issue.created_at).getTime();
}
// check inactivity // check inactivity
if (now - lastActivityTime > inactivePeriod) { if (now - lastActivityTime > inactivePeriod) {
console.log(`Closing inactive issue: #${issue.number} (No recent replies for 7 days)`); console.log(`Closing inactive issue: #${issue.number} (No activity for 7 days)`);
await github.rest.issues.createComment({ await github.rest.issues.createComment({
owner: context.repo.owner, owner: context.repo.owner,
@@ -76,5 +67,3 @@ jobs:
console.log(`Skipping issue #${issue.number} (Active within 7 days).`); console.log(`Skipping issue #${issue.number} (Active within 7 days).`);
} }
} }
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+27 -7
View File
@@ -20,21 +20,39 @@ jobs:
- name: Extract version from tag - name: Extract version from tag
id: version id: version
shell: bash
run: | run: |
TAG_NAME="${{ github.event.release.tag_name }}" TAG_NAME="${{ github.event.release.tag_name }}"
VERSION_ONLY="${TAG_NAME#v}" TAG_VERSION="${TAG_NAME#v}"
echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT VERSION_FULL="${TAG_VERSION}"
echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT VERSION_BASE="${TAG_VERSION}"
PATCH_NUMBER=0
# Extract date from tag if available (format: v1.2.3-20251113-abc) # Extract date and patch from tags such as v1.2.3-1-20251113.
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then
DATE_STR="${BASH_REMATCH[1]}" 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}" BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}"
else else
# Use release published date # Use release published date
BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1) BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1)
fi 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 "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT
- name: Install jq - name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq run: sudo apt-get update && sudo apt-get install -y jq
@@ -122,8 +140,10 @@ jobs:
# Generate version.json using cat and heredoc # Generate version.json using cat and heredoc
cat > version.json << EOF 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 }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"patch": ${{ steps.version.outputs.PATCH_NUMBER }},
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }}, "releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }}, "releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
"tagName": "${{ steps.version.outputs.TAG_NAME }}", "tagName": "${{ steps.version.outputs.TAG_NAME }}",
+23
View File
@@ -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" /> <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服务不可用”。
## 如何编译 ## 如何编译
依赖: 依赖:
+25
View File
@@ -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" /> <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 ## How to build
Requirements: Requirements:
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
PKG_NAME="crossdesk" PKG_NAME="crossdesk"
APP_NAME="CrossDesk" APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="amd64" ARCHITECTURE="amd64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>" MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client." DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64" ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" 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) # Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}" DEB_VERSION="${APP_VERSION#v}"
+19 -1
View File
@@ -4,13 +4,31 @@ set -e
PKG_NAME="crossdesk" PKG_NAME="crossdesk"
APP_NAME="CrossDesk" APP_NAME="CrossDesk"
APP_VERSION="$1"
ARCHITECTURE="arm64" ARCHITECTURE="arm64"
MAINTAINER="Junkun Di <junkun.di@hotmail.com>" MAINTAINER="Junkun Di <junkun.di@hotmail.com>"
DESCRIPTION="A simple cross-platform remote desktop client." DESCRIPTION="A simple cross-platform remote desktop client."
ALSA_RUNTIME_DEP="libasound2 | libasound2t64" ALSA_RUNTIME_DEP="libasound2 | libasound2t64"
PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" 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) # Remove 'v' prefix from version for Debian package (Debian version must start with digit)
DEB_VERSION="${APP_VERSION#v}" DEB_VERSION="${APP_VERSION#v}"
+85 -51
View File
@@ -4,17 +4,36 @@ set -e
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk" EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos" PLATFORM="macos"
ARCH="arm64" ARCH="arm64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" 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" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources" RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg" PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg" DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
@@ -73,67 +92,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
</plist> </plist>
EOF EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
echo ".app created successfully." 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..." echo "building pkg..."
pkgbuild \ pkgbuild \
--identifier "${IDENTIFIER}" \ --identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \ --version "${APP_VERSION}" \
--install-location "/Applications" \ --install-location "/Applications" \
--component "${APP_BUNDLE}" \ --component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p build_pkg_scripts cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
cat > build_pkg_scripts/postinstall <<'EOF' <installer-gui-script minSpecVersion="1">
#!/bin/bash <title>${INSTALLER_TITLE}</title>
set -e <welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
IDENTIFIER="cn.crossdesk.app" <choices-outline>
<line choice="default">
# 获取当前登录用户 <line choice="${IDENTIFIER}"/>
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console ) </line>
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' ) </choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
# 清除应用的权限授权,以便重新授权 <choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
# 使用 tccutil 重置录屏权限和辅助功能权限 <pkg-ref id="${IDENTIFIER}"/>
if command -v tccutil >/dev/null 2>&1; then </choice>
# 重置录屏权限 <pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true </installer-gui-script>
# 重置辅助功能权限
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
EOF EOF
chmod +x build_pkg_scripts/postinstall
productbuild \ 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}" "${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}" echo "PKG package created: ${PKG_NAME}"
@@ -171,8 +205,8 @@ APPLESCRIPT
fi fi
echo "Set icon finished" 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 "PKG package created successfully."
echo "package ${APP_BUNDLE}" echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}" echo "installer ${PKG_NAME}"
+85 -51
View File
@@ -4,17 +4,36 @@ set -e
APP_NAME="crossdesk" APP_NAME="crossdesk"
APP_NAME_UPPER="CrossDesk" APP_NAME_UPPER="CrossDesk"
EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk" EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk"
APP_VERSION="$1"
PLATFORM="macos" PLATFORM="macos"
ARCH="x64" ARCH="x64"
IDENTIFIER="cn.crossdesk.app" IDENTIFIER="cn.crossdesk.app"
ICON_PATH="icons/macos/crossdesk.icns" ICON_PATH="icons/macos/crossdesk.icns"
MACOS_MIN_VERSION="10.12" 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" APP_BUNDLE="${APP_NAME_UPPER}.app"
CONTENTS_DIR="${APP_BUNDLE}/Contents" CONTENTS_DIR="${APP_BUNDLE}/Contents"
MACOS_DIR="${CONTENTS_DIR}/MacOS" MACOS_DIR="${CONTENTS_DIR}/MacOS"
RESOURCES_DIR="${CONTENTS_DIR}/Resources" RESOURCES_DIR="${CONTENTS_DIR}/Resources"
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg" PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg" DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
@@ -73,67 +92,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
</plist> </plist>
EOF EOF
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
find "${APP_BUNDLE}" -name '._*' -delete
echo ".app created successfully." 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..." echo "building pkg..."
pkgbuild \ pkgbuild \
--identifier "${IDENTIFIER}" \ --identifier "${IDENTIFIER}" \
--version "${APP_VERSION}" \ --version "${APP_VERSION}" \
--install-location "/Applications" \ --install-location "/Applications" \
--component "${APP_BUNDLE}" \ --component "${APP_BUNDLE}" \
--scripts build_pkg_scripts \
build_pkg_temp/${APP_NAME}-component.pkg build_pkg_temp/${APP_NAME}-component.pkg
mkdir -p build_pkg_scripts cat > build_pkg_temp/Distribution <<EOF
<?xml version="1.0" encoding="utf-8"?>
cat > build_pkg_scripts/postinstall <<'EOF' <installer-gui-script minSpecVersion="1">
#!/bin/bash <title>${INSTALLER_TITLE}</title>
set -e <welcome file="welcome.html" mime-type="text/html"/>
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
IDENTIFIER="cn.crossdesk.app" <choices-outline>
<line choice="default">
# 获取当前登录用户 <line choice="${IDENTIFIER}"/>
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console ) </line>
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' ) </choices-outline>
<choice id="default" title="${INSTALLER_TITLE}"/>
# 清除应用的权限授权,以便重新授权 <choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
# 使用 tccutil 重置录屏权限和辅助功能权限 <pkg-ref id="${IDENTIFIER}"/>
if command -v tccutil >/dev/null 2>&1; then </choice>
# 重置录屏权限 <pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true </installer-gui-script>
# 重置辅助功能权限
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
EOF EOF
chmod +x build_pkg_scripts/postinstall
productbuild \ 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}" "${PKG_NAME}"
echo "PKG package created: ${PKG_NAME}" echo "PKG package created: ${PKG_NAME}"
@@ -171,8 +205,8 @@ APPLESCRIPT
fi fi
echo "Set icon finished" 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 "PKG package created successfully."
echo "package ${APP_BUNDLE}" echo "package ${APP_BUNDLE}"
echo "installer ${PKG_NAME}" echo "installer ${PKG_NAME}"
+112
View File
@@ -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
@@ -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>
+8
View File
@@ -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"
+40
View File
@@ -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
}
+5 -1
View File
@@ -128,7 +128,11 @@ bool Daemon::start(MainLoopFunc loop) {
if (pid > 0) _exit(0); if (pid > 0) _exit(0);
umask(0); umask(0);
chdir("/"); if (chdir("/") != 0) {
std::cerr << "Failed to change daemon working directory to /: "
<< std::strerror(errno) << std::endl;
return false;
}
// redirect file descriptors: keep stdout/stderr if from terminal, else // redirect file descriptors: keep stdout/stderr if from terminal, else
// redirect to /dev/null // redirect to /dev/null
+21
View File
@@ -79,6 +79,9 @@ int ConfigCenter::Load() {
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_); enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue( enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_); 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 = const char* file_transfer_save_path_value =
ini_.GetValue(section_, "file_transfer_save_path", nullptr); 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_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray", ini_.SetBoolValue(section_, "enable_minimize_to_tray",
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", ini_.SetValue(section_, "file_transfer_save_path",
file_transfer_save_path_.c_str()); file_transfer_save_path_.c_str());
@@ -325,6 +330,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
return 0; 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 // getters
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; } 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::IsEnableDaemon() const { return enable_daemon_; }
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
return portable_service_prompt_suppressed_;
}
int ConfigCenter::SetFileTransferSavePath(const std::string& path) { int ConfigCenter::SetFileTransferSavePath(const std::string& path) {
file_transfer_save_path_ = path; file_transfer_save_path_ = path;
ini_.SetValue(section_, "file_transfer_save_path", ini_.SetValue(section_, "file_transfer_save_path",
+3
View File
@@ -39,6 +39,7 @@ class ConfigCenter {
int SetMinimizeToTray(bool enable_minimize_to_tray); int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart); int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon); int SetDaemon(bool enable_daemon);
int SetPortableServicePromptSuppressed(bool suppressed);
int SetFileTransferSavePath(const std::string& path); int SetFileTransferSavePath(const std::string& path);
// read config // read config
@@ -60,6 +61,7 @@ class ConfigCenter {
bool IsMinimizeToTray() const; bool IsMinimizeToTray() const;
bool IsEnableAutostart() const; bool IsEnableAutostart() const;
bool IsEnableDaemon() const; bool IsEnableDaemon() const;
bool IsPortableServicePromptSuppressed() const;
std::string GetFileTransferSavePath() const; std::string GetFileTransferSavePath() const;
int Load(); int Load();
@@ -87,6 +89,7 @@ class ConfigCenter {
bool enable_minimize_to_tray_ = false; bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false; bool enable_autostart_ = false;
bool enable_daemon_ = false; bool enable_daemon_ = false;
bool portable_service_prompt_suppressed_ = false;
std::string file_transfer_save_path_ = ""; std::string file_transfer_save_path_ = "";
}; };
} // namespace crossdesk } // namespace crossdesk
+63 -6
View File
@@ -21,12 +21,13 @@ namespace crossdesk {
typedef enum { typedef enum {
mouse = 0, mouse = 0,
keyboard, keyboard = 1,
audio_capture, audio_capture = 2,
host_infomation, host_infomation = 3,
display_id, display_id = 4,
service_status, service_status = 5,
service_command, service_command = 6,
keyboard_state = 7,
} ControlType; } ControlType;
typedef enum { typedef enum {
move = 0, move = 0,
@@ -55,6 +56,20 @@ typedef struct {
KeyFlag flag; KeyFlag flag;
} Key; } Key;
inline constexpr size_t kMaxKeyboardStateKeys = 32;
typedef struct {
size_t key_value;
uint32_t scan_code;
bool extended;
} KeyboardStateKey;
typedef struct {
uint32_t seq;
size_t pressed_count;
KeyboardStateKey pressed_keys[kMaxKeyboardStateKeys];
} KeyboardState;
typedef struct { typedef struct {
char host_name[64]; char host_name[64];
size_t host_name_size; size_t host_name_size;
@@ -80,6 +95,7 @@ struct RemoteAction {
union { union {
Mouse m; Mouse m;
Key k; Key k;
KeyboardState ks;
HostInfo i; HostInfo i;
bool a; bool a;
int d; int d;
@@ -111,6 +127,20 @@ struct RemoteAction {
{"extended", a.k.extended}, {"extended", a.k.extended},
{"flag", a.k.flag}}; {"flag", a.k.flag}};
break; break;
case ControlType::keyboard_state: {
json keys = json::array();
const size_t pressed_count =
a.ks.pressed_count < kMaxKeyboardStateKeys
? a.ks.pressed_count
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < pressed_count; ++idx) {
keys.push_back({{"key_value", a.ks.pressed_keys[idx].key_value},
{"scan_code", a.ks.pressed_keys[idx].scan_code},
{"extended", a.ks.pressed_keys[idx].extended}});
}
j["keyboard_state"] = {{"seq", a.ks.seq}, {"pressed_keys", keys}};
break;
}
case ControlType::audio_capture: case ControlType::audio_capture:
j["audio_capture"] = a.a; j["audio_capture"] = a.a;
break; break;
@@ -162,6 +192,33 @@ struct RemoteAction {
out.k.extended = j.at("keyboard").value("extended", false); out.k.extended = j.at("keyboard").value("extended", false);
out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>(); out.k.flag = (KeyFlag)j.at("keyboard").at("flag").get<int>();
break; break;
case ControlType::keyboard_state: {
const auto& keyboard_state_json = j.at("keyboard_state");
out.ks.seq = keyboard_state_json.value("seq", 0u);
out.ks.pressed_count = 0;
const auto keys_json =
keyboard_state_json.value("pressed_keys", json::array());
if (!keys_json.is_array()) {
break;
}
const size_t count =
keys_json.size() < kMaxKeyboardStateKeys
? keys_json.size()
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < count; ++idx) {
const auto& key_json = keys_json[idx];
out.ks.pressed_keys[idx].key_value =
key_json.at("key_value").get<size_t>();
out.ks.pressed_keys[idx].scan_code =
key_json.value("scan_code", static_cast<uint32_t>(0));
out.ks.pressed_keys[idx].extended =
key_json.value("extended", false);
}
out.ks.pressed_count = count;
break;
}
case ControlType::audio_capture: case ControlType::audio_capture:
out.a = j.at("audio_capture").get<bool>(); out.a = j.at("audio_capture").get<bool>();
break; break;
@@ -8,6 +8,7 @@
#include <dbus/dbus.h> #include <dbus/dbus.h>
#endif #endif
#include "linux_evdev_keycode.h"
#include "rd_log.h" #include "rd_log.h"
#include "wayland_portal_shared.h" #include "wayland_portal_shared.h"
@@ -579,33 +580,46 @@ int KeyboardCapturer::SendWaylandKeyboardCommand(int key_code, bool is_down,
uint32_t scan_code, uint32_t scan_code,
bool extended) { bool extended) {
#if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER #if defined(CROSSDESK_HAS_WAYLAND_CAPTURER) && CROSSDESK_HAS_WAYLAND_CAPTURER
(void)scan_code;
(void)extended;
if (!dbus_connection_ || wayland_session_handle_.empty()) { if (!dbus_connection_ || wayland_session_handle_.empty()) {
return -1; return -1;
} }
const auto key_it = vkCodeToX11KeySym.find(key_code); const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased;
if (key_it == vkCodeToX11KeySym.end()) {
const int evdev_keycode =
ResolveLinuxEvdevKeycodeFromWindowsKey(key_code, scan_code, extended);
if (evdev_keycode >= 0 &&
NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) {
return 0; return 0;
} }
const uint32_t key_state = is_down ? kKeyboardPressed : kKeyboardReleased; const auto key_it = vkCodeToX11KeySym.find(key_code);
const int keysym = key_it->second; if (key_it == vkCodeToX11KeySym.end()) {
if (evdev_keycode >= 0) {
LOG_ERROR(
"Failed to send Wayland keyboard keycode event, vk_code={}, "
"evdev_keycode={}, is_down={}",
key_code, evdev_keycode, is_down);
return -3;
}
return 0;
}
// Prefer keycode injection to preserve physical-key semantics and avoid // Prefer keycode injection to preserve physical-key semantics and avoid
// implicit Shift interpretation for uppercase keysyms. // implicit Shift interpretation for uppercase keysyms.
if (display_) { if (display_) {
const int keysym = key_it->second;
const KeyCode x11_keycode = const KeyCode x11_keycode =
XKeysymToKeycode(display_, static_cast<KeySym>(keysym)); XKeysymToKeycode(display_, static_cast<KeySym>(keysym));
if (x11_keycode > 8) { if (x11_keycode > 8) {
const int evdev_keycode = static_cast<int>(x11_keycode) - 8; const int x11_evdev_keycode = static_cast<int>(x11_keycode) - 8;
if (NotifyWaylandKeyboardKeycode(evdev_keycode, key_state)) { if (NotifyWaylandKeyboardKeycode(x11_evdev_keycode, key_state)) {
return 0; return 0;
} }
} }
} }
const int keysym = key_it->second;
const int fallback_keysym = NormalizeFallbackKeysym(keysym); const int fallback_keysym = NormalizeFallbackKeysym(keysym);
if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) { if (NotifyWaylandKeyboardKeysym(fallback_keysym, key_state)) {
return 0; return 0;
@@ -310,6 +310,10 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
if (IsFunctionKey(cg_key_code) && !is_down) { if (IsFunctionKey(cg_key_code) && !is_down) {
CGEventRef fn_release_event = CGEventRef fn_release_event =
CGEventCreateKeyboardEvent(NULL, fn_key_code_, false); CGEventCreateKeyboardEvent(NULL, fn_key_code_, false);
if (!fn_release_event) {
LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release");
return -1;
}
CGEventPost(kCGHIDEventTap, fn_release_event); CGEventPost(kCGHIDEventTap, fn_release_event);
CFRelease(fn_release_event); CFRelease(fn_release_event);
} }
+295
View File
@@ -0,0 +1,295 @@
#ifndef _LINUX_EVDEV_KEYCODE_H_
#define _LINUX_EVDEV_KEYCODE_H_
#include <linux/input-event-codes.h>
#include <cstdint>
namespace crossdesk {
inline int LinuxEvdevKeycodeFromWindowsScanCode(uint32_t scan_code,
bool extended) {
const uint32_t base_scan_code = scan_code & 0xFFu;
if (base_scan_code == 0) {
return -1;
}
if (extended) {
switch (base_scan_code) {
case 0x1C:
return KEY_KPENTER;
case 0x1D:
return KEY_RIGHTCTRL;
case 0x35:
return KEY_KPSLASH;
case 0x38:
return KEY_RIGHTALT;
case 0x47:
return KEY_HOME;
case 0x48:
return KEY_UP;
case 0x49:
return KEY_PAGEUP;
case 0x4B:
return KEY_LEFT;
case 0x4D:
return KEY_RIGHT;
case 0x4F:
return KEY_END;
case 0x50:
return KEY_DOWN;
case 0x51:
return KEY_PAGEDOWN;
case 0x52:
return KEY_INSERT;
case 0x53:
return KEY_DELETE;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
default:
return -1;
}
}
// For the common PC set-1 keys, Linux evdev key codes intentionally line up
// with the low byte of the Windows scan code.
if ((base_scan_code >= 0x01 && base_scan_code <= 0x53) ||
base_scan_code == 0x56 || base_scan_code == 0x57 ||
base_scan_code == 0x58) {
return static_cast<int>(base_scan_code);
}
return -1;
}
inline int LinuxEvdevKeycodeFromWindowsVk(int key_code) {
switch (key_code) {
case 0x08:
return KEY_BACKSPACE;
case 0x09:
return KEY_TAB;
case 0x0D:
return KEY_ENTER;
case 0x10:
case 0xA0:
return KEY_LEFTSHIFT;
case 0x11:
case 0xA2:
return KEY_LEFTCTRL;
case 0x12:
case 0xA4:
return KEY_LEFTALT;
case 0x13:
return KEY_PAUSE;
case 0x14:
return KEY_CAPSLOCK;
case 0x1B:
return KEY_ESC;
case 0x20:
return KEY_SPACE;
case 0x21:
return KEY_PAGEUP;
case 0x22:
return KEY_PAGEDOWN;
case 0x23:
return KEY_END;
case 0x24:
return KEY_HOME;
case 0x25:
return KEY_LEFT;
case 0x26:
return KEY_UP;
case 0x27:
return KEY_RIGHT;
case 0x28:
return KEY_DOWN;
case 0x2C:
return KEY_SYSRQ;
case 0x2D:
return KEY_INSERT;
case 0x2E:
return KEY_DELETE;
case 0x30:
return KEY_0;
case 0x31:
return KEY_1;
case 0x32:
return KEY_2;
case 0x33:
return KEY_3;
case 0x34:
return KEY_4;
case 0x35:
return KEY_5;
case 0x36:
return KEY_6;
case 0x37:
return KEY_7;
case 0x38:
return KEY_8;
case 0x39:
return KEY_9;
case 0x41:
return KEY_A;
case 0x42:
return KEY_B;
case 0x43:
return KEY_C;
case 0x44:
return KEY_D;
case 0x45:
return KEY_E;
case 0x46:
return KEY_F;
case 0x47:
return KEY_G;
case 0x48:
return KEY_H;
case 0x49:
return KEY_I;
case 0x4A:
return KEY_J;
case 0x4B:
return KEY_K;
case 0x4C:
return KEY_L;
case 0x4D:
return KEY_M;
case 0x4E:
return KEY_N;
case 0x4F:
return KEY_O;
case 0x50:
return KEY_P;
case 0x51:
return KEY_Q;
case 0x52:
return KEY_R;
case 0x53:
return KEY_S;
case 0x54:
return KEY_T;
case 0x55:
return KEY_U;
case 0x56:
return KEY_V;
case 0x57:
return KEY_W;
case 0x58:
return KEY_X;
case 0x59:
return KEY_Y;
case 0x5A:
return KEY_Z;
case 0x5B:
return KEY_LEFTMETA;
case 0x5C:
return KEY_RIGHTMETA;
case 0x60:
return KEY_KP0;
case 0x61:
return KEY_KP1;
case 0x62:
return KEY_KP2;
case 0x63:
return KEY_KP3;
case 0x64:
return KEY_KP4;
case 0x65:
return KEY_KP5;
case 0x66:
return KEY_KP6;
case 0x67:
return KEY_KP7;
case 0x68:
return KEY_KP8;
case 0x69:
return KEY_KP9;
case 0x6A:
return KEY_KPASTERISK;
case 0x6B:
return KEY_KPPLUS;
case 0x6D:
return KEY_KPMINUS;
case 0x6E:
return KEY_KPDOT;
case 0x6F:
return KEY_KPSLASH;
case 0x70:
return KEY_F1;
case 0x71:
return KEY_F2;
case 0x72:
return KEY_F3;
case 0x73:
return KEY_F4;
case 0x74:
return KEY_F5;
case 0x75:
return KEY_F6;
case 0x76:
return KEY_F7;
case 0x77:
return KEY_F8;
case 0x78:
return KEY_F9;
case 0x79:
return KEY_F10;
case 0x7A:
return KEY_F11;
case 0x7B:
return KEY_F12;
case 0x90:
return KEY_NUMLOCK;
case 0x91:
return KEY_SCROLLLOCK;
case 0xA1:
return KEY_RIGHTSHIFT;
case 0xA3:
return KEY_RIGHTCTRL;
case 0xA5:
return KEY_RIGHTALT;
case 0xBA:
return KEY_SEMICOLON;
case 0xBB:
return KEY_EQUAL;
case 0xBC:
return KEY_COMMA;
case 0xBD:
return KEY_MINUS;
case 0xBE:
return KEY_DOT;
case 0xBF:
return KEY_SLASH;
case 0xC0:
return KEY_GRAVE;
case 0xDB:
return KEY_LEFTBRACE;
case 0xDC:
return KEY_BACKSLASH;
case 0xDD:
return KEY_RIGHTBRACE;
case 0xDE:
return KEY_APOSTROPHE;
default:
return -1;
}
}
inline int ResolveLinuxEvdevKeycodeFromWindowsKey(int key_code,
uint32_t scan_code,
bool extended) {
const int scan_keycode =
LinuxEvdevKeycodeFromWindowsScanCode(scan_code, extended);
if (scan_keycode >= 0) {
return scan_keycode;
}
return LinuxEvdevKeycodeFromWindowsVk(key_code);
}
} // namespace crossdesk
#endif
@@ -2,9 +2,35 @@
#include <ApplicationServices/ApplicationServices.h> #include <ApplicationServices/ApplicationServices.h>
#include <algorithm>
#include <chrono>
#include <cstdlib>
#include "rd_log.h" #include "rd_log.h"
namespace crossdesk { namespace crossdesk {
namespace {
constexpr auto kDoubleClickInterval = std::chrono::milliseconds(500);
constexpr int kDoubleClickMaxDistance = 8;
constexpr int kMaxClickState = 3;
bool IsWithinClickDistance(int x1, int y1, int x2, int y2) {
return std::abs(x1 - x2) <= kDoubleClickMaxDistance &&
std::abs(y1 - y2) <= kDoubleClickMaxDistance;
}
void SetClickState(CGEventRef event, int click_state) {
if (!event) {
return;
}
CGEventSetIntegerValueField(
event, kCGMouseEventClickState,
std::max(1, std::min(click_state, kMaxClickState)));
}
} // namespace
MouseController::MouseController() {} MouseController::MouseController() {}
@@ -18,87 +44,144 @@ int MouseController::Init(std::vector<DisplayInfo> display_info_list) {
int MouseController::Destroy() { return 0; } int MouseController::Destroy() { return 0; }
int MouseController::BeginClick(ClickTracker& tracker, int x, int y) {
const auto now = std::chrono::steady_clock::now();
const bool continues_previous_click =
tracker.has_last_down &&
now - tracker.last_down_time <= kDoubleClickInterval &&
IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y);
tracker.click_state = continues_previous_click
? std::min(tracker.click_state + 1, kMaxClickState)
: 1;
tracker.active_click_state = tracker.click_state;
tracker.has_last_down = true;
tracker.last_down_time = now;
tracker.last_down_x = x;
tracker.last_down_y = y;
return tracker.active_click_state;
}
int MouseController::EndClick(ClickTracker& tracker, int x, int y) {
const int click_state = tracker.active_click_state;
if (!IsWithinClickDistance(tracker.last_down_x, tracker.last_down_y, x, y)) {
tracker.has_last_down = false;
tracker.click_state = 0;
tracker.active_click_state = 1;
}
return click_state;
}
int MouseController::SendMouseCommand(RemoteAction remote_action, int MouseController::SendMouseCommand(RemoteAction remote_action,
int display_index) { int display_index) {
int mouse_pos_x = if (remote_action.type != ControlType::mouse) {
remote_action.m.x * display_info_list_[display_index].width + return 0;
display_info_list_[display_index].left; }
int mouse_pos_y =
remote_action.m.y * display_info_list_[display_index].height +
display_info_list_[display_index].top;
if (remote_action.type == ControlType::mouse) { if (display_index < 0 ||
CGEventRef mouse_event = nullptr; display_index >= static_cast<int>(display_info_list_.size())) {
CGEventType mouse_type; LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
CGMouseButton mouse_button; display_index, display_info_list_.size());
CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y); return -1;
}
switch (remote_action.m.flag) { const DisplayInfo& display_info = display_info_list_[display_index];
case MouseFlag::left_down: if (display_info.width <= 0 || display_info.height <= 0) {
mouse_type = kCGEventLeftMouseDown; LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}",
left_dragging_ = true; display_info.width, display_info.height);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, return -1;
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, const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f);
mouse_button); const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f);
break; int mouse_pos_x = normalized_x * display_info.width + display_info.left;
} int mouse_pos_y = normalized_y * display_info.height + display_info.top;
if (mouse_event) { CGEventRef mouse_event = nullptr;
CGEventPost(kCGHIDEventTap, mouse_event); CGEventType mouse_type;
CFRelease(mouse_event); CGMouseButton mouse_button;
} CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y);
int click_state = 1;
switch (remote_action.m.flag) {
case MouseFlag::left_down:
mouse_type = kCGEventLeftMouseDown;
left_dragging_ = true;
click_state = BeginClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::left_up:
mouse_type = kCGEventLeftMouseUp;
left_dragging_ = false;
click_state = EndClick(left_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonLeft);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::right_down:
mouse_type = kCGEventRightMouseDown;
right_dragging_ = true;
click_state = BeginClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::right_up:
mouse_type = kCGEventRightMouseUp;
right_dragging_ = false;
click_state = EndClick(right_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonRight);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::middle_down:
mouse_type = kCGEventOtherMouseDown;
click_state = BeginClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
SetClickState(mouse_event, click_state);
break;
case MouseFlag::middle_up:
mouse_type = kCGEventOtherMouseUp;
click_state = EndClick(middle_click_tracker_, mouse_pos_x, mouse_pos_y);
mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point,
kCGMouseButtonCenter);
SetClickState(mouse_event, click_state);
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;
}
if (mouse_event) {
CGEventPost(kCGHIDEventTap, mouse_event);
CFRelease(mouse_event);
} }
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
@@ -7,6 +7,7 @@
#ifndef _MOUSE_CONTROLLER_H_ #ifndef _MOUSE_CONTROLLER_H_
#define _MOUSE_CONTROLLER_H_ #define _MOUSE_CONTROLLER_H_
#include <chrono>
#include <vector> #include <vector>
#include "device_controller.h" #include "device_controller.h"
@@ -24,9 +25,24 @@ class MouseController : public DeviceController {
virtual int SendMouseCommand(RemoteAction remote_action, int display_index); virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
private: private:
struct ClickTracker {
bool has_last_down = false;
std::chrono::steady_clock::time_point last_down_time{};
int last_down_x = 0;
int last_down_y = 0;
int click_state = 0;
int active_click_state = 1;
};
int BeginClick(ClickTracker& tracker, int x, int y);
int EndClick(ClickTracker& tracker, int x, int y);
std::vector<DisplayInfo> display_info_list_; std::vector<DisplayInfo> display_info_list_;
bool left_dragging_ = false; bool left_dragging_ = false;
bool right_dragging_ = false; bool right_dragging_ = false;
ClickTracker left_click_tracker_;
ClickTracker right_click_tracker_;
ClickTracker middle_click_tracker_;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif
+86 -24
View File
@@ -33,10 +33,15 @@ struct TranslationRow {
X(recent_connections, u8"近期连接", "Recent Connections", \ X(recent_connections, u8"近期连接", "Recent Connections", \
u8"Недавние подключения") \ u8"Недавние подключения") \
X(disconnect, u8"断开连接", "Disconnect", 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(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"Показать статистику трафика") \ u8"Показать статистику трафика") \
X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \ X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
u8"Скрыть статистику трафика") \ u8"Скрыть статистику трафика") \
X(video, u8"视频", "Video", u8"Видео") \ X(video, u8"视频", "Video", u8"Видео") \
X(audio, u8"音频", "Audio", u8"Аудио") \ X(audio, u8"音频", "Audio", u8"Аудио") \
@@ -47,28 +52,70 @@ struct TranslationRow {
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \ X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \ X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
u8"Выйти из полноэкранного режима") \ u8"Выйти из полноэкранного режима") \
X(control_mouse, u8"控制", "Control", u8"Управление") \ X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
X(release_mouse, u8"释放", "Release", u8"Освободить") \ X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
X(audio_capture, u8"声音", "Audio", u8"Звук") \ X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
X(mute, u8" 静音", " Mute", u8"Без звука") \ X(mute, u8" 静音", " Mute", u8"Без звука") \
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \ X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \ X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \ X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
X(remote_password_box_visible, u8"远端密码框已出现", \ X(remote_password_box_visible, u8"远端密码框已出现", \
"Remote password box visible", u8"Окно ввода пароля видно") \ "Remote password box visible", u8"Окно ввода пароля видно") \
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \ X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
"Remote lock screen visible, send SAS", \ "Remote lock screen visible, send SAS", \
u8"Видна блокировка, отправьте SAS") \ u8"Видна блокировка, отправьте SAS") \
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \ X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
"Remote secure desktop active", \ "Remote secure desktop active", u8"Активен защищенный рабочий стол") \
u8"Активен защищенный рабочий стол") \ X(remote_service_unavailable, u8"远端Windows服务不可用", \
X(remote_service_unavailable, u8"远端Windows服务不可用", \ "Remote Windows service unavailable", \
"Remote Windows service unavailable", \ u8"Служба Windows на удаленной стороне недоступна") \
u8"Служба Windows на удаленной стороне недоступна") \ X(windows_service_setup_title, u8"安装 CrossDesk Service", \
X(remote_unlock_requires_secure_desktop, \ "Install CrossDesk Service", u8"Установить CrossDesk Service") \
u8"当前仍需要安全桌面专用采集/输入", \ X(windows_service_setup_message, \
"Secure desktop capture/input is still required", \ u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего стола") \ 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(settings, u8"设置", "Settings", u8"Настройки") \
X(language, u8"语言:", "Language:", u8"Язык:") \ X(language, u8"语言:", "Language:", u8"Язык:") \
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \ X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
@@ -111,11 +158,17 @@ struct TranslationRow {
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \ X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
X(signal_disconnected, u8"未连接服务器", "Disconnected", \ X(signal_disconnected, u8"未连接服务器", "Disconnected", \
u8"Нет подключения к серверу") \ u8"Нет подключения к серверу") \
X(signal_tls_cert_error, u8"证书验证失败,请重新安装自托管根证书", \
"Certificate verification failed. Reinstall the self-hosted root " \
"certificate.", \
u8"Ошибка проверки сертификата. Переустановите корневой сертификат.") \
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \ X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \ X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
u8"P2P отключено") \ u8"P2P отключено") \
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \ X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
u8"Подключение P2P...") \ u8"Подключение P2P...") \
X(p2p_gathering, u8"正在收集候选地址...", "Gathering candidates ...", \
u8"Сбор кандидатов...") \
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \ X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
u8"Получение изображения...") \ u8"Получение изображения...") \
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \ X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
@@ -130,13 +183,22 @@ struct TranslationRow {
X(access_website, u8"访问官网: ", \ X(access_website, u8"访问官网: ", \
"Access Website: ", u8"Официальный сайт: ") \ "Access Website: ", u8"Официальный сайт: ") \
X(update, u8"更新", "Update", u8"Обновить") \ X(update, u8"更新", "Update", u8"Обновить") \
X(connection_alias, u8"修改名称", "Edit Alias", \
u8"Изменить имя подключения") \
X(delete_connection, u8"删除连接", "Delete Connection", \
u8"Удалить подключение") \
X(connect_to_this_connection, u8"发起连接", "Connect to this connection", \
u8"Подключиться") \
X(input_connection_alias, u8"请输入连接名称:", \
"Please input connection name:", u8"Введите имя подключения:") \
X(confirm_delete_connection, u8"确认删除此连接", \ X(confirm_delete_connection, u8"确认删除此连接", \
"Confirm to delete this connection", u8"Удалить это подключение?") \ "Confirm to delete this connection", u8"Удалить это подключение?") \
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \ X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \ X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \ X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
u8"Вступит в силу после перезапуска") \ 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", \ X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
u8"Прогресс передачи файлов") \ u8"Прогресс передачи файлов") \
X(queued, u8"队列中", "Queued", u8"В очереди") \ X(queued, u8"队列中", "Queued", u8"В очереди") \
+389 -110
View File
@@ -1,9 +1,37 @@
#include <algorithm>
#include <cctype>
#include "layout_relative.h" #include "layout_relative.h"
#include "localization.h" #include "localization.h"
#include "rd_log.h" #include "rd_log.h"
#include "render.h" #include "render.h"
namespace crossdesk { namespace crossdesk {
namespace {
std::string TrimConnectionAlias(const char* value) {
std::string alias = value ? value : "";
auto not_space = [](unsigned char ch) { return !std::isspace(ch); };
alias.erase(alias.begin(),
std::find_if(alias.begin(), alias.end(), not_space));
alias.erase(std::find_if(alias.rbegin(), alias.rend(), not_space).base(),
alias.end());
return alias;
}
void SetDarkTextTooltip(const char* text) {
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.05f, 0.05f, 0.05f, 1.0f));
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", text);
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
ImGui::PopStyleColor();
}
} // namespace
int Render::RecentConnectionsWindow() { int Render::RecentConnectionsWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
@@ -51,8 +79,9 @@ int Render::ShowRecentConnections() {
float recent_connection_button_width = recent_connection_image_width * 0.15f; float recent_connection_button_width = recent_connection_image_width * 0.15f;
float recent_connection_button_height = float recent_connection_button_height =
recent_connection_image_height * 0.25f; recent_connection_image_height * 0.25f;
float recent_connection_dummy_button_width = float recent_connection_footer_height =
recent_connection_image_width - 2 * recent_connection_button_width; recent_connection_button_height * 1.18f;
float recent_connection_name_width = recent_connection_image_width;
ImGui::SetCursorPos( ImGui::SetCursorPos(
ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f)); ImVec2(io.DisplaySize.x * 0.045f, io.DisplaySize.y * 0.1f));
@@ -61,14 +90,16 @@ int Render::ShowRecentConnections() {
ImGui::PushStyleColor(ImGuiCol_ChildBg, ImGui::PushStyleColor(ImGuiCol_ChildBg,
ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f)); ImVec4(239.0f / 255, 240.0f / 255, 242.0f / 255, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f); ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f);
const ImGuiWindowFlags container_flags =
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus | ImGuiWindowFlags_NoScrollWithMouse |
(recent_connections_.empty()
? ImGuiWindowFlags_None
: ImGuiWindowFlags_AlwaysHorizontalScrollbar);
ImGui::BeginChild( ImGui::BeginChild(
"RecentConnectionsContainer", "RecentConnectionsContainer",
ImVec2(recent_connection_panel_width, recent_connection_panel_height), ImVec2(recent_connection_panel_width, recent_connection_panel_height),
ImGuiChildFlags_Borders, ImGuiChildFlags_Borders, container_flags);
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoBringToFrontOnFocus |
ImGuiWindowFlags_AlwaysHorizontalScrollbar |
ImGuiWindowFlags_NoScrollWithMouse);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
ImGui::PopStyleColor(); ImGui::PopStyleColor();
size_t recent_connections_count = recent_connections_.size(); size_t recent_connections_count = recent_connections_.size();
@@ -90,149 +121,311 @@ int Render::ShowRecentConnections() {
// password length is 6 // password length is 6
// connection_info -> remote_id + 'Y' + host_name + '@' + password // connection_info -> remote_id + 'Y' + host_name + '@' + password
// -> remote_id + 'N' + host_name // -> remote_id + 'N' + host_name
if ('Y' == connection_info[9] && connection_info.size() >= 16) { bool invalid_connection_info = false;
if (connection_info.size() > 9 && 'Y' == connection_info[9] &&
connection_info.size() >= 16) {
size_t pos_y = connection_info.find('Y'); size_t pos_y = connection_info.find('Y');
size_t pos_at = connection_info.find('@'); size_t pos_at = connection_info.find('@');
if (pos_y == std::string::npos || pos_at == std::string::npos || if (pos_y == std::string::npos || pos_at == std::string::npos ||
pos_y >= pos_at) { pos_y >= pos_at) {
LOG_ERROR("Invalid filename"); LOG_ERROR("Invalid filename");
continue; invalid_connection_info = true;
} else {
it.second.remote_id = connection_info.substr(0, pos_y);
it.second.remote_host_name =
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
it.second.password = connection_info.substr(pos_at + 1);
it.second.remember_password = true;
} }
} else if (connection_info.size() > 9 && 'N' == connection_info[9] &&
it.second.remote_id = connection_info.substr(0, pos_y); connection_info.size() >= 10) {
it.second.remote_host_name =
connection_info.substr(pos_y + 1, pos_at - pos_y - 1);
it.second.password = connection_info.substr(pos_at + 1);
it.second.remember_password = true;
} else if ('N' == connection_info[9] && connection_info.size() >= 10) {
size_t pos_n = connection_info.find('N'); size_t pos_n = connection_info.find('N');
size_t pos_at = connection_info.find('@');
if (pos_n == std::string::npos) { if (pos_n == std::string::npos) {
LOG_ERROR("Invalid filename"); LOG_ERROR("Invalid filename");
continue; invalid_connection_info = true;
} else {
it.second.remote_id = connection_info.substr(0, pos_n);
it.second.remote_host_name = connection_info.substr(pos_n + 1);
it.second.password = "";
it.second.remember_password = false;
} }
it.second.remote_id = connection_info.substr(0, pos_n);
it.second.remote_host_name = connection_info.substr(pos_n + 1);
it.second.password = "";
it.second.remember_password = false;
} else { } else {
it.second.remote_host_name = "unknown"; invalid_connection_info = true;
} }
if (invalid_connection_info) {
it.second.remote_id = connection_info.substr(
0, std::min<size_t>(connection_info.size(), 9));
it.second.remote_host_name = "unknown";
it.second.password = "";
it.second.remember_password = false;
}
std::string display_name = GetRecentConnectionDisplayName(it.second);
bool online = device_presence_.IsOnline(it.second.remote_id); bool online = device_presence_.IsOnline(it.second.remote_id);
ImVec2 image_screen_pos = ImVec2(
ImGui::GetCursorScreenPos().x + recent_connection_image_width * 0.04f,
ImGui::GetCursorScreenPos().y + recent_connection_image_height * 0.08f);
ImVec2 image_pos = ImVec2 image_pos =
ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f, ImVec2(ImGui::GetCursorPosX() + recent_connection_image_width * 0.05f,
ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f); ImGui::GetCursorPosY() + recent_connection_image_height * 0.08f);
ImGui::SetCursorPos(image_pos); ImGui::SetCursorPos(image_pos);
ImVec2 image_screen_pos = ImGui::GetCursorScreenPos();
ImGui::Image( ImGui::Image(
(ImTextureID)(intptr_t)it.second.texture, (ImTextureID)(intptr_t)it.second.texture,
ImVec2(recent_connection_image_width, recent_connection_image_height)); ImVec2(recent_connection_image_width, recent_connection_image_height));
if (ImGui::IsItemHovered()) {
const bool image_item_hovered = ImGui::IsItemHovered();
ImVec2 card_screen_min = image_screen_pos;
ImVec2 card_screen_max =
ImVec2(image_screen_pos.x + recent_connection_image_width,
image_screen_pos.y + recent_connection_image_height +
recent_connection_footer_height);
const bool card_hovered =
ImGui::IsMouseHoveringRect(card_screen_min, card_screen_max, true);
const float recent_connection_toolbar_width =
3.0f * recent_connection_button_width;
const float recent_connection_toolbar_padding =
recent_connection_image_width * 0.025f;
const ImVec2 toolbar_pos = ImVec2(
image_pos.x + recent_connection_image_width -
recent_connection_toolbar_width - recent_connection_toolbar_padding,
image_pos.y + recent_connection_toolbar_padding);
const ImVec2 toolbar_screen_pos = ImVec2(
image_screen_pos.x + recent_connection_image_width -
recent_connection_toolbar_width - recent_connection_toolbar_padding,
image_screen_pos.y + recent_connection_toolbar_padding);
const ImVec2 toolbar_screen_end =
ImVec2(toolbar_screen_pos.x + recent_connection_toolbar_width,
toolbar_screen_pos.y + recent_connection_button_height);
const bool toolbar_hovered =
card_hovered && ImGui::IsMouseHoveringRect(toolbar_screen_pos,
toolbar_screen_end, true);
const bool show_image_tooltip = image_item_hovered && !toolbar_hovered;
if (show_image_tooltip) {
ImGui::BeginTooltip(); ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
std::string display_host_name_with_presence =
it.second.remote_host_name + " " + ImGui::Text("%s", display_name.c_str());
(online ? localization::online[localization_language_index_]
: localization::offline[localization_language_index_]); if (!it.second.remote_host_name.empty() &&
ImGui::Text("%s", display_host_name_with_presence.c_str()); it.second.remote_host_name != display_name) {
ImGui::Text("%s", it.second.remote_host_name.c_str());
}
ImGui::Text("%s: %s",
localization::remote_id[localization_language_index_].c_str(),
it.second.remote_id.c_str());
ImGui::Text("%s",
(online ? localization::online[localization_language_index_]
: localization::offline[localization_language_index_])
.c_str());
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip(); ImGui::EndTooltip();
} }
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList* draw_list = ImGui::GetWindowDrawList();
ImVec2 circle_pos =
ImVec2(image_screen_pos.x + recent_connection_image_width * 0.07f,
image_screen_pos.y + recent_connection_image_height * 0.12f);
ImU32 fill_color =
online ? IM_COL32(0, 255, 0, 255) : IM_COL32(140, 140, 140, 255);
ImU32 border_color = IM_COL32(255, 255, 255, 255);
float dot_radius = recent_connection_image_height * 0.06f;
draw_list->AddCircleFilled(circle_pos, dot_radius * 1.25f, border_color,
100);
draw_list->AddCircleFilled(circle_pos, dot_radius, fill_color, 100);
// remote id display button // connection name footer
{ {
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f)); ImVec2 footer_pos =
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(0, 0, 0, 0.2f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(0, 0, 0, 0.2f));
ImVec2 dummy_button_pos =
ImVec2(image_pos.x, image_pos.y + recent_connection_image_height); ImVec2(image_pos.x, image_pos.y + recent_connection_image_height);
std::string dummy_button_name = "##DummyButton" + it.second.remote_id;
ImGui::SetCursorPos(dummy_button_pos);
ImGui::SetWindowFontScale(0.6f);
ImGui::Button(dummy_button_name.c_str(),
ImVec2(recent_connection_dummy_button_width,
recent_connection_button_height));
ImGui::SetWindowFontScale(1.0f);
ImGui::SetCursorPos(ImVec2(
dummy_button_pos.x + recent_connection_dummy_button_width * 0.05f,
dummy_button_pos.y + recent_connection_button_height * 0.05f));
ImGui::SetWindowFontScale(0.65f);
ImGui::Text("%s", it.second.remote_id.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
}
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0.2f)); ImVec2 footer_screen_pos =
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec2(image_screen_pos.x,
ImVec4(0.1f, 0.4f, 0.8f, 1.0f)); image_screen_pos.y + recent_connection_image_height);
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(1.0f, 1.0f, 1.0f, 0.7f));
ImGui::SetWindowFontScale(0.5f);
// trash button
{
ImVec2 trash_can_button_pos =
ImVec2(image_pos.x + recent_connection_image_width -
2 * recent_connection_button_width,
image_pos.y + recent_connection_image_height);
ImGui::SetCursorPos(trash_can_button_pos);
std::string trash_can = ICON_FA_TRASH_CAN;
std::string recent_connection_delete_button_name =
trash_can + "##RecentConnectionDelete" +
std::to_string(trash_can_button_pos.x);
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
show_confirm_delete_connection_ = true;
delete_connection_name_ = it.first;
}
if (delete_connection_ && delete_connection_name_ == it.first) { ImVec2 footer_screen_end =
if (!thumbnail_->DeleteThumbnail(it.first)) { ImVec2(footer_screen_pos.x + recent_connection_name_width,
reload_recent_connections_ = true; footer_screen_pos.y + recent_connection_footer_height);
delete_connection_ = false;
float footer_rounding = recent_connection_footer_height * 0.16f;
draw_list->AddRectFilled(footer_screen_pos, footer_screen_end,
IM_COL32(0, 0, 0, 40), footer_rounding,
ImDrawFlags_RoundCornersBottom);
const float status_left_margin = recent_connection_footer_height * 0.22f;
const float status_gap = recent_connection_footer_height * 0.20f;
const float dot_radius = recent_connection_footer_height * 0.18f;
const ImVec2 dot_center(
footer_screen_pos.x + status_left_margin + dot_radius,
footer_screen_pos.y + recent_connection_footer_height * 0.5f);
const ImU32 dot_color =
online ? IM_COL32(34, 197, 94, 255) : IM_COL32(156, 163, 175, 255);
// Layered halo simulates a radial gradient glow for the online state.
if (online) {
const float halo_radius = dot_radius * 2.2f;
const int halo_layers = 8;
for (int i = halo_layers; i > 0; --i) {
const float t = static_cast<float>(i) / halo_layers;
const float r = dot_radius + (halo_radius - dot_radius) * t;
const int alpha = static_cast<int>(14.0f * (1.0f - t) + 4.0f);
draw_list->AddCircleFilled(
dot_center, r, IM_COL32(74, 222, 128, alpha));
} }
} }
draw_list->AddCircleFilled(dot_center, dot_radius, dot_color);
const float status_block_end_x = dot_center.x + dot_radius;
ImVec2 text_min =
ImVec2(status_block_end_x + status_gap, footer_screen_pos.y);
ImVec2 text_max =
ImVec2(footer_screen_end.x - recent_connection_name_width * 0.05f,
footer_screen_end.y);
ImGui::SetWindowFontScale(0.52f);
ImGui::RenderTextClipped(text_min, text_max, display_name.c_str(),
nullptr, nullptr, ImVec2(0.0f, 0.5f));
ImGui::SetWindowFontScale(1.0f);
} }
// connect button // toolbar / three buttons
{ if (card_hovered) {
ImVec2 connect_button_pos = float toolbar_rounding = recent_connection_button_height * 0.22f;
ImVec2(image_pos.x + recent_connection_image_width -
recent_connection_button_width, draw_list->AddRectFilled(
image_pos.y + recent_connection_image_height); ImVec2(toolbar_screen_pos.x, toolbar_screen_pos.y + 1.0f),
ImGui::SetCursorPos(connect_button_pos); ImVec2(toolbar_screen_end.x, toolbar_screen_end.y + 1.0f),
std::string connect = ICON_FA_ARROW_RIGHT_LONG; IM_COL32(0, 0, 0, 70), toolbar_rounding);
std::string connect_to_this_connection_button_name =
connect + "##ConnectionTo" + it.first; draw_list->AddRectFilled(toolbar_screen_pos, toolbar_screen_end,
if (ImGui::Button(connect_to_this_connection_button_name.c_str(), IM_COL32(20, 24, 30, 170), toolbar_rounding);
ImVec2(recent_connection_button_width,
recent_connection_button_height))) { draw_list->AddRect(toolbar_screen_pos, toolbar_screen_end,
ConnectTo(it.second.remote_id, it.second.password.c_str(), IM_COL32(255, 255, 255, 48), toolbar_rounding);
it.second.remember_password);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, toolbar_rounding);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.0f);
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered,
ImVec4(1.0f, 1.0f, 1.0f, 0.18f));
ImGui::PushStyleColor(ImGuiCol_ButtonActive,
ImVec4(0.35f, 0.55f, 0.95f, 0.45f));
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.0f, 1.0f, 1.0f, 0.95f));
ImGui::SetWindowFontScale(0.5f);
// edit alias button
{
ImGui::SetCursorPos(toolbar_pos);
std::string edit = ICON_FA_PEN;
std::string recent_connection_edit_button_name =
edit + "##RecentConnectionAlias" + it.first;
if (ImGui::Button(recent_connection_edit_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
BeginEditRecentConnectionAlias(it.second);
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(
localization::connection_alias[localization_language_index_]
.c_str());
}
}
// trash button
{
ImVec2 trash_can_button_pos = ImVec2(
toolbar_pos.x + recent_connection_button_width, toolbar_pos.y);
ImGui::SetCursorPos(trash_can_button_pos);
std::string trash_can = ICON_FA_TRASH_CAN;
std::string recent_connection_delete_button_name =
trash_can + "##RecentConnectionDelete" + it.first;
if (ImGui::Button(recent_connection_delete_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
show_confirm_delete_connection_ = true;
delete_connection_name_ = it.first;
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(
localization::delete_connection[localization_language_index_]
.c_str());
}
}
// connect button
{
ImVec2 connect_button_pos = ImVec2(
toolbar_pos.x + 2 * recent_connection_button_width, toolbar_pos.y);
ImGui::SetCursorPos(connect_button_pos);
std::string connect = ICON_FA_ARROW_RIGHT_LONG;
std::string connect_to_this_connection_button_name =
connect + "##ConnectionTo" + it.first;
if (ImGui::Button(connect_to_this_connection_button_name.c_str(),
ImVec2(recent_connection_button_width,
recent_connection_button_height))) {
ConnectTo(it.second.remote_id, it.second.password.c_str(),
it.second.remember_password);
}
if (ImGui::IsItemHovered()) {
SetDarkTextTooltip(localization::connect_to_this_connection
[localization_language_index_]
.c_str());
}
}
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(4);
ImGui::PopStyleVar(3);
}
if (count != recent_connections_count - 1) {
ImVec2 line_start =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y);
ImVec2 line_end =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y + recent_connection_image_height +
recent_connection_footer_height);
ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
IM_COL32(0, 0, 0, 122), 1.0f);
}
if (delete_connection_ && delete_connection_name_ == it.first) {
if (!thumbnail_->DeleteThumbnail(it.first)) {
recent_connection_aliases_.erase(it.second.remote_id);
SaveRecentConnectionAliases();
reload_recent_connections_ = true;
delete_connection_ = false;
} }
} }
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleColor(3);
ImGui::EndChild(); ImGui::EndChild();
@@ -243,7 +436,7 @@ int Render::ShowRecentConnections() {
ImVec2 line_end = ImVec2 line_end =
ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f, ImVec2(image_screen_pos.x + recent_connection_image_width * 1.19f,
image_screen_pos.y + recent_connection_image_height + image_screen_pos.y + recent_connection_image_height +
recent_connection_button_height); recent_connection_footer_height);
ImGui::GetWindowDrawList()->AddLine(line_start, line_end, ImGui::GetWindowDrawList()->AddLine(line_start, line_end,
IM_COL32(0, 0, 0, 122), 1.0f); IM_COL32(0, 0, 0, 122), 1.0f);
} }
@@ -259,6 +452,9 @@ int Render::ShowRecentConnections() {
if (show_confirm_delete_connection_) { if (show_confirm_delete_connection_) {
ConfirmDeleteConnection(); ConfirmDeleteConnection();
} }
if (show_edit_connection_alias_window_) {
EditRecentConnectionAliasWindow();
}
if (show_offline_warning_window_) { if (show_offline_warning_window_) {
OfflineWarningWindow(); OfflineWarningWindow();
} }
@@ -320,6 +516,89 @@ int Render::ConfirmDeleteConnection() {
return 0; return 0;
} }
int Render::EditRecentConnectionAliasWindow() {
ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::SetNextWindowSize(
ImVec2(io.DisplaySize.x * 0.33f, io.DisplaySize.y * 0.33f));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
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::Begin("EditRecentConnectionAliasWindow", nullptr,
ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings);
ImGui::PopStyleVar(2);
ImGui::PopStyleColor();
auto window_width = ImGui::GetWindowSize().x;
auto window_height = ImGui::GetWindowSize().y;
std::string text =
localization::input_connection_alias[localization_language_index_];
ImGui::SetWindowFontScale(0.5f);
auto text_width = ImGui::CalcTextSize(text.c_str()).x;
ImGui::SetCursorPosX((window_width - text_width) * 0.5f);
ImGui::SetCursorPosY(window_height * 0.2f);
ImGui::Text("%s", text.c_str());
ImGui::SetCursorPosX(window_width * 0.2f);
ImGui::SetCursorPosY(window_height * 0.4f);
ImGui::SetNextItemWidth(window_width * 0.6f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f);
if (focus_on_input_widget_) {
ImGui::SetKeyboardFocusHere();
focus_on_input_widget_ = false;
}
bool enter_pressed =
ImGui::InputText("##recent_connection_alias", edit_connection_alias_,
IM_ARRAYSIZE(edit_connection_alias_),
ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::PopStyleVar();
ImGui::SetCursorPosX(window_width * 0.315f);
ImGui::SetCursorPosY(window_height * 0.75f);
if (ImGui::Button(localization::ok[localization_language_index_].c_str()) ||
enter_pressed) {
std::string alias = TrimConnectionAlias(edit_connection_alias_);
if (alias.empty()) {
recent_connection_aliases_.erase(edit_connection_alias_remote_id_);
} else {
recent_connection_aliases_[edit_connection_alias_remote_id_] = alias;
}
SaveRecentConnectionAliases();
show_edit_connection_alias_window_ = false;
focus_on_input_widget_ = true;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
edit_connection_alias_remote_id_.clear();
}
ImGui::SameLine();
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_edit_connection_alias_window_ = false;
focus_on_input_widget_ = true;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
edit_connection_alias_remote_id_.clear();
}
ImGui::SetWindowFontScale(1.0f);
ImGui::End();
ImGui::PopStyleVar();
return 0;
}
int Render::OfflineWarningWindow() { int Render::OfflineWarningWindow() {
ImGuiIO& io = ImGui::GetIO(); ImGuiIO& io = ImGui::GetIO();
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
@@ -360,4 +639,4 @@ int Render::OfflineWarningWindow() {
ImGui::PopStyleVar(); ImGui::PopStyleVar();
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
+2 -2
View File
@@ -211,7 +211,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
props->control_window_max_width_ = title_bar_height_ * 10.0f; props->control_window_max_width_ = title_bar_height_ * 10.0f;
props->control_window_max_height_ = title_bar_height_ * 7.0f; props->control_window_max_height_ = title_bar_height_ * 7.0f;
props->connection_status_ = ConnectionStatus::Connecting; props->connection_status_.store(ConnectionStatus::Connecting);
show_connection_status_window_ = true; show_connection_status_window_ = true;
if (!props->peer_) { if (!props->peer_) {
@@ -231,7 +231,7 @@ int Render::ConnectTo(const std::string& remote_id, const char* password,
AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true); AddDataStream(props->peer_, props->file_feedback_label_.c_str(), true);
AddDataStream(props->peer_, props->clipboard_label_.c_str(), true); AddDataStream(props->peer_, props->clipboard_label_.c_str(), true);
props->connection_status_ = ConnectionStatus::Connecting; props->connection_status_.store(ConnectionStatus::Connecting);
peer_to_init = props->peer_; peer_to_init = props->peer_;
local_id = props->local_id_; local_id = props->local_id_;
+403 -43
View File
@@ -13,6 +13,9 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <string> #include <string>
#include <thread> #include <thread>
@@ -42,6 +45,8 @@
namespace crossdesk { namespace crossdesk {
namespace { namespace {
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
const ImWchar* GetMultilingualGlyphRanges() { const ImWchar* GetMultilingualGlyphRanges() {
static std::vector<ImWchar> glyph_ranges; static std::vector<ImWchar> glyph_ranges;
if (glyph_ranges.empty()) { if (glyph_ranges.empty()) {
@@ -82,15 +87,22 @@ HICON LoadTrayIcon() {
struct WindowsServiceInteractiveStatus { struct WindowsServiceInteractiveStatus {
bool available = false; bool available = false;
bool sas_secure_desktop_grace_active = false;
unsigned int error_code = 0; unsigned int error_code = 0;
std::string interactive_stage; std::string interactive_stage;
std::string error; std::string error;
}; };
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000; constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
constexpr DWORD kWindowsServiceQueryTimeoutMs = 100; constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
constexpr DWORD kWindowsServiceSasTimeoutMs = 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( RemoteAction BuildWindowsServiceStatusAction(
const WindowsServiceInteractiveStatus& status) { const WindowsServiceInteractiveStatus& status) {
RemoteAction action{}; RemoteAction action{};
@@ -125,6 +137,8 @@ bool QueryWindowsServiceInteractiveStatus(
} }
status->interactive_stage = json.value("interactive_stage", std::string()); 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( if (ShouldNormalizeUnlockToUserDesktop(
json.value("interactive_lock_screen_visible", false), json.value("interactive_lock_screen_visible", false),
@@ -598,6 +612,11 @@ int Render::LoadSettingsFromCacheFile() {
enable_autostart_ = config_center_->IsEnableAutostart(); enable_autostart_ = config_center_->IsEnableAutostart();
enable_daemon_ = config_center_->IsEnableDaemon(); enable_daemon_ = config_center_->IsEnableDaemon();
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray(); 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 // File transfer save path
{ {
@@ -624,7 +643,120 @@ int Render::LoadSettingsFromCacheFile() {
return 0; return 0;
} }
int Render::LoadRecentConnectionAliases() {
recent_connection_aliases_.clear();
std::ifstream alias_file(cache_path_ + "/recent_connection_aliases.json");
if (!alias_file.good()) {
return 0;
}
try {
nlohmann::json alias_json;
alias_file >> alias_json;
const nlohmann::json* aliases = &alias_json;
if (alias_json.contains("aliases") && alias_json["aliases"].is_object()) {
aliases = &alias_json["aliases"];
}
if (!aliases->is_object()) {
LOG_WARN("Invalid recent connection alias file");
return -1;
}
for (auto it = aliases->begin(); it != aliases->end(); ++it) {
if (!it.value().is_string()) {
continue;
}
std::string remote_id = it.key();
std::string alias = it.value().get<std::string>();
if (!remote_id.empty() && !alias.empty()) {
recent_connection_aliases_[remote_id] = alias;
}
}
} catch (const std::exception& e) {
LOG_WARN("Load recent connection aliases failed: {}", e.what());
return -1;
}
return 0;
}
int Render::SaveRecentConnectionAliases() const {
std::error_code ec;
std::filesystem::create_directories(cache_path_, ec);
if (ec) {
LOG_WARN("Create cache directory failed while saving aliases: {}",
ec.message());
return -1;
}
nlohmann::json alias_json;
alias_json["aliases"] = nlohmann::json::object();
for (const auto& [remote_id, alias] : recent_connection_aliases_) {
if (!remote_id.empty() && !alias.empty()) {
alias_json["aliases"][remote_id] = alias;
}
}
std::ofstream alias_file(cache_path_ + "/recent_connection_aliases.json",
std::ios::trunc);
if (!alias_file.good()) {
LOG_WARN("Open recent connection alias file failed");
return -1;
}
alias_file << alias_json.dump(2);
return 0;
}
std::string Render::GetRecentConnectionDisplayName(
const Thumbnail::RecentConnection& connection) const {
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
if (alias_it != recent_connection_aliases_.end() &&
!alias_it->second.empty()) {
return alias_it->second;
}
if (!connection.remote_host_name.empty() &&
connection.remote_host_name != "unknown") {
return connection.remote_host_name;
}
return connection.remote_id;
}
void Render::BeginEditRecentConnectionAlias(
const Thumbnail::RecentConnection& connection) {
edit_connection_alias_remote_id_ = connection.remote_id;
memset(edit_connection_alias_, 0, sizeof(edit_connection_alias_));
const auto alias_it = recent_connection_aliases_.find(connection.remote_id);
std::string alias =
alias_it != recent_connection_aliases_.end()
? alias_it->second
: GetRecentConnectionDisplayName(connection);
if (!alias.empty()) {
strncpy(edit_connection_alias_, alias.c_str(),
sizeof(edit_connection_alias_) - 1);
edit_connection_alias_[sizeof(edit_connection_alias_) - 1] = '\0';
}
focus_on_input_widget_ = true;
show_edit_connection_alias_window_ = true;
}
int Render::ScreenCapturerInit() { int Render::ScreenCapturerInit() {
#ifdef __APPLE__
if (!EnsureMacScreenRecordingPermission()) {
return -1;
}
#endif
if (!screen_capturer_) { if (!screen_capturer_) {
screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create(); screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create();
} }
@@ -642,18 +774,37 @@ int Render::ScreenCapturerInit() {
fps, fps,
[this, fps](unsigned char* data, int size, int width, int height, [this, fps](unsigned char* data, int size, int width, int height,
const char* display_name) -> void { const char* display_name) -> void {
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>( const auto now_time =
std::chrono::steady_clock::now().time_since_epoch()) static_cast<uint64_t>(std::chrono::duration_cast<
.count(); std::chrono::milliseconds>(
std::chrono::steady_clock::now()
.time_since_epoch())
.count());
auto duration = now_time - last_frame_time_; auto duration = now_time - last_frame_time_;
if (duration * fps >= 1000) { // ~60 FPS 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; XVideoFrame frame;
frame.data = (const char*)data; frame.data = (const char*)data;
frame.size = size; frame.size = size;
frame.width = width; frame.width = width;
frame.height = height; frame.height = height;
frame.captured_timestamp = GetSystemTimeMicros(peer_); 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; last_frame_time_ = now_time;
} }
}); });
@@ -675,6 +826,12 @@ int Render::ScreenCapturerInit() {
} }
int Render::StartScreenCapturer() { int Render::StartScreenCapturer() {
#ifdef __APPLE__
if (!EnsureMacScreenRecordingPermission()) {
return -1;
}
#endif
if (!screen_capturer_) { if (!screen_capturer_) {
LOG_INFO("Screen capturer instance missing, recreating before start"); LOG_INFO("Screen capturer instance missing, recreating before start");
if (0 != ScreenCapturerInit()) { if (0 != ScreenCapturerInit()) {
@@ -722,11 +879,16 @@ int Render::StartSpeakerCapturer() {
} }
if (speaker_capturer_) { 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; start_speaker_capturer_ = true;
return 0;
} }
return 0; return -1;
} }
int Render::StopSpeakerCapturer() { int Render::StopSpeakerCapturer() {
@@ -739,6 +901,12 @@ int Render::StopSpeakerCapturer() {
} }
int Render::StartMouseController() { int Render::StartMouseController() {
#ifdef __APPLE__
if (!EnsureMacAccessibilityPermission()) {
return -1;
}
#endif
if (!device_controller_factory_) { if (!device_controller_factory_) {
LOG_INFO("Device controller factory is nullptr"); LOG_INFO("Device controller factory is nullptr");
return -1; return -1;
@@ -796,6 +964,13 @@ int Render::StopMouseController() {
int Render::StartKeyboardCapturer() { int Render::StartKeyboardCapturer() {
keyboard_capturer_uses_sdl_events_ = false; 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 defined(__linux__) && !defined(__APPLE__)
if (IsWaylandSession()) { if (IsWaylandSession()) {
keyboard_capturer_uses_sdl_events_ = true; keyboard_capturer_uses_sdl_events_ = true;
@@ -1094,8 +1269,9 @@ void Render::UpdateInteractions() {
} }
if (start_speaker_capturer_ && !speaker_capturer_is_started_) { if (start_speaker_capturer_ && !speaker_capturer_is_started_) {
StartSpeakerCapturer(); if (0 == StartSpeakerCapturer()) {
speaker_capturer_is_started_ = true; speaker_capturer_is_started_ = true;
}
} else if (!start_speaker_capturer_ && speaker_capturer_is_started_) { } else if (!start_speaker_capturer_ && speaker_capturer_is_started_) {
StopSpeakerCapturer(); StopSpeakerCapturer();
speaker_capturer_is_started_ = false; speaker_capturer_is_started_ = false;
@@ -1126,10 +1302,16 @@ void Render::UpdateInteractions() {
keyboard_capturer_is_started_ = true; keyboard_capturer_is_started_ = true;
} }
} }
if (keyboard_capturer_is_started_) {
SendKeyboardHeartbeat(false);
}
} else if (keyboard_capturer_is_started_) { } else if (keyboard_capturer_is_started_) {
ForceReleasePressedKeys();
StopKeyboardCapturer(); StopKeyboardCapturer();
keyboard_capturer_is_started_ = false; keyboard_capturer_is_started_ = false;
} }
CheckRemoteKeyboardTimeouts();
} }
int Render::CreateMainWindow() { int Render::CreateMainWindow() {
@@ -1192,6 +1374,13 @@ int Render::CreateMainWindow() {
HICON tray_icon = LoadTrayIcon(); HICON tray_icon = LoadTrayIcon();
tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk", tray_ = std::make_unique<WinTray>(main_hwnd, tray_icon, L"CrossDesk",
localization_language_index_); localization_language_index_);
#elif defined(__APPLE__)
tray_ = std::make_unique<MacTray>(main_window_, "CrossDesk",
localization_language_index_);
#elif defined(__linux__) && !defined(__APPLE__)
tray_ = std::make_unique<LinuxTray>(main_window_, "CrossDesk",
localization_language_index_,
APP_EXIT_EVENT);
#endif #endif
ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_); ImGui_ImplSDL3_InitForSDLRenderer(main_window_, main_renderer_);
@@ -1491,20 +1680,47 @@ int Render::SetupFontAndStyle(ImFont** system_chinese_font_out) {
} }
int Render::DestroyMainWindowContext() { int Render::DestroyMainWindowContext() {
if (!main_ctx_) {
return 0;
}
ImGui::SetCurrentContext(main_ctx_); ImGui::SetCurrentContext(main_ctx_);
ImGui_ImplSDLRenderer3_Shutdown(); ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown(); ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(main_ctx_); ImGui::DestroyContext(main_ctx_);
main_ctx_ = nullptr;
return 0; return 0;
} }
int Render::DestroyStreamWindowContext() { int Render::DestroyStreamWindowContext() {
if (!stream_ctx_) {
stream_window_inited_ = false;
return 0;
}
stream_window_inited_ = false; stream_window_inited_ = false;
ImGui::SetCurrentContext(stream_ctx_); ImGui::SetCurrentContext(stream_ctx_);
ImGui_ImplSDLRenderer3_Shutdown(); ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown(); ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(stream_ctx_); ImGui::DestroyContext(stream_ctx_);
stream_ctx_ = nullptr;
return 0;
}
int Render::DestroyServerWindowContext() {
if (!server_ctx_) {
server_window_inited_ = false;
return 0;
}
server_window_inited_ = false;
ImGui::SetCurrentContext(server_ctx_);
ImGui_ImplSDLRenderer3_Shutdown();
ImGui_ImplSDL3_Shutdown();
ImGui::DestroyContext(server_ctx_);
server_ctx_ = nullptr;
return 0; return 0;
} }
@@ -1540,6 +1756,10 @@ int Render::DrawMainWindow() {
UpdateNotificationWindow(); UpdateNotificationWindow();
#if _WIN32 && CROSSDESK_PORTABLE
PortableServiceInstallWindow();
#endif
#ifdef __APPLE__ #ifdef __APPLE__
if (show_request_permission_window_) { if (show_request_permission_window_) {
RequestPermissionWindow(); RequestPermissionWindow();
@@ -1649,26 +1869,6 @@ int Render::DrawServerWindow() {
} }
int Render::Run() { 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"); path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) { if (path_manager_) {
exec_log_path_ = path_manager_->GetLogPath().string(); exec_log_path_ = path_manager_->GetLogPath().string();
@@ -1697,11 +1897,50 @@ int Render::Run() {
InitializeLogger(); InitializeLogger();
LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION); 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(); InitializeSettings();
InitializeSDL(); InitializeSDL();
InitializeModules(); InitializeModules();
InitializeMainWindow(); InitializeMainWindow();
#if _WIN32 && CROSSDESK_PORTABLE
CheckPortableWindowsService();
#endif
const int scaled_video_width_ = 160; const int scaled_video_width_ = 160;
const int scaled_video_height_ = 90; const int scaled_video_height_ = 90;
@@ -1716,6 +1955,7 @@ void Render::InitializeLogger() { InitLogger(exec_log_path_); }
void Render::InitializeSettings() { void Render::InitializeSettings() {
LoadSettingsFromCacheFile(); LoadSettingsFromCacheFile();
LoadRecentConnectionAliases();
localization_language_index_ = localization_language_index_ =
localization::detail::ClampLanguageIndex(language_button_value_); localization::detail::ClampLanguageIndex(language_button_value_);
@@ -1749,9 +1989,12 @@ void Render::InitializeSDL() {
screen_height_ = dm->h; screen_height_ = dm->h;
} }
STREAM_REFRESH_EVENT = SDL_RegisterEvents(1); const uint32_t custom_event_base = SDL_RegisterEvents(2);
if (STREAM_REFRESH_EVENT == (uint32_t)-1) { if (custom_event_base == static_cast<uint32_t>(-1)) {
LOG_ERROR("Failed to register custom SDL event"); LOG_ERROR("Failed to register custom SDL events");
} else {
STREAM_REFRESH_EVENT = custom_event_base;
APP_EXIT_EVENT = custom_event_base + 1;
} }
LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_); LOG_INFO("Screen resolution: [{}x{}]", screen_width_, screen_height_);
@@ -1825,6 +2068,10 @@ void Render::MainLoop() {
TranslateMessage(&msg); TranslateMessage(&msg);
DispatchMessage(&msg); DispatchMessage(&msg);
} }
#elif defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->ProcessEvents();
}
#endif #endif
UpdateLabels(); UpdateLabels();
@@ -1835,7 +2082,11 @@ void Render::MainLoop() {
HandleServerWindow(); HandleServerWindow();
HandleWindowsServiceIntegration(); HandleWindowsServiceIntegration();
DrawMainWindow(); const bool main_window_visible =
main_window_ && !(SDL_GetWindowFlags(main_window_) & SDL_WINDOW_HIDDEN);
if (main_window_visible) {
DrawMainWindow();
}
if (stream_window_inited_) { if (stream_window_inited_) {
DrawStreamWindow(); DrawStreamWindow();
} }
@@ -1848,6 +2099,29 @@ void Render::MainLoop() {
} }
} }
bool Render::MinimizeMainWindowToTray() {
if (!enable_minimize_to_tray_ || !main_window_) {
return false;
}
#if defined(_WIN32) || defined(__APPLE__)
if (!tray_) {
return false;
}
tray_->MinimizeToTray();
return true;
#elif defined(__linux__) && !defined(__APPLE__)
if (!tray_) {
return false;
}
return tray_->MinimizeToTray();
#else
return false;
#endif
}
void Render::UpdateLabels() { void Render::UpdateLabels() {
if (!label_inited_ || if (!label_inited_ ||
localization_language_index_last_ != localization_language_index_) { localization_language_index_last_ != localization_language_index_) {
@@ -1904,11 +2178,13 @@ void Render::HandleWindowsServiceIntegration() {
return; return;
} }
const bool has_connected_remote = const bool has_connected_remote = [&] {
std::any_of(connection_status_.begin(), connection_status_.end(), std::shared_lock lock(connection_status_mutex_);
[](const auto& entry) { return std::any_of(connection_status_.begin(), connection_status_.end(),
return entry.second == ConnectionStatus::Connected; [](const auto& entry) {
}); return entry.second == ConnectionStatus::Connected;
});
}();
if (!has_connected_remote) { if (!has_connected_remote) {
ResetLocalWindowsServiceState(false); ResetLocalWindowsServiceState(false);
return; return;
@@ -1923,6 +2199,12 @@ void Render::HandleWindowsServiceIntegration() {
LOG_WARN("Remote SAS request failed: {}", response); LOG_WARN("Remote SAS request failed: {}", response);
} else { } else {
LOG_INFO("Remote SAS request forwarded to local Windows service"); 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; last_windows_service_status_tick_ = 0;
force_broadcast = true; force_broadcast = true;
@@ -1938,9 +2220,32 @@ void Render::HandleWindowsServiceIntegration() {
WindowsServiceInteractiveStatus status; WindowsServiceInteractiveStatus status;
const bool status_ok = QueryWindowsServiceInteractiveStatus(&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_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) { if (status_ok) {
const bool availability_changed = const bool availability_changed =
@@ -1953,6 +2258,11 @@ void Render::HandleWindowsServiceIntegration() {
if (status.available) { if (status.available) {
LOG_INFO( LOG_INFO(
"Local Windows service available for secure desktop integration"); "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 { } else {
LOG_WARN( LOG_WARN(
"Local Windows service unavailable, secure desktop integration " "Local Windows service unavailable, secure desktop integration "
@@ -1973,7 +2283,7 @@ void Render::HandleWindowsServiceIntegration() {
last_logged_service_error_code = 0; last_logged_service_error_code = 0;
} }
RemoteAction remote_action = BuildWindowsServiceStatusAction(status); RemoteAction remote_action = BuildWindowsServiceStatusAction(broadcast_status);
std::string msg = remote_action.to_json(); std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(), int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
control_data_label_.c_str()); control_data_label_.c_str());
@@ -1992,6 +2302,7 @@ void Render::ResetLocalWindowsServiceState(bool clear_pending_sas) {
local_service_status_received_ = false; local_service_status_received_ = false;
local_service_available_ = false; local_service_available_ = false;
local_interactive_stage_.clear(); local_interactive_stage_.clear();
optimistic_windows_secure_desktop_until_tick_ = 0;
} }
#endif #endif
@@ -2149,6 +2460,7 @@ void Render::HandleServerWindow() {
if (need_to_destroy_server_window_) { if (need_to_destroy_server_window_) {
DestroyServerWindow(); DestroyServerWindow();
DestroyServerWindowContext();
need_to_destroy_server_window_ = false; need_to_destroy_server_window_ = false;
} }
} }
@@ -2182,9 +2494,34 @@ void Render::Cleanup() {
CleanupFactories(); CleanupFactories();
CleanupPeers(); CleanupPeers();
#if _WIN32 && CROSSDESK_PORTABLE
JoinPortableWindowsServiceInstallThread();
#endif
WaitForThumbnailSaveTasks(); WaitForThumbnailSaveTasks();
AudioDeviceDestroy(); AudioDeviceDestroy();
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
tray_.reset();
#endif
if (stream_window_created_) {
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
DestroyStreamWindow();
}
if (stream_ctx_) {
DestroyStreamWindowContext();
}
if (server_window_created_) {
DestroyServerWindow();
}
if (server_ctx_) {
DestroyServerWindowContext();
}
DestroyMainWindowContext(); DestroyMainWindowContext();
DestroyMainWindow(); DestroyMainWindow();
SDL_Quit(); SDL_Quit();
@@ -2598,6 +2935,20 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
} }
} }
if (APP_EXIT_EVENT != 0 && event.type == APP_EXIT_EVENT) {
LOG_INFO("Quit program from system tray");
if (stream_window_) {
SDL_SetWindowMouseGrab(stream_window_, false);
}
#if defined(__linux__) && !defined(__APPLE__)
if (tray_) {
tray_->RemoveTrayIcon();
}
#endif
exit_ = true;
return;
}
switch (event.type) { switch (event.type) {
case SDL_EVENT_QUIT: case SDL_EVENT_QUIT:
if (stream_window_inited_) { if (stream_window_inited_) {
@@ -2668,9 +3019,18 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
break; break;
case SDL_EVENT_WINDOW_CLOSE_REQUESTED: case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
if (event.window.windowID != SDL_GetWindowID(stream_window_)) { if (stream_window_ &&
exit_ = true; event.window.windowID == SDL_GetWindowID(stream_window_)) {
break;
} }
if (main_window_ &&
event.window.windowID == SDL_GetWindowID(main_window_) &&
MinimizeMainWindowToTray()) {
break;
}
exit_ = true;
break; break;
case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED: case SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED:
+100 -8
View File
@@ -39,6 +39,10 @@
#if _WIN32 #if _WIN32
#include "win_tray.h" #include "win_tray.h"
#elif defined(__APPLE__)
#include "mac_tray.h"
#elif defined(__linux__)
#include "linux_tray.h"
#endif #endif
namespace crossdesk { namespace crossdesk {
@@ -180,7 +184,11 @@ class Render {
SDL_Rect stream_render_rect_; SDL_Rect stream_render_rect_;
SDL_Rect stream_render_rect_last_; SDL_Rect stream_render_rect_last_;
ImVec2 control_window_pos_; ImVec2 control_window_pos_;
ConnectionStatus connection_status_ = ConnectionStatus::Closed; // Written from the minirtc/libnice callback thread (OnConnectionStatusCb)
// and the SDL main thread (remote_peer_panel connect button); read from
// the SDL main render thread (stream/control windows) and the SDL audio
// thread (SdlCaptureAudioIn). Atomic so those reads/writes are defined.
std::atomic<ConnectionStatus> connection_status_ = ConnectionStatus::Closed;
TraversalMode traversal_mode_ = TraversalMode::UnknownMode; TraversalMode traversal_mode_ = TraversalMode::UnknownMode;
int fps_ = 0; int fps_ = 0;
int frame_count_ = 0; int frame_count_ = 0;
@@ -278,6 +286,7 @@ class Render {
int DrawStreamWindow(); int DrawStreamWindow();
int DrawServerWindow(); int DrawServerWindow();
int ConfirmDeleteConnection(); int ConfirmDeleteConnection();
int EditRecentConnectionAliasWindow();
int OfflineWarningWindow(); int OfflineWarningWindow();
int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props); int NetTrafficStats(std::shared_ptr<SubStreamWindowProperties>& props);
void DrawConnectionStatusText( void DrawConnectionStatusText(
@@ -296,6 +305,9 @@ class Render {
void OpenScreenRecordingPreferences(); void OpenScreenRecordingPreferences();
void OpenAccessibilityPreferences(); void OpenAccessibilityPreferences();
bool DrawToggleSwitch(const char* id, bool active, bool enabled); bool DrawToggleSwitch(const char* id, bool active, bool enabled);
void RefreshMacPermissionStatus(bool force);
bool EnsureMacScreenRecordingPermission();
bool EnsureMacAccessibilityPermission();
#endif #endif
public: public:
@@ -340,11 +352,35 @@ class Render {
static void FreeRemoteAction(RemoteAction& action); static void FreeRemoteAction(RemoteAction& action);
private: private:
struct PressedKeyboardKey {
int key_code = 0;
uint32_t scan_code = 0;
bool extended = false;
};
struct RemoteKeyboardState {
std::unordered_map<int, PressedKeyboardKey> pressed_keys;
uint32_t last_seq = 0;
uint32_t last_seen_tick = 0;
bool keyboard_state_seen = false;
};
int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0, int SendKeyCommand(int key_code, bool is_down, uint32_t scan_code = 0,
bool extended = false); bool extended = false);
static bool IsModifierVkKey(int key_code); static bool IsModifierVkKey(int key_code);
void TrackPressedKeyState(int key_code, bool is_down); void TrackPressedKeyState(int key_code, bool is_down, uint32_t scan_code,
bool extended);
void ForceReleasePressedKeys(); void ForceReleasePressedKeys();
void SendKeyboardHeartbeat(bool force);
void ApplyRemoteKeyboardEvent(const std::string& remote_id,
const RemoteAction& remote_action);
void ApplyRemoteKeyboardState(const std::string& remote_id,
const RemoteAction& remote_action);
bool InjectRemoteKeyboardKey(int key_code, bool is_down, uint32_t scan_code,
bool extended);
void ReleaseRemotePressedKeys(const std::string& remote_id,
const char* reason);
void CheckRemoteKeyboardTimeouts();
int ProcessKeyboardEvent(const SDL_Event& event); int ProcessKeyboardEvent(const SDL_Event& event);
int ProcessMouseEvent(const SDL_Event& event); int ProcessMouseEvent(const SDL_Event& event);
@@ -354,6 +390,12 @@ class Render {
private: private:
int SaveSettingsIntoCacheFile(); int SaveSettingsIntoCacheFile();
int LoadSettingsFromCacheFile(); int LoadSettingsFromCacheFile();
int LoadRecentConnectionAliases();
int SaveRecentConnectionAliases() const;
std::string GetRecentConnectionDisplayName(
const Thumbnail::RecentConnection& connection) const;
void BeginEditRecentConnectionAlias(
const Thumbnail::RecentConnection& connection);
int ScreenCapturerInit(); int ScreenCapturerInit();
int StartScreenCapturer(); int StartScreenCapturer();
@@ -380,8 +422,22 @@ class Render {
int AudioDeviceInit(); int AudioDeviceInit();
int AudioDeviceDestroy(); int AudioDeviceDestroy();
void HandleWindowsServiceIntegration(); void HandleWindowsServiceIntegration();
bool MinimizeMainWindowToTray();
#if _WIN32 #if _WIN32
void ResetLocalWindowsServiceState(bool clear_pending_sas); 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 #endif
private: private:
@@ -463,6 +519,10 @@ class Render {
const int sdl_refresh_ms_ = 16; // ~60 FPS const int sdl_refresh_ms_ = 16; // ~60 FPS
#if _WIN32 #if _WIN32
std::unique_ptr<WinTray> tray_; std::unique_ptr<WinTray> tray_;
#elif defined(__APPLE__)
std::unique_ptr<MacTray> tray_;
#elif defined(__linux__)
std::unique_ptr<LinuxTray> tray_;
#endif #endif
// main window properties // main window properties
@@ -535,11 +595,16 @@ class Render {
std::string controlled_remote_id_ = ""; std::string controlled_remote_id_ = "";
std::string focused_remote_id_ = ""; std::string focused_remote_id_ = "";
std::string remote_client_id_ = ""; std::string remote_client_id_ = "";
std::unordered_set<int> pressed_keyboard_keys_; std::unordered_map<int, PressedKeyboardKey> pressed_keyboard_keys_;
std::mutex pressed_keyboard_keys_mutex_; std::mutex pressed_keyboard_keys_mutex_;
SDL_Event last_mouse_event; uint32_t keyboard_state_seq_ = 0;
SDL_AudioStream* output_stream_; uint32_t last_keyboard_heartbeat_tick_ = 0;
std::unordered_map<std::string, RemoteKeyboardState> remote_keyboard_states_;
std::mutex remote_keyboard_states_mutex_;
SDL_Event last_mouse_event{};
SDL_AudioStream* output_stream_ = nullptr;
uint32_t STREAM_REFRESH_EVENT = 0; uint32_t STREAM_REFRESH_EVENT = 0;
uint32_t APP_EXIT_EVENT = 0;
#if _WIN32 #if _WIN32
std::atomic<bool> pending_windows_service_sas_{false}; std::atomic<bool> pending_windows_service_sas_{false};
bool local_service_status_received_ = false; bool local_service_status_received_ = false;
@@ -547,6 +612,17 @@ class Render {
std::string local_interactive_stage_; std::string local_interactive_stage_;
uint32_t last_local_secure_input_block_log_tick_ = 0; uint32_t last_local_secure_input_block_log_tick_ = 0;
uint32_t last_windows_service_status_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 #endif
// stream window render // stream window render
@@ -628,10 +704,14 @@ class Render {
bool is_server_mode_ = false; bool is_server_mode_ = false;
bool reload_recent_connections_ = true; bool reload_recent_connections_ = true;
bool show_confirm_delete_connection_ = false; bool show_confirm_delete_connection_ = false;
bool show_edit_connection_alias_window_ = false;
bool show_offline_warning_window_ = false; bool show_offline_warning_window_ = false;
bool delete_connection_ = false; bool delete_connection_ = false;
bool is_tab_bar_hovered_ = false; bool is_tab_bar_hovered_ = false;
std::string delete_connection_name_ = ""; std::string delete_connection_name_ = "";
std::unordered_map<std::string, std::string> recent_connection_aliases_;
std::string edit_connection_alias_remote_id_ = "";
char edit_connection_alias_[128] = "";
std::string offline_warning_text_ = ""; std::string offline_warning_text_ = "";
bool re_enter_remote_id_ = false; bool re_enter_remote_id_ = false;
double copy_start_time_ = 0; double copy_start_time_ = 0;
@@ -660,8 +740,8 @@ class Render {
// Map file_id to FileTransferState for global file transfer (props == null) // Map file_id to FileTransferState for global file transfer (props == null)
std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_; std::unordered_map<uint32_t, FileTransferState*> file_id_to_transfer_state_;
std::shared_mutex file_id_to_transfer_state_mutex_; std::shared_mutex file_id_to_transfer_state_mutex_;
SDL_AudioDeviceID input_dev_; SDL_AudioDeviceID input_dev_ = 0;
SDL_AudioDeviceID output_dev_; SDL_AudioDeviceID output_dev_ = 0;
ScreenCapturerFactory* screen_capturer_factory_ = nullptr; ScreenCapturerFactory* screen_capturer_factory_ = nullptr;
ScreenCapturer* screen_capturer_ = nullptr; ScreenCapturer* screen_capturer_ = nullptr;
SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr; SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr;
@@ -670,13 +750,20 @@ class Render {
MouseController* mouse_controller_ = nullptr; MouseController* mouse_controller_ = nullptr;
KeyboardCapturer* keyboard_capturer_ = nullptr; KeyboardCapturer* keyboard_capturer_ = nullptr;
std::vector<DisplayInfo> display_info_list_; 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_ = false;
bool show_new_version_icon_in_menu_ = true; bool show_new_version_icon_in_menu_ = true;
double new_version_icon_last_trigger_time_ = 0.0; double new_version_icon_last_trigger_time_ = 0.0;
double new_version_icon_render_start_time_ = 0.0; double new_version_icon_render_start_time_ = 0.0;
#ifdef __APPLE__ #ifdef __APPLE__
bool show_request_permission_window_ = true; 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 #endif
char client_id_[10] = ""; char client_id_[10] = "";
char client_id_display_[12] = ""; char client_id_display_[12] = "";
@@ -733,6 +820,11 @@ class Render {
void WaitForThumbnailSaveTasks(); void WaitForThumbnailSaveTasks();
/* ------ server mode ------ */ /* ------ server mode ------ */
// connection_status_ / connection_host_names_ are read on the main
// render thread (DrawServerWindow, HandleWindowsServiceIntegration) and
// written from minirtc/libnice callback threads (OnConnectionStatusCb,
// OnReceiveDataBufferCb). Guard every access with this shared mutex.
std::shared_mutex connection_status_mutex_;
std::unordered_map<std::string, ConnectionStatus> connection_status_; std::unordered_map<std::string, ConnectionStatus> connection_status_;
std::unordered_map<std::string, std::string> connection_host_names_; std::unordered_map<std::string, std::string> connection_host_names_;
std::string selected_server_remote_id_ = ""; std::string selected_server_remote_id_ = "";
+357 -78
View File
@@ -6,6 +6,9 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <limits> #include <limits>
#include <memory>
#include <mutex>
#include <shared_mutex>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -28,6 +31,8 @@
namespace crossdesk { namespace crossdesk {
namespace { namespace {
constexpr uint32_t kKeyboardHeartbeatIntervalMs = 500;
constexpr uint32_t kRemoteKeyboardReleaseTimeoutMs = 2500;
int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) { int TranslateSdlKeypadScancodeToVk(const SDL_KeyboardEvent& event) {
const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0; const bool numlock_enabled = (event.mod & SDL_KMOD_NUM) != 0;
@@ -317,6 +322,22 @@ void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* side,
"cannot drive the Windows password UI", "cannot drive the Windows password UI",
side != nullptr ? side : "unknown", stage != nullptr ? stage : ""); 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 #endif
} // namespace } // namespace
@@ -399,34 +420,92 @@ bool Render::IsModifierVkKey(int key_code) {
} }
} }
void Render::TrackPressedKeyState(int key_code, bool is_down) { void Render::TrackPressedKeyState(int key_code, bool is_down,
if (!IsWaylandSession() && !IsModifierVkKey(key_code)) { uint32_t scan_code, bool extended) {
return;
}
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_); std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
if (is_down) { if (is_down) {
pressed_keyboard_keys_.insert(key_code); pressed_keyboard_keys_[key_code] =
PressedKeyboardKey{key_code, scan_code, extended};
} else { } else {
pressed_keyboard_keys_.erase(key_code); pressed_keyboard_keys_.erase(key_code);
} }
} }
void Render::ForceReleasePressedKeys() { void Render::ForceReleasePressedKeys() {
std::vector<int> pressed_keys; std::vector<PressedKeyboardKey> pressed_keys;
{ {
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_); std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
if (pressed_keyboard_keys_.empty()) { pressed_keys.reserve(pressed_keyboard_keys_.size());
return; for (const auto& [_, key] : pressed_keyboard_keys_) {
pressed_keys.push_back(key);
} }
pressed_keys.assign(pressed_keyboard_keys_.begin(),
pressed_keyboard_keys_.end());
pressed_keyboard_keys_.clear(); pressed_keyboard_keys_.clear();
} }
for (int key_code : pressed_keys) { for (const PressedKeyboardKey& key : pressed_keys) {
SendKeyCommand(key_code, false); SendKeyCommand(key.key_code, false, key.scan_code, key.extended);
} }
SendKeyboardHeartbeat(true);
}
void Render::SendKeyboardHeartbeat(bool force) {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
if (!force && now - last_keyboard_heartbeat_tick_ <
kKeyboardHeartbeatIntervalMs) {
return;
}
RemoteAction remote_action{};
remote_action.type = ControlType::keyboard_state;
remote_action.ks.seq = ++keyboard_state_seq_;
{
std::lock_guard<std::mutex> lock(pressed_keyboard_keys_mutex_);
size_t idx = 0;
for (const auto& [_, key] : pressed_keyboard_keys_) {
if (idx >= kMaxKeyboardStateKeys) {
LOG_WARN("Keyboard heartbeat truncated, pressed_keys={}",
pressed_keyboard_keys_.size());
break;
}
remote_action.ks.pressed_keys[idx].key_value =
static_cast<size_t>(key.key_code);
remote_action.ks.pressed_keys[idx].scan_code = key.scan_code;
remote_action.ks.pressed_keys[idx].extended = key.extended;
++idx;
}
remote_action.ks.pressed_count = idx;
}
const std::string target_id = controlled_remote_id_.empty()
? focused_remote_id_
: controlled_remote_id_;
if (target_id.empty()) {
last_keyboard_heartbeat_tick_ = now;
return;
}
auto props_it = client_properties_.find(target_id);
if (props_it == client_properties_.end()) {
last_keyboard_heartbeat_tick_ = now;
return;
}
const auto props = props_it->second;
if (props->connection_status_.load() != ConnectionStatus::Connected ||
!props->peer_) {
last_keyboard_heartbeat_tick_ = now;
return;
}
const std::string msg = remote_action.to_json();
const int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->keyboard_label_.c_str());
if (ret != 0) {
LOG_WARN("Send keyboard heartbeat failed, remote_id={}, ret={}", target_id,
ret);
}
last_keyboard_heartbeat_tick_ = now;
} }
int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code, int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
@@ -455,7 +534,7 @@ int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
if (!target_id.empty()) { if (!target_id.empty()) {
if (client_properties_.find(target_id) != client_properties_.end()) { if (client_properties_.find(target_id) != client_properties_.end()) {
auto props = client_properties_[target_id]; auto props = client_properties_[target_id];
if (props->connection_status_ == ConnectionStatus::Connected && if (props->connection_status_.load() == ConnectionStatus::Connected &&
props->peer_) { props->peer_) {
std::string msg = remote_action.to_json(); std::string msg = remote_action.to_json();
int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(), int ret = SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
@@ -468,7 +547,7 @@ int Render::SendKeyCommand(int key_code, bool is_down, uint32_t scan_code,
} }
} }
TrackPressedKeyState(key_code, is_down); TrackPressedKeyState(key_code, is_down, scan_code, extended);
return 0; return 0;
} }
@@ -490,9 +569,184 @@ int Render::ProcessKeyboardEvent(const SDL_Event& event) {
return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN); return SendKeyCommand(key_code, event.type == SDL_EVENT_KEY_DOWN);
} }
bool Render::InjectRemoteKeyboardKey(int key_code, bool is_down,
uint32_t scan_code, bool extended) {
#if _WIN32
if (local_service_status_received_ &&
IsSecureDesktopInteractionRequired(local_interactive_stage_)) {
const std::string response = SendCrossDeskSecureDesktopKeyInput(
key_code, is_down, scan_code, extended, 1000);
auto json = nlohmann::json::parse(response, nullptr, false);
if (json.is_discarded() || !json.value("ok", false)) {
RemoteAction action{};
action.type = ControlType::keyboard;
action.k.key_value = static_cast<size_t>(key_code);
action.k.scan_code = scan_code;
action.k.extended = extended;
action.k.flag = is_down ? KeyFlag::key_down : KeyFlag::key_up;
if (!json.is_discarded() &&
IsTransientSecureDesktopInputFailure(json, action)) {
LOG_INFO(
"Secure desktop keyboard injection transient failure, "
"key_code={}, is_down={}, response={}",
key_code, is_down, response);
return true;
}
LogSecureDesktopInputBlocked(&last_local_secure_input_block_log_tick_,
"local",
local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, is_down={}, "
"response={}",
key_code, is_down, response);
return false;
}
return true;
}
#endif
if (!keyboard_capturer_) {
return false;
}
return keyboard_capturer_->SendKeyboardCommand(key_code, is_down, scan_code,
extended) == 0;
}
void Render::ApplyRemoteKeyboardEvent(const std::string& remote_id,
const RemoteAction& remote_action) {
const int key_code = static_cast<int>(remote_action.k.key_value);
const bool is_down = remote_action.k.flag == KeyFlag::key_down;
const uint32_t scan_code = remote_action.k.scan_code;
const bool extended = remote_action.k.extended;
const bool injected =
InjectRemoteKeyboardKey(key_code, is_down, scan_code, extended);
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto& state = remote_keyboard_states_[remote_id];
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
if (is_down) {
if (injected) {
state.pressed_keys[key_code] =
PressedKeyboardKey{key_code, scan_code, extended};
}
} else if (injected) {
state.pressed_keys.erase(key_code);
}
}
void Render::ApplyRemoteKeyboardState(const std::string& remote_id,
const RemoteAction& remote_action) {
std::vector<PressedKeyboardKey> keys_to_release;
std::vector<PressedKeyboardKey> keys_to_press;
{
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto& state = remote_keyboard_states_[remote_id];
if (remote_action.ks.seq != 0 && state.last_seq != 0 &&
static_cast<int32_t>(remote_action.ks.seq - state.last_seq) <= 0) {
return;
}
state.last_seq = remote_action.ks.seq;
state.last_seen_tick = static_cast<uint32_t>(SDL_GetTicks());
state.keyboard_state_seen = true;
std::unordered_map<int, PressedKeyboardKey> desired_keys;
const size_t pressed_count =
remote_action.ks.pressed_count < kMaxKeyboardStateKeys
? remote_action.ks.pressed_count
: kMaxKeyboardStateKeys;
for (size_t idx = 0; idx < pressed_count; ++idx) {
const auto& key = remote_action.ks.pressed_keys[idx];
const int key_code = static_cast<int>(key.key_value);
desired_keys[key_code] =
PressedKeyboardKey{key_code, key.scan_code, key.extended};
}
for (const auto& [key_code, key] : state.pressed_keys) {
if (desired_keys.find(key_code) == desired_keys.end()) {
keys_to_release.push_back(key);
}
}
for (const auto& [key_code, key] : desired_keys) {
if (state.pressed_keys.find(key_code) == state.pressed_keys.end()) {
keys_to_press.push_back(key);
}
}
}
for (const PressedKeyboardKey& key : keys_to_release) {
if (InjectRemoteKeyboardKey(key.key_code, false, key.scan_code,
key.extended)) {
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto state_it = remote_keyboard_states_.find(remote_id);
if (state_it != remote_keyboard_states_.end()) {
state_it->second.pressed_keys.erase(key.key_code);
}
}
}
for (const PressedKeyboardKey& key : keys_to_press) {
if (InjectRemoteKeyboardKey(key.key_code, true, key.scan_code,
key.extended)) {
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto& state = remote_keyboard_states_[remote_id];
state.pressed_keys[key.key_code] = key;
}
}
}
void Render::ReleaseRemotePressedKeys(const std::string& remote_id,
const char* reason) {
std::vector<PressedKeyboardKey> keys_to_release;
{
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
auto state_it = remote_keyboard_states_.find(remote_id);
if (state_it == remote_keyboard_states_.end()) {
return;
}
keys_to_release.reserve(state_it->second.pressed_keys.size());
for (const auto& [_, key] : state_it->second.pressed_keys) {
keys_to_release.push_back(key);
}
remote_keyboard_states_.erase(state_it);
}
if (!keys_to_release.empty()) {
LOG_WARN("Releasing {} remote keyboard keys for remote_id={}, reason={}",
keys_to_release.size(), remote_id, reason ? reason : "unknown");
}
for (const PressedKeyboardKey& key : keys_to_release) {
InjectRemoteKeyboardKey(key.key_code, false, key.scan_code, key.extended);
}
}
void Render::CheckRemoteKeyboardTimeouts() {
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
std::vector<std::string> timed_out_remotes;
{
std::lock_guard<std::mutex> lock(remote_keyboard_states_mutex_);
for (const auto& [remote_id, state] : remote_keyboard_states_) {
if (state.keyboard_state_seen && !state.pressed_keys.empty() &&
state.last_seen_tick != 0 &&
now - state.last_seen_tick > kRemoteKeyboardReleaseTimeoutMs) {
timed_out_remotes.push_back(remote_id);
}
}
}
for (const std::string& remote_id : timed_out_remotes) {
ReleaseRemotePressedKeys(remote_id, "keyboard_heartbeat_timeout");
}
}
int Render::ProcessMouseEvent(const SDL_Event& event) { int Render::ProcessMouseEvent(const SDL_Event& event) {
controlled_remote_id_ = ""; controlled_remote_id_ = "";
RemoteAction remote_action; RemoteAction remote_action{};
float cursor_x = last_mouse_event.motion.x; float cursor_x = last_mouse_event.motion.x;
float cursor_y = last_mouse_event.motion.y; float cursor_y = last_mouse_event.motion.y;
@@ -677,10 +931,10 @@ void Render::SdlCaptureAudioIn(void* userdata, Uint8* stream, int len) {
} }
if (1) { if (1) {
// std::shared_lock lock(render->client_properties_mutex_); std::shared_lock lock(render->client_properties_mutex_);
for (const auto& it : render->client_properties_) { for (const auto& it : render->client_properties_) {
auto props = it.second; auto props = it.second;
if (props->connection_status_ == ConnectionStatus::Connected) { if (props->connection_status_.load() == ConnectionStatus::Connected) {
if (props->peer_) { if (props->peer_) {
SendAudioFrame(props->peer_, (const char*)stream, len, SendAudioFrame(props->peer_, (const char*)stream, len,
render->audio_label_.c_str()); render->audio_label_.c_str());
@@ -1073,12 +1327,20 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
return; return;
} }
// std::shared_lock lock(render->client_properties_mutex_);
if (remote_action.type == ControlType::host_infomation) { if (remote_action.type == ControlType::host_infomation) {
if (render->client_properties_.find(remote_id) != bool is_client_mode = false;
render->client_properties_.end()) { std::shared_ptr<SubStreamWindowProperties> props;
{
std::shared_lock lock(render->client_properties_mutex_);
auto props_it = render->client_properties_.find(remote_id);
if (props_it != render->client_properties_.end()) {
is_client_mode = true;
props = props_it->second;
}
}
if (is_client_mode) {
// client mode // client mode
auto props = render->client_properties_.find(remote_id)->second;
if (props && props->remote_host_name_.empty()) { if (props && props->remote_host_name_.empty()) {
props->remote_host_name_ = std::string(remote_action.i.host_name, props->remote_host_name_ = std::string(remote_action.i.host_name,
remote_action.i.host_name_size); remote_action.i.host_name_size);
@@ -1094,18 +1356,22 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
FreeRemoteAction(remote_action); FreeRemoteAction(remote_action);
} else { } else {
// server mode // server mode
render->connection_host_names_[remote_id] = std::string( std::string host_name(remote_action.i.host_name,
remote_action.i.host_name, remote_action.i.host_name_size); remote_action.i.host_name_size);
LOG_INFO("Remote hostname: [{}]", {
render->connection_host_names_[remote_id]); std::unique_lock lock(render->connection_status_mutex_);
render->connection_host_names_[remote_id] = host_name;
}
LOG_INFO("Remote hostname: [{}]", host_name);
FreeRemoteAction(remote_action); FreeRemoteAction(remote_action);
} }
} else { } else {
// remote // remote
#if _WIN32 #if _WIN32
if (render->local_service_status_received_ && if (render->local_service_status_received_ &&
render->local_service_available_ && IsSecureDesktopInteractionRequired(render->local_interactive_stage_) &&
IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) { remote_action.type != ControlType::keyboard &&
remote_action.type != ControlType::keyboard_state) {
if (remote_action.type == ControlType::mouse) { if (remote_action.type == ControlType::mouse) {
int absolute_x = 0; int absolute_x = 0;
int absolute_y = 0; int absolute_y = 0;
@@ -1136,25 +1402,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
} }
return; return;
} }
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, 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)) {
LogSecureDesktopInputBlocked(
&render->last_local_secure_input_block_log_tick_, "local",
render->local_interactive_stage_.c_str());
LOG_WARN(
"Secure desktop keyboard injection failed, key_code={}, "
"is_down={}, response={}",
key_code, is_down, response);
}
return;
}
} }
#endif #endif
if (remote_action.type == ControlType::mouse && render->mouse_controller_) { if (remote_action.type == ControlType::mouse && render->mouse_controller_) {
@@ -1165,16 +1412,19 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
render->StartSpeakerCapturer(); render->StartSpeakerCapturer();
else if (!remote_action.a && render->start_speaker_capturer_) else if (!remote_action.a && render->start_speaker_capturer_)
render->StopSpeakerCapturer(); render->StopSpeakerCapturer();
} else if (remote_action.type == ControlType::keyboard && } else if (remote_action.type == ControlType::keyboard) {
render->keyboard_capturer_) { render->ApplyRemoteKeyboardEvent(remote_id, remote_action);
render->keyboard_capturer_->SendKeyboardCommand( } else if (remote_action.type == ControlType::keyboard_state) {
(int)remote_action.k.key_value, render->ApplyRemoteKeyboardState(remote_id, remote_action);
remote_action.k.flag == KeyFlag::key_down, remote_action.k.scan_code,
remote_action.k.extended);
} else if (remote_action.type == ControlType::display_id && } else if (remote_action.type == ControlType::display_id &&
render->screen_capturer_) { render->screen_capturer_) {
render->selected_display_ = remote_action.d; const int ret = render->screen_capturer_->SwitchTo(remote_action.d);
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);
}
} }
} }
} }
@@ -1203,6 +1453,8 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
render->signal_connected_ = false; render->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) { } else if (SignalStatus::SignalServerClosed == status) {
render->signal_connected_ = false; render->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
render->signal_connected_ = false;
} }
} else { } else {
if (client_id.rfind("C-", 0) != 0) { if (client_id.rfind("C-", 0) != 0) {
@@ -1230,6 +1482,8 @@ void Render::OnSignalStatusCb(SignalStatus status, const char* user_id,
props->signal_connected_ = false; props->signal_connected_ = false;
} else if (SignalStatus::SignalServerClosed == status) { } else if (SignalStatus::SignalServerClosed == status) {
props->signal_connected_ = false; props->signal_connected_ = false;
} else if (SignalStatus::SignalTlsCertError == status) {
props->signal_connected_ = false;
} }
} }
} }
@@ -1240,14 +1494,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
if (!render) return; if (!render) return;
std::string remote_id(user_id, user_id_size); std::string remote_id(user_id, user_id_size);
// std::shared_lock lock(render->client_properties_mutex_); std::shared_ptr<SubStreamWindowProperties> props;
auto it = render->client_properties_.find(remote_id); {
auto props = (it != render->client_properties_.end()) ? it->second : nullptr; std::shared_lock lock(render->client_properties_mutex_);
auto it = render->client_properties_.find(remote_id);
if (it != render->client_properties_.end()) {
props = it->second;
}
}
if (props) { if (props) {
render->is_client_mode_ = true; render->is_client_mode_ = true;
render->show_connection_status_window_ = true; render->show_connection_status_window_ = true;
props->connection_status_ = status; props->connection_status_.store(status);
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
@@ -1313,6 +1572,7 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
case ConnectionStatus::Disconnected: case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed: case ConnectionStatus::Failed:
case ConnectionStatus::Closed: { case ConnectionStatus::Closed: {
render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
props->connection_established_ = false; props->connection_established_ = false;
props->enable_mouse_control_ = false; props->enable_mouse_control_ = false;
render->ResetRemoteServiceStatus(*props); render->ResetRemoteServiceStatus(*props);
@@ -1362,7 +1622,10 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
} else { } else {
render->is_client_mode_ = false; render->is_client_mode_ = false;
render->show_connection_status_window_ = true; render->show_connection_status_window_ = true;
render->connection_status_[remote_id] = status; {
std::unique_lock lock(render->connection_status_mutex_);
render->connection_status_[remote_id] = status;
}
switch (status) { switch (status) {
case ConnectionStatus::Connected: { case ConnectionStatus::Connected: {
@@ -1418,11 +1681,14 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->start_speaker_capturer_ = true; render->start_speaker_capturer_ = true;
render->remote_client_id_ = remote_id; render->remote_client_id_ = remote_id;
render->start_mouse_controller_ = true; render->start_mouse_controller_ = true;
if (std::all_of(render->connection_status_.begin(), {
render->connection_status_.end(), [](const auto& kv) { std::shared_lock lock(render->connection_status_mutex_);
return kv.first.find("web") != std::string::npos; if (std::all_of(render->connection_status_.begin(),
})) { render->connection_status_.end(), [](const auto& kv) {
render->show_cursor_ = true; return kv.first.find("web") != std::string::npos;
})) {
render->show_cursor_ = true;
}
} }
break; break;
@@ -1430,12 +1696,19 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
case ConnectionStatus::Disconnected: case ConnectionStatus::Disconnected:
case ConnectionStatus::Failed: case ConnectionStatus::Failed:
case ConnectionStatus::Closed: { case ConnectionStatus::Closed: {
if (std::all_of(render->connection_status_.begin(), render->ReleaseRemotePressedKeys(remote_id, "connection_closed");
render->connection_status_.end(), [](const auto& kv) { bool all_disconnected = false;
return kv.second == ConnectionStatus::Closed || {
kv.second == ConnectionStatus::Failed || std::shared_lock lock(render->connection_status_mutex_);
kv.second == ConnectionStatus::Disconnected; all_disconnected = std::all_of(
})) { render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.second == ConnectionStatus::Closed ||
kv.second == ConnectionStatus::Failed ||
kv.second == ConnectionStatus::Disconnected;
});
}
if (all_disconnected) {
render->need_to_destroy_server_window_ = true; render->need_to_destroy_server_window_ = true;
render->is_server_mode_ = false; render->is_server_mode_ = false;
#if defined(__linux__) && !defined(__APPLE__) #if defined(__linux__) && !defined(__APPLE__)
@@ -1462,18 +1735,24 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->audio_capture_ = false; render->audio_capture_ = false;
} }
render->connection_status_.erase(remote_id); {
render->connection_host_names_.erase(remote_id); std::unique_lock lock(render->connection_status_mutex_);
render->connection_status_.erase(remote_id);
render->connection_host_names_.erase(remote_id);
}
if (render->screen_capturer_) { if (render->screen_capturer_) {
render->screen_capturer_->ResetToInitialMonitor(); render->screen_capturer_->ResetToInitialMonitor();
} }
} }
if (std::all_of(render->connection_status_.begin(), {
render->connection_status_.end(), [](const auto& kv) { std::shared_lock lock(render->connection_status_mutex_);
return kv.first.find("web") == std::string::npos; if (std::all_of(render->connection_status_.begin(),
})) { render->connection_status_.end(), [](const auto& kv) {
render->show_cursor_ = false; return kv.first.find("web") == std::string::npos;
})) {
render->show_cursor_ = false;
}
} }
break; break;
+63 -24
View File
@@ -15,6 +15,22 @@
namespace crossdesk { 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) { int CountDigits(int number) {
if (number == 0) return 1; if (number == 0) return 1;
return (int)std::floor(std::log10(std::abs(number))) + 1; return (int)std::floor(std::log10(std::abs(number))) + 1;
@@ -162,6 +178,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2 btn_min = ImGui::GetItemRectMin(); ImVec2 btn_min = ImGui::GetItemRectMin();
ImVec2 btn_size_actual = ImGui::GetItemRectSize(); ImVec2 btn_size_actual = ImGui::GetItemRectSize();
ShowControlBarTooltip(
localization::select_display[localization_language_index_]);
props->display_selectable_hovered_ = false; props->display_selectable_hovered_ = false;
if (ImGui::BeginPopup("display")) { if (ImGui::BeginPopup("display")) {
@@ -173,7 +191,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
RemoteAction remote_action; RemoteAction remote_action;
remote_action.type = ControlType::display_id; remote_action.type = ControlType::display_id;
remote_action.d = i; remote_action.d = i;
if (props->connection_status_ == ConnectionStatus::Connected) { if (props->connection_status_.load() == ConnectionStatus::Connected) {
std::string msg = remote_action.to_json(); std::string msg = remote_action.to_json();
SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(), SendReliableDataFrame(props->peer_, msg.c_str(), msg.size(),
props->control_data_label_.c_str()); props->control_data_label_.c_str());
@@ -185,19 +203,19 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImGui::EndPopup(); ImGui::EndPopup();
} }
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.35f);
ImVec2 text_size = ImGui::CalcTextSize( ImVec2 text_size = ImGui::CalcTextSize(
std::to_string(props->selected_display_ + 1).c_str()); std::to_string(props->selected_display_ + 1).c_str());
ImVec2 text_pos = ImVec2 text_pos =
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.5f, ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.55f,
btn_min.y + (btn_size_actual.y - text_size.y) * 0.35f); btn_min.y + (btn_size_actual.y - text_size.y) * 0.33f);
ImGui::GetWindowDrawList()->AddText( ImGui::GetWindowDrawList()->AddText(
text_pos, IM_COL32(0, 0, 0, 255), text_pos, IM_COL32(0, 0, 0, 255),
std::to_string(props->selected_display_ + 1).c_str()); std::to_string(props->selected_display_ + 1).c_str());
auto send_service_command = [&](ServiceCommandFlag flag, auto send_service_command = [&](ServiceCommandFlag flag,
const char* log_action) { const char* log_action) {
if (props->connection_status_ == ConnectionStatus::Connected && if (props->connection_status_.load() == ConnectionStatus::Connected &&
props->peer_) { props->peer_) {
RemoteAction remote_action; RemoteAction remote_action;
remote_action.type = ControlType::service_command; remote_action.type = ControlType::service_command;
@@ -218,25 +236,14 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) { if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) {
ImGui::OpenPopup("shortcut"); ImGui::OpenPopup("shortcut");
} }
ShowControlBarTooltip(
if (ImGui::IsItemHovered()) { localization::send_shortcut[localization_language_index_]);
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text(
"%s",
localization::send_shortcut[localization_language_index_].c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
props->shortcut_selectable_hovered_ = false; props->shortcut_selectable_hovered_ = false;
if (ImGui::BeginPopup("shortcut")) { if (ImGui::BeginPopup("shortcut")) {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
std::string sas_label = std::string sas_label = "Ctrl+Alt+Del";
"Ctrl+Alt+Del - " + std::string lock_label = "Win+L";
localization::send_sas[localization_language_index_];
std::string lock_label =
"Win+L - " + localization::lock_remote[localization_language_index_];
if (ImGui::Selectable(sas_label.c_str())) { if (ImGui::Selectable(sas_label.c_str())) {
send_service_command(ServiceCommandFlag::send_sas, "SAS"); send_service_command(ServiceCommandFlag::send_sas, "SAS");
} }
@@ -268,6 +275,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: localization::control_mouse[localization_language_index_]; : 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_) { if (!props->enable_mouse_control_) {
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y), draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
@@ -280,8 +293,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2( ImVec2(
mouse_x + button_width - line_padding - line_thickness * 0.7f, mouse_x + button_width - line_padding - line_thickness * 0.7f,
mouse_y + button_height - line_padding + line_thickness * 0.7f), mouse_y + button_height - line_padding + line_thickness * 0.7f),
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255) mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255), : IM_COL32(179, 213, 253, 255),
line_thickness); line_thickness);
} }
@@ -313,6 +326,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
props->control_data_label_.c_str()); 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_) { if (!props->audio_capture_button_pressed_) {
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y), draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
@@ -325,8 +344,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2( ImVec2(
audio_x + button_width - line_padding - line_thickness * 0.7f, audio_x + button_width - line_padding - line_thickness * 0.7f,
audio_y + button_height - line_padding + line_thickness * 0.7f), audio_y + button_height - line_padding + line_thickness * 0.7f),
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255) audio_button_hovered ? IM_COL32(66, 150, 250, 255)
: IM_COL32(179, 213, 253, 255), : IM_COL32(179, 213, 253, 255),
line_thickness); line_thickness);
} }
@@ -339,6 +358,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
std::string path = OpenFileDialog(title); std::string path = OpenFileDialog(title);
ProcessSelectedFile(path, props, file_label_); ProcessSelectedFile(path, props, file_label_);
} }
ShowControlBarTooltip(
localization::select_file[localization_language_index_]);
ImGui::SameLine(); ImGui::SameLine();
// net traffic stats button // net traffic stats button
@@ -363,6 +384,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: localization::show_net_traffic_stats : localization::show_net_traffic_stats
[localization_language_index_]; [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) { if (button_color_style_pushed) {
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -389,6 +416,11 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
} }
props->reset_control_bar_pos_ = true; 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(); ImGui::SameLine();
// close button // close button
@@ -398,6 +430,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
ImVec2(button_width, button_height))) { ImVec2(button_width, button_height))) {
CleanupPeer(props); CleanupPeer(props);
} }
ShowControlBarTooltip(
localization::disconnect[localization_language_index_]);
ImGui::SameLine(); ImGui::SameLine();
@@ -427,6 +461,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
: ICON_FA_ANGLE_RIGHT) : ICON_FA_ANGLE_RIGHT)
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT : (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
: ICON_FA_ANGLE_LEFT); : 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(), if (ImGui::Button(control_bar.c_str(),
ImVec2(button_height * 0.6f, button_height))) { ImVec2(button_height * 0.6f, button_height))) {
props->control_bar_expand_ = !props->control_bar_expand_; props->control_bar_expand_ = !props->control_bar_expand_;
@@ -438,6 +476,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
props->net_traffic_stats_button_pressed_ = false; props->net_traffic_stats_button_pressed_ = false;
} }
} }
ShowControlBarTooltip(control_bar_tooltip);
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) { if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
NetTrafficStats(props); NetTrafficStats(props);
+18 -7
View File
@@ -26,20 +26,31 @@ int Render::StatusBar() {
ImDrawList* draw_list = ImGui::GetWindowDrawList(); ImDrawList* draw_list = ImGui::GetWindowDrawList();
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f, draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.25f,
ImColor(1.0f, 1.0f, 1.0f), 100); ImColor(1.0f, 1.0f, 1.0f), 100);
bool tls_cert_error =
signal_status_ == SignalStatus::SignalTlsCertError;
draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f, draw_list->AddCircleFilled(dot_pos, status_bar_height * 0.2f,
ImColor(signal_connected_ ? 0.0f : 1.0f, tls_cert_error
signal_connected_ ? 1.0f : 0.0f, 0.0f), ? ImColor(1.0f, 0.65f, 0.0f)
: ImColor(signal_connected_ ? 0.0f : 1.0f,
signal_connected_ ? 1.0f : 0.0f,
0.0f),
100); 100);
ImGui::SetWindowFontScale(0.6f); ImGui::SetWindowFontScale(0.6f);
const char* signal_status_text =
tls_cert_error
? localization::signal_tls_cert_error[localization_language_index_]
.c_str()
: (signal_connected_
? localization::signal_connected[localization_language_index_]
.c_str()
: localization::signal_disconnected
[localization_language_index_]
.c_str());
draw_list->AddText( draw_list->AddText(
ImVec2(status_bar_width * 0.045f, ImVec2(status_bar_width * 0.045f,
io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)), io.DisplaySize.y * (1 - STATUS_BAR_HEIGHT * 0.9f)),
ImColor(0.0f, 0.0f, 0.0f), ImColor(0.0f, 0.0f, 0.0f), signal_status_text);
signal_connected_
? localization::signal_connected[localization_language_index_].c_str()
: localization::signal_disconnected[localization_language_index_]
.c_str());
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
ImGui::EndChild(); ImGui::EndChild();
+2 -7
View File
@@ -300,17 +300,12 @@ int Render::TitleBar(bool main_window) {
} }
if (close_button_clicked) { if (close_button_clicked) {
#if _WIN32 const bool minimized_to_tray = main_window && MinimizeMainWindowToTray();
if (enable_minimize_to_tray_) { if (!minimized_to_tray) {
tray_->MinimizeToTray();
} else {
#endif
SDL_Event event; SDL_Event event;
event.type = SDL_EVENT_QUIT; event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event); SDL_PushEvent(&event);
#if _WIN32
} }
#endif
} }
draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f, draw_list->AddLine(ImVec2(xmark_pos_x - xmark_size / 2 - 0.25f,
File diff suppressed because it is too large Load Diff
+40
View File
@@ -0,0 +1,40 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _LINUX_TRAY_H_
#define _LINUX_TRAY_H_
#if defined(__linux__) && !defined(__APPLE__)
#include <cstdint>
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct LinuxTrayImpl;
class LinuxTray {
public:
LinuxTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index, uint32_t exit_event_type);
~LinuxTray();
bool MinimizeToTray();
void RemoveTrayIcon();
void ProcessEvents();
private:
std::unique_ptr<LinuxTrayImpl> impl_;
};
} // namespace crossdesk
#endif // defined(__linux__) && !defined(__APPLE__)
#endif // _LINUX_TRAY_H_
+34
View File
@@ -0,0 +1,34 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-06-23
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _MAC_TRAY_H_
#define _MAC_TRAY_H_
#include <memory>
#include <string>
struct SDL_Window;
namespace crossdesk {
struct MacTrayImpl;
class MacTray {
public:
MacTray(::SDL_Window* app_window, const std::string& tooltip,
int language_index);
~MacTray();
void MinimizeToTray();
void RemoveTrayIcon();
private:
std::unique_ptr<MacTrayImpl> impl_;
};
} // namespace crossdesk
#endif // _MAC_TRAY_H_
+249
View File
@@ -0,0 +1,249 @@
#include "mac_tray.h"
#if defined(__APPLE__)
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
#include "localization.h"
#include <utility>
@interface CrossDeskMacTrayTarget : NSObject
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner;
- (void)statusItemClicked:(id)sender;
- (void)exitApplication:(id)sender;
@end
namespace crossdesk {
struct MacTrayImpl {
explicit MacTrayImpl(::SDL_Window *window, std::string tray_tooltip,
int language_index_value)
: app_window(window),
tooltip(std::move(tray_tooltip)),
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {}
~MacTrayImpl() {
RemoveTrayIcon();
target = nil;
}
void MinimizeToTray() {
EnsureStatusItem();
if (app_window) {
SDL_HideWindow(app_window);
}
}
void RemoveTrayIcon() {
if (!status_item) {
return;
}
[[NSStatusBar systemStatusBar] removeStatusItem:status_item];
status_item = nil;
}
void ShowWindow() {
if (!app_window) {
return;
}
SDL_ShowWindow(app_window);
SDL_RaiseWindow(app_window);
[NSApp activateIgnoringOtherApps:YES];
}
void ShowMenu() {
EnsureStatusItem();
if (!status_item) {
return;
}
NSMenu *menu = [[NSMenu alloc] initWithTitle:@"CrossDesk"];
NSString *exit_title =
NSStringFromUtf8(localization::exit_program
[localization::detail::ClampLanguageIndex(
language_index)]);
NSMenuItem *exit_item = [[NSMenuItem alloc] initWithTitle:exit_title
action:@selector(exitApplication:)
keyEquivalent:@""];
[exit_item setTarget:target];
[menu addItem:exit_item];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
const NSRect bounds = [button bounds];
[menu popUpMenuPositioningItem:nil
atLocation:NSMakePoint(NSMinX(bounds), NSMinY(bounds))
inView:button];
}
void RequestExit() {
SDL_Event event;
event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&event);
}
private:
void EnsureStatusItem() {
if (status_item) {
return;
}
status_item = [[NSStatusBar systemStatusBar]
statusItemWithLength:NSSquareStatusItemLength];
NSStatusBarButton *button = [status_item button];
if (!button) {
return;
}
[button setToolTip:NSStringFromUtf8(tooltip)];
NSImage *crossdesk_icon = LoadCrossDeskIcon();
if (crossdesk_icon) {
NSImage *status_icon = [crossdesk_icon copy];
[status_icon setSize:NSMakeSize(18.0, 18.0)];
[status_icon setTemplate:NO];
[button setImage:status_icon];
[button setImagePosition:NSImageOnly];
} else {
[button setTitle:@"CD"];
}
[button setTarget:target];
[button setAction:@selector(statusItemClicked:)];
[button sendActionOn:NSEventMaskLeftMouseUp | NSEventMaskRightMouseUp];
}
NSString *NSStringFromUtf8(const std::string &text) {
return [NSString stringWithUTF8String:text.c_str()];
}
NSImage *LoadCrossDeskIcon() {
NSImage *icon = LoadIconFromBundleResource(@"crossdesk");
if (!icon) {
icon = LoadIconFromBundleResource(@"crossedesk");
}
if (!icon) {
icon = LoadIconFromDevelopmentPath();
}
if (!icon) {
icon = [NSApp applicationIconImage];
}
return icon;
}
NSImage *LoadIconFromBundleResource(NSString *resource_name) {
NSString *icon_path =
[[NSBundle mainBundle] pathForResource:resource_name ofType:@"icns"];
return LoadIconFromPath(icon_path);
}
NSImage *LoadIconFromDevelopmentPath() {
NSMutableArray<NSString *> *candidate_paths = [NSMutableArray array];
NSString *current_directory =
[[NSFileManager defaultManager] currentDirectoryPath];
[candidate_paths
addObject:[current_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
const char *base_path = SDL_GetBasePath();
if (base_path && base_path[0] != '\0') {
NSString *base_directory = NSStringFromUtf8(base_path);
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"icons/macos/crossdesk.icns"]];
[candidate_paths
addObject:[base_directory
stringByAppendingPathComponent:
@"../../../../icons/macos/crossdesk.icns"]];
}
for (NSString *candidate_path in candidate_paths) {
NSImage *icon = LoadIconFromPath(
[candidate_path stringByStandardizingPath]);
if (icon) {
return icon;
}
}
return nil;
}
NSImage *LoadIconFromPath(NSString *icon_path) {
if (![icon_path length]) {
return nil;
}
if (![[NSFileManager defaultManager] fileExistsAtPath:icon_path]) {
return nil;
}
return [[NSImage alloc] initWithContentsOfFile:icon_path];
}
::SDL_Window *app_window = nullptr;
std::string tooltip;
int language_index = 0;
NSStatusItem *status_item = nil;
CrossDeskMacTrayTarget *target = nil;
};
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
int language_index)
: impl_(
std::make_unique<MacTrayImpl>(app_window, tooltip, language_index)) {}
MacTray::~MacTray() = default;
void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
} // namespace crossdesk
@implementation CrossDeskMacTrayTarget {
crossdesk::MacTrayImpl *owner_;
}
- (instancetype)initWithOwner:(crossdesk::MacTrayImpl *)owner {
self = [super init];
if (self) {
owner_ = owner;
}
return self;
}
- (void)statusItemClicked:(id)sender {
(void)sender;
if (!owner_) {
return;
}
NSEvent *event = [NSApp currentEvent];
if (event && [event type] == NSEventTypeRightMouseUp) {
owner_->ShowMenu();
return;
}
owner_->ShowWindow();
}
- (void)exitApplication:(id)sender {
(void)sender;
if (owner_) {
owner_->RequestExit();
}
}
@end
#endif // __APPLE__
+24 -8
View File
@@ -31,8 +31,9 @@ bool Render::ConnectionStatusWindow(
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
std::string text; std::string text;
const ConnectionStatus status = props->connection_status_.load();
if (ConnectionStatus::Connecting == props->connection_status_) { if (ConnectionStatus::Connecting == status) {
text = localization::p2p_connecting[localization_language_index_]; text = localization::p2p_connecting[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -48,7 +49,23 @@ bool Render::ConnectionStatusWindow(
} }
ret_flag = true; ret_flag = true;
} }
} else if (ConnectionStatus::Connected == props->connection_status_) { } else if (ConnectionStatus::Gathering == status) {
text = localization::p2p_gathering[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
// cancel
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str()) ||
ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false;
re_enter_remote_id_ = true;
LOG_INFO("User cancelled connecting to [{}]", props->remote_id_);
if (props->peer_) {
LeaveConnection(props->peer_, props->remote_id_.c_str());
}
ret_flag = true;
}
} else if (ConnectionStatus::Connected == status) {
text = localization::p2p_connected[localization_language_index_]; text = localization::p2p_connected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -58,7 +75,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::Disconnected == props->connection_status_) { } else if (ConnectionStatus::Disconnected == status) {
text = localization::p2p_disconnected[localization_language_index_]; text = localization::p2p_disconnected[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -68,7 +85,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::Failed == props->connection_status_) { } else if (ConnectionStatus::Failed == status) {
text = localization::p2p_failed[localization_language_index_]; text = localization::p2p_failed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -78,7 +95,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::Closed == props->connection_status_) { } else if (ConnectionStatus::Closed == status) {
text = localization::p2p_closed[localization_language_index_]; text = localization::p2p_closed[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
@@ -88,7 +105,7 @@ bool Render::ConnectionStatusWindow(
ImGui::IsKeyPressed(ImGuiKey_Escape)) { ImGui::IsKeyPressed(ImGuiKey_Escape)) {
show_connection_status_window_ = false; show_connection_status_window_ = false;
} }
} else if (ConnectionStatus::IncorrectPassword == props->connection_status_) { } else if (ConnectionStatus::IncorrectPassword == status) {
if (!password_validating_) { if (!password_validating_) {
if (password_validating_time_ == 1) { if (password_validating_time_ == 1) {
text = localization::input_password[localization_language_index_]; text = localization::input_password[localization_language_index_];
@@ -151,8 +168,7 @@ bool Render::ConnectionStatusWindow(
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
} }
} else if (ConnectionStatus::NoSuchTransmissionId == } else if (ConnectionStatus::NoSuchTransmissionId == status) {
props->connection_status_) {
text = localization::no_such_id[localization_language_index_]; text = localization::no_such_id[localization_language_index_];
ImGui::SetCursorPosX(connection_status_window_width * 0.43f); ImGui::SetCursorPosX(connection_status_window_width * 0.43f);
ImGui::SetCursorPosY(connection_status_window_height * 0.67f); ImGui::SetCursorPosY(connection_status_window_height * 0.67f);
+113 -39
View File
@@ -4,10 +4,19 @@
#include "render.h" #include "render.h"
#include "tinyfiledialogs.h" #include "tinyfiledialogs.h"
#if _WIN32 && CROSSDESK_PORTABLE
#include "service_host.h"
#endif
namespace crossdesk { namespace crossdesk {
int Render::SettingWindow() { int Render::SettingWindow() {
ImGuiIO& io = ImGui::GetIO(); 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 (show_settings_window_) {
if (settings_window_pos_reset_) { if (settings_window_pos_reset_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport(); const ImGuiViewport* viewport = ImGui::GetMainViewport();
@@ -18,12 +27,14 @@ int Render::SettingWindow() {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( 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 #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f)); ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( 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 #endif
} else { } else {
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
@@ -32,12 +43,14 @@ int Render::SettingWindow() {
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f));
ImGui::SetNextWindowSize( 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 #else
ImGui::SetNextWindowPos( ImGui::SetNextWindowPos(
ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f)); ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f));
ImGui::SetNextWindowSize( 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 #endif
} }
@@ -73,23 +86,21 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } 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); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
if (ImGui::BeginCombo( if (ImGui::BeginCombo("##language",
"##language", localization::GetSupportedLanguages()
localization::GetSupportedLanguages() [localization::detail::ClampLanguageIndex(
[localization::detail::ClampLanguageIndex( language_button_value_)]
language_button_value_)] .display_name.c_str())) {
.display_name
.c_str())) {
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
for (int i = 0; i < static_cast<int>(supported_languages.size()); for (int i = 0; i < static_cast<int>(supported_languages.size());
++i) { ++i) {
bool selected = (i == language_button_value_); bool selected = (i == language_button_value_);
if (ImGui::Selectable( if (ImGui::Selectable(supported_languages[i].display_name.c_str(),
supported_languages[i].display_name.c_str(), selected)) selected))
language_button_value_ = i; language_button_value_ = i;
if (selected) { if (selected) {
ImGui::SetItemDefaultFocus(); ImGui::SetItemDefaultFocus();
@@ -125,7 +136,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } 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); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -158,7 +169,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } 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); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -194,7 +205,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f);
} else { } 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); ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f);
@@ -228,7 +239,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_hardware_video_codec", ImGui::Checkbox("##enable_hardware_video_codec",
@@ -249,7 +260,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_turn", &enable_turn_); ImGui::Checkbox("##enable_turn", &enable_turn_);
@@ -268,7 +279,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_srtp", &enable_srtp_); ImGui::Checkbox("##enable_srtp", &enable_srtp_);
@@ -289,7 +300,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } 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_); ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_);
@@ -308,7 +319,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_autostart_", &enable_autostart_); ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
@@ -327,7 +338,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_daemon_", &enable_daemon_); ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
@@ -345,10 +356,6 @@ int Render::SettingWindow() {
ImGui::Separator(); ImGui::Separator();
{ {
#ifndef _WIN32
ImGui::BeginDisabled();
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(0.5f, 0.5f, 0.5f, 1.0f));
#endif
settings_items_offset += settings_items_padding; settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset); ImGui::SetCursorPosY(settings_items_offset);
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
@@ -359,15 +366,11 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
} }
ImGui::Checkbox("##enable_minimize_to_tray_", ImGui::Checkbox("##enable_minimize_to_tray_",
&enable_minimize_to_tray_); &enable_minimize_to_tray_);
#ifndef _WIN32
ImGui::PopStyleColor();
ImGui::EndDisabled();
#endif
} }
ImGui::Separator(); ImGui::Separator();
@@ -384,7 +387,7 @@ int Render::SettingWindow() {
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f); ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f);
} else { } else {
ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f); ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f);
} }
std::string display_path = std::string display_path =
@@ -429,6 +432,80 @@ int Render::SettingWindow() {
ImGui::EndDisabled(); 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_) { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f); ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f);
} else { } else {
@@ -436,7 +513,7 @@ int Render::SettingWindow() {
} }
settings_items_offset += 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::SetCursorPosY(settings_items_offset);
ImGui::PopStyleVar(); ImGui::PopStyleVar();
@@ -463,9 +540,8 @@ int Render::SettingWindow() {
LOG_INFO("Set localization language: {}", LOG_INFO("Set localization language: {}",
localization::GetSupportedLanguages() localization::GetSupportedLanguages()
[localization::detail::ClampLanguageIndex( [localization::detail::ClampLanguageIndex(
localization_language_index_)] localization_language_index_)]
.code .code.c_str());
.c_str());
// Video quality // Video quality
if (video_quality_button_value_ == 0) { if (video_quality_button_value_ == 0) {
@@ -542,14 +618,12 @@ int Render::SettingWindow() {
} }
enable_daemon_last_ = enable_daemon_; enable_daemon_last_ = enable_daemon_;
#if _WIN32
if (enable_minimize_to_tray_) { if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true); config_center_->SetMinimizeToTray(true);
} else { } else {
config_center_->SetMinimizeToTray(false); config_center_->SetMinimizeToTray(false);
} }
enable_minimize_to_tray_last_ = enable_minimize_to_tray_; enable_minimize_to_tray_last_ = enable_minimize_to_tray_;
#endif
// File transfer save path // File transfer save path
config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_); config_center_->SetFileTransferSavePath(file_transfer_save_path_buf_);
@@ -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
+99 -24
View File
@@ -6,11 +6,27 @@
#include <ApplicationServices/ApplicationServices.h> #include <ApplicationServices/ApplicationServices.h>
#include <CoreGraphics/CoreGraphics.h> #include <CoreGraphics/CoreGraphics.h>
#import <Foundation/Foundation.h> #import <Foundation/Foundation.h>
#include <unistd.h>
#include <cstdlib> #include <cstdlib>
#include <string>
namespace crossdesk { 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) { bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
const float TRACK_HEIGHT = ImGui::GetFrameHeight(); const float TRACK_HEIGHT = ImGui::GetFrameHeight();
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f; 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 hovered = ImGui::IsItemHovered();
bool clicked = ImGui::IsItemClicked() && enabled; bool clicked = ImGui::IsItemClicked() && enabled;
ImVec4 track_color = active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE) ImVec4 track_color =
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE); active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
if (!enabled) { if (!enabled) {
track_color.w *= DISABLED_ALPHA; track_color.w *= DISABLED_ALPHA;
} }
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f); 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); ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH,
draw_list->AddRectFilled(track_min, track_max, ImGui::GetColorU32(track_color), TRACK_RADIUS); 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_position = active ? 1.0f : 0.0f;
float knob_min_x = track_pos.x + KNOB_PADDING; 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_min = ImVec2(knob_x, knob_y);
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT); 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; return clicked;
} }
@@ -81,29 +101,82 @@ bool Render::CheckAccessibilityPermission() {
} }
void Render::OpenAccessibilityPreferences() { void Render::OpenAccessibilityPreferences() {
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES}; if (!mac_accessibility_permission_requested_) {
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options); NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
system("open " } else {
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_" OpenPrivacyPreferences("Privacy_Accessibility");
"Accessibility\""); }
} }
void Render::OpenScreenRecordingPreferences() { void Render::OpenScreenRecordingPreferences() {
if (@available(macOS 10.15, *)) { 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 " const bool old_screen_recording_granted =
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_" mac_screen_recording_permission_granted_;
"ScreenCapture\""); 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() { int Render::RequestPermissionWindow() {
bool screen_recording_granted = CheckScreenRecordingPermission(); RefreshMacPermissionStatus(false);
bool accessibility_granted = CheckAccessibilityPermission();
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_) { if (!show_request_permission_window_) {
return 0; return 0;
@@ -162,8 +235,10 @@ int Render::RequestPermissionWindow() {
if (accessibility_granted) { if (accessibility_granted) {
DrawToggleSwitch("accessibility_toggle_on", true, false); DrawToggleSwitch("accessibility_toggle_on", true, false);
} else { } else {
if (DrawToggleSwitch("accessibility_toggle", accessibility_granted, !accessibility_granted)) { if (DrawToggleSwitch("accessibility_toggle", false, true)) {
OpenAccessibilityPreferences(); OpenAccessibilityPreferences();
mac_accessibility_permission_requested_ = true;
RefreshMacPermissionStatus(true);
} }
} }
@@ -178,12 +253,12 @@ int Render::RequestPermissionWindow() {
ImGui::AlignTextToFramePadding(); ImGui::AlignTextToFramePadding();
ImGui::SetCursorPosX(checkbox_padding); ImGui::SetCursorPosX(checkbox_padding);
if (screen_recording_granted) { if (screen_recording_granted) {
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.0f);
DrawToggleSwitch("screen_recording_toggle_on", true, false); DrawToggleSwitch("screen_recording_toggle_on", true, false);
} else { } else {
if (DrawToggleSwitch("screen_recording_toggle", screen_recording_granted, if (DrawToggleSwitch("screen_recording_toggle", false, true)) {
!screen_recording_granted)) {
OpenScreenRecordingPreferences(); OpenScreenRecordingPreferences();
mac_screen_recording_permission_requested_ = true;
RefreshMacPermissionStatus(true);
} }
} }
@@ -202,4 +277,4 @@ int Render::RequestPermissionWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
+22 -13
View File
@@ -1,6 +1,8 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <cstdio> #include <cstdio>
#include <memory>
#include <shared_mutex>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -149,14 +151,17 @@ int Render::RemoteClientInfoWindow() {
float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f; float font_scale = localization_language_index_ == 0 ? 0.5f : 0.45f;
std::vector<std::pair<std::string, std::string>> remote_entries; std::vector<std::pair<std::string, std::string>> remote_entries;
remote_entries.reserve(connection_status_.size()); {
for (const auto& kv : connection_status_) { std::shared_lock lock(connection_status_mutex_);
const auto host_it = connection_host_names_.find(kv.first); remote_entries.reserve(connection_status_.size());
const std::string display_name = for (const auto& kv : connection_status_) {
(host_it != connection_host_names_.end() && !host_it->second.empty()) const auto host_it = connection_host_names_.find(kv.first);
? host_it->second const std::string display_name =
: kv.first; (host_it != connection_host_names_.end() && !host_it->second.empty())
remote_entries.emplace_back(kv.first, display_name); ? host_it->second
: kv.first;
remote_entries.emplace_back(kv.first, display_name);
}
} }
auto find_display_name_by_remote_id = auto find_display_name_by_remote_id =
@@ -220,10 +225,14 @@ int Render::RemoteClientInfoWindow() {
ImGui::SetWindowFontScale(font_scale); ImGui::SetWindowFontScale(font_scale);
if (!selected_server_remote_id_.empty()) { if (!selected_server_remote_id_.empty()) {
auto it = connection_status_.find(selected_server_remote_id_); ConnectionStatus status = ConnectionStatus::Closed;
const ConnectionStatus status = (it == connection_status_.end()) {
? ConnectionStatus::Closed std::shared_lock lock(connection_status_mutex_);
: it->second; auto it = connection_status_.find(selected_server_remote_id_);
if (it != connection_status_.end()) {
status = it->second;
}
}
ImGui::Text( ImGui::Text(
"%s", "%s",
@@ -376,4 +385,4 @@ int Render::RemoteClientInfoWindow() {
return 0; return 0;
} }
} // namespace crossdesk } // namespace crossdesk
+2 -2
View File
@@ -7,7 +7,7 @@ namespace crossdesk {
void Render::DrawConnectionStatusText( void Render::DrawConnectionStatusText(
std::shared_ptr<SubStreamWindowProperties>& props) { std::shared_ptr<SubStreamWindowProperties>& props) {
std::string text; std::string text;
switch (props->connection_status_) { switch (props->connection_status_.load()) {
case ConnectionStatus::Disconnected: case ConnectionStatus::Disconnected:
text = localization::p2p_disconnected[localization_language_index_]; text = localization::p2p_disconnected[localization_language_index_];
break; break;
@@ -34,7 +34,7 @@ void Render::DrawConnectionStatusText(
void Render::DrawReceivingScreenText( void Render::DrawReceivingScreenText(
std::shared_ptr<SubStreamWindowProperties>& props) { std::shared_ptr<SubStreamWindowProperties>& props) {
if (!props->connection_established_ || if (!props->connection_established_ ||
props->connection_status_ != ConnectionStatus::Connected) { props->connection_status_.load() != ConnectionStatus::Connected) {
return; return;
} }
@@ -16,7 +16,11 @@ int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) {
} }
screen_capturer_sck_impl_ = CreateScreenCapturerSck(); 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; return 0;
} }
@@ -29,8 +33,11 @@ int ScreenCapturerSck::Destroy() {
} }
int ScreenCapturerSck::Start(bool show_cursor) { int ScreenCapturerSck::Start(bool show_cursor) {
screen_capturer_sck_impl_->Start(show_cursor); if (!screen_capturer_sck_impl_) {
return 0; return -1;
}
return screen_capturer_sck_impl_->Start(show_cursor);
} }
int ScreenCapturerSck::Stop() { int ScreenCapturerSck::Stop() {
@@ -80,4 +87,4 @@ std::vector<DisplayInfo> ScreenCapturerSck::GetDisplayInfoList() {
void ScreenCapturerSck::OnFrame() {} void ScreenCapturerSck::OnFrame() {}
void ScreenCapturerSck::CleanUp() {} void ScreenCapturerSck::CleanUp() {}
} // namespace crossdesk } // namespace crossdesk
@@ -16,7 +16,12 @@
#include <IOKit/graphics/IOGraphicsLib.h> #include <IOKit/graphics/IOGraphicsLib.h>
#include <IOSurface/IOSurface.h> #include <IOSurface/IOSurface.h>
#include <ScreenCaptureKit/ScreenCaptureKit.h> #include <ScreenCaptureKit/ScreenCaptureKit.h>
#include <algorithm>
#include <atomic> #include <atomic>
#include <cctype>
#include <cstring>
#include <limits>
#include <map>
#include <mutex> #include <mutex>
#include <vector> #include <vector>
#include "display_info.h" #include "display_info.h"
@@ -28,6 +33,15 @@ class ScreenCapturerSckImpl;
static const int kFullDesktopScreenId = -1; 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 // 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. // was reported to be broken before macOS 13 - see http://crbug.com/40234870.
// Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were // 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, int> display_id_map_reverse_;
std::map<CGDirectDisplayID, std::string> display_id_name_map_; std::map<CGDirectDisplayID, std::string> display_id_name_map_;
unsigned char *nv12_frame_ = nullptr; unsigned char *nv12_frame_ = nullptr;
size_t nv12_frame_size_ = 0;
int width_ = 0; int width_ = 0;
int height_ = 0; int height_ = 0;
int fps_ = 60; 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++ // 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 // 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. // 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 // Callback for returning captured frames, or errors, to the caller. Only used on the caller's
// thread. // thread.
cb_desktop_data _on_data = nullptr; 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. // Guards some variables that may be accessed on different threads.
std::mutex lock_; std::mutex lock_;
// Provides captured desktop frames. // 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 // 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. // support full-desktop capture, and will fall back to the first display.
CGDirectDisplayID current_display_ = 0; CGDirectDisplayID current_display_ = 0;
@@ -182,6 +197,19 @@ ScreenCapturerSckImpl::ScreenCapturerSckImpl() {
} }
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_info_list_.clear();
display_id_map_.clear(); display_id_map_.clear();
display_id_map_reverse_.clear(); display_id_map_reverse_.clear();
@@ -190,15 +218,22 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() {
if (nv12_frame_) { if (nv12_frame_) {
delete[] nv12_frame_; delete[] nv12_frame_;
nv12_frame_ = nullptr; nv12_frame_ = nullptr;
nv12_frame_size_ = 0;
} }
[stream_ stopCaptureWithCompletionHandler:nil];
[helper_ releaseCapturer];
} }
int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) { int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
if (!cb) {
LOG_ERROR("Screen capturer callback is null");
return -1;
}
_on_data = cb; _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, *)) { if (@available(macOS 10.15, *)) {
bool has_permission = CGPreflightScreenCaptureAccess(); bool has_permission = CGPreflightScreenCaptureAccess();
@@ -216,8 +251,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) { getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) {
if (error) { if (error) {
capture_error = error; capture_error = error;
LOG_ERROR("Failed to get shareable content: {}", LOG_ERROR("Failed to get shareable content: {}", NSErrorToString(error));
std::string([error.localizedDescription UTF8String]));
} else { } else {
content = result; content = result;
} }
@@ -227,7 +261,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) {
if (capture_error || !content || content.displays.count == 0) { if (capture_error || !content || content.displays.count == 0) {
LOG_ERROR("Failed to get display info, error: {}", LOG_ERROR("Failed to get display info, error: {}",
std::string([capture_error.localizedDescription UTF8String])); NSErrorToString(capture_error));
return -1; return -1;
} }
@@ -284,51 +318,58 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
} }
int ScreenCapturerSckImpl::SwitchTo(int monitor_index) { int ScreenCapturerSckImpl::SwitchTo(int monitor_index) {
if (stream_) { auto display_it = display_id_map_.find(monitor_index);
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) { if (display_it == display_id_map_.end()) {
std::lock_guard<std::mutex> lock(lock_); LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}",
stream_ = nil; monitor_index, display_id_map_.size());
current_display_ = display_id_map_[monitor_index]; return -1;
StartOrReconfigureCapturer();
}];
} else {
current_display_ = display_id_map_[monitor_index];
StartOrReconfigureCapturer();
} }
const CGDirectDisplayID target_display = display_it->second;
{
std::lock_guard<std::mutex> lock(lock_);
current_display_ = target_display;
}
StartOrReconfigureCapturer();
return 0; return 0;
} }
int ScreenCapturerSckImpl::ResetToInitialMonitor() { int ScreenCapturerSckImpl::ResetToInitialMonitor() {
int target = initial_monitor_index_; int target = initial_monitor_index_;
if (display_info_list_.empty()) return -1; if (display_info_list_.empty()) return -1;
CGDirectDisplayID target_display = display_id_map_[target]; auto display_it = display_id_map_.find(target);
if (current_display_ == target_display) return 0; if (display_it == display_id_map_.end()) {
if (stream_) { LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target);
[stream_ stopCaptureWithCompletionHandler:^(NSError *error) { return -1;
std::lock_guard<std::mutex> lock(lock_);
stream_ = nil;
current_display_ = target_display;
StartOrReconfigureCapturer();
}];
} else {
current_display_ = target_display;
StartOrReconfigureCapturer();
} }
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; return 0;
} }
int ScreenCapturerSckImpl::Destroy() { int ScreenCapturerSckImpl::Destroy() {
std::lock_guard<std::mutex> lock(lock_); SckHelper *helper_to_release = nil;
if (stream_) { {
LOG_INFO("Destroying stream"); std::lock_guard<std::mutex> lock(lock_);
[stream_ stopCaptureWithCompletionHandler:nil]; if (stream_) {
stream_ = nil; 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; [helper_to_release releaseCapturer];
_on_data = nullptr;
[helper_ releaseCapturer];
helper_ = nil;
return 0; return 0;
} }
@@ -416,7 +457,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
// TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for // TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for
// best performance. // 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); dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL);
bool add_stream_output_result = [stream_ addStreamOutput:helper_ bool add_stream_output_result = [stream_ addStreamOutput:helper_
type:SCStreamOutputTypeScreen type:SCStreamOutputTypeScreen
@@ -425,7 +466,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
if (!add_stream_output_result) { if (!add_stream_output_result) {
stream_ = nil; stream_ = nil;
LOG_ERROR("addStreamOutput failed"); LOG_ERROR("addStreamOutput failed: {}", NSErrorToString(add_stream_output_error));
permanent_error_ = true; permanent_error_ = true;
return; return;
} }
@@ -436,7 +477,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
// calls stopCaptureWithCompletionHandler on the stream, which cancels // calls stopCaptureWithCompletionHandler on the stream, which cancels
// this handler. // this handler.
permanent_error_ = true; permanent_error_ = true;
LOG_ERROR("startCaptureWithCompletionHandler failed"); LOG_ERROR("startCaptureWithCompletionHandler failed: {}", NSErrorToString(error));
} else { } else {
LOG_INFO("Capture started"); LOG_INFO("Capture started");
} }
@@ -448,8 +489,18 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer, void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
CFDictionaryRef attachment) { CFDictionaryRef attachment) {
(void)attachment;
if (!pixelBuffer) {
return;
}
size_t width = CVPixelBufferGetWidth(pixelBuffer); size_t width = CVPixelBufferGetWidth(pixelBuffer);
size_t height = CVPixelBufferGetHeight(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); CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
if (status != kCVReturnSuccess) { if (status != kCVReturnSuccess) {
@@ -458,18 +509,37 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
} }
size_t required_size = width * height * 3 / 2; 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_; delete[] nv12_frame_;
nv12_frame_ = new unsigned char[required_size]; nv12_frame_ = new unsigned char[required_size];
width_ = width; nv12_frame_size_ = required_size;
height_ = height;
} }
width_ = static_cast<int>(width);
height_ = static_cast<int>(height);
void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0);
size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0);
void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1);
size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(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_; unsigned char *dst_y = nv12_frame_;
for (size_t row = 0; row < height; ++row) { 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); 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()); display_id_name_map_[current_display_].c_str());
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
@@ -503,10 +574,14 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
} }
SckHelper *local_helper = helper_; SckHelper *local_helper = helper_;
if (!local_helper) {
LOG_ERROR("Cannot reconfigure capturer: helper is null");
return;
}
auto handler = ^(SCShareableContent *content, NSError *error) { auto handler = ^(SCShareableContent *content, NSError *error) {
if (error) { if (error) {
LOG_ERROR("getShareableContent failed: {}", LOG_ERROR("getShareableContent failed: {}", NSErrorToString(error));
std::string([error.localizedDescription UTF8String]));
[local_helper onShareableContentCreated:nil]; [local_helper onShareableContentCreated:nil];
return; return;
} }
@@ -576,4 +651,4 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() { std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() {
return std::make_unique<ScreenCapturerSckImpl>(); return std::make_unique<ScreenCapturerSckImpl>();
} }
@@ -111,6 +111,7 @@ int ScreenCapturerDxgi::Resume(int monitor_index) {
} }
int ScreenCapturerDxgi::SwitchTo(int monitor_index) { int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) { if (monitor_index < 0 || monitor_index >= (int)display_info_list_.size()) {
LOG_ERROR("DXGI: invalid monitor index {}", monitor_index); LOG_ERROR("DXGI: invalid monitor index {}", monitor_index);
return -1; return -1;
@@ -121,6 +122,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
if (!CreateDuplicationForMonitor(monitor_index_)) { if (!CreateDuplicationForMonitor(monitor_index_)) {
LOG_ERROR("DXGI: create duplication failed for monitor {}", LOG_ERROR("DXGI: create duplication failed for monitor {}",
monitor_index_.load()); monitor_index_.load());
paused_ = false; // Reset paused_ on failure
return -2; return -2;
} }
paused_ = false; paused_ = false;
@@ -130,6 +132,7 @@ int ScreenCapturerDxgi::SwitchTo(int monitor_index) {
} }
int ScreenCapturerDxgi::ResetToInitialMonitor() { int ScreenCapturerDxgi::ResetToInitialMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
if (display_info_list_.empty()) return -1; if (display_info_list_.empty()) return -1;
int target = initial_monitor_index_; int target = initial_monitor_index_;
if (target < 0 || target >= (int)display_info_list_.size()) return -1; if (target < 0 || target >= (int)display_info_list_.size()) return -1;
@@ -244,6 +247,32 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
return true; return true;
} }
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
std::lock_guard<std::mutex> lock(switch_mutex_);
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() { void ScreenCapturerDxgi::ReleaseDuplication() {
staging_.Reset(); staging_.Reset();
if (duplication_) { if (duplication_) {
@@ -254,6 +283,8 @@ void ScreenCapturerDxgi::ReleaseDuplication() {
void ScreenCapturerDxgi::CaptureLoop() { void ScreenCapturerDxgi::CaptureLoop() {
const int timeout_ms = 33; const int timeout_ms = 33;
auto last_duplication_retry =
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
while (running_) { while (running_) {
if (paused_) { if (paused_) {
std::this_thread::sleep_for(std::chrono::milliseconds(10)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
@@ -261,6 +292,11 @@ void ScreenCapturerDxgi::CaptureLoop() {
} }
if (!duplication_) { 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)); std::this_thread::sleep_for(std::chrono::milliseconds(10));
continue; continue;
} }
@@ -274,9 +310,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
} }
if (FAILED(hr)) { if (FAILED(hr)) {
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr); LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
// attempt to recreate duplication RecreateDuplicationForCurrentMonitor();
ReleaseDuplication();
CreateDuplicationForMonitor(monitor_index_);
continue; continue;
} }
@@ -344,8 +378,14 @@ void ScreenCapturerDxgi::CaptureLoop() {
even_width, even_width, even_height); even_width, even_width, even_height);
if (callback_) { if (callback_) {
callback_(nv12_frame_, nv12_size, even_width, even_height, int idx = monitor_index_.load();
display_info_list_[monitor_index_].name.c_str()); if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
callback_(nv12_frame_, nv12_size, even_width, even_height,
display_info_list_[idx].name.c_str());
} else {
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
}
} }
d3d_context_->Unmap(staging_.Get(), 0); d3d_context_->Unmap(staging_.Get(), 0);
@@ -353,4 +393,4 @@ void ScreenCapturerDxgi::CaptureLoop() {
} }
} }
} // namespace crossdesk } // namespace crossdesk
@@ -50,6 +50,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
bool InitializeDxgi(); bool InitializeDxgi();
void EnumerateDisplays(); void EnumerateDisplays();
bool CreateDuplicationForMonitor(int monitor_index); bool CreateDuplicationForMonitor(int monitor_index);
bool RecreateDuplicationForCurrentMonitor();
void CaptureLoop(); void CaptureLoop();
void ReleaseDuplication(); void ReleaseDuplication();
@@ -71,6 +72,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
std::thread thread_; std::thread thread_;
int fps_ = 60; int fps_ = 60;
cb_desktop_data callback_ = nullptr; cb_desktop_data callback_ = nullptr;
std::mutex switch_mutex_;
unsigned char* nv12_frame_ = nullptr; unsigned char* nv12_frame_ = nullptr;
int nv12_width_ = 0; int nv12_width_ = 0;
@@ -78,4 +80,4 @@ class ScreenCapturerDxgi : public ScreenCapturer {
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif
@@ -148,7 +148,14 @@ void ScreenCapturerGdi::CaptureLoop() {
continue; continue;
} }
const auto& di = display_info_list_[monitor_index_]; int idx = monitor_index_.load();
if (idx < 0 || idx >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("GDI: CaptureLoop invalid monitor_index {} (list size {})",
idx, display_info_list_.size());
std::this_thread::sleep_for(std::chrono::milliseconds(interval_ms));
continue;
}
const auto& di = display_info_list_[idx];
int left = di.left; int left = di.left;
int top = di.top; int top = di.top;
int width = di.width & ~1; int width = di.width & ~1;
@@ -100,8 +100,7 @@ bool ScreenCapturerWgc::IsWgcSupported() {
} }
int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) { int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
int error = 0; if (inited_ == true) return 0;
if (inited_ == true) return error;
// nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2]; // nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2];
// nv12_frame_scaled_ = new unsigned char[1280 * 720 * 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()) { if (!IsWgcSupported()) {
LOG_ERROR("WGC not supported"); LOG_ERROR("WGC not supported");
error = 2; return 2;
return error; }
return RebuildSessions(monitor_index_);
}
int ScreenCapturerWgc::RebuildSessions(int preferred_monitor_index) {
CleanUp();
if (!IsWgcSupported()) {
LOG_ERROR("WGC not supported");
return 2;
} }
monitor_ = GetPrimaryMonitor(); monitor_ = GetPrimaryMonitor();
@@ -125,6 +134,13 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
return -1; 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++) { for (int i = 0; i < display_info_list_.size(); i++) {
const auto& display = display_info_list_[i]; const auto& display = display_info_list_[i];
LOG_INFO( LOG_INFO(
@@ -138,20 +154,28 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
sessions_.back().session_->RegisterObserver(this); sessions_.back().session_->RegisterObserver(this);
error = sessions_.back().session_->Initialize((HMONITOR)display.handle); error = sessions_.back().session_->Initialize((HMONITOR)display.handle);
if (error != 0) { if (error != 0) {
LOG_ERROR("WGC: initialize session {} failed, ret={}", i, error);
CleanUp();
return error; return error;
} }
sessions_[i].inited_ = true; sessions_[i].inited_ = true;
inited_ = true;
} }
LOG_INFO("Default on monitor {}:{}", monitor_index_, LOG_INFO("Default on monitor {}:{}", monitor_index_,
display_info_list_[monitor_index_].name); 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; return 0;
} }
int ScreenCapturerWgc::Destroy() { return 0; } int ScreenCapturerWgc::Destroy() {
CleanUp();
return 0;
}
int ScreenCapturerWgc::Start(bool show_cursor) { int ScreenCapturerWgc::Start(bool show_cursor) {
if (running_ == true) { if (running_ == true) {
@@ -160,13 +184,37 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
} }
if (inited_ == false) { if (inited_ == false) {
LOG_ERROR("Screen capturer not inited"); const int ret = RebuildSessions(monitor_index_);
return 4; 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 any_started = false;
bool active_started = false;
int last_error = 0; 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) { if (sessions_[i].inited_ == false) {
LOG_ERROR("Session {} not inited", i); LOG_ERROR("Session {} not inited", i);
continue; continue;
@@ -182,16 +230,27 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
continue; continue;
} }
if (i != 0) { if (i != active_monitor) {
sessions_[i].session_->Pause(); sessions_[i].session_->Pause();
sessions_[i].paused_ = true; sessions_[i].paused_ = true;
} else {
sessions_[i].session_->Resume();
sessions_[i].paused_ = false;
} }
sessions_[i].running_ = true; sessions_[i].running_ = true;
any_started = 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) { if (!any_started) {
LOG_ERROR("WGC: no session started successfully"); LOG_ERROR("WGC: no session started successfully");
return last_error != 0 ? last_error : -1; return last_error != 0 ? last_error : -1;
@@ -247,7 +306,7 @@ int ScreenCapturerWgc::SwitchTo(int monitor_index) {
return 0; return 0;
} }
if (monitor_index >= display_info_list_.size()) { if (monitor_index < 0 || monitor_index >= static_cast<int>(display_info_list_.size())) {
LOG_ERROR("Invalid monitor index: {}", monitor_index); LOG_ERROR("Invalid monitor index: {}", monitor_index);
return -1; return -1;
} }
@@ -349,13 +408,16 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
} }
void ScreenCapturerWgc::CleanUp() { void ScreenCapturerWgc::CleanUp() {
if (inited_) { running_ = false;
for (auto& session : sessions_) { for (auto& session : sessions_) {
if (session.session_) { if (session.session_) {
session.session_->Stop(); 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: protected:
void CleanUp(); void CleanUp();
int RebuildSessions(int preferred_monitor_index);
int StartSessions(bool show_cursor);
private: private:
HMONITOR monitor_; HMONITOR monitor_;
@@ -74,4 +76,4 @@ class ScreenCapturerWgc : public ScreenCapturer,
std::mutex frame_mutex_; std::mutex frame_mutex_;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif
@@ -29,11 +29,15 @@ namespace {
using Json = nlohmann::json; using Json = nlohmann::json;
constexpr DWORD kSecureDesktopStatusIntervalMs = 250; constexpr DWORD kSecureDesktopStatusIntervalMs = 250;
constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 150; constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500;
constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120; constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120;
constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500; constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500;
constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000; 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 { struct SecureDesktopServiceStatus {
bool service_available = false; bool service_available = false;
@@ -129,10 +133,28 @@ class WgcPluginCapturer final : public ScreenCapturer {
}; };
std::string BuildSecureCaptureCommand(int left, int top, int width, int height, std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
bool show_cursor) { bool show_cursor,
const std::string& stage) {
std::ostringstream stream; std::ostringstream stream;
stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0); << ":" << 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(); return stream.str();
} }
@@ -148,6 +170,11 @@ bool IsTransientSecureDesktopFrameError(const std::string& error_message) {
error_message.find("\"error\":\"bitblt_failed\"") != std::string::npos; 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, bool ReadPipeMessage(HANDLE pipe, std::vector<uint8_t>* response_out,
DWORD* error_code_out = nullptr) { DWORD* error_code_out = nullptr) {
if (response_out == nullptr) { if (response_out == nullptr) {
@@ -274,17 +301,15 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
return true; return true;
} }
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top, bool QuerySecureDesktopHelperCommand(DWORD session_id,
int width, int height, bool show_cursor, const std::string& command,
std::vector<uint8_t>* nv12_frame_out, std::vector<uint8_t>* response_out,
int* captured_width_out, std::string* error_out) {
int* captured_height_out, if (response_out == nullptr) {
std::string* error_out) {
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
captured_height_out == nullptr) {
return false; return false;
} }
response_out->clear();
const std::wstring pipe_name = const std::wstring pipe_name =
GetCrossDeskSecureInputHelperPipeName(session_id); GetCrossDeskSecureInputHelperPipeName(session_id);
if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) { 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; DWORD pipe_mode = PIPE_READMODE_MESSAGE;
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr); SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
const std::string command =
BuildSecureCaptureCommand(left, top, width, height, show_cursor);
DWORD bytes_written = 0; DWORD bytes_written = 0;
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()), if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
&bytes_written, nullptr)) { &bytes_written, nullptr)) {
@@ -319,9 +342,8 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
return false; return false;
} }
std::vector<uint8_t> response;
DWORD read_error = 0; 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); CloseHandle(pipe);
if (!read_ok) { if (!read_ok) {
if (error_out != nullptr) { if (error_out != nullptr) {
@@ -330,6 +352,29 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
return false; 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, return ParseSecureDesktopFrameResponse(response, nv12_frame_out,
captured_width_out, captured_width_out,
captured_height_out, error_out); captured_height_out, error_out);
@@ -349,21 +394,46 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
return; return;
} }
const char* raw_display_name = display_name ? display_name : "";
std::string mapped_name; std::string mapped_name;
{ {
std::lock_guard<std::mutex> lock(alias_mutex_); 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()) if (it != label_alias_.end())
mapped_name = it->second; mapped_name = it->second;
else else
mapped_name = display_name; mapped_name = raw_display_name;
} }
{ {
std::lock_guard<std::mutex> lock(alias_mutex_); std::lock_guard<std::mutex> lock(alias_mutex_);
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) { 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; 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()); 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); running_.store(true, std::memory_order_relaxed);
secure_desktop_capture_active_.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);
if (!secure_capture_thread_.joinable()) { if (!secure_capture_thread_.joinable()) {
secure_capture_thread_ = secure_capture_thread_ =
std::thread([this]() { SecureDesktopCaptureLoop(); }); std::thread([this]() { SecureDesktopCaptureLoop(); });
@@ -491,11 +565,16 @@ int ScreenCapturerWin::Start(bool show_cursor) {
int ScreenCapturerWin::Stop() { int ScreenCapturerWin::Stop() {
running_.store(false, std::memory_order_relaxed); running_.store(false, std::memory_order_relaxed);
secure_desktop_capture_active_.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; int ret = 0;
if (impl_) { if (impl_) {
ret = impl_->Stop(); ret = impl_->Stop();
} }
StopSecureCaptureThread(); StopSecureCaptureThread();
StopSecureDesktopSharedCapture(secure_shared_session_id_);
return ret; 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, bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
int* height, int* height,
std::string* display_name) { std::string* display_name) {
@@ -616,10 +782,239 @@ bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
return true; 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() { void ScreenCapturerWin::SecureDesktopCaptureLoop() {
const int frame_interval_ms = const int frame_interval_ms =
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinIntervalMs, 1000 / fps_) fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_)
: kSecureDesktopCaptureMinIntervalMs; : kSecureDesktopCaptureMaxIntervalMs;
ULONGLONG last_status_tick = 0; ULONGLONG last_status_tick = 0;
ULONGLONG last_error_tick = 0; ULONGLONG last_error_tick = 0;
bool last_capture_active = false; bool last_capture_active = false;
@@ -627,6 +1022,9 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
std::string last_stage; std::string last_stage;
std::string last_service_error; std::string last_service_error;
ULONGLONG capture_stage_started_tick = 0; 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; SecureDesktopServiceStatus status;
std::vector<uint8_t> secure_frame; std::vector<uint8_t> secure_frame;
@@ -653,6 +1051,11 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
"Windows capturer secure desktop service available, polling " "Windows capturer secure desktop service available, polling "
"session_id={}", "session_id={}",
status.active_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 { } else {
LOG_WARN( LOG_WARN(
"Windows capturer secure desktop service unavailable: " "Windows capturer secure desktop service unavailable: "
@@ -673,6 +1076,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
std::memory_order_relaxed); std::memory_order_relaxed);
if (status.capture_active != last_capture_active || if (status.capture_active != last_capture_active ||
status.interactive_stage != last_stage) { 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; capture_stage_started_tick = now;
LOG_INFO( LOG_INFO(
"Windows capturer secure desktop state: active={}, stage='{}', " "Windows capturer secure desktop state: active={}, stage='{}', "
@@ -681,17 +1088,53 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
status.active_session_id); status.active_session_id);
last_capture_active = status.capture_active; last_capture_active = status.capture_active;
last_stage = status.interactive_stage; 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; last_status_tick = now;
} }
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) { 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::this_thread::sleep_for(
std::chrono::milliseconds(status.service_available ? 50 : 200)); std::chrono::milliseconds(status.service_available ? 50 : 200));
continue; continue;
} }
if (!status.helper_running) { if (!status.helper_running) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
std::this_thread::sleep_for(std::chrono::milliseconds(30)); std::this_thread::sleep_for(std::chrono::milliseconds(30));
continue; continue;
} }
@@ -702,6 +1145,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
int height = 0; int height = 0;
std::string display_name; std::string display_name;
if (!GetCurrentCaptureRegion(&left, &top, &width, &height, &display_name)) { if (!GetCurrentCaptureRegion(&left, &top, &width, &height, &display_name)) {
StopSecureDesktopSharedCapture(secure_shared_session_id_);
std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::this_thread::sleep_for(std::chrono::milliseconds(100));
continue; continue;
} }
@@ -709,15 +1153,40 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
int captured_width = 0; int captured_width = 0;
int captured_height = 0; int captured_height = 0;
std::string error_message; std::string error_message;
if (QuerySecureDesktopHelperFrame( bool frame_delivered = false;
status.active_session_id, left, top, width, height, const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
show_cursor_.load(std::memory_order_relaxed), &secure_frame, 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)) { &captured_width, &captured_height, &error_message)) {
if (cb_orig_ && !secure_frame.empty()) { if (cb_orig_ && !secure_frame.empty()) {
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()), cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
captured_width, captured_height, display_name.c_str()); 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 = const bool transient_error =
IsTransientSecureDesktopFrameError(error_message); IsTransientSecureDesktopFrameError(error_message);
const bool in_grace_period = capture_stage_started_tick != 0 && const bool in_grace_period = capture_stage_started_tick != 0 &&
@@ -731,10 +1200,19 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
continue; continue;
} }
if (now - last_error_tick >= log_interval) { if (now - last_error_tick >= log_interval) {
LOG_WARN( if (transient_error) {
"Windows capturer secure desktop frame query failed, stage='{}', " LOG_INFO(
"session_id={}, error={}", "Windows capturer secure desktop transient frame query failed, "
status.interactive_stage, status.active_session_id, error_message); "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; last_error_tick = now;
} }
} }
@@ -742,7 +1220,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms)); 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); secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
} }
} // namespace crossdesk } // namespace crossdesk
@@ -10,6 +10,7 @@
#include <Windows.h> #include <Windows.h>
#include <atomic> #include <atomic>
#include <cstdint>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <thread> #include <thread>
@@ -58,14 +59,44 @@ class ScreenCapturerWin : public ScreenCapturer {
std::atomic<int> monitor_index_{0}; std::atomic<int> monitor_index_{0};
int initial_monitor_index_ = 0; int initial_monitor_index_ = 0;
std::atomic<bool> secure_desktop_capture_active_{false}; 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_; 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 BuildCanonicalFromImpl();
void RebuildAliasesFromImpl(); void RebuildAliasesFromImpl();
void StopSecureCaptureThread(); void StopSecureCaptureThread();
bool RestartCaptureBackendAfterSecureDesktop();
void SecureDesktopCaptureLoop(); void SecureDesktopCaptureLoop();
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height, bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
std::string* display_name); 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 } // namespace crossdesk
#endif #endif
+197 -150
View File
@@ -2,23 +2,10 @@
#include <Windows.Graphics.Capture.Interop.h> #include <Windows.Graphics.Capture.Interop.h>
#include <atomic> #include <string>
#include <functional>
#include <memory>
#include "rd_log.h" #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 { namespace crossdesk {
extern "C" { extern "C" {
@@ -40,7 +27,7 @@ int WgcSessionImpl::Initialize(HWND hwnd) {
target_.hwnd = hwnd; target_.hwnd = hwnd;
target_.is_window = true; target_.is_window = true;
return Initialize(); return InitializeLocked();
} }
int WgcSessionImpl::Initialize(HMONITOR hmonitor) { int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
@@ -48,7 +35,7 @@ int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
target_.hmonitor = hmonitor; target_.hmonitor = hmonitor;
target_.is_window = false; target_.is_window = false;
return Initialize(); return InitializeLocked();
} }
void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) { void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
@@ -59,68 +46,13 @@ void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
int WgcSessionImpl::Start(bool show_cursor) { int WgcSessionImpl::Start(bool show_cursor) {
std::lock_guard locker(lock_); std::lock_guard locker(lock_);
if (is_running_) return 0; return StartLocked(show_cursor);
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;
} }
int WgcSessionImpl::Stop() { int WgcSessionImpl::Stop() {
std::lock_guard locker(lock_); std::lock_guard locker(lock_);
CHECK_INIT; CleanUpLocked();
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;
}
return 0; return 0;
} }
@@ -129,7 +61,10 @@ int WgcSessionImpl::Pause() {
is_paused_ = true; is_paused_ = true;
CHECK_INIT; if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
return 0; return 0;
} }
@@ -138,7 +73,10 @@ int WgcSessionImpl::Resume() {
is_paused_ = false; is_paused_ = false;
CHECK_INIT; if (!is_initialized_) {
LOG_ERROR("AE_NEED_INIT");
return 4;
}
return 0; return 0;
} }
@@ -175,10 +113,10 @@ auto WgcSessionImpl::CreateCaptureItemForWindow(HWND hwnd) {
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>(); winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>(); auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr}; winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
interop_factory->CreateForWindow( winrt::check_hresult(interop_factory->CreateForWindow(
hwnd, hwnd,
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(), winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(item))); reinterpret_cast<void**>(winrt::put_abi(item))));
return item; return item;
} }
@@ -187,10 +125,10 @@ auto WgcSessionImpl::CreateCaptureItemForMonitor(HMONITOR hmonitor) {
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>(); winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>(); auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr}; winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
interop_factory->CreateForMonitor( winrt::check_hresult(interop_factory->CreateForMonitor(
hmonitor, hmonitor,
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(), winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(item))); reinterpret_cast<void**>(winrt::put_abi(item))));
return item; return item;
} }
@@ -218,6 +156,104 @@ HRESULT WgcSessionImpl::CreateMappedTexture(
d3d11_texture_mapped_.put()); 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( void WgcSessionImpl::OnFrame(
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender, winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) { [[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
@@ -225,7 +261,7 @@ void WgcSessionImpl::OnFrame(
auto is_new_size = false; auto is_new_size = false;
{ try {
auto frame = sender.TryGetNextFrame(); auto frame = sender.TryGetNextFrame();
auto frame_size = frame.ContentSize(); auto frame_size = frame.ContentSize();
@@ -239,60 +275,63 @@ void WgcSessionImpl::OnFrame(
} }
// copy to mapped texture // copy to mapped texture
{ if (is_paused_) {
if (is_paused_) { return;
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_new_size) { auto frame_captured =
capture_framepool_.Recreate(d3d11_direct_device_, GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
winrt::Windows::Graphics::DirectX::
DirectXPixelFormat::B8G8R8A8UIntNormalized, if (!d3d11_texture_mapped_ || is_new_size) {
2, capture_frame_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::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable const&) { winrt::Windows::Foundation::IInspectable const&) {
std::lock_guard locker(lock_); std::lock_guard locker(lock_);
const bool was_running = is_running_;
const bool was_paused = is_paused_;
try { try {
CleanUp(); CleanUpLocked();
is_initialized_ = false; is_paused_ = was_paused;
if (Initialize() == 0) { if (InitializeLocked() == 0) {
int ret = Start(last_show_cursor_); int ret = was_running ? StartCaptureLocked(last_show_cursor_) : 0;
if (ret == 0) { if (ret == 0) {
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered"); OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
} else { } else {
@@ -319,9 +360,14 @@ void WgcSessionImpl::OnClosed(
} }
} }
int WgcSessionImpl::Initialize() { int WgcSessionImpl::InitializeLocked() {
if (is_initialized_) return 0; 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())) { if (!(d3d11_direct_device_ = CreateD3D11Device())) {
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED"); LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
return 1; return 1;
@@ -332,6 +378,10 @@ int WgcSessionImpl::Initialize() {
capture_item_ = CreateCaptureItemForWindow(target_.hwnd); capture_item_ = CreateCaptureItemForWindow(target_.hwnd);
else else
capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor); capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor);
if (!capture_item_) {
LOG_ERROR("WGC: create capture item returned null");
return 86;
}
// Set up // Set up
auto d3d11_device = auto d3d11_device =
@@ -353,21 +403,18 @@ int WgcSessionImpl::Initialize() {
void WgcSessionImpl::CleanUp() { void WgcSessionImpl::CleanUp() {
std::lock_guard locker(lock_); std::lock_guard locker(lock_);
auto expected = false; CleanUpLocked();
if (cleaned_.compare_exchange_strong(expected, true)) { }
capture_close_trigger_.revoke();
capture_framepool_trigger_.revoke();
if (capture_framepool_) capture_framepool_.Close(); void WgcSessionImpl::CleanUpLocked() {
StopLocked();
if (capture_session_) capture_session_.Close(); capture_item_ = nullptr;
d3d11_device_context_ = nullptr;
capture_framepool_ = nullptr; d3d11_direct_device_ = nullptr;
capture_session_ = nullptr; capture_frame_size_ = {};
capture_item_ = nullptr; is_initialized_ = false;
is_paused_ = false;
is_initialized_ = false;
}
} }
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param, 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&, void OnClosed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
winrt::Windows::Foundation::IInspectable 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 CleanUp();
void CleanUpLocked();
// void message_func(); // void message_func();
@@ -94,7 +98,6 @@ class WgcSessionImpl : public WgcSession {
winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr}; winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr}; winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};
std::atomic<bool> cleaned_ = false;
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
capture_framepool_{nullptr}; capture_framepool_{nullptr};
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool:: winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
+3 -2
View File
@@ -13,7 +13,8 @@ namespace crossdesk {
inline bool IsSecureDesktopInteractionRequired( inline bool IsSecureDesktopInteractionRequired(
const std::string& interactive_stage) { const std::string& interactive_stage) {
return interactive_stage == "credential-ui" || return interactive_stage == "lock-screen" ||
interactive_stage == "credential-ui" ||
interactive_stage == "secure-desktop"; interactive_stage == "secure-desktop";
} }
@@ -38,4 +39,4 @@ inline bool ShouldNormalizeUnlockToUserDesktop(
} // namespace crossdesk } // namespace crossdesk
#endif #endif
+182 -36
View File
@@ -31,6 +31,7 @@ constexpr char kSecureDesktopMouseIpcCommandPrefix[] = "secure-input-mouse:";
constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe"; constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe";
constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000; constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000;
constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000; constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000;
constexpr ULONGLONG kSasSecureDesktopGraceMs = 15000;
using SendSasFunction = VOID(WINAPI*)(BOOL); using SendSasFunction = VOID(WINAPI*)(BOOL);
@@ -262,8 +263,8 @@ bool GrantCrossDeskServiceStartAccessToAuthenticatedUsers(SC_HANDLE service) {
std::string QueryNamedPipeMessage(const std::wstring& pipe_name, std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
const std::string& command, const std::string& command,
DWORD timeout_ms) { DWORD timeout_ms) {
constexpr int kPipeConnectRetryCount = 3;
constexpr DWORD kPipeConnectRetryDelayMs = 15; constexpr DWORD kPipeConnectRetryDelayMs = 15;
const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms;
auto is_transient_pipe_error = [](DWORD error) { auto is_transient_pipe_error = [](DWORD error) {
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY || return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY ||
@@ -271,12 +272,23 @@ std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
}; };
HANDLE pipe = INVALID_HANDLE_VALUE; HANDLE pipe = INVALID_HANDLE_VALUE;
for (int attempt = 0; attempt < kPipeConnectRetryCount; ++attempt) { DWORD last_error = ERROR_SEM_TIMEOUT;
if (!WaitNamedPipeW(pipe_name.c_str(), timeout_ms)) { 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(); const DWORD error = GetLastError();
if (attempt + 1 < kPipeConnectRetryCount && last_error = error;
is_transient_pipe_error(error)) { const ULONGLONG retry_tick = GetTickCount64();
Sleep(kPipeConnectRetryDelayMs); 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; continue;
} }
return BuildErrorJson("pipe_unavailable", error); return BuildErrorJson("pipe_unavailable", error);
@@ -289,14 +301,21 @@ std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
} }
const DWORD error = GetLastError(); const DWORD error = GetLastError();
if (attempt + 1 < kPipeConnectRetryCount && last_error = error;
is_transient_pipe_error(error)) { const ULONGLONG retry_tick = GetTickCount64();
Sleep(kPipeConnectRetryDelayMs); 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; continue;
} }
return BuildErrorJson("pipe_connect_failed", error); return BuildErrorJson("pipe_connect_failed", error);
} }
if (pipe == INVALID_HANDLE_VALUE) {
return BuildErrorJson("pipe_unavailable", last_error);
}
DWORD pipe_mode = PIPE_READMODE_MESSAGE; DWORD pipe_mode = PIPE_READMODE_MESSAGE;
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr); SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
@@ -337,20 +356,27 @@ std::string BuildSecureDesktopMouseIpcCommand(int x, int y, int wheel,
return stream.str(); return stream.str();
} }
std::string BuildSecureInputHelperKeyboardCommand(int key_code, bool is_down, std::string BuildSecureInputHelperKeyboardCommand(
uint32_t scan_code, int key_code, bool is_down, uint32_t scan_code, bool extended,
bool extended) { const std::string& interactive_stage) {
std::ostringstream stream; std::ostringstream stream;
stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":" stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":"
<< (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0); << (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0);
if (!interactive_stage.empty()) {
stream << ":" << interactive_stage;
}
return stream.str(); return stream.str();
} }
std::string BuildSecureInputHelperMouseCommand(int x, int y, int wheel, std::string BuildSecureInputHelperMouseCommand(
int flag) { int x, int y, int wheel, int flag,
const std::string& interactive_stage) {
std::ostringstream stream; std::ostringstream stream;
stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":" stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":"
<< wheel << ":" << flag; << wheel << ":" << flag;
if (!interactive_stage.empty()) {
stream << ":" << interactive_stage;
}
return stream.str(); return stream.str();
} }
@@ -565,6 +591,23 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
return "user-desktop"; 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) { bool GetSessionUserName(DWORD session_id, std::wstring* username_out) {
if (username_out == nullptr) { if (username_out == nullptr) {
return false; return false;
@@ -993,12 +1036,14 @@ int CrossDeskServiceHost::InitializeRuntime() {
session_helper_report_credential_ui_visible_ = false; session_helper_report_credential_ui_visible_ = false;
session_helper_report_unlock_ui_visible_ = false; session_helper_report_unlock_ui_visible_ = false;
secure_input_helper_running_ = false; secure_input_helper_running_ = false;
sas_secure_desktop_seen_ = false;
last_sas_error_code_ = 0; last_sas_error_code_ = 0;
last_sas_success_ = false; last_sas_success_ = false;
session_helper_started_at_tick_ = 0; session_helper_started_at_tick_ = 0;
session_helper_report_state_age_ms_ = 0; session_helper_report_state_age_ms_ = 0;
session_helper_report_uptime_ms_ = 0; session_helper_report_uptime_ms_ = 0;
secure_input_helper_started_at_tick_ = 0; secure_input_helper_started_at_tick_ = 0;
sas_secure_desktop_until_tick_ = 0;
session_helper_process_handle_ = nullptr; session_helper_process_handle_ = nullptr;
session_helper_stop_event_ = nullptr; session_helper_stop_event_ = nullptr;
secure_input_helper_process_handle_ = nullptr; secure_input_helper_process_handle_ = nullptr;
@@ -1010,6 +1055,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
session_helper_report_input_desktop_.clear(); session_helper_report_input_desktop_.clear();
session_helper_report_interactive_stage_.clear(); session_helper_report_interactive_stage_.clear();
secure_input_helper_last_error_.clear(); secure_input_helper_last_error_.clear();
secure_input_helper_interactive_stage_.clear();
last_session_event_type_ = 0; last_session_event_type_ = 0;
last_session_event_session_id_ = active_session_id_; last_session_event_session_id_ = active_session_id_;
RefreshSessionState(); RefreshSessionState();
@@ -1285,7 +1331,13 @@ bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
} }
bool CrossDeskServiceHost::HasSecureInputUiLocked() 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_credential_ui_visible_ ||
session_helper_report_secure_desktop_active_ || session_helper_report_secure_desktop_active_ ||
session_helper_report_unlock_ui_visible_ || session_helper_report_unlock_ui_visible_ ||
@@ -1293,6 +1345,30 @@ bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
session_helper_report_interactive_stage_ == "secure-desktop"; 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( bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
DWORD target_session_id) const { DWORD target_session_id) const {
if (target_session_id == 0xFFFFFFFF) { if (target_session_id == 0xFFFFFFFF) {
@@ -1303,6 +1379,28 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
IsHelperReportingLockScreenLocked()); 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 CrossDeskServiceHost::GetSessionHelperPath() const {
std::wstring current_executable = GetCurrentExecutablePathW(); std::wstring current_executable = GetCurrentExecutablePathW();
if (current_executable.empty()) { if (current_executable.empty()) {
@@ -1392,6 +1490,7 @@ void CrossDeskServiceHost::ReapSecureInputHelper() {
secure_input_helper_process_id_ = 0; secure_input_helper_process_id_ = 0;
secure_input_helper_exit_code_ = exit_code; secure_input_helper_exit_code_ = exit_code;
secure_input_helper_started_at_tick_ = 0; secure_input_helper_started_at_tick_ = 0;
secure_input_helper_interactive_stage_.clear();
} }
if (process_handle != nullptr) { if (process_handle != nullptr) {
@@ -1450,6 +1549,7 @@ void CrossDeskServiceHost::StopSecureInputHelper() {
secure_input_helper_running_ = false; secure_input_helper_running_ = false;
secure_input_helper_process_id_ = 0; secure_input_helper_process_id_ = 0;
secure_input_helper_started_at_tick_ = 0; secure_input_helper_started_at_tick_ = 0;
secure_input_helper_interactive_stage_.clear();
} }
if (stop_event_handle != nullptr) { if (stop_event_handle != nullptr) {
@@ -1577,7 +1677,8 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) {
return true; return true;
} }
bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) { bool CrossDeskServiceHost::LaunchSecureInputHelper(
DWORD session_id, const std::string& interactive_stage) {
std::wstring helper_path = GetSecureInputHelperPath(); std::wstring helper_path = GetSecureInputHelperPath();
if (helper_path.empty() || !std::filesystem::exists(helper_path)) { if (helper_path.empty() || !std::filesystem::exists(helper_path)) {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
@@ -1611,7 +1712,10 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
STARTUPINFOW startup_info{}; STARTUPINFOW startup_info{};
startup_info.cb = sizeof(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{}; PROCESS_INFORMATION process_info{};
BOOL created = FALSE; BOOL created = FALSE;
@@ -1660,10 +1764,14 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
secure_input_helper_last_error_.clear(); secure_input_helper_last_error_.clear();
secure_input_helper_running_ = true; secure_input_helper_running_ = true;
secure_input_helper_started_at_tick_ = GetTickCount64(); 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, LOG_INFO(
process_info.dwProcessId); "Secure input helper started: session_id={}, pid={}, stage='{}', "
"desktop='{}'",
session_id, process_info.dwProcessId, interactive_stage,
WideToUtf8(secure_input_helper_desktop));
return true; return true;
} }
@@ -1762,6 +1870,7 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
json.value("interactive_stage", std::string()); json.value("interactive_stage", std::string());
session_helper_report_state_age_ms_ = json.value("state_age_ms", 0ull); session_helper_report_state_age_ms_ = json.value("state_age_ms", 0ull);
session_helper_report_uptime_ms_ = json.value("uptime_ms", 0ull); session_helper_report_uptime_ms_ = json.value("uptime_ms", 0ull);
UpdateSasSecureDesktopGraceLocked(session_helper_report_interactive_stage_);
} }
void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type, void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
@@ -1845,21 +1954,26 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
bool keep_secure_input_helper = false; bool keep_secure_input_helper = false;
bool launch_secure_input_helper = false; bool launch_secure_input_helper = false;
DWORD secure_input_target_session_id = 0xFFFFFFFF; DWORD secure_input_target_session_id = 0xFFFFFFFF;
std::string secure_input_interactive_stage;
{ {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
secure_input_target_session_id = active_session_id_; secure_input_target_session_id = active_session_id_;
secure_input_interactive_stage = ResolveInteractiveStageLocked();
keep_secure_input_helper = keep_secure_input_helper =
ShouldKeepSecureInputHelperLocked(secure_input_target_session_id); ShouldKeepSecureInputHelperLocked(secure_input_target_session_id);
launch_secure_input_helper = launch_secure_input_helper =
keep_secure_input_helper && keep_secure_input_helper &&
(!secure_input_helper_running_ || (!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 (keep_secure_input_helper) {
if (launch_secure_input_helper) { if (launch_secure_input_helper) {
StopSecureInputHelper(); StopSecureInputHelper();
LaunchSecureInputHelper(secure_input_target_session_id); LaunchSecureInputHelper(secure_input_target_session_id,
secure_input_interactive_stage);
} }
} else { } else {
StopSecureInputHelper(); StopSecureInputHelper();
@@ -1883,7 +1997,11 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
EscapeJsonString(session_helper_report_input_desktop_); EscapeJsonString(session_helper_report_input_desktop_);
std::string secure_input_helper_last_error = std::string secure_input_helper_last_error =
EscapeJsonString(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_; bool interactive_state_ready = session_helper_status_ok_;
const bool sas_secure_desktop_grace_active =
IsSasSecureDesktopGraceActiveLocked();
const char* interactive_state_source = const char* interactive_state_source =
interactive_state_ready ? "session-helper" : "service-host"; interactive_state_ready ? "session-helper" : "service-host";
const bool effective_session_locked = GetEffectiveSessionLockedLocked(); const bool effective_session_locked = GetEffectiveSessionLockedLocked();
@@ -1891,27 +2009,34 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
interactive_state_ready interactive_state_ready
? (effective_session_locked && IsHelperReportingLockScreenLocked()) ? (effective_session_locked && IsHelperReportingLockScreenLocked())
: false; : 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 = bool credential_ui_visible =
interactive_state_ready ? session_helper_report_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 bool unlock_ui_visible = interactive_state_ready
? session_helper_report_unlock_ui_visible_ ? 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 = bool interactive_secure_desktop_active =
interactive_state_ready ? session_helper_report_secure_desktop_active_ interactive_state_ready ? session_helper_report_secure_desktop_active_
: secure_desktop_active_; : secure_desktop_active_;
interactive_secure_desktop_active =
interactive_secure_desktop_active || sas_secure_desktop_grace_active;
bool interactive_logon_ui_visible = bool interactive_logon_ui_visible =
interactive_state_ready ? session_helper_report_logon_ui_visible_ credential_ui_visible;
: logon_ui_visible_;
bool interactive_session_locked = effective_session_locked || bool interactive_session_locked = effective_session_locked ||
interactive_lock_screen_visible || interactive_lock_screen_visible ||
unlock_ui_visible; unlock_ui_visible ||
sas_secure_desktop_grace_active;
std::string interactive_input_desktop = EscapeJsonString( std::string interactive_input_desktop = EscapeJsonString(
interactive_state_ready ? session_helper_report_input_desktop_ interactive_state_ready ? session_helper_report_input_desktop_
: input_desktop_name_); : input_desktop_name_);
std::string interactive_stage = EscapeJsonString(DetermineInteractiveStage( std::string raw_interactive_stage = ResolveInteractiveStageLocked();
interactive_lock_screen_visible, credential_ui_visible, std::string interactive_stage = EscapeJsonString(raw_interactive_stage);
interactive_secure_desktop_active));
std::ostringstream stream; std::ostringstream stream;
stream << "{\"ok\":true,\"service\":\"CrossDeskService\"" stream << "{\"ok\":true,\"service\":\"CrossDeskService\""
<< ",\"active_session_id\":" << active_session_id_ << ",\"active_session_id\":" << active_session_id_
@@ -1932,6 +2057,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
<< (interactive_logon_ui_visible ? "true" : "false") << (interactive_logon_ui_visible ? "true" : "false")
<< ",\"interactive_secure_desktop_active\":" << ",\"interactive_secure_desktop_active\":"
<< (interactive_secure_desktop_active ? "true" : "false") << (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") << ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false")
<< ",\"credential_ui_visible\":" << ",\"credential_ui_visible\":"
<< (credential_ui_visible ? "true" : "false") << (credential_ui_visible ? "true" : "false")
@@ -2005,6 +2132,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
<< secure_input_helper_last_error << "\"" << secure_input_helper_last_error << "\""
<< ",\"secure_input_helper_last_error_code\":" << ",\"secure_input_helper_last_error_code\":"
<< 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_uptime_ms\":"
<< (secure_input_helper_started_at_tick_ >= started_at_tick_ << (secure_input_helper_started_at_tick_ >= started_at_tick_
? (GetTickCount64() - secure_input_helper_started_at_tick_) ? (GetTickCount64() - secure_input_helper_started_at_tick_)
@@ -2034,10 +2163,14 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence() {
SasResult result = SendSasNow(); SasResult result = SendSasNow();
{ {
std::lock_guard<std::mutex> lock(state_mutex_); 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_success_ = result.success;
last_sas_error_code_ = result.error_code; last_sas_error_code_ = result.error_code;
last_sas_error_ = result.error; last_sas_error_ = result.error;
sas_secure_desktop_until_tick_ =
result.success ? now + kSasSecureDesktopGraceMs : 0;
sas_secure_desktop_seen_ = false;
} }
if (!result.success) { if (!result.success) {
@@ -2051,15 +2184,21 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
RefreshSessionState(); RefreshSessionState();
ReapSecureInputHelper(); ReapSecureInputHelper();
EnsureSessionHelper(); EnsureSessionHelper();
RefreshSessionHelperReportedState();
DWORD target_session_id = 0xFFFFFFFF; DWORD target_session_id = 0xFFFFFFFF;
bool helper_running = false; bool helper_running = false;
bool can_inject = false; bool can_inject = false;
std::string interactive_stage;
{ {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
target_session_id = active_session_id_; 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_ && 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(); can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
} }
@@ -2072,7 +2211,7 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
if (!helper_running) { if (!helper_running) {
StopSecureInputHelper(); StopSecureInputHelper();
if (!LaunchSecureInputHelper(target_session_id)) { if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
return BuildErrorJson(secure_input_helper_last_error_.c_str(), return BuildErrorJson(secure_input_helper_last_error_.c_str(),
secure_input_helper_last_error_code_); secure_input_helper_last_error_code_);
@@ -2082,7 +2221,7 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
return QueryNamedPipeMessage( return QueryNamedPipeMessage(
GetCrossDeskSecureInputHelperPipeName(target_session_id), GetCrossDeskSecureInputHelperPipeName(target_session_id),
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code, BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
extended), extended, interactive_stage),
1000); 1000);
} }
@@ -2092,15 +2231,21 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
RefreshSessionState(); RefreshSessionState();
ReapSecureInputHelper(); ReapSecureInputHelper();
EnsureSessionHelper(); EnsureSessionHelper();
RefreshSessionHelperReportedState();
DWORD target_session_id = 0xFFFFFFFF; DWORD target_session_id = 0xFFFFFFFF;
bool helper_running = false; bool helper_running = false;
bool can_inject = false; bool can_inject = false;
std::string interactive_stage;
{ {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
target_session_id = active_session_id_; 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_ && 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(); can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
} }
@@ -2113,7 +2258,7 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
if (!helper_running) { if (!helper_running) {
StopSecureInputHelper(); StopSecureInputHelper();
if (!LaunchSecureInputHelper(target_session_id)) { if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
std::lock_guard<std::mutex> lock(state_mutex_); std::lock_guard<std::mutex> lock(state_mutex_);
return BuildErrorJson(secure_input_helper_last_error_.c_str(), return BuildErrorJson(secure_input_helper_last_error_.c_str(),
secure_input_helper_last_error_code_); secure_input_helper_last_error_code_);
@@ -2122,7 +2267,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
return QueryNamedPipeMessage( return QueryNamedPipeMessage(
GetCrossDeskSecureInputHelperPipeName(target_session_id), GetCrossDeskSecureInputHelperPipeName(target_session_id),
BuildSecureInputHelperMouseCommand(x, y, wheel, flag), 1000); BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage),
1000);
} }
bool InstallCrossDeskService(const std::wstring& binary_path) { bool InstallCrossDeskService(const std::wstring& binary_path) {
+8 -1
View File
@@ -45,7 +45,8 @@ class CrossDeskServiceHost {
bool LaunchSessionHelper(DWORD session_id); bool LaunchSessionHelper(DWORD session_id);
void ReapSecureInputHelper(); void ReapSecureInputHelper();
void StopSecureInputHelper(); void StopSecureInputHelper();
bool LaunchSecureInputHelper(DWORD session_id); bool LaunchSecureInputHelper(DWORD session_id,
const std::string& interactive_stage);
std::wstring GetSessionHelperPath() const; std::wstring GetSessionHelperPath() const;
std::wstring GetSessionHelperStopEventName(DWORD session_id) const; std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
std::wstring GetSecureInputHelperPath() const; std::wstring GetSecureInputHelperPath() const;
@@ -55,7 +56,10 @@ class CrossDeskServiceHost {
bool GetEffectiveSessionLockedLocked() const; bool GetEffectiveSessionLockedLocked() const;
bool IsHelperReportingLockScreenLocked() const; bool IsHelperReportingLockScreenLocked() const;
bool HasSecureInputUiLocked() const; bool HasSecureInputUiLocked() const;
void UpdateSasSecureDesktopGraceLocked(const std::string& observed_stage);
bool IsSasSecureDesktopGraceActiveLocked() const;
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const; bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
std::string ResolveInteractiveStageLocked() const;
void RefreshSessionHelperReportedState(); void RefreshSessionHelperReportedState();
void RecordSessionEvent(DWORD event_type, DWORD session_id); void RecordSessionEvent(DWORD event_type, DWORD session_id);
std::string HandleIpcCommand(const std::string& command); std::string HandleIpcCommand(const std::string& command);
@@ -101,6 +105,7 @@ class CrossDeskServiceHost {
ULONGLONG session_helper_report_state_age_ms_ = 0; ULONGLONG session_helper_report_state_age_ms_ = 0;
ULONGLONG session_helper_report_uptime_ms_ = 0; ULONGLONG session_helper_report_uptime_ms_ = 0;
ULONGLONG secure_input_helper_started_at_tick_ = 0; ULONGLONG secure_input_helper_started_at_tick_ = 0;
ULONGLONG sas_secure_desktop_until_tick_ = 0;
bool session_locked_ = false; bool session_locked_ = false;
bool logon_ui_visible_ = false; bool logon_ui_visible_ = false;
bool prelogin_ = false; bool prelogin_ = false;
@@ -117,6 +122,7 @@ class CrossDeskServiceHost {
bool session_helper_report_unlock_ui_visible_ = false; bool session_helper_report_unlock_ui_visible_ = false;
bool secure_input_helper_running_ = false; bool secure_input_helper_running_ = false;
bool console_mode_ = false; bool console_mode_ = false;
bool sas_secure_desktop_seen_ = false;
DWORD last_sas_error_code_ = 0; DWORD last_sas_error_code_ = 0;
bool last_sas_success_ = false; bool last_sas_success_ = false;
HANDLE session_helper_process_handle_ = nullptr; HANDLE session_helper_process_handle_ = nullptr;
@@ -130,6 +136,7 @@ class CrossDeskServiceHost {
std::string session_helper_report_input_desktop_; std::string session_helper_report_input_desktop_;
std::string session_helper_report_interactive_stage_; std::string session_helper_report_interactive_stage_;
std::string secure_input_helper_last_error_; std::string secure_input_helper_last_error_;
std::string secure_input_helper_interactive_stage_;
static CrossDeskServiceHost* instance_; static CrossDeskServiceHost* instance_;
}; };
File diff suppressed because it is too large Load Diff
+34 -1
View File
@@ -23,7 +23,15 @@ inline constexpr char kCrossDeskSecureInputKeyboardCommandPrefix[] =
"keyboard:"; "keyboard:";
inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = "mouse:"; inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = "mouse:";
inline constexpr char kCrossDeskSecureInputCaptureCommandPrefix[] = "capture:"; 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 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 kCrossDeskSecureDesktopFrameMagic = 0x50444358;
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1; inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
@@ -37,6 +45,19 @@ struct CrossDeskSecureDesktopFrameHeader {
uint32_t height; uint32_t height;
uint32_t payload_size; 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) #pragma pack(pop)
inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) { inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) {
@@ -49,6 +70,18 @@ inline std::wstring GetCrossDeskSecureInputHelperPipeName(DWORD session_id) {
std::to_wstring(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 } // namespace crossdesk
#endif #endif
@@ -9,6 +9,17 @@ namespace crossdesk {
class SpeakerCapturerMacosx; 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> @interface SpeakerCaptureDelegate : NSObject <SCStreamDelegate, SCStreamOutput>
@property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner; @property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner;
- (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner; - (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner;
@@ -28,15 +39,36 @@ class SpeakerCapturerMacosx;
ofType:(SCStreamOutputType)type { ofType:(SCStreamOutputType)type {
if (type != SCStreamOutputTypeAudio) return; if (type != SCStreamOutputTypeAudio) return;
crossdesk::SpeakerCapturerMacosx* owner = _owner;
if (!owner || !owner->cb_) {
return;
}
CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
if (!blockBuffer) {
return;
}
size_t length = CMBlockBufferGetDataLength(blockBuffer); size_t length = CMBlockBufferGetDataLength(blockBuffer);
char* dataPtr = NULL; 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); CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer);
if (!formatDesc) {
return;
}
const AudioStreamBasicDescription* asbd = const AudioStreamBasicDescription* asbd =
CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc); CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc);
if (!asbd || asbd->mChannelsPerFrame == 0) {
return;
}
if (_owner->cb_ && dataPtr && length > 0 && asbd) { if (owner->cb_) {
std::vector<short> out_pcm16; std::vector<short> out_pcm16;
if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) { if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) {
int channels = asbd->mChannelsPerFrame; int channels = asbd->mChannelsPerFrame;
@@ -86,7 +118,10 @@ class SpeakerCapturerMacosx;
size_t total_bytes = out_pcm16.size() * sizeof(short); size_t total_bytes = out_pcm16.size() * sizeof(short);
unsigned char* p = (unsigned char*)out_pcm16.data(); unsigned char* p = (unsigned char*)out_pcm16.data();
for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) { 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) { if (error || !impl_->content) {
LOG_ERROR("Failed to get shareable content: {}", LOG_ERROR("Failed to get shareable content: {}",
std::string([error.localizedDescription UTF8String])); NSErrorToString(error));
return -1; return -1;
} }
@@ -209,7 +244,7 @@ int SpeakerCapturerMacosx::Start() {
error:&addOutputError]; error:&addOutputError];
if (!ok || addOutputError) { if (!ok || addOutputError) {
LOG_ERROR("addStreamOutput error: {}", LOG_ERROR("addStreamOutput error: {}",
std::string([addOutputError.localizedDescription UTF8String])); NSErrorToString(addOutputError));
impl_->stream = nil; impl_->stream = nil;
impl_->delegate = nil; impl_->delegate = nil;
return -1; return -1;
@@ -220,7 +255,7 @@ int SpeakerCapturerMacosx::Start() {
[impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) { [impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) {
if (error) { if (error) {
LOG_ERROR("startCaptureWithCompletionHandler error: {}", LOG_ERROR("startCaptureWithCompletionHandler error: {}",
std::string([error.localizedDescription UTF8String])); NSErrorToString(error));
ret = -1; ret = -1;
} }
dispatch_semaphore_signal(semaStart); dispatch_semaphore_signal(semaStart);
@@ -238,13 +273,14 @@ int SpeakerCapturerMacosx::Stop() {
[impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) { [impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) {
if (error) { if (error) {
LOG_ERROR("stopCaptureWithCompletionHandler error: {}", LOG_ERROR("stopCaptureWithCompletionHandler error: {}",
std::string([error.localizedDescription UTF8String])); NSErrorToString(error));
} }
dispatch_semaphore_signal(sema); dispatch_semaphore_signal(sema);
}]; }];
dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);
impl_->stream = nil; impl_->stream = nil;
impl_->delegate.owner = nullptr;
impl_->delegate = nil; impl_->delegate = nil;
return 0; return 0;
@@ -269,4 +305,4 @@ int SpeakerCapturerMacosx::Destroy() {
int SpeakerCapturerMacosx::Pause() { return 0; } int SpeakerCapturerMacosx::Pause() { return 0; }
int SpeakerCapturerMacosx::Resume() { return Start(); } int SpeakerCapturerMacosx::Resume() { return Start(); }
} // namespace crossdesk } // namespace crossdesk
+17 -10
View File
@@ -225,9 +225,12 @@ int Thumbnail::LoadThumbnail(
std::string remote_id; std::string remote_id;
std::string cipher_password; std::string cipher_password;
std::string remote_host_name; std::string remote_host_name;
std::string password;
std::string original_image_name; std::string original_image_name;
bool remember_password = false;
if ('Y' == cipher_image_name[9] && cipher_image_name.size() >= 16) { if (cipher_image_name.size() > 9 && 'Y' == cipher_image_name[9] &&
cipher_image_name.size() >= 16) {
size_t pos_y = cipher_image_name.find('Y'); size_t pos_y = cipher_image_name.find('Y');
size_t pos_at = cipher_image_name.find('@'); size_t pos_at = cipher_image_name.find('@');
@@ -241,10 +244,11 @@ int Thumbnail::LoadThumbnail(
remote_host_name = remote_host_name =
cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1); cipher_image_name.substr(pos_y + 1, pos_at - pos_y - 1);
cipher_password = cipher_image_name.substr(pos_at + 1); cipher_password = cipher_image_name.substr(pos_at + 1);
password = AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
remember_password = true;
original_image_name = original_image_name = remote_id + 'Y' + remote_host_name + "@" +
remote_id + 'Y' + remote_host_name + "@" + password;
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
} else { } else {
size_t pos_n = cipher_image_name.find('N'); size_t pos_n = cipher_image_name.find('N');
// size_t pos_at = cipher_image_name.find('@'); // size_t pos_at = cipher_image_name.find('@');
@@ -257,16 +261,19 @@ int Thumbnail::LoadThumbnail(
remote_id = cipher_image_name.substr(0, pos_n); remote_id = cipher_image_name.substr(0, pos_n);
remote_host_name = cipher_image_name.substr(pos_n + 1); remote_host_name = cipher_image_name.substr(pos_n + 1);
original_image_name = original_image_name = remote_id + 'N' + remote_host_name;
remote_id + 'N' + remote_host_name + "@" +
AES_decrypt(cipher_password, aes128_key_, aes128_iv_);
} }
std::string image_path = save_path_ + cipher_image_name; std::string image_path = save_path_ + cipher_image_name;
Thumbnail::RecentConnection recent_connection;
recent_connection.remote_id = remote_id;
recent_connection.remote_host_name = remote_host_name;
recent_connection.password = password;
recent_connection.remember_password = remember_password;
recent_connections.emplace_back( recent_connections.emplace_back(
std::make_pair(original_image_name, Thumbnail::RecentConnection())); std::make_pair(original_image_name, recent_connection));
LoadTextureFromFile(image_path.c_str(), renderer, LoadTextureFromFile(image_path.c_str(), renderer,
&(recent_connections[i].second.texture), width, &(recent_connections.back().second.texture), width,
height); height);
} }
return 0; return 0;
@@ -436,4 +443,4 @@ std::string Thumbnail::AES_decrypt(const std::string& ciphertext,
return std::string(reinterpret_cast<char*>(plaintext), plaintext_len); return std::string(reinterpret_cast<char*>(plaintext), plaintext_len);
} }
} // namespace crossdesk } // namespace crossdesk
+351 -70
View File
@@ -8,7 +8,15 @@
#include <httplib.h> #include <httplib.h>
#include "rd_log.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <cstdlib>
#include <filesystem>
#include <iostream> #include <iostream>
#include <limits>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include <vector> #include <vector>
@@ -16,12 +24,296 @@
namespace crossdesk { namespace crossdesk {
static std::string latest_release_date_ = ""; 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) { std::string ExtractNumericPart(const std::string& ver) {
size_t start = 0; const size_t start = FindNumericStart(ver);
while (start < ver.size() && !std::isdigit(ver[start])) start++; const size_t end = FindNumericEnd(ver, start);
size_t end = start;
while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++;
return ver.substr(start, end - 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 // extract date from version string (format: v1.2.3-20251113-abc
// or 1.2.3-20251113-abc) // or 1.2.3-20251113-abc)
std::string ExtractDateFromVersion(const std::string& version) { std::string ExtractDateFromVersion(const std::string& version) {
size_t dash1 = version.find('-'); const size_t numeric_start = FindNumericStart(version);
if (dash1 != std::string::npos) { const size_t numeric_end = FindNumericEnd(version, numeric_start);
size_t dash2 = version.find('-', dash1 + 1); const std::string suffix = version.substr(numeric_end);
if (dash2 != std::string::npos) { std::string date;
std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1); size_t date_end = 0;
if (ExtractDateFromText(suffix, &date, &date_end)) {
bool is_date = true; return date;
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);
}
}
} }
return ""; 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) { bool IsNewerVersion(const std::string& current, const std::string& latest) {
auto v1 = SplitVersion(ExtractNumericPart(current)); return IsNewerVersionWithMetadata(
auto v2 = SplitVersion(ExtractNumericPart(latest)); current, latest, latest_release_date_,
latest_patch_available_ ? latest_patch_ : -1);
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;
} }
bool IsNewerVersionWithDate(const std::string& current_version, bool IsNewerVersionWithMetadata(const std::string& current,
const std::string& current_date, const std::string& latest,
const std::string& latest_version, const std::string& latest_date,
const std::string& latest_date) { int latest_patch) {
// compare versions (void)latest_date;
auto v1 = SplitVersion(ExtractNumericPart(current_version));
auto v2 = SplitVersion(ExtractNumericPart(latest_version));
size_t len = std::max(v1.size(), v2.size()); const ParsedVersion current_version = ParseVersion(current);
v1.resize(len, 0); const ParsedVersion latest_version = ParseVersion(latest);
v2.resize(len, 0);
for (size_t i = 0; i < len; ++i) { const int numeric_compare =
if (v2[i] > v1[i]) return true; CompareNumericVersion(current_version.numbers, latest_version.numbers);
if (v2[i] < v1[i]) return false; if (numeric_compare > 0) {
return true;
}
if (numeric_compare < 0) {
return false;
} }
// if versions are equal, compare by release date const bool metadata_has_patch = latest_patch >= 0;
if (!current_date.empty() && !latest_date.empty()) { const bool latest_has_patch = metadata_has_patch || latest_version.has_patch;
return IsNewerDate(current_date, latest_date); 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; return false;
} }
@@ -130,8 +396,14 @@ nlohmann::json CheckUpdate() {
cli.set_connection_timeout(5); cli.set_connection_timeout(5);
cli.set_read_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) { if (res->status == 200) {
try { try {
auto j = nlohmann::json::parse(res->body); auto j = nlohmann::json::parse(res->body);
@@ -140,19 +412,28 @@ nlohmann::json CheckUpdate() {
} else { } else {
latest_release_date_ = ""; 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; return j;
} catch (std::exception&) { } catch (const std::exception& e) {
latest_release_date_ = ""; LOG_WARN("Failed to parse version.json: {}", e.what());
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} else { } else {
latest_release_date_ = ""; LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status);
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} else { } else {
latest_release_date_ = ""; LogHttpError(res);
ResetLatestMetadata();
return nlohmann::json{}; return nlohmann::json{};
} }
} }
} // namespace crossdesk } // namespace crossdesk
+7 -1
View File
@@ -16,6 +16,12 @@ nlohmann::json CheckUpdate();
bool IsNewerVersion(const std::string& current, const std::string& latest); 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 } // namespace crossdesk
#endif #endif
+86
View File
@@ -39,6 +39,35 @@ bool ExpectContains(const char* name, const std::string& value,
return false; 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) { bool ExpectResetBeforeDisplayPopup(const std::string& value) {
const std::string reset = "props->display_selectable_hovered_ = false;"; const std::string reset = "props->display_selectable_hovered_ = false;";
const std::string popup = "ImGui::BeginPopup(\"display\")"; const std::string popup = "ImGui::BeginPopup(\"display\")";
@@ -93,5 +122,62 @@ int main() {
ok &= ExpectContains("control_bar.cpp", control_bar, ok &= ExpectContains("control_bar.cpp", control_bar,
"props->shortcut_selectable_hovered_ ="); "props->shortcut_selectable_hovered_ =");
ok &= ExpectResetBeforeShortcutPopup(control_bar); 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; return ok ? 0 : 1;
} }
+71
View File
@@ -0,0 +1,71 @@
#include "device_controller.h"
#include <iostream>
#include <string>
namespace {
bool ExpectEqual(const char* name, size_t actual, size_t expected) {
if (actual == expected) {
return true;
}
std::cerr << name << " mismatch\n"
<< " expected: " << expected << "\n"
<< " actual: " << actual << "\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() {
bool ok = true;
ok &= ExpectEqual("mouse type", crossdesk::ControlType::mouse, 0);
ok &= ExpectEqual("keyboard type", crossdesk::ControlType::keyboard, 1);
ok &= ExpectEqual("audio_capture type", crossdesk::ControlType::audio_capture,
2);
ok &= ExpectEqual("host_infomation type",
crossdesk::ControlType::host_infomation, 3);
ok &= ExpectEqual("display_id type", crossdesk::ControlType::display_id, 4);
ok &= ExpectEqual("service_status type",
crossdesk::ControlType::service_status, 5);
ok &= ExpectEqual("service_command type",
crossdesk::ControlType::service_command, 6);
ok &= ExpectEqual("keyboard_state type",
crossdesk::ControlType::keyboard_state, 7);
crossdesk::RemoteAction action{};
action.type = crossdesk::ControlType::keyboard_state;
action.ks.seq = 42;
action.ks.pressed_count = 2;
action.ks.pressed_keys[0] = {65, 30, false};
action.ks.pressed_keys[1] = {0xA3, 29, true};
const std::string json = action.to_json();
crossdesk::RemoteAction parsed{};
ok &= ExpectTrue("parse keyboard_state", parsed.from_json(json));
ok &= ExpectEqual("parsed type", parsed.type,
crossdesk::ControlType::keyboard_state);
ok &= ExpectEqual("parsed seq", parsed.ks.seq, 42);
ok &= ExpectEqual("parsed pressed_count", parsed.ks.pressed_count, 2);
ok &= ExpectEqual("parsed key 0", parsed.ks.pressed_keys[0].key_value, 65);
ok &= ExpectEqual("parsed scan 0", parsed.ks.pressed_keys[0].scan_code, 30);
ok &= ExpectTrue("parsed extended 0",
!parsed.ks.pressed_keys[0].extended);
ok &= ExpectEqual("parsed key 1", parsed.ks.pressed_keys[1].key_value,
0xA3);
ok &= ExpectEqual("parsed scan 1", parsed.ks.pressed_keys[1].scan_code, 29);
ok &= ExpectTrue("parsed extended 1", parsed.ks.pressed_keys[1].extended);
return ok ? 0 : 1;
}
+54
View File
@@ -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;
}
+23
View File
@@ -82,16 +82,27 @@ int main() {
} }
const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc"); 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 = const std::string manifest =
ReadFile(repo_root / "scripts/windows/crossdesk.manifest"); ReadFile(repo_root / "scripts/windows/crossdesk.manifest");
const std::string debug_manifest = const std::string debug_manifest =
ReadFile(repo_root / "scripts/windows/crossdesk_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; bool ok = true;
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest"); ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest");
ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest"); ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest");
ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG"); ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG");
ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST"); 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, ok &= ExpectContains("crossdesk.manifest", manifest,
"level=\"requireAdministrator\""); "level=\"requireAdministrator\"");
ok &= ExpectContains("crossdesk.manifest", manifest, ok &= ExpectContains("crossdesk.manifest", manifest,
@@ -108,10 +119,22 @@ int main() {
"http://schemas.microsoft.com/SMI/2016/WindowsSettings"); "http://schemas.microsoft.com/SMI/2016/WindowsSettings");
ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest, ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest,
"processorArchitecture=\"*\""); "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 #ifdef _WIN32
ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest"); ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest");
ok &= ExpectActivationContext( ok &= ExpectActivationContext(
repo_root / "scripts/windows/crossdesk_debug.manifest"); repo_root / "scripts/windows/crossdesk_debug.manifest");
ok &= ExpectActivationContext(
repo_root / "scripts/windows/crossdesk_portable.manifest");
#endif #endif
return ok ? 0 : 1; return ok ? 0 : 1;
} }
+149
View File
@@ -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;
}
+163
View File
@@ -39,6 +39,16 @@ bool ExpectContains(const char* name, const std::string& value,
return false; 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 } // namespace
int main() { int main() {
@@ -50,13 +60,166 @@ int main() {
const std::string service_host = const std::string service_host =
ReadFile(repo_root / "src/service/windows/service_host.cpp"); 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; bool ok = true;
ok &= ExpectContains("service_host.cpp", service_host, ok &= ExpectContains("service_host.cpp", service_host,
"ParseSecureDesktopMouseIpcCommand"); "ParseSecureDesktopMouseIpcCommand");
ok &= ExpectContains("service_host.cpp", service_host, ok &= ExpectContains("service_host.cpp", service_host,
"BuildSecureInputHelperMouseCommand"); "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, ok &= ExpectContains("service_host.cpp", service_host,
"return SendSecureDesktopMouseInput"); "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; return ok ? 0 : 1;
} }
+10 -4
View File
@@ -35,7 +35,12 @@ function setup_platform_settings()
add_links("pulse-simple", "pulse") add_links("pulse-simple", "pulse")
add_requires("libyuv") add_requires("libyuv")
add_syslinks("pthread", "dl") add_syslinks("pthread", "dl")
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr", "Xfixes") add_links("SDL3", "asound", "X11", "Xext", "Xrender", "Xft", "Xtst",
"Xrandr", "Xfixes")
add_existing_include_dirs({
"/usr/include/freetype2",
"/usr/local/include/freetype2"
}, {system = true})
if is_config("USE_DRM", true) then if is_config("USE_DRM", true) then
add_links("drm") add_links("drm")
@@ -75,7 +80,8 @@ function setup_platform_settings()
add_links("SDL3") add_links("SDL3")
add_ldflags("-Wl,-ld_classic") add_ldflags("-Wl,-ld_classic")
add_cxflags("-Wno-unused-variable") add_cxflags("-Wno-unused-variable")
add_frameworks("OpenGL", "IOSurface", "ScreenCaptureKit", "AVFoundation", add_frameworks("Cocoa", "OpenGL", "IOSurface", "ScreenCaptureKit",
"CoreMedia", "CoreVideo", "CoreAudio", "AudioToolbox") "AVFoundation", "CoreMedia", "CoreVideo", "CoreAudio",
"AudioToolbox")
end end
end end
+42 -3
View File
@@ -3,6 +3,11 @@ function setup_targets()
includes("submodules", "thirdparty") 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") target("rd_log")
set_kind("object") set_kind("object")
add_packages("spdlog") add_packages("spdlog")
@@ -39,6 +44,12 @@ function setup_targets()
add_includedirs("src/device_controller") add_includedirs("src/device_controller")
add_files("tests/macos_keyboard_modifier_state_test.cpp") add_files("tests/macos_keyboard_modifier_state_test.cpp")
target("keyboard_state_protocol_test")
set_kind("binary")
set_default(false)
add_includedirs("src/device_controller", "src/common")
add_files("tests/keyboard_state_protocol_test.cpp")
target("windows_manifest_resource_test") target("windows_manifest_resource_test")
set_kind("binary") set_kind("binary")
set_default(false) set_default(false)
@@ -54,11 +65,30 @@ function setup_targets()
set_default(false) set_default(false)
add_files("tests/windows_mouse_controller_safety_test.cpp") 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") target("display_popup_hover_state_test")
set_kind("binary") set_kind("binary")
set_default(false) set_default(false)
add_files("tests/display_popup_hover_state_test.cpp") 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") target("screen_capturer")
set_kind("object") set_kind("object")
add_deps("rd_log", "common") add_deps("rd_log", "common")
@@ -158,6 +188,10 @@ function setup_targets()
add_deps("rd_log") add_deps("rd_log")
add_files("src/version_checker/*.cpp") add_files("src/version_checker/*.cpp")
add_includedirs("src/version_checker", {public = true}) 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") target("tools")
set_kind("object") set_kind("object")
@@ -180,11 +214,15 @@ function setup_targets()
add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars", add_includedirs("src/gui", "src/gui/panels", "src/gui/toolbars",
"src/gui/windows", {public = true}) "src/gui/windows", {public = true})
if is_os("windows") then if is_os("windows") then
add_files("src/gui/tray/*.cpp") add_files("src/gui/tray/win_tray.cpp")
add_includedirs("src/gui/tray", "src/service/windows", add_includedirs("src/gui/tray", "src/service/windows",
{public = true}) {public = true})
elseif is_os("macosx") then elseif is_os("macosx") then
add_files("src/gui/windows/*.mm") add_files("src/gui/windows/*.mm", "src/gui/tray/*.mm")
add_includedirs("src/gui/tray", {public = true})
elseif is_os("linux") then
add_files("src/gui/tray/linux_tray.cpp")
add_includedirs("src/gui/tray", {public = true})
end end
if is_os("windows") then if is_os("windows") then
@@ -217,6 +255,7 @@ function setup_targets()
add_deps("rd_log", "path_manager") add_deps("rd_log", "path_manager")
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32") add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
add_files("src/service/windows/session_helper_main.cpp") add_files("src/service/windows/session_helper_main.cpp")
add_files(crossdesk_windows_resource)
add_includedirs("src/service/windows", {public = true}) add_includedirs("src/service/windows", {public = true})
end end
@@ -230,6 +269,6 @@ function setup_targets()
add_includedirs("src/service/windows", {public = true}) add_includedirs("src/service/windows", {public = true})
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv") add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper") add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
add_files("scripts/windows/crossdesk.rc") add_files(crossdesk_windows_resource)
end end
end end