mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-19 18:33:00 +08:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c76caa918 | ||
|
|
211d4de241 | ||
|
|
c2c74913e3 |
@@ -0,0 +1,62 @@
|
||||
name: Sign Windows artifact
|
||||
description: Submit one raw EXE or ZIP to SignPath and download the signed result.
|
||||
inputs:
|
||||
api-token:
|
||||
description: SignPath submitter token.
|
||||
required: true
|
||||
organization-id:
|
||||
description: SignPath organization ID.
|
||||
required: true
|
||||
signing-policy:
|
||||
description: test-signing or release-signing.
|
||||
required: true
|
||||
artifact-configuration:
|
||||
description: Artifact configuration slug in the crossdesk project.
|
||||
required: true
|
||||
artifact:
|
||||
description: Absolute path to one EXE or ZIP file.
|
||||
required: true
|
||||
output-directory:
|
||||
description: Absolute directory for the downloaded signed file.
|
||||
required: true
|
||||
version:
|
||||
description: CrossDesk ProductVersion and FileVersion.
|
||||
required: true
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Upload unsigned artifact
|
||||
id: unsigned
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ inputs.artifact }}
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Submit signing request and wait for approval
|
||||
id: sign
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
with:
|
||||
api-token: ${{ inputs.api-token }}
|
||||
organization-id: ${{ inputs.organization-id }}
|
||||
project-slug: crossdesk
|
||||
signing-policy-slug: ${{ inputs.signing-policy }}
|
||||
artifact-configuration-slug: ${{ inputs.artifact-configuration }}
|
||||
github-artifact-id: ${{ steps.unsigned.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
wait-for-completion-timeout-in-seconds: 3600
|
||||
output-artifact-directory: ${{ inputs.output-directory }}
|
||||
skip-decompress: true
|
||||
parameters: |
|
||||
version: ${{ toJSON(inputs.version) }}
|
||||
|
||||
- name: Record signing request
|
||||
if: always() && steps.sign.outputs.signing-request-web-url != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_REQUEST_URL: ${{ steps.sign.outputs.signing-request-web-url }}
|
||||
SIGNING_CONFIGURATION: ${{ inputs.artifact-configuration }}
|
||||
SIGNING_POLICY: ${{ inputs.signing-policy }}
|
||||
run: |
|
||||
"- $env:SIGNING_CONFIGURATION ($env:SIGNING_POLICY): $env:SIGNING_REQUEST_URL" >> $env:GITHUB_STEP_SUMMARY
|
||||
+116
-45
@@ -14,7 +14,17 @@ on:
|
||||
required: false
|
||||
default: "0"
|
||||
sign_windows_test:
|
||||
description: "Submit the Windows installer to SignPath using test-signing."
|
||||
description: "Test-sign Windows binaries and installer (Actions artifacts only)."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
sign_windows_release:
|
||||
description: "Test release-signing with the production certificate (Actions artifacts only)."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
windows_only:
|
||||
description: "Build only Windows, for signing tests."
|
||||
required: false
|
||||
type: boolean
|
||||
default: false
|
||||
@@ -28,6 +38,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
if: github.event_name != 'workflow_dispatch' || !inputs.windows_only
|
||||
name: Build Linux (${{ matrix.arch }})
|
||||
runs-on: ${{ matrix.runner }}
|
||||
strategy:
|
||||
@@ -176,6 +187,7 @@ jobs:
|
||||
|
||||
# macOS
|
||||
build-macos:
|
||||
if: github.event_name != 'workflow_dispatch' || !inputs.windows_only
|
||||
name: Build on macOS
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
@@ -271,7 +283,7 @@ jobs:
|
||||
|
||||
- name: Build CrossDesk
|
||||
run: |
|
||||
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=false -vyD
|
||||
xmake f --target_minver=${MACOSX_DEPLOYMENT_TARGET} --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=false -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Package CrossDesk app
|
||||
@@ -308,6 +320,7 @@ jobs:
|
||||
|
||||
# iOS
|
||||
build-ios:
|
||||
if: github.event_name != 'workflow_dispatch' || !inputs.windows_only
|
||||
name: Build on iOS (arm64)
|
||||
runs-on: macos-26
|
||||
timeout-minutes: 120
|
||||
@@ -504,9 +517,37 @@ jobs:
|
||||
build-windows-x64:
|
||||
name: Build on Windows x64
|
||||
runs-on: windows-2022
|
||||
timeout-minutes: 240
|
||||
outputs:
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
env:
|
||||
XMAKE_GLOBALDIR: D:\xmake_global
|
||||
steps:
|
||||
- name: Select Windows signing policy
|
||||
id: signing
|
||||
shell: pwsh
|
||||
env:
|
||||
TEST_SIGNING: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows_test }}
|
||||
RELEASE_SIGNING: ${{ github.event_name == 'workflow_dispatch' && inputs.sign_windows_release }}
|
||||
PUBLISH_RELEASE: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') }}
|
||||
SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
run: |
|
||||
if ($env:TEST_SIGNING -eq 'true' -and $env:RELEASE_SIGNING -eq 'true') {
|
||||
throw 'Select only one of sign_windows_test and sign_windows_release.'
|
||||
}
|
||||
$policy = ''
|
||||
if ($env:PUBLISH_RELEASE -eq 'true' -or $env:RELEASE_SIGNING -eq 'true') {
|
||||
$policy = 'release-signing'
|
||||
} elseif ($env:TEST_SIGNING -eq 'true') {
|
||||
$policy = 'test-signing'
|
||||
}
|
||||
if ($policy -and (!$env:SIGNPATH_API_TOKEN -or !$env:SIGNPATH_ORGANIZATION_ID)) {
|
||||
throw 'Signing requires SIGNPATH_API_TOKEN and SIGNPATH_ORGANIZATION_ID.'
|
||||
}
|
||||
"policy=$policy" >> $env:GITHUB_OUTPUT
|
||||
"Windows signing policy: $(if ($policy) { $policy } else { 'unsigned development build' })" >> $env:GITHUB_STEP_SUMMARY
|
||||
|
||||
- name: Extract version number
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -611,67 +652,99 @@ jobs:
|
||||
$nsisPluginDir = "C:\Program Files (x86)\NSIS\Plugins\x86-unicode"
|
||||
copy "${{ github.workspace }}\apps\desktop\scripts\windows\nsProcess.dll" $nsisPluginDir
|
||||
|
||||
- name: Test Windows signing helpers
|
||||
shell: pwsh
|
||||
run: ./apps/desktop/tests/windows_signing_test.ps1
|
||||
|
||||
- name: Build CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Prepare installer binaries for signing
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation PrepareBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/unsigned/installer-binaries.zip
|
||||
|
||||
- name: Sign installer binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
uses: ./.github/actions/sign-windows
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
artifact-configuration: windows-binaries
|
||||
artifact: ${{ github.workspace }}/signpath/unsigned/installer-binaries.zip
|
||||
output-directory: ${{ github.workspace }}/signpath/signed/installer-binaries
|
||||
version: ${{ env.VERSION_NUM }}
|
||||
|
||||
- name: Verify and restore signed installer binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_POLICY: ${{ steps.signing.outputs.policy }}
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation RestoreBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/signed/installer-binaries/installer-binaries.zip -SigningPolicy $env:SIGNING_POLICY
|
||||
|
||||
- name: Package
|
||||
shell: pwsh
|
||||
run: |
|
||||
& "${{ github.workspace }}\apps\desktop\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM
|
||||
|
||||
- name: Upload unsigned Windows installer for test signing
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
id: upload-unsigned-windows-installer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
- name: Sign Windows installer with SignPath test certificate
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
uses: signpath/github-action-submit-signing-request@v2
|
||||
- name: Sign Windows installer
|
||||
if: steps.signing.outputs.policy != ''
|
||||
uses: ./.github/actions/sign-windows
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
project-slug: crossdesk
|
||||
signing-policy-slug: test-signing
|
||||
github-artifact-id: ${{ steps.upload-unsigned-windows-installer.outputs.artifact-id }}
|
||||
wait-for-completion: true
|
||||
output-artifact-directory: ${{ github.workspace }}\signpath\signed-installer
|
||||
skip-decompress: true
|
||||
parameters: |
|
||||
version: ${{ toJSON(env.VERSION_NUM) }}
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
artifact-configuration: initial
|
||||
artifact: ${{ github.workspace }}/apps/desktop/scripts/windows/crossdesk-win-x64-${{ env.VERSION_NUM }}.exe
|
||||
output-directory: ${{ github.workspace }}/signpath/signed/installer
|
||||
version: ${{ env.VERSION_NUM }}
|
||||
|
||||
- name: Verify and use test-signed Windows installer
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.sign_windows_test
|
||||
- name: Verify and use signed Windows installer
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_POLICY: ${{ steps.signing.outputs.policy }}
|
||||
run: |
|
||||
$installerName = "crossdesk-win-x64-${{ env.VERSION_NUM }}.exe"
|
||||
$signedInstaller = Join-Path "${{ github.workspace }}\signpath\signed-installer" $installerName
|
||||
$destination = Join-Path "${{ github.workspace }}\apps\desktop\scripts\windows" $installerName
|
||||
|
||||
if (!(Test-Path $signedInstaller)) {
|
||||
throw "SignPath did not return the signed installer: $signedInstaller"
|
||||
}
|
||||
|
||||
$signature = Get-AuthenticodeSignature $signedInstaller
|
||||
if (!$signature.SignerCertificate -or $signature.Status -eq "NotSigned") {
|
||||
throw "SignPath returned an installer without an Authenticode signature."
|
||||
}
|
||||
|
||||
Write-Host "Signer: $($signature.SignerCertificate.Subject)"
|
||||
Write-Host "Signature status: $($signature.Status)"
|
||||
Copy-Item $signedInstaller $destination -Force
|
||||
$installerName = "crossdesk-win-x64-$env:VERSION_NUM.exe"
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation VerifyInstaller -Artifact "./signpath/signed/installer/$installerName" -SigningPolicy $env:SIGNING_POLICY
|
||||
Copy-Item "./signpath/signed/installer/$installerName" ./apps/desktop/scripts/windows/ -Force
|
||||
|
||||
- name: Build Portable CrossDesk
|
||||
run: |
|
||||
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true --CROSSDESK_PORTABLE=true -y
|
||||
xmake b -vy crossdesk
|
||||
|
||||
- name: Prepare portable binaries for signing
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation PrepareBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/unsigned/portable-binaries.zip
|
||||
|
||||
- name: Sign portable binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
uses: ./.github/actions/sign-windows
|
||||
with:
|
||||
api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
|
||||
organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
|
||||
signing-policy: ${{ steps.signing.outputs.policy }}
|
||||
artifact-configuration: windows-binaries
|
||||
artifact: ${{ github.workspace }}/signpath/unsigned/portable-binaries.zip
|
||||
output-directory: ${{ github.workspace }}/signpath/signed/portable-binaries
|
||||
version: ${{ env.VERSION_NUM }}
|
||||
|
||||
- name: Verify and restore signed portable binaries
|
||||
if: steps.signing.outputs.policy != ''
|
||||
shell: pwsh
|
||||
env:
|
||||
SIGNING_POLICY: ${{ steps.signing.outputs.policy }}
|
||||
run: |
|
||||
./apps/desktop/scripts/windows/signpath-artifact.ps1 -Operation RestoreBinaries -Directory ./build/windows/x64/release -Artifact ./signpath/signed/portable-binaries/portable-binaries.zip -SigningPolicy $env:SIGNING_POLICY
|
||||
|
||||
- name: Package Portable
|
||||
shell: pwsh
|
||||
run: |
|
||||
@@ -682,9 +755,7 @@ jobs:
|
||||
$portableFiles = @(
|
||||
@("crossdesk.exe", "CrossDesk.exe"),
|
||||
@("crossdesk_service.exe", "crossdesk_service.exe"),
|
||||
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe"),
|
||||
@("crossdesk_privacy_cursor_helper.exe", "crossdesk_privacy_cursor_helper.exe"),
|
||||
@("crossdesk_privacy_window.dll", "crossdesk_privacy_window.dll")
|
||||
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
|
||||
)
|
||||
|
||||
foreach ($file in $portableFiles) {
|
||||
@@ -721,7 +792,7 @@ jobs:
|
||||
|
||||
release:
|
||||
name: Publish Release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && needs.build-windows-x64.outputs.signing-policy == 'release-signing'
|
||||
needs: [build-linux, build-macos, build-ios, build-windows-x64]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
@@ -92,8 +92,9 @@ jobs:
|
||||
|
||||
- name: Generate or update version.json
|
||||
run: |
|
||||
# If version.json exists, try to preserve downloads section
|
||||
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
|
||||
# Preserve download URLs only for this release, never an older version.
|
||||
if [ -f version.json ] && jq -e --arg tag "${{ steps.version.outputs.TAG_NAME }}" \
|
||||
'.tagName == $tag and (.downloads | type == "object")' version.json > /dev/null 2>&1; then
|
||||
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
|
||||
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
|
||||
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
IDI_CROSSDESK_LOGO ICON "crossdesk.ico"
|
||||
|
||||
#define CROSSDESK_FILE_DESCRIPTION "CrossDesk Privacy Window"
|
||||
#define CROSSDESK_ORIGINAL_FILENAME "crossdesk_privacy_window.dll"
|
||||
#define CROSSDESK_FILE_TYPE VFT_DLL
|
||||
|
||||
#include "version_info.rcinc"
|
||||
@@ -87,7 +87,7 @@ if [[ ! -x "$BUILD_BINARY" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for command_name in readelf ldd dpkg-deb strip; do
|
||||
for command_name in readelf ldd dpkg-deb; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "Required packaging command is missing: $command_name" >&2
|
||||
exit 1
|
||||
@@ -128,9 +128,6 @@ if [[ -n "$SLINT_NEEDED" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
install -m 0755 "$SLINT_LIBRARY" "$PRIVATE_DIR/$SLINT_NEEDED"
|
||||
# Keep dynamic exports/relocations while removing unneeded symbols from the
|
||||
# staged copy. Never strip Xmake's cached runtime in place.
|
||||
strip --strip-unneeded "$PRIVATE_DIR/$SLINT_NEEDED"
|
||||
fi
|
||||
|
||||
cat > "$BIN_DIR/$PKG_NAME" <<'EOF'
|
||||
@@ -164,7 +161,7 @@ Architecture: $DEBIAN_ARCH
|
||||
Maintainer: $MAINTAINER
|
||||
Description: $DESCRIPTION
|
||||
Depends: libc6 (>= 2.31), libstdc++6 (>= 10), libx11-6, libxext6,
|
||||
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxi6, libxcb1, libxcb-randr0,
|
||||
libxrender1, libxft2, libxrandr2, libxfixes3, libxcursor1, libxcb1, libxcb-randr0,
|
||||
libxcb-xtest0, libxcb-xinerama0, libxcb-shape0, libxcb-xkb1,
|
||||
libxcb-xfixes0, libxv1, libxtst6, $ALSA_RUNTIME_DEP, libsndio7.0,
|
||||
libxcb-shm0, libpulse0, libdrm2, libdbus-1-3, libgl1
|
||||
|
||||
@@ -11,7 +11,7 @@ EXPECTED_ARCH="$2"
|
||||
APP_EXECUTABLE="$APP_BUNDLE/Contents/MacOS/CrossDesk"
|
||||
FRAMEWORKS_DIR="$APP_BUNDLE/Contents/Frameworks"
|
||||
|
||||
for command_name in codesign install_name_tool lipo otool strip; do
|
||||
for command_name in codesign install_name_tool lipo otool; do
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
echo "Required bundling command is missing: $command_name" >&2
|
||||
exit 1
|
||||
@@ -123,11 +123,7 @@ if ! otool -l "$APP_EXECUTABLE" | awk '
|
||||
install_name_tool -add_rpath "@executable_path/../Frameworks" "$APP_EXECUTABLE"
|
||||
fi
|
||||
|
||||
# Strip only the staged runtime, retaining exported symbols for Slint's C ABI.
|
||||
# The package cache remains intact for debugging and other consumers.
|
||||
strip -S -x "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
|
||||
# install_name_tool and strip invalidate the linker's ad-hoc signature. Sign nested code
|
||||
# install_name_tool invalidates the linker's ad-hoc signature. Sign nested code
|
||||
# first, then seal the complete app bundle so local packages remain launchable.
|
||||
codesign --force --sign - "$FRAMEWORKS_DIR/$SLINT_NAME"
|
||||
codesign --force --deep --sign - "$APP_BUNDLE"
|
||||
|
||||
@@ -88,10 +88,8 @@ installApp:
|
||||
; Bundle service-side binaries required by the Windows service flow
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_service.exe"
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_session_helper.exe"
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_privacy_cursor_helper.exe"
|
||||
File "..\..\..\..\build\windows\x64\release\crossdesk_privacy_window.dll"
|
||||
; Bundle runtime DLLs from the release output directory
|
||||
File /x crossdesk_privacy_window.dll "..\..\..\..\build\windows\x64\release\*.dll"
|
||||
File "..\..\..\..\build\windows\x64\release\*.dll"
|
||||
|
||||
Call RegisterInstalledService
|
||||
|
||||
@@ -147,8 +145,6 @@ uninstallApp:
|
||||
Delete "$INSTDIR\CrossDesk.exe"
|
||||
Delete "$INSTDIR\crossdesk_service.exe"
|
||||
Delete "$INSTDIR\crossdesk_session_helper.exe"
|
||||
Delete "$INSTDIR\crossdesk_privacy_cursor_helper.exe"
|
||||
Delete "$INSTDIR\crossdesk_privacy_window.dll"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
; Recursively delete installation directory
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
[ValidateSet('PrepareBinaries', 'RestoreBinaries', 'VerifyInstaller')]
|
||||
[string]$Operation,
|
||||
[Parameter(Mandatory)] [string]$Artifact,
|
||||
[string]$Directory,
|
||||
[ValidateSet('test-signing', 'release-signing')] [string]$SigningPolicy
|
||||
)
|
||||
|
||||
. "$PSScriptRoot/signpath-common.ps1"
|
||||
|
||||
if ($Operation -ne 'PrepareBinaries' -and !$SigningPolicy) {
|
||||
throw 'SigningPolicy is required when verifying signed artifacts.'
|
||||
}
|
||||
if ($Operation -eq 'VerifyInstaller') {
|
||||
Assert-CrossDeskSignature -Path $Artifact -SigningPolicy $SigningPolicy
|
||||
return
|
||||
}
|
||||
if (!$Directory -or !(Test-Path -LiteralPath $Directory -PathType Container)) {
|
||||
throw "Binaries directory does not exist: $Directory"
|
||||
}
|
||||
|
||||
$files = @(Get-CrossDeskSigningFiles)
|
||||
$temporaryDirectory = Join-Path ([IO.Path]::GetTempPath()) "crossdesk-signing-$([guid]::NewGuid())"
|
||||
New-Item -ItemType Directory -Path $temporaryDirectory | Out-Null
|
||||
try {
|
||||
if ($Operation -eq 'PrepareBinaries') {
|
||||
foreach ($name in $files) {
|
||||
$source = Join-Path $Directory $name
|
||||
if (!(Test-Path -LiteralPath $source -PathType Leaf)) {
|
||||
throw "Missing CrossDesk binary: $source"
|
||||
}
|
||||
Copy-Item -LiteralPath $source -Destination (Join-Path $temporaryDirectory $name)
|
||||
}
|
||||
$artifactPath = [IO.Path]::GetFullPath($Artifact)
|
||||
New-Item -ItemType Directory -Path (Split-Path -Parent $artifactPath) -Force | Out-Null
|
||||
Compress-Archive -Path "$temporaryDirectory/*" -DestinationPath $artifactPath -Force
|
||||
return
|
||||
}
|
||||
|
||||
# Inspect the ZIP before extraction: reject missing, duplicate or unexpected
|
||||
# entries (including paths) so only the four intended files can be replaced.
|
||||
$archive = [IO.Compression.ZipFile]::OpenRead([IO.Path]::GetFullPath($Artifact))
|
||||
try {
|
||||
$entries = @($archive.Entries | ForEach-Object { $_.FullName })
|
||||
$uniqueEntries = @($entries | Sort-Object -Unique)
|
||||
if ($entries.Count -ne $files.Count -or $uniqueEntries.Count -ne $files.Count -or
|
||||
@(Compare-Object $files $entries).Count -ne 0) {
|
||||
throw "Signed archive must contain exactly: $($files -join ', ')"
|
||||
}
|
||||
} finally {
|
||||
$archive.Dispose()
|
||||
}
|
||||
Expand-Archive -LiteralPath $Artifact -DestinationPath $temporaryDirectory
|
||||
|
||||
# Verify every file before replacing any build output.
|
||||
foreach ($name in $files) {
|
||||
Assert-CrossDeskSignature -Path (Join-Path $temporaryDirectory $name) -SigningPolicy $SigningPolicy
|
||||
}
|
||||
foreach ($name in $files) {
|
||||
Copy-Item -LiteralPath (Join-Path $temporaryDirectory $name) -Destination (Join-Path $Directory $name) -Force
|
||||
}
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $temporaryDirectory -Recurse -Force
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<artifact-configuration xmlns="http://signpath.io/artifact-configuration/v1">
|
||||
<parameters>
|
||||
<parameter name="version" required="true" />
|
||||
</parameters>
|
||||
<zip-file>
|
||||
<pe-file-set product-name="CrossDesk"
|
||||
product-version="${version}"
|
||||
file-version="${version}"
|
||||
company-name="CrossDesk"
|
||||
copyright="Copyright (C) CrossDesk contributors">
|
||||
<include path="crossdesk.exe" original-filename="CrossDesk.exe" />
|
||||
<include path="crossdesk_service.exe" original-filename="crossdesk_service.exe" />
|
||||
<include path="crossdesk_session_helper.exe" original-filename="crossdesk_session_helper.exe" />
|
||||
<include path="wgc_plugin.dll" original-filename="wgc_plugin.dll" />
|
||||
<for-each>
|
||||
<authenticode-sign />
|
||||
</for-each>
|
||||
</pe-file-set>
|
||||
</zip-file>
|
||||
</artifact-configuration>
|
||||
@@ -0,0 +1,57 @@
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-CrossDeskSigningFiles {
|
||||
# Only binaries built from this project's sources. Never include third-party DLLs.
|
||||
return @('crossdesk.exe', 'crossdesk_service.exe', 'crossdesk_session_helper.exe', 'wgc_plugin.dll')
|
||||
}
|
||||
|
||||
function Find-CrossDeskSignTool {
|
||||
$command = Get-Command signtool.exe -ErrorAction SilentlyContinue
|
||||
if ($command) {
|
||||
return $command.Source
|
||||
}
|
||||
$sdk = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits/10/bin'
|
||||
$tool = Get-ChildItem "$sdk/*/x64/signtool.exe" |
|
||||
Sort-Object { [version]$_.Directory.Parent.Name } |
|
||||
Select-Object -Last 1
|
||||
if (!$tool) {
|
||||
throw 'SignTool from the Windows SDK is required to verify release signatures.'
|
||||
}
|
||||
return $tool.FullName
|
||||
}
|
||||
|
||||
function Assert-CrossDeskSignature {
|
||||
param(
|
||||
[Parameter(Mandatory)] [string]$Path,
|
||||
[Parameter(Mandatory)] [ValidateSet('test-signing', 'release-signing')] [string]$SigningPolicy
|
||||
)
|
||||
|
||||
if (!(Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "Missing signed file: $Path"
|
||||
}
|
||||
$signature = Get-AuthenticodeSignature -LiteralPath $Path
|
||||
if (!$signature.SignerCertificate) {
|
||||
throw "File has no Authenticode signing certificate: $Path"
|
||||
}
|
||||
Write-Host "Signature: $Path | $($signature.Status) | $($signature.SignerCertificate.Subject)"
|
||||
if ($SigningPolicy -eq 'test-signing') {
|
||||
# Self-signed test certificates are not trusted by stock GitHub runners.
|
||||
# HashMismatch, NotSigned, UnknownError, etc. must still stop the build.
|
||||
if ($signature.Status -notin @('Valid', 'NotTrusted')) {
|
||||
throw "Invalid test signature on ${Path}: $($signature.Status)"
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if ($signature.Status -ne 'Valid') {
|
||||
throw "Release signature is not trusted on ${Path}: $($signature.Status)"
|
||||
}
|
||||
if (!$signature.TimeStamperCertificate) {
|
||||
throw "Release signature has no timestamp: $Path"
|
||||
}
|
||||
$signTool = Find-CrossDeskSignTool
|
||||
& $signTool verify /pa /all /v $Path
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "SignTool verification failed for $Path (exit $LASTEXITCODE)."
|
||||
}
|
||||
}
|
||||
@@ -24,10 +24,6 @@
|
||||
#include "path_manager.h"
|
||||
#include "render.h"
|
||||
|
||||
#ifdef __linux__
|
||||
#include "platform/linux/privacy/privacy_guard.h"
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
namespace {
|
||||
|
||||
@@ -179,14 +175,6 @@ int HandleServiceCliCommand(const std::string& command) {
|
||||
#endif
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
#ifdef __linux__
|
||||
// Run before GUI, configuration, logging or daemon initialization. The
|
||||
// guard owns native privacy resources and restores them if this app exits.
|
||||
if (argc == 2 &&
|
||||
std::strcmp(argv[1], crossdesk::kLinuxPrivacyGuardArgument) == 0) {
|
||||
return crossdesk::RunLinuxPrivacyGuard();
|
||||
}
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
if (argc == 2 &&
|
||||
std::strcmp(argv[1], crossdesk::kSlintRendererProbeArgument) == 0) {
|
||||
@@ -245,4 +233,4 @@ int main(int argc, char* argv[]) {
|
||||
crossdesk::Render render;
|
||||
render.Run();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -58,14 +58,6 @@ int ConfigCenter::Load() {
|
||||
video_frame_rate_ = static_cast<VIDEO_FRAME_RATE>(ini_.GetLongValue(
|
||||
section_, "video_frame_rate", static_cast<long>(video_frame_rate_)));
|
||||
|
||||
const long screen_capture_method_value = ini_.GetLongValue(
|
||||
section_, "screen_capture_method",
|
||||
static_cast<long>(ScreenCaptureMethod::Auto));
|
||||
screen_capture_method_ = IsValidScreenCaptureMethod(screen_capture_method_value)
|
||||
? static_cast<ScreenCaptureMethod>(
|
||||
screen_capture_method_value)
|
||||
: ScreenCaptureMethod::Auto;
|
||||
|
||||
const long video_adaptation_policy_value = ini_.GetLongValue(
|
||||
section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
@@ -103,12 +95,7 @@ int ConfigCenter::Load() {
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
persist_config_migration = true;
|
||||
}
|
||||
// Migrate the former user switch. Keep true in the file for older builds,
|
||||
// while the current application always requests encrypted native media.
|
||||
if (!ini_.GetBoolValue(section_, "enable_srtp", false)) {
|
||||
ini_.SetBoolValue(section_, "enable_srtp", true);
|
||||
persist_config_migration = true;
|
||||
}
|
||||
enable_srtp_ = ini_.GetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
enable_self_hosted_ =
|
||||
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
@@ -132,8 +119,6 @@ int ConfigCenter::Load() {
|
||||
enable_autostart_ =
|
||||
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
enable_privacy_screen_.store(
|
||||
ini_.GetBoolValue(section_, "enable_privacy_screen", true));
|
||||
portable_service_prompt_suppressed_ =
|
||||
ini_.GetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
@@ -160,8 +145,6 @@ int ConfigCenter::Save() {
|
||||
static_cast<long>(video_quality_));
|
||||
ini_.SetLongValue(section_, "video_frame_rate",
|
||||
static_cast<long>(video_frame_rate_));
|
||||
ini_.SetLongValue(section_, "screen_capture_method",
|
||||
static_cast<long>(screen_capture_method_));
|
||||
ini_.SetLongValue(section_, "video_adaptation_policy",
|
||||
static_cast<long>(video_adaptation_policy_));
|
||||
ini_.SetLongValue(section_, "video_encode_format",
|
||||
@@ -170,7 +153,7 @@ int ConfigCenter::Save() {
|
||||
ini_.SetLongValue(section_, "turn_mode", static_cast<long>(turn_mode_));
|
||||
ini_.SetBoolValue(section_, "enable_turn",
|
||||
turn_mode_ != TURN_MODE::DISABLED);
|
||||
ini_.SetBoolValue(section_, "enable_srtp", true);
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
|
||||
|
||||
// only save when self hosted
|
||||
@@ -182,8 +165,6 @@ int ConfigCenter::Save() {
|
||||
|
||||
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
|
||||
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
|
||||
ini_.SetBoolValue(section_, "enable_privacy_screen",
|
||||
enable_privacy_screen_.load());
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
portable_service_prompt_suppressed_);
|
||||
|
||||
@@ -232,20 +213,6 @@ int ConfigCenter::SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetScreenCaptureMethod(ScreenCaptureMethod method) {
|
||||
if (!IsValidScreenCaptureMethod(static_cast<long>(method))) {
|
||||
return -1;
|
||||
}
|
||||
ini_.SetLongValue(section_, "screen_capture_method", static_cast<long>(method));
|
||||
if (ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||
ini_.SetLongValue(section_, "screen_capture_method",
|
||||
static_cast<long>(screen_capture_method_));
|
||||
return -1;
|
||||
}
|
||||
screen_capture_method_ = method;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetVideoAdaptationPolicy(
|
||||
VIDEO_ADAPTATION_POLICY policy) {
|
||||
video_adaptation_policy_ = policy;
|
||||
@@ -306,6 +273,16 @@ int ConfigCenter::SetTurn(bool enable_turn) {
|
||||
return SetTurnMode(turn_mode_);
|
||||
}
|
||||
|
||||
int ConfigCenter::SetSrtp(bool enable_srtp) {
|
||||
enable_srtp_ = enable_srtp;
|
||||
ini_.SetBoolValue(section_, "enable_srtp", enable_srtp_);
|
||||
SI_Error rc = ini_.SaveFile(config_path_.c_str());
|
||||
if (rc < 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetServerHost(const std::string& signal_server_host) {
|
||||
signal_server_host_ = signal_server_host;
|
||||
ini_.SetValue(section_, "signal_server_host", signal_server_host_.c_str());
|
||||
@@ -393,21 +370,6 @@ int ConfigCenter::SetDaemon(bool enable_daemon) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetPrivacyScreen(bool enable_privacy_screen) {
|
||||
const bool previous = enable_privacy_screen_.load();
|
||||
if (previous == enable_privacy_screen) return 0;
|
||||
ini_.SetBoolValue(section_, "enable_privacy_screen", enable_privacy_screen);
|
||||
if (ini_.SaveFile(config_path_.c_str()) < 0) {
|
||||
ini_.SetBoolValue(section_, "enable_privacy_screen", previous);
|
||||
LOG_ERROR("Failed to save automatic privacy screen preference");
|
||||
return -1;
|
||||
}
|
||||
// RTC callbacks read only the value committed by the settings thread.
|
||||
enable_privacy_screen_.store(enable_privacy_screen);
|
||||
LOG_INFO("Privacy screen on incoming connection: {}", enable_privacy_screen);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) {
|
||||
portable_service_prompt_suppressed_ = suppressed;
|
||||
ini_.SetBoolValue(section_, "portable_service_prompt_suppressed",
|
||||
@@ -432,10 +394,6 @@ ConfigCenter::VIDEO_FRAME_RATE ConfigCenter::GetVideoFrameRate() const {
|
||||
return video_frame_rate_;
|
||||
}
|
||||
|
||||
ScreenCaptureMethod ConfigCenter::GetScreenCaptureMethod() const {
|
||||
return screen_capture_method_;
|
||||
}
|
||||
|
||||
ConfigCenter::VIDEO_ADAPTATION_POLICY
|
||||
ConfigCenter::GetVideoAdaptationPolicy() const {
|
||||
return video_adaptation_policy_;
|
||||
@@ -445,18 +403,8 @@ ConfigCenter::VIDEO_ENCODE_FORMAT ConfigCenter::GetVideoEncodeFormat() const {
|
||||
return video_encode_format_;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsHardwareVideoCodecAvailable() {
|
||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||
!defined(__arm__) && USE_CUDA) || \
|
||||
defined(__APPLE__))
|
||||
return true;
|
||||
#else
|
||||
return false;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsHardwareVideoCodec() const {
|
||||
return IsHardwareVideoCodecAvailable() && hardware_video_codec_;
|
||||
return hardware_video_codec_;
|
||||
}
|
||||
|
||||
ConfigCenter::TURN_MODE ConfigCenter::GetTurnMode() const {
|
||||
@@ -467,7 +415,7 @@ bool ConfigCenter::IsEnableTurn() const {
|
||||
return turn_mode_ != TURN_MODE::DISABLED;
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsEnableSrtp() const { return true; }
|
||||
bool ConfigCenter::IsEnableSrtp() const { return enable_srtp_; }
|
||||
|
||||
std::string ConfigCenter::GetSignalServerHost() const {
|
||||
return signal_server_host_;
|
||||
@@ -489,10 +437,6 @@ bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
|
||||
|
||||
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
|
||||
|
||||
bool ConfigCenter::IsEnablePrivacyScreen() const {
|
||||
return enable_privacy_screen_.load();
|
||||
}
|
||||
|
||||
bool ConfigCenter::IsPortableServicePromptSuppressed() const {
|
||||
return portable_service_prompt_suppressed_;
|
||||
}
|
||||
|
||||
@@ -7,11 +7,9 @@
|
||||
#ifndef _CONFIG_CENTER_H_
|
||||
#define _CONFIG_CENTER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
#include "SimpleIni.h"
|
||||
#include "screen_capturer/screen_capture_method.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -41,18 +39,17 @@ class ConfigCenter {
|
||||
int SetLanguage(LANGUAGE language);
|
||||
int SetVideoQuality(VIDEO_QUALITY video_quality);
|
||||
int SetVideoFrameRate(VIDEO_FRAME_RATE video_frame_rate);
|
||||
int SetScreenCaptureMethod(ScreenCaptureMethod method);
|
||||
int SetVideoAdaptationPolicy(VIDEO_ADAPTATION_POLICY policy);
|
||||
int SetVideoEncodeFormat(VIDEO_ENCODE_FORMAT video_encode_format);
|
||||
int SetHardwareVideoCodec(bool hardware_video_codec);
|
||||
int SetTurnMode(TURN_MODE turn_mode);
|
||||
int SetTurn(bool enable_turn);
|
||||
int SetSrtp(bool enable_srtp);
|
||||
int SetServerHost(const std::string& signal_server_host);
|
||||
int SetServerPort(int signal_server_port);
|
||||
int SetSelfHosted(bool enable_self_hosted);
|
||||
int SetAutostart(bool enable_autostart);
|
||||
int SetDaemon(bool enable_daemon);
|
||||
int SetPrivacyScreen(bool enable_privacy_screen);
|
||||
int SetPortableServicePromptSuppressed(bool suppressed);
|
||||
int SetFileTransferSavePath(const std::string& path);
|
||||
|
||||
@@ -61,11 +58,8 @@ class ConfigCenter {
|
||||
LANGUAGE GetLanguage() const;
|
||||
VIDEO_QUALITY GetVideoQuality() const;
|
||||
VIDEO_FRAME_RATE GetVideoFrameRate() const;
|
||||
ScreenCaptureMethod GetScreenCaptureMethod() const;
|
||||
VIDEO_ADAPTATION_POLICY GetVideoAdaptationPolicy() const;
|
||||
VIDEO_ENCODE_FORMAT GetVideoEncodeFormat() const;
|
||||
// Whether this build includes a hardware video codec backend.
|
||||
static bool IsHardwareVideoCodecAvailable();
|
||||
bool IsHardwareVideoCodec() const;
|
||||
TURN_MODE GetTurnMode() const;
|
||||
bool IsEnableTurn() const;
|
||||
@@ -77,7 +71,6 @@ class ConfigCenter {
|
||||
bool IsSelfHosted() const;
|
||||
bool IsEnableAutostart() const;
|
||||
bool IsEnableDaemon() const;
|
||||
bool IsEnablePrivacyScreen() const;
|
||||
bool IsPortableServicePromptSuppressed() const;
|
||||
std::string GetFileTransferSavePath() const;
|
||||
|
||||
@@ -92,12 +85,12 @@ class ConfigCenter {
|
||||
LANGUAGE language_ = LANGUAGE::CHINESE;
|
||||
VIDEO_QUALITY video_quality_ = VIDEO_QUALITY::HIGH;
|
||||
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
|
||||
ScreenCaptureMethod screen_capture_method_ = ScreenCaptureMethod::Auto;
|
||||
VIDEO_ADAPTATION_POLICY video_adaptation_policy_ =
|
||||
VIDEO_ADAPTATION_POLICY::QUALITY_PRIORITY;
|
||||
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
|
||||
bool hardware_video_codec_ = false;
|
||||
TURN_MODE turn_mode_ = TURN_MODE::AUTO_UDP_TCP;
|
||||
bool enable_srtp_ = false;
|
||||
std::string signal_server_host_ = "";
|
||||
std::string signal_server_host_default_ = "api.crossdesk.cn";
|
||||
int signal_server_port_ = 0;
|
||||
@@ -105,7 +98,6 @@ class ConfigCenter {
|
||||
bool enable_self_hosted_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_daemon_ = false;
|
||||
std::atomic<bool> enable_privacy_screen_{true};
|
||||
bool portable_service_prompt_suppressed_ = false;
|
||||
std::string file_transfer_save_path_ = "";
|
||||
};
|
||||
|
||||
@@ -34,7 +34,6 @@
|
||||
#include "platform.h"
|
||||
#include "platform/video_renderer.h"
|
||||
#include "rendering/slint_video_presenter.h"
|
||||
#include "runtime/privacy_control_state.h"
|
||||
#if _WIN32
|
||||
#include <windows.h>
|
||||
|
||||
@@ -1028,8 +1027,6 @@ void GuiApplication::InitializeSettings() {
|
||||
language_button_value_ = localization_language_index_;
|
||||
localization_language_ =
|
||||
static_cast<ConfigCenter::LANGUAGE>(localization_language_index_);
|
||||
privacy_.SetText({
|
||||
localization::privacy_screen_unlock_hint[localization_language_index_]});
|
||||
}
|
||||
|
||||
bool GuiApplication::InitializeSDL() {
|
||||
@@ -1224,27 +1221,12 @@ void GuiApplication::ResetSettingsUi() {
|
||||
ui_->main->set_adaptation_policy_index(
|
||||
video_adaptation_policy_button_value_);
|
||||
ui_->main->set_codec_index(video_encode_format_button_value_);
|
||||
const bool hardware_codec_available =
|
||||
ConfigCenter::IsHardwareVideoCodecAvailable();
|
||||
ui_->main->set_hardware_codec_available(hardware_codec_available);
|
||||
ui_->main->set_hardware_codec_enabled(hardware_codec_available &&
|
||||
enable_hardware_video_codec_);
|
||||
ui_->main->set_hardware_codec_enabled(enable_hardware_video_codec_);
|
||||
ui_->main->set_turn_enabled(enable_turn_);
|
||||
ui_->main->set_srtp_enabled(enable_srtp_);
|
||||
ui_->main->set_self_hosted_enabled(enable_self_hosted_);
|
||||
ui_->main->set_autostart_enabled(enable_autostart_);
|
||||
ui_->main->set_daemon_enabled(enable_daemon_);
|
||||
ui_->main->set_privacy_on_connect_enabled(
|
||||
config_center_->IsEnablePrivacyScreen());
|
||||
#ifdef _WIN32
|
||||
ui_->main->set_capture_method_visible(true);
|
||||
ui_->main->set_capture_method_index(
|
||||
static_cast<int>(config_center_->GetScreenCaptureMethod()));
|
||||
#endif
|
||||
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
||||
ui_->main->set_privacy_setting_available(true);
|
||||
#else
|
||||
ui_->main->set_privacy_setting_available(false);
|
||||
#endif
|
||||
ui_->main->set_file_save_path(file_transfer_save_path_buf_);
|
||||
ui_->main->set_server_host(signal_server_ip_self_);
|
||||
ui_->main->set_server_port(signal_server_port_self_);
|
||||
@@ -1431,6 +1413,21 @@ void GuiApplication::BindMainCallbacks() {
|
||||
: slint::SharedString{});
|
||||
});
|
||||
main->on_open_download([this] { OpenUrl("https://crossdesk.cn"); });
|
||||
main->on_start_update([this] {
|
||||
#if _WIN32
|
||||
windows_updater_.Start(latest_version_info_);
|
||||
SyncWindowsUpdate();
|
||||
#else
|
||||
OpenUrl("https://crossdesk.cn");
|
||||
ui_->main->set_update_open(false);
|
||||
#endif
|
||||
});
|
||||
main->on_cancel_update([this] {
|
||||
#if _WIN32
|
||||
windows_updater_.Cancel();
|
||||
#endif
|
||||
ui_->main->set_update_open(false);
|
||||
});
|
||||
main->on_connection_cancel([this] {
|
||||
const auto props = FindRemoteSession(ui_->connection_dialog_remote_id);
|
||||
if (!props) {
|
||||
@@ -1605,36 +1602,6 @@ void GuiApplication::BindStreamCallbacks() {
|
||||
SendReliableDataFrame(props->peer_, message.c_str(), message.size(),
|
||||
props->control_data_label_.c_str());
|
||||
});
|
||||
stream->on_toggle_privacy([this] {
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->peer_ || !props->control_mouse_ ||
|
||||
props->connection_status_.load() != ConnectionStatus::Connected) return;
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::privacy_command;
|
||||
{
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
const auto& status = props->privacy_status_;
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
const auto controls = GetPrivacyControlState(
|
||||
status, props->privacy_status_received_, props->privacy_command_pending_,
|
||||
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
||||
if (!controls.can_toggle) return;
|
||||
action.pc.flag = controls.active ? PrivacyCommandFlag::disable
|
||||
: PrivacyCommandFlag::enable;
|
||||
action.pc.block_local_input = true;
|
||||
props->privacy_command_pending_ = true;
|
||||
props->privacy_command_tick_ = now;
|
||||
props->privacy_request_revision_ = status.revision;
|
||||
}
|
||||
const auto message = action.to_json();
|
||||
if (SendReliableDataFrame(props->peer_, message.data(), message.size(),
|
||||
props->control_data_label_.c_str()) != 0) {
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
props->privacy_command_pending_ = false;
|
||||
props->privacy_status_received_ = false;
|
||||
LOG_WARN("Privacy command send failed, remote_id={}", props->remote_id_);
|
||||
}
|
||||
});
|
||||
stream->on_toggle_mouse_control([this] {
|
||||
auto props = SelectedSession();
|
||||
if (!props || !props->connection_established_) {
|
||||
@@ -1842,7 +1809,6 @@ void GuiApplication::Tick() {
|
||||
HandleConnectionStatusChange();
|
||||
HandlePendingPresenceProbe();
|
||||
HandlePresenceProbeTimeout();
|
||||
HandlePrivacy();
|
||||
HandleWindowsServiceIntegration();
|
||||
#if defined(__linux__) && !defined(__APPLE__)
|
||||
SyncXWaylandWindowActivation();
|
||||
@@ -2149,11 +2115,22 @@ void GuiApplication::SyncMainWindow() {
|
||||
ui_->main->set_signal_tls_error(signal_status_ ==
|
||||
SignalStatus::SignalTlsCertError);
|
||||
ui_->main->set_update_available(update_available_);
|
||||
#if _WIN32
|
||||
SyncWindowsUpdate();
|
||||
#endif
|
||||
ui_->main->set_current_version(CROSSDESK_VERSION);
|
||||
ui_->main->set_latest_version(UiText(latest_version_));
|
||||
ui_->main->set_release_name(UiText(release_name_));
|
||||
ui_->main->set_release_date(UiText(release_date_));
|
||||
ui_->main->set_settings_session_active(HasActiveSession());
|
||||
ui_->main->set_settings_session_active(stream_window_inited_);
|
||||
#if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \
|
||||
!defined(__arm__) && USE_CUDA) || \
|
||||
defined(__APPLE__))
|
||||
ui_->main->set_hardware_codec_available(true);
|
||||
#else
|
||||
ui_->main->set_hardware_codec_available(false);
|
||||
#endif
|
||||
|
||||
if (show_offline_warning_window_) {
|
||||
ui_->main->set_offline_warning(UiText(offline_warning_text_));
|
||||
show_offline_warning_window_ = false;
|
||||
@@ -2418,6 +2395,7 @@ void GuiApplication::SyncStreamWindow() {
|
||||
return;
|
||||
}
|
||||
(*ui_->stream)->set_window_maximized((*ui_->stream)->window().is_maximized());
|
||||
(*ui_->stream)->set_srtp_enabled(enable_srtp_);
|
||||
#if defined(__APPLE__)
|
||||
if (ui_->stream_live_resize_configuration_attempts > 0) {
|
||||
if (ConfigureStreamWindowLiveResize(
|
||||
@@ -2547,29 +2525,6 @@ void GuiApplication::SyncStreamWindow() {
|
||||
? localization::receiving_screen[localization_language_index_]
|
||||
: std::string{}));
|
||||
(*ui_->stream)->set_mouse_control_enabled(props->control_mouse_);
|
||||
{
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
const auto& status = props->privacy_status_;
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
const auto controls = GetPrivacyControlState(
|
||||
status, props->privacy_status_received_, props->privacy_command_pending_,
|
||||
now - props->privacy_status_tick_, now - props->privacy_command_tick_);
|
||||
const bool connected =
|
||||
props->connection_status_.load() == ConnectionStatus::Connected;
|
||||
(*ui_->stream)->set_privacy_active(controls.active);
|
||||
(*ui_->stream)->set_privacy_off(connected && controls.off);
|
||||
(*ui_->stream)->set_privacy_unsupported(controls.unsupported);
|
||||
(*ui_->stream)->set_privacy_can_toggle(
|
||||
props->control_mouse_ && connected && controls.can_toggle);
|
||||
const int language = localization_language_index_;
|
||||
std::string text = !connected || !props->privacy_status_received_
|
||||
? localization::privacy_screen[language]
|
||||
: controls.unsupported ? localization::privacy_unsupported[language]
|
||||
: controls.enabled ? localization::privacy_on[language]
|
||||
: controls.off ? localization::privacy_off[language]
|
||||
: localization::privacy_screen[language];
|
||||
(*ui_->stream)->set_privacy_status_text(UiText(text));
|
||||
}
|
||||
int remote_cursor_shape =
|
||||
static_cast<int>(RemoteCursorShape::default_cursor);
|
||||
bool remote_cursor_active = false;
|
||||
@@ -2813,19 +2768,7 @@ void GuiApplication::SyncServerWindow() {
|
||||
});
|
||||
}
|
||||
|
||||
// Privacy protects local display output while preserving capture.
|
||||
// Keep the panel alive throughout privacy transitions so remote
|
||||
// controllers can still see and operate it without resetting its UI state.
|
||||
bool show_controller_window = has_connected_controller;
|
||||
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__linux__)
|
||||
// Preserve the existing panel behavior on other platforms.
|
||||
const auto privacy_status = privacy_.Snapshot();
|
||||
const bool privacy_covering = privacy_status.overlay_active ||
|
||||
privacy_status.state == PrivacyState::starting ||
|
||||
privacy_status.state == PrivacyState::stopping;
|
||||
show_controller_window = show_controller_window && !privacy_covering;
|
||||
#endif
|
||||
if (show_controller_window && !ui_->server) {
|
||||
if (has_connected_controller && !ui_->server) {
|
||||
ui_->server.emplace(ui::ServerWindow::create());
|
||||
RegisterFontAwesome((*ui_->server)->window());
|
||||
(*ui_->server)->set_controllers(ui_->controller_model);
|
||||
@@ -2849,7 +2792,7 @@ void GuiApplication::SyncServerWindow() {
|
||||
server_window_created_ = true;
|
||||
server_window_inited_ = true;
|
||||
}
|
||||
if (!show_controller_window && ui_->server) {
|
||||
if (!has_connected_controller && ui_->server) {
|
||||
(*ui_->server)->hide();
|
||||
ui_->server.reset();
|
||||
server_window_created_ = false;
|
||||
@@ -2982,9 +2925,9 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
video_adaptation_policy_button_value_ =
|
||||
std::clamp(main->get_adaptation_policy_index(), 0, 2);
|
||||
video_encode_format_button_value_ = std::clamp(main->get_codec_index(), 0, 1);
|
||||
enable_hardware_video_codec_ = ConfigCenter::IsHardwareVideoCodecAvailable() &&
|
||||
main->get_hardware_codec_enabled();
|
||||
enable_hardware_video_codec_ = main->get_hardware_codec_enabled();
|
||||
enable_turn_ = main->get_turn_enabled();
|
||||
enable_srtp_ = main->get_srtp_enabled();
|
||||
enable_self_hosted_ = main->get_self_hosted_enabled();
|
||||
enable_autostart_ = main->get_autostart_enabled();
|
||||
enable_daemon_ = main->get_daemon_enabled();
|
||||
@@ -2993,8 +2936,6 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
static_cast<ConfigCenter::LANGUAGE>(language_button_value_);
|
||||
localization_language_index_ = language_button_value_;
|
||||
config_center_->SetLanguage(localization_language_);
|
||||
privacy_.SetText({
|
||||
localization::privacy_screen_unlock_hint[localization_language_index_]});
|
||||
config_center_->SetVideoQuality(
|
||||
static_cast<ConfigCenter::VIDEO_QUALITY>(video_quality_button_value_));
|
||||
config_center_->SetVideoFrameRate(static_cast<ConfigCenter::VIDEO_FRAME_RATE>(
|
||||
@@ -3007,27 +2948,10 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
video_encode_format_button_value_));
|
||||
config_center_->SetHardwareVideoCodec(enable_hardware_video_codec_);
|
||||
config_center_->SetTurn(enable_turn_);
|
||||
config_center_->SetSrtp(enable_srtp_);
|
||||
config_center_->SetSelfHosted(enable_self_hosted_);
|
||||
config_center_->SetAutostart(enable_autostart_);
|
||||
config_center_->SetDaemon(enable_daemon_);
|
||||
#ifdef _WIN32
|
||||
if (!HasActiveSession()) {
|
||||
const auto capture_method = static_cast<ScreenCaptureMethod>(
|
||||
std::clamp(main->get_capture_method_index(), 0, 3));
|
||||
if (config_center_->SetScreenCaptureMethod(capture_method) != 0) {
|
||||
LOG_ERROR("Failed to save screen capture method");
|
||||
}
|
||||
}
|
||||
main->set_capture_method_index(
|
||||
static_cast<int>(config_center_->GetScreenCaptureMethod()));
|
||||
#endif
|
||||
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
|
||||
if (config_center_->SetPrivacyScreen(main->get_privacy_on_connect_enabled()) !=
|
||||
0) {
|
||||
main->set_privacy_on_connect_enabled(
|
||||
config_center_->IsEnablePrivacyScreen());
|
||||
}
|
||||
#endif
|
||||
|
||||
const std::string path(main->get_file_save_path());
|
||||
std::memset(file_transfer_save_path_buf_, 0,
|
||||
@@ -3052,13 +2976,14 @@ void GuiApplication::SaveSettingsFromUi() {
|
||||
video_encode_format_button_value_last_ = video_encode_format_button_value_;
|
||||
enable_hardware_video_codec_last_ = enable_hardware_video_codec_;
|
||||
enable_turn_last_ = enable_turn_;
|
||||
enable_srtp_last_ = enable_srtp_;
|
||||
enable_self_hosted_last_ = enable_self_hosted_;
|
||||
enable_autostart_last_ = enable_autostart_;
|
||||
enable_daemon_last_ = enable_daemon_;
|
||||
file_transfer_save_path_last_ = path;
|
||||
ui_->localized_language = -1;
|
||||
|
||||
if (!HasActiveSession()) {
|
||||
if (!stream_window_inited_) {
|
||||
CloseAllRemoteSessions();
|
||||
CreateConnectionPeer();
|
||||
}
|
||||
@@ -3350,7 +3275,59 @@ bool GuiApplication::OpenUrl(const std::string& url) {
|
||||
return SDL_OpenURL(url.c_str());
|
||||
}
|
||||
|
||||
#if _WIN32
|
||||
void GuiApplication::SyncWindowsUpdate() {
|
||||
using State = WindowsUpdater::State;
|
||||
if (windows_updater_.GetState() == State::Ready) {
|
||||
windows_updater_.Launch();
|
||||
if (windows_updater_.GetState() == State::Launched) {
|
||||
ui_->main->set_update_open(false);
|
||||
}
|
||||
}
|
||||
const State state = windows_updater_.GetState();
|
||||
const bool downloading = state == State::Downloading;
|
||||
ui_->main->set_update_downloading(downloading || state == State::Checking ||
|
||||
state == State::Launching);
|
||||
ui_->main->set_update_security_blocked(state == State::SecurityBlocked);
|
||||
ui_->main->set_update_failed(state == State::Failed ||
|
||||
state == State::LaunchFailed ||
|
||||
state == State::SecurityBlocked);
|
||||
const int language = localization_language_index_;
|
||||
std::string status;
|
||||
if (downloading) {
|
||||
status = localization::update_downloading[language];
|
||||
const uint64_t total = windows_updater_.Total();
|
||||
const uint64_t received = windows_updater_.Downloaded();
|
||||
if (total > 0) {
|
||||
const int percent = static_cast<int>(std::min(100.0, 100.0 * received / total));
|
||||
status += " " + std::to_string(percent) + "%";
|
||||
ui_->main->set_update_progress(static_cast<float>(percent) / 100.0f);
|
||||
} else {
|
||||
status += " " + std::to_string(received / (1024 * 1024)) + " MB";
|
||||
ui_->main->set_update_progress(-1.0f);
|
||||
}
|
||||
} else if (state == State::Failed) {
|
||||
status = localization::update_download_failed[language];
|
||||
} else if (state == State::LaunchFailed) {
|
||||
status = localization::update_launch_failed[language];
|
||||
} else if (state == State::Checking) {
|
||||
status = localization::update_checking[language];
|
||||
ui_->main->set_update_progress(1.0f);
|
||||
} else if (state == State::SecurityBlocked) {
|
||||
status = localization::update_security_blocked[language];
|
||||
} else if (state == State::Launching) {
|
||||
status = localization::update_launching[language];
|
||||
} else if (state == State::Launched) {
|
||||
status = localization::update_installer_opened[language];
|
||||
}
|
||||
ui_->main->set_update_status(UiText(status));
|
||||
}
|
||||
#endif
|
||||
|
||||
void GuiApplication::Cleanup() {
|
||||
#if _WIN32
|
||||
windows_updater_.Cancel();
|
||||
#endif
|
||||
if (!ui_) {
|
||||
SDL_Quit();
|
||||
return;
|
||||
@@ -3360,12 +3337,9 @@ void GuiApplication::Cleanup() {
|
||||
#if _WIN32 && CROSSDESK_PORTABLE
|
||||
JoinPortableWindowsServiceInstallThread();
|
||||
#endif
|
||||
privacy_.Disable();
|
||||
clipboard_.Shutdown();
|
||||
keyboard_.ForceReleasePressedKeys();
|
||||
keyboard_.ReleaseAllRemotePressedKeys("application_exit");
|
||||
devices_.DestroyDevices();
|
||||
privacy_.Shutdown();
|
||||
devices_.DestroyFactories();
|
||||
CloseAllRemoteSessions();
|
||||
WaitForSessionCleanup();
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
|
||||
#include "runtime/cursor_state_provider.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#if _WIN32
|
||||
#include "platform/windows/windows_updater.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -52,6 +55,9 @@ private:
|
||||
void HandlePasswordChangeResult();
|
||||
void HandleCredentialRecovery();
|
||||
void SyncMainWindow();
|
||||
#if _WIN32
|
||||
void SyncWindowsUpdate();
|
||||
#endif
|
||||
void SyncConnectionDialog();
|
||||
void SyncPlatformDialogs();
|
||||
void SyncStreamWindow();
|
||||
@@ -87,6 +93,9 @@ private:
|
||||
bool OpenUrl(const std::string &url);
|
||||
|
||||
std::unique_ptr<SlintUi> ui_;
|
||||
#if _WIN32
|
||||
WindowsUpdater windows_updater_;
|
||||
#endif
|
||||
CursorStateProvider cursor_state_provider_;
|
||||
std::unordered_map<std::string, CursorDeliveryState>
|
||||
cursor_delivery_states_;
|
||||
@@ -100,4 +109,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@@ -53,6 +53,8 @@
|
||||
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171 * dpi_scale_
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218 * dpi_scale_
|
||||
#define ENABLE_AUTOSTART_PADDING_CN 171 * dpi_scale_
|
||||
|
||||
@@ -57,6 +57,8 @@
|
||||
#define ENABLE_HARDWARE_VIDEO_CODEC_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_CN 171
|
||||
#define ENABLE_TURN_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_CN 171
|
||||
#define ENABLE_SRTP_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_CN 171
|
||||
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
|
||||
#define ENABLE_AUTOSTART_PADDING_CN 171
|
||||
|
||||
@@ -20,12 +20,6 @@ struct TranslationRow {
|
||||
|
||||
// Single source of truth for all UI strings.
|
||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||
X(privacy_screen, u8"隐私屏", "Privacy screen", u8"Приватный экран") \
|
||||
X(privacy_enable, u8"开启隐私屏", "Enable privacy screen", u8"Включить приватный экран") \
|
||||
X(privacy_disable, u8"关闭隐私屏", "Disable privacy screen", u8"Выключить приватный экран") \
|
||||
X(privacy_on, u8"隐私屏:已开启", "Privacy screen: on", u8"Приватный экран: включён") \
|
||||
X(privacy_off, u8"隐私屏:已关闭", "Privacy screen: off", u8"Приватный экран: выключен") \
|
||||
X(privacy_unsupported, u8"隐私屏:不可用", "Privacy screen: unavailable", u8"Приватный экран: недоступен") \
|
||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||
@@ -126,9 +120,6 @@ struct TranslationRow {
|
||||
u8"стола") \
|
||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||
X(screen_capture_method, u8"采集方式:", "Capture Method:", \
|
||||
u8"Способ захвата:") \
|
||||
X(screen_capture_method_auto, u8"自动", "Auto", u8"Авто") \
|
||||
X(video_quality, u8"画面质量:", "Video Quality:", u8"Качество видео:") \
|
||||
X(video_frame_rate, u8"画面采集帧率:", \
|
||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||
@@ -149,6 +140,7 @@ struct TranslationRow {
|
||||
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
|
||||
X(enable_turn, u8"启用中继服务:", \
|
||||
"Enable TURN Service:", u8"Включить TURN-сервис:") \
|
||||
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
|
||||
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
|
||||
u8"Конфигурация self-hosted") \
|
||||
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
|
||||
@@ -192,6 +184,26 @@ struct TranslationRow {
|
||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||
u8"Доступна новая версия") \
|
||||
X(update_downloading, u8"正在下载安装包…", "Downloading installer…", \
|
||||
u8"Загрузка установщика…") \
|
||||
X(update_checking, u8"正在进行 Windows 安全检查…", \
|
||||
"Running Windows security checks…", u8"Проверка безопасности Windows…") \
|
||||
X(update_security_blocked, \
|
||||
u8"Windows 安全检查未通过,请查看安全软件日志并核实发布者。", \
|
||||
"Windows security checks failed. Review security logs and verify the publisher.", \
|
||||
u8"Проверка Windows не пройдена. Проверьте журнал защиты и издателя.") \
|
||||
X(update_launching, u8"正在打开安装包,请确认管理员权限请求…", \
|
||||
"Opening installer. Please confirm the administrator permission request…", \
|
||||
u8"Запуск установщика. Подтвердите запрос прав администратора…") \
|
||||
X(update_download_failed, u8"下载失败,请点击更新重试,或访问官网。", \
|
||||
"Download failed. Click Update to retry, or visit the website.", \
|
||||
u8"Ошибка загрузки. Повторите обновление или откройте сайт.") \
|
||||
X(update_launch_failed, u8"安装包未能启动或授权已取消,请点击更新重试。", \
|
||||
"Installer launch failed or permission was cancelled. Click Update to retry.", \
|
||||
u8"Установщик не запущен или доступ отменён. Повторите обновление.") \
|
||||
X(update_installer_opened, u8"安装包已打开,请在安装向导中完成更新。", \
|
||||
"Installer opened. Follow the setup wizard to finish updating.", \
|
||||
u8"Установщик открыт. Следуйте указаниям мастера установки.") \
|
||||
X(release_notes, u8"更新内容", "Release Notes", u8"Содержание обновления") \
|
||||
X(version, u8"版本", "Version", u8"Версия") \
|
||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||
@@ -210,8 +222,6 @@ struct TranslationRow {
|
||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
||||
X(privacy_on_connect, u8"被连接时开启隐私屏:", "Privacy screen on connect:", \
|
||||
u8"Приватность при подключении:") \
|
||||
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
||||
u8"Вступит в силу после перезапуска") \
|
||||
X(select_file, u8"选择文件发送", "Select File to Send", \
|
||||
@@ -247,9 +257,6 @@ struct TranslationRow {
|
||||
u8"Для работы приложения требуются следующие разрешения:") \
|
||||
X(show_main_window, u8"显示主界面", "Show Main Window", \
|
||||
u8"Показать главное окно") \
|
||||
X(privacy_screen_unlock_hint, u8"按下快捷键,解除隐私屏", \
|
||||
"To turn off the privacy screen, press", \
|
||||
u8"Чтобы отключить приватный экран, нажмите") \
|
||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||
|
||||
inline constexpr TranslationRow kTranslationRows[] = {
|
||||
|
||||
@@ -10,10 +10,6 @@
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
#include "speaker_capturer_factory.h"
|
||||
#ifdef _WIN32
|
||||
#include "platform/windows/screen_capturer/screen_capturer_win.h"
|
||||
#include "platform/windows/input/mouse/mouse_controller.h"
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
@@ -79,12 +75,6 @@ int SessionDeviceManager::InitializeScreenCapturer() {
|
||||
screen_capturer_ =
|
||||
static_cast<ScreenCapturer *>(screen_capturer_factory_->Create());
|
||||
}
|
||||
screen_capturer_->SetPrivacyController(&owner_.privacy_);
|
||||
#ifdef _WIN32
|
||||
if (auto windows = dynamic_cast<ScreenCapturerWin*>(screen_capturer_)) {
|
||||
windows->SetCaptureMethod(owner_.config_center_->GetScreenCaptureMethod());
|
||||
}
|
||||
#endif
|
||||
|
||||
last_frame_time_ = {};
|
||||
next_frame_deadline_ = {};
|
||||
@@ -226,13 +216,6 @@ int SessionDeviceManager::StopScreenCapturer() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
void SessionDeviceManager::ReleaseRemoteMouseButtons() {
|
||||
if (auto mouse = dynamic_cast<PlatformMouseController*>(mouse_controller_))
|
||||
mouse->ReleasePressedButtons();
|
||||
}
|
||||
#endif
|
||||
|
||||
void SessionDeviceManager::StartSpeakerCapturer() {
|
||||
speaker_capture_.SetEnabled(true);
|
||||
}
|
||||
@@ -505,7 +488,6 @@ void SessionDeviceManager::DrainCapturedKeyboardInput() {
|
||||
std::lock_guard<std::mutex> lock(captured_keyboard_inputs_mutex_);
|
||||
inputs.swap(captured_keyboard_inputs_);
|
||||
}
|
||||
if (owner_.privacy_.Snapshot().input_blocked) return;
|
||||
for (const CapturedKeyboardInput &input : inputs) {
|
||||
owner_.keyboard_.SendKeyCommand(input.key_code, input.is_down,
|
||||
input.scan_code, input.extended);
|
||||
|
||||
@@ -46,9 +46,6 @@ public:
|
||||
int InitializeScreenCapturer();
|
||||
int StartScreenCapturer();
|
||||
int StopScreenCapturer();
|
||||
#ifdef _WIN32
|
||||
void ReleaseRemoteMouseButtons();
|
||||
#endif
|
||||
// Requests are asynchronous; Stop drains/revokes audio delivery before
|
||||
// returning so the connection peer can be destroyed safely.
|
||||
void StartSpeakerCapturer();
|
||||
@@ -105,4 +102,4 @@ private:
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -213,9 +213,6 @@ int KeyboardController::SendKeyCommand(int key_code, bool is_down,
|
||||
bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
uint32_t scan_code, bool extended) {
|
||||
#if _WIN32
|
||||
if (owner_.privacy_.Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
||||
owner_.privacy_.Fail("Secure desktop or locked session; privacy screen will turn off");
|
||||
}
|
||||
if (owner_.local_service_status_received_ &&
|
||||
IsSecureDesktopInteractionRequired(owner_.local_interactive_stage_)) {
|
||||
const std::string response = SendCrossDeskSecureDesktopKeyInput(
|
||||
@@ -249,7 +246,6 @@ bool KeyboardController::InjectRemoteKey(int key_code, bool is_down,
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
// Recheck after the desktop query; key-up cleanup remains allowed while paused.
|
||||
return owner_.devices_.SendKeyboardCommand(key_code, is_down, scan_code,
|
||||
extended);
|
||||
}
|
||||
@@ -346,50 +342,11 @@ void KeyboardController::ReleaseRemotePressedKeys(const std::string& remote_id,
|
||||
keys_to_release.size(), remote_id, reason ? reason : "unknown");
|
||||
}
|
||||
for (const PressedKey& key : keys_to_release) {
|
||||
if (!InjectRemoteKey(key.key_code, false, key.scan_code, key.extended)) {
|
||||
#ifdef _WIN32
|
||||
if (owner_.privacy_.Engaged()) {
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
pending_privacy_releases_[key.key_code] = key;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
InjectRemoteKey(key.key_code, false, key.scan_code, key.extended);
|
||||
}
|
||||
}
|
||||
|
||||
void KeyboardController::ReleaseAllRemotePressedKeys(const char* reason) {
|
||||
std::vector<std::string> remotes;
|
||||
{
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
for (const auto& [id, state] : remote_states_) remotes.push_back(id);
|
||||
}
|
||||
for (const auto& id : remotes) ReleaseRemotePressedKeys(id, reason);
|
||||
CheckRemoteTimeouts();
|
||||
}
|
||||
|
||||
void KeyboardController::CheckRemoteTimeouts() {
|
||||
#ifdef _WIN32
|
||||
bool pending = false;
|
||||
{
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
pending = !pending_privacy_releases_.empty();
|
||||
}
|
||||
// A key-up rejected on the secure desktop is retained until the ordinary
|
||||
// desktop returns. Never route cleanup through the security UI helper.
|
||||
if (pending && IsWindowsPrivacyDesktopAvailable()) {
|
||||
std::unordered_map<int, PressedKey> releases;
|
||||
{
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
releases.swap(pending_privacy_releases_);
|
||||
}
|
||||
for (const auto& [code, key] : releases) {
|
||||
if (!owner_.devices_.SendKeyboardCommand(code, false, key.scan_code, key.extended)) {
|
||||
std::lock_guard lock(remote_states_mutex_);
|
||||
pending_privacy_releases_[code] = key;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||
std::vector<std::string> timed_out_remotes;
|
||||
{
|
||||
|
||||
@@ -30,7 +30,6 @@ public:
|
||||
const RemoteAction &remote_action);
|
||||
void ReleaseRemotePressedKeys(const std::string &remote_id,
|
||||
const char *reason);
|
||||
void ReleaseAllRemotePressedKeys(const char* reason);
|
||||
void CheckRemoteTimeouts();
|
||||
|
||||
private:
|
||||
@@ -59,9 +58,6 @@ private:
|
||||
uint32_t last_heartbeat_tick_ = 0;
|
||||
std::unordered_map<std::string, RemoteState> remote_states_;
|
||||
std::mutex remote_states_mutex_;
|
||||
#ifdef _WIN32
|
||||
std::unordered_map<int, PressedKey> pending_privacy_releases_;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
@@ -308,6 +308,7 @@ int SettingsManager::Load() {
|
||||
owner_.enable_hardware_video_codec_ =
|
||||
owner_.config_center_->IsHardwareVideoCodec();
|
||||
owner_.enable_turn_ = owner_.config_center_->IsEnableTurn();
|
||||
owner_.enable_srtp_ = owner_.config_center_->IsEnableSrtp();
|
||||
owner_.enable_self_hosted_ = owner_.config_center_->IsSelfHosted();
|
||||
owner_.enable_autostart_ = owner_.config_center_->IsEnableAutostart();
|
||||
owner_.enable_daemon_ = owner_.config_center_->IsEnableDaemon();
|
||||
@@ -334,6 +335,7 @@ int SettingsManager::Load() {
|
||||
owner_.enable_hardware_video_codec_last_ =
|
||||
owner_.enable_hardware_video_codec_;
|
||||
owner_.enable_turn_last_ = owner_.enable_turn_;
|
||||
owner_.enable_srtp_last_ = owner_.enable_srtp_;
|
||||
owner_.enable_self_hosted_last_ = owner_.enable_self_hosted_;
|
||||
owner_.enable_autostart_last_ = owner_.enable_autostart_;
|
||||
|
||||
|
||||
@@ -25,16 +25,6 @@ constexpr auto kPresenceRefreshInterval = std::chrono::seconds(30);
|
||||
constexpr auto kPresenceRetryInterval = std::chrono::seconds(1);
|
||||
} // namespace
|
||||
|
||||
bool GuiRuntime::HasActiveSession() {
|
||||
if (stream_window_inited_) return true;
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
return std::any_of(connection_status_.begin(), connection_status_.end(),
|
||||
[](const auto& entry) {
|
||||
return entry.second == ConnectionStatus::Connecting ||
|
||||
entry.second == ConnectionStatus::Connected;
|
||||
});
|
||||
}
|
||||
|
||||
void GuiRuntime::HandleConnectionStatusChange() {
|
||||
if (!signal_connected_ || !peer_) {
|
||||
return;
|
||||
@@ -131,9 +121,6 @@ void GuiRuntime::HandlePresenceProbeTimeout() {
|
||||
void GuiRuntime::HandleServerControllerDisconnected(
|
||||
const std::string& remote_id, const char* reason) {
|
||||
keyboard_.ReleaseRemotePressedKeys(remote_id, reason);
|
||||
#ifdef _WIN32
|
||||
devices_.ReleaseRemoteMouseButtons();
|
||||
#endif
|
||||
{
|
||||
std::lock_guard lock(remote_pointer_input_mutex_);
|
||||
last_remote_pointer_input_time_.erase(remote_id);
|
||||
@@ -146,7 +133,6 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
||||
std::unique_lock lock(connection_status_mutex_);
|
||||
connection_status_.erase(remote_id);
|
||||
connection_host_names_.erase(remote_id);
|
||||
privacy_sessions_.Disconnected(remote_id);
|
||||
for (const auto& [id, status] : connection_status_) {
|
||||
if (status != ConnectionStatus::Connected) {
|
||||
continue;
|
||||
@@ -165,10 +151,6 @@ void GuiRuntime::HandleServerControllerDisconnected(
|
||||
return;
|
||||
}
|
||||
|
||||
LOG_INFO("Last remote controller disconnected; recovering privacy, reason={}",
|
||||
reason ? reason : "unknown");
|
||||
privacy_.Disconnected();
|
||||
|
||||
need_to_create_server_window_.store(false, std::memory_order_release);
|
||||
need_to_destroy_server_window_.store(true, std::memory_order_release);
|
||||
is_server_mode_ = false;
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#define _GUI_RUNTIME_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <deque>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
@@ -17,10 +16,8 @@
|
||||
#include "features/file_transfer/file_transfer_manager.h"
|
||||
#include "features/input/keyboard_controller.h"
|
||||
#include "features/settings/settings_manager.h"
|
||||
#include "privacy_controller.h"
|
||||
#include "runtime/gui_state.h"
|
||||
#include "runtime/peer_event_handler.h"
|
||||
#include "runtime/privacy_session_policy.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -58,14 +55,9 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
void HandleConnectionStatusChange();
|
||||
void HandlePendingPresenceProbe();
|
||||
void HandlePresenceProbeTimeout();
|
||||
bool HasActiveSession();
|
||||
void HandleServerControllerDisconnected(const std::string& remote_id,
|
||||
const char* reason);
|
||||
void HandleWindowsServiceIntegration();
|
||||
void HandlePrivacy();
|
||||
bool IsAuthorizedController(const std::string& remote_id);
|
||||
void SetControllerPrivacySupport(const std::string& remote_id, bool supported);
|
||||
void QueuePrivacyCommand(const std::string& remote_id, const PrivacyCommand& command);
|
||||
|
||||
void CloseRemoteSession(std::shared_ptr<RemoteSession> props);
|
||||
void CloseAllRemoteSessions();
|
||||
@@ -95,12 +87,6 @@ class GuiRuntime : protected gui_detail::GuiState {
|
||||
bool EnsureMacAccessibilityPermission();
|
||||
#endif
|
||||
|
||||
PrivacyController privacy_;
|
||||
PrivacySessionPolicy privacy_sessions_;
|
||||
std::mutex privacy_commands_mutex_;
|
||||
std::deque<std::pair<std::string, PrivacyCommand>> privacy_commands_;
|
||||
uint32_t last_privacy_revision_ = 0;
|
||||
uint64_t last_privacy_status_tick_ = 0;
|
||||
ClipboardController clipboard_;
|
||||
SessionDeviceManager devices_;
|
||||
FileTransferManager transfers_;
|
||||
|
||||
@@ -147,46 +147,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
}
|
||||
|
||||
std::string remote_id(user_id, user_id_size);
|
||||
if (remote_action.type == ControlType::privacy_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
if (source_id != props->control_data_label_) return;
|
||||
std::lock_guard lock(props->privacy_status_mutex_);
|
||||
if (!props->privacy_status_received_ ||
|
||||
static_cast<int32_t>(remote_action.ps.revision - props->privacy_status_.revision) >= 0) {
|
||||
props->privacy_status_ = remote_action.ps;
|
||||
props->privacy_status_received_ = true;
|
||||
props->privacy_status_tick_ = SDL_GetTicks();
|
||||
if (static_cast<int32_t>(remote_action.ps.revision - props->privacy_request_revision_) > 0 &&
|
||||
remote_action.ps.state != PrivacyState::starting &&
|
||||
remote_action.ps.state != PrivacyState::stopping)
|
||||
props->privacy_command_pending_ = false;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (remote_action.type == ControlType::privacy_command) {
|
||||
if (source_id == runtime->control_data_label_)
|
||||
runtime->QueuePrivacyCommand(remote_id, remote_action.pc);
|
||||
return;
|
||||
}
|
||||
const bool operates_host = remote_action.type == ControlType::mouse ||
|
||||
remote_action.type == ControlType::keyboard || remote_action.type == ControlType::keyboard_state ||
|
||||
remote_action.type == ControlType::service_command || remote_action.type == ControlType::display_id ||
|
||||
remote_action.type == ControlType::audio_capture;
|
||||
if (operates_host) {
|
||||
if (!runtime->IsAuthorizedController(remote_id)) return;
|
||||
#ifdef _WIN32
|
||||
// KeyboardController checks the desktop immediately before each native
|
||||
// key injection, including state reconciliation and cleanup. Avoid a
|
||||
// second synchronous session query for every received keyboard packet.
|
||||
const bool keyboard_input = remote_action.type == ControlType::keyboard ||
|
||||
remote_action.type == ControlType::keyboard_state;
|
||||
if (!keyboard_input && runtime->privacy_.Engaged() &&
|
||||
!IsWindowsPrivacyDesktopAvailable()) {
|
||||
runtime->privacy_.Fail("Secure desktop or lock screen; privacy screen will turn off");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (remote_action.type == ControlType::service_status) {
|
||||
if (auto props = runtime->FindRemoteSession(remote_id)) {
|
||||
runtime->ApplyRemoteServiceStatus(*props, remote_action.ss);
|
||||
@@ -195,9 +155,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
}
|
||||
|
||||
if (remote_action.type == ControlType::service_command) {
|
||||
if (runtime->privacy_.Engaged()) {
|
||||
runtime->privacy_.Fail("Security shortcut requested; privacy screen will turn off");
|
||||
}
|
||||
#if _WIN32
|
||||
if (remote_action.c.flag == ServiceCommandFlag::send_sas) {
|
||||
runtime->pending_windows_service_sas_.store(true,
|
||||
@@ -290,10 +247,6 @@ void PeerEventHandler::OnReceiveDataBuffer(
|
||||
runtime->connection_host_names_[remote_id] = host_name;
|
||||
}
|
||||
LOG_INFO("Remote hostname: [{}]", host_name);
|
||||
if (source_id == runtime->control_data_label_) {
|
||||
runtime->SetControllerPrivacySupport(
|
||||
remote_id, remote_action.i.supports_privacy_screen);
|
||||
}
|
||||
remote_action_codec::Free(remote_action);
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -264,8 +264,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
props->remote_cursor_state_received_ = false;
|
||||
}
|
||||
{
|
||||
RemoteAction remote_action{};
|
||||
remote_action.i.supports_privacy_screen = true;
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num =
|
||||
runtime->devices_.display_info_list().size();
|
||||
remote_action.i.display_list =
|
||||
@@ -406,16 +405,6 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
runtime->show_connection_status_window_ = true;
|
||||
{
|
||||
std::unique_lock lock(runtime->connection_status_mutex_);
|
||||
if (status == ConnectionStatus::Connected) {
|
||||
// Wait for explicit desktop capability before enabling privacy or
|
||||
// sending privacy messages. Legacy and web controllers never opt in.
|
||||
runtime->privacy_sessions_.Connected(
|
||||
remote_id, runtime->config_center_->IsEnablePrivacyScreen());
|
||||
if (!runtime->privacy_sessions_.CanEnable() &&
|
||||
runtime->privacy_.Engaged()) {
|
||||
runtime->privacy_.Disable();
|
||||
}
|
||||
}
|
||||
runtime->connection_status_[remote_id] = status;
|
||||
}
|
||||
|
||||
@@ -425,8 +414,7 @@ void PeerEventHandler::OnConnectionStatus(ConnectionStatus status,
|
||||
runtime->last_windows_service_status_tick_ = 0;
|
||||
#endif
|
||||
{
|
||||
RemoteAction remote_action{};
|
||||
remote_action.i.supports_privacy_screen = true;
|
||||
RemoteAction remote_action;
|
||||
remote_action.i.display_num =
|
||||
runtime->devices_.display_info_list().size();
|
||||
remote_action.i.display_list =
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-12
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_CONTROL_STATE_H_
|
||||
#define _PRIVACY_CONTROL_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
#include "remote_action.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct PrivacyControlState {
|
||||
bool active = false;
|
||||
bool enabled = false;
|
||||
bool off = false;
|
||||
bool unsupported = false;
|
||||
bool can_toggle = false;
|
||||
};
|
||||
|
||||
// Use the same host-confirmed state for presentation and command admission.
|
||||
// Missing confirmation or a failure makes only this toolbar control unavailable.
|
||||
inline PrivacyControlState GetPrivacyControlState(
|
||||
const PrivacyStatus& status, bool received, bool command_pending,
|
||||
uint64_t status_age_ms, uint64_t command_age_ms) {
|
||||
PrivacyControlState result;
|
||||
const bool timeout = command_pending && command_age_ms > 15000;
|
||||
result.unsupported = timeout ||
|
||||
(received && (status_age_ms > 5000 || !status.supported ||
|
||||
status.state == PrivacyState::failed));
|
||||
if (result.unsupported) return result;
|
||||
|
||||
result.active = status.overlay_active || status.remote_paused ||
|
||||
status.state == PrivacyState::on ||
|
||||
status.state == PrivacyState::starting ||
|
||||
status.state == PrivacyState::stopping ||
|
||||
status.state == PrivacyState::failed;
|
||||
// A command in flight only gates repeated clicks; it adds no display state.
|
||||
result.enabled = received &&
|
||||
status.state == PrivacyState::on && status.overlay_active &&
|
||||
!status.remote_paused;
|
||||
result.off = received &&
|
||||
status.state == PrivacyState::off && !result.active;
|
||||
result.can_toggle = !command_pending && (result.active || received);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,114 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <cstring>
|
||||
#include <shared_mutex>
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "runtime/gui_runtime.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool GuiRuntime::IsAuthorizedController(const std::string& remote_id) {
|
||||
// Connections appear here only after the existing password/signalling/RTC
|
||||
// admission path. Outgoing/viewer sessions live in remote_sessions_ instead.
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
const auto found = connection_status_.find(remote_id);
|
||||
return found != connection_status_.end() &&
|
||||
found->second == ConnectionStatus::Connected;
|
||||
}
|
||||
|
||||
void GuiRuntime::SetControllerPrivacySupport(const std::string& remote_id,
|
||||
bool supported) {
|
||||
std::unique_lock lock(connection_status_mutex_);
|
||||
const auto found = connection_status_.find(remote_id);
|
||||
if (found == connection_status_.end() ||
|
||||
found->second != ConnectionStatus::Connected) return;
|
||||
|
||||
const bool automatic_enable =
|
||||
privacy_sessions_.SetSupported(remote_id, supported);
|
||||
if (!privacy_sessions_.CanEnable()) {
|
||||
if (privacy_.Engaged()) privacy_.Disable();
|
||||
} else if (automatic_enable && config_center_->IsEnablePrivacyScreen()) {
|
||||
privacy_.EnableOnConnection();
|
||||
}
|
||||
}
|
||||
|
||||
void GuiRuntime::QueuePrivacyCommand(const std::string& remote_id,
|
||||
const PrivacyCommand& command) {
|
||||
std::shared_lock session_lock(connection_status_mutex_);
|
||||
const auto found = connection_status_.find(remote_id);
|
||||
if (found == connection_status_.end() ||
|
||||
found->second != ConnectionStatus::Connected ||
|
||||
!privacy_sessions_.Supports(remote_id)) return;
|
||||
std::lock_guard lock(privacy_commands_mutex_);
|
||||
if (privacy_commands_.size() < 32)
|
||||
privacy_commands_.emplace_back(remote_id, command);
|
||||
}
|
||||
|
||||
void GuiRuntime::HandlePrivacy() {
|
||||
std::deque<std::pair<std::string, PrivacyCommand>> commands;
|
||||
{
|
||||
std::lock_guard lock(privacy_commands_mutex_);
|
||||
commands.swap(privacy_commands_);
|
||||
}
|
||||
for (const auto& [remote_id, command] : commands) {
|
||||
// Keep admission and activation under the connection lock so an unknown
|
||||
// controller cannot join between the capability check and enabling.
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
const auto found = connection_status_.find(remote_id);
|
||||
if (found == connection_status_.end() ||
|
||||
found->second != ConnectionStatus::Connected ||
|
||||
!privacy_sessions_.Supports(remote_id)) continue;
|
||||
if (command.flag == PrivacyCommandFlag::enable) {
|
||||
if (privacy_sessions_.CanEnable())
|
||||
privacy_.Enable(command.block_local_input);
|
||||
} else if (command.flag == PrivacyCommandFlag::disable) {
|
||||
privacy_.Disable();
|
||||
}
|
||||
last_privacy_status_tick_ = 0;
|
||||
}
|
||||
auto status = privacy_.Snapshot();
|
||||
const uint64_t now = SDL_GetTicks();
|
||||
if (!peer_ || (last_privacy_status_tick_ != 0 &&
|
||||
status.revision == last_privacy_revision_ &&
|
||||
now - last_privacy_status_tick_ < 1000))
|
||||
return;
|
||||
std::vector<std::string> controllers;
|
||||
{
|
||||
std::shared_lock lock(connection_status_mutex_);
|
||||
for (const auto& [id, state] : connection_status_)
|
||||
if (state == ConnectionStatus::Connected && privacy_sessions_.Supports(id))
|
||||
controllers.push_back(id);
|
||||
if (!privacy_sessions_.CanEnable()) {
|
||||
status.state = PrivacyState::unsupported;
|
||||
status.supported = false;
|
||||
status.input_block_supported = false;
|
||||
std::strcpy(status.reason,
|
||||
"A connected controller does not support privacy screen");
|
||||
}
|
||||
}
|
||||
RemoteAction action{};
|
||||
action.type = ControlType::privacy_status;
|
||||
action.ps = status;
|
||||
const auto message = action.to_json();
|
||||
bool sent = true;
|
||||
for (const auto& id : controllers) {
|
||||
const int result = SendReliableDataFrameToPeer(
|
||||
peer_, message.data(), message.size(), control_data_label_.c_str(),
|
||||
id.data(), id.size());
|
||||
if (result != 0) {
|
||||
sent = false;
|
||||
LOG_WARN("Privacy status send failed, peer={}, ret={}", id, result);
|
||||
}
|
||||
}
|
||||
if (sent) {
|
||||
last_privacy_revision_ = status.revision;
|
||||
last_privacy_status_tick_ = now;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,61 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-15
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_SESSION_POLICY_H_
|
||||
#define _PRIVACY_SESSION_POLICY_H_
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Guarded by GuiRuntime's connection_status_mutex_. Privacy covers the shared
|
||||
// desktop, so every connected controller must explicitly support it.
|
||||
class PrivacySessionPolicy {
|
||||
public:
|
||||
void Connected(const std::string& remote_id, bool automatic_privacy) {
|
||||
const auto [entry, inserted] = controllers_.try_emplace(remote_id, false);
|
||||
if (inserted && controllers_.size() == 1 && automatic_privacy) {
|
||||
automatic_controller_ = remote_id;
|
||||
}
|
||||
}
|
||||
|
||||
void Disconnected(const std::string& remote_id) {
|
||||
controllers_.erase(remote_id);
|
||||
if (automatic_controller_ == remote_id) automatic_controller_.clear();
|
||||
}
|
||||
|
||||
// Consume automatic activation once, on the first controller's host info.
|
||||
// Repeated metadata or a later join must not undo an explicit privacy exit.
|
||||
bool SetSupported(const std::string& remote_id, bool supported) {
|
||||
const auto entry = controllers_.find(remote_id);
|
||||
if (entry == controllers_.end()) return false;
|
||||
entry->second = supported && remote_id.find("web") == std::string::npos;
|
||||
if (automatic_controller_ != remote_id) return false;
|
||||
automatic_controller_.clear();
|
||||
return CanEnable();
|
||||
}
|
||||
|
||||
bool Supports(const std::string& remote_id) const {
|
||||
const auto entry = controllers_.find(remote_id);
|
||||
return entry != controllers_.end() && entry->second;
|
||||
}
|
||||
|
||||
bool CanEnable() const {
|
||||
return !controllers_.empty() &&
|
||||
std::all_of(controllers_.begin(), controllers_.end(),
|
||||
[](const auto& entry) { return entry.second; });
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, bool> controllers_;
|
||||
std::string automatic_controller_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -55,7 +55,6 @@ bool Deserialize(const char *data, size_t size, RemoteAction &out) {
|
||||
out.type = static_cast<ControlType>(data[offset++]);
|
||||
|
||||
if (out.type == ControlType::host_infomation) {
|
||||
out.i.supports_privacy_screen = false;
|
||||
size_t name_len;
|
||||
if (!read(&name_len, sizeof(size_t)) ||
|
||||
name_len >= sizeof(out.i.host_name)) {
|
||||
|
||||
@@ -157,13 +157,6 @@ struct RemoteSession {
|
||||
std::string audio_capture_button_label_ = "Audio Capture";
|
||||
std::string remote_host_name_;
|
||||
bool remote_service_status_received_ = false;
|
||||
std::mutex privacy_status_mutex_;
|
||||
PrivacyStatus privacy_status_{};
|
||||
bool privacy_status_received_ = false;
|
||||
bool privacy_command_pending_ = false;
|
||||
uint64_t privacy_command_tick_ = 0;
|
||||
uint32_t privacy_request_revision_ = 0;
|
||||
uint64_t privacy_status_tick_ = 0;
|
||||
bool remote_service_available_ = false;
|
||||
std::string remote_interactive_stage_;
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
@@ -191,4 +184,4 @@ using RemoteSessionPtr = std::shared_ptr<RemoteSession>;
|
||||
|
||||
} // namespace crossdesk::gui_detail
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -136,6 +136,7 @@ struct UserSettingsState {
|
||||
int video_encode_format_button_value_ = 0;
|
||||
bool enable_hardware_video_codec_ = true;
|
||||
bool enable_turn_ = true;
|
||||
bool enable_srtp_ = false;
|
||||
char signal_server_ip_[256] = "api.crossdesk.cn";
|
||||
char signal_server_port_[6] = "9099";
|
||||
bool enable_self_hosted_ = false;
|
||||
@@ -146,6 +147,7 @@ struct UserSettingsState {
|
||||
int video_encode_format_button_value_last_ = 0;
|
||||
bool enable_hardware_video_codec_last_ = false;
|
||||
bool enable_turn_last_ = true;
|
||||
bool enable_srtp_last_ = false;
|
||||
bool enable_self_hosted_last_ = false;
|
||||
bool enable_autostart_ = false;
|
||||
bool enable_autostart_last_ = false;
|
||||
|
||||
@@ -45,7 +45,6 @@ export global FontAwesomeIcons {
|
||||
out property <string> shield-halved: "\u{f3ed}";
|
||||
out property <string> square-full: "\u{f45c}";
|
||||
out property <string> trash-can: "\u{f2ed}";
|
||||
out property <string> user-secret: "\u{f21b}";
|
||||
out property <string> volume-high: "\u{f028}";
|
||||
out property <string> volume-xmark: "\u{f6a9}";
|
||||
out property <string> xmark: "\u{f00d}";
|
||||
|
||||
@@ -39,8 +39,6 @@ export global UiStrings {
|
||||
in-out property <string> language: "Language";
|
||||
in-out property <string> video-quality: "Video quality";
|
||||
in-out property <string> frame-rate: "Frame rate";
|
||||
in-out property <string> capture-method: "Capture method";
|
||||
in-out property <string> capture-method-auto: "Auto";
|
||||
in-out property <string> adaptation-policy: "Video preference";
|
||||
in-out property <string> priority-frame-rate: "Frame rate";
|
||||
in-out property <string> priority-quality: "Quality";
|
||||
@@ -48,10 +46,10 @@ export global UiStrings {
|
||||
in-out property <string> codec: "Video codec";
|
||||
in-out property <string> hardware-codec: "Hardware video codec";
|
||||
in-out property <string> turn-relay: "TURN relay";
|
||||
in-out property <string> srtp: "SRTP encryption";
|
||||
in-out property <string> self-hosted: "Self-hosted server";
|
||||
in-out property <string> autostart: "Start with system";
|
||||
in-out property <string> daemon: "Run in background";
|
||||
in-out property <string> privacy-on-connect: "Privacy screen on connect";
|
||||
in-out property <string> file-save-path: "File save path";
|
||||
in-out property <string> default-desktop: "Desktop";
|
||||
in-out property <string> server-host: "Server host";
|
||||
@@ -212,6 +210,11 @@ export component MainWindow inherits Window {
|
||||
in property <bool> signal-tls-error: false;
|
||||
in property <[RecentConnection]> recent-connections;
|
||||
in property <bool> update-available: false;
|
||||
in property <bool> update-downloading: false;
|
||||
in property <bool> update-failed: false;
|
||||
in property <bool> update-security-blocked: false;
|
||||
in property <float> update-progress: 0;
|
||||
in property <string> update-status: "";
|
||||
in property <string> current-version: "";
|
||||
in property <string> latest-version: "";
|
||||
in property <string> release-name: "";
|
||||
@@ -240,18 +243,14 @@ export component MainWindow inherits Window {
|
||||
in-out property <int> language-index: 0;
|
||||
in-out property <int> video-quality-index: 2;
|
||||
in-out property <int> frame-rate-index: 1;
|
||||
in property <bool> capture-method-visible: false;
|
||||
in-out property <int> capture-method-index: 0;
|
||||
in-out property <int> adaptation-policy-index: 1;
|
||||
in-out property <int> codec-index: 0;
|
||||
in-out property <bool> hardware-codec-enabled: true;
|
||||
in-out property <bool> turn-enabled: true;
|
||||
in-out property <bool> srtp-enabled: false;
|
||||
in-out property <bool> self-hosted-enabled: false;
|
||||
in-out property <bool> autostart-enabled: false;
|
||||
in-out property <bool> daemon-enabled: false;
|
||||
// Show the preference on every platform; only adapted hosts can change it.
|
||||
in property <bool> privacy-setting-available: false;
|
||||
in-out property <bool> privacy-on-connect-enabled: true;
|
||||
in-out property <string> file-save-path: "";
|
||||
in-out property <string> server-host: "";
|
||||
in-out property <string> server-port: "";
|
||||
@@ -274,6 +273,8 @@ export component MainWindow inherits Window {
|
||||
callback cancel-self-hosted-settings;
|
||||
callback browse-save-path;
|
||||
callback open-download;
|
||||
callback start-update;
|
||||
callback cancel-update;
|
||||
callback connection-cancel;
|
||||
callback connection-acknowledge;
|
||||
callback connection-submit-password(string, bool);
|
||||
@@ -1451,11 +1452,6 @@ export component MainWindow inherits Window {
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.language; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; model: ["中文", "English", "Русский"]; current-index <=> root.language-index; }
|
||||
}
|
||||
if root.capture-method-visible: Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.capture-method; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
CompactComboBox { x: root.compact-language ? 106px : 174px; y: 2px; enabled: !root.settings-session-active; model: [UiStrings.capture-method-auto, "DXGI", "WGC", "GDI"]; current-index <=> root.capture-method-index; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 94px : 162px; height: 24px; text: UiStrings.video-quality; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
@@ -1488,8 +1484,8 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.privacy-on-connect; color: root.privacy-setting-available ? #202124 : #8b8f95; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.privacy-on-connect-enabled; enabled: root.privacy-setting-available; }
|
||||
Text { x: 8px; y: 3px; width: root.compact-language ? 145px : 212px; height: 24px; text: UiStrings.srtp; font-size: ImGuiFontStyle.body; overflow: elide; vertical-alignment: center; }
|
||||
ImGuiCheckBox { x: root.compact-language ? 157px : 224px; y: 3px; checked <=> root.srtp-enabled; enabled: !root.settings-session-active; }
|
||||
}
|
||||
Rectangle {
|
||||
height: 30px;
|
||||
@@ -1655,7 +1651,7 @@ export component MainWindow inherits Window {
|
||||
x: 30px;
|
||||
y: 105px;
|
||||
width: 360px;
|
||||
height: 190px;
|
||||
height: root.update-status != "" ? 136px : 190px;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
border-color: #b9bdc3;
|
||||
@@ -1682,8 +1678,27 @@ export component MainWindow inherits Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; clicked => { root.open-download(); root.update-open = false; } }
|
||||
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.update-open = false; } }
|
||||
if root.update-status != "": Text {
|
||||
x: 30px; y: 247px; width: 340px; height: 40px;
|
||||
text: root.update-status;
|
||||
color: root.update-failed ? #b23b3b : #3d434d;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
wrap: word-wrap;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
if root.update-downloading: Rectangle {
|
||||
x: 30px; y: 291px; width: 340px; height: 4px;
|
||||
background: #e2e4e7;
|
||||
clip: true;
|
||||
Rectangle {
|
||||
x: root.update-progress < 0 ? (parent.width + self.width) * (mod(animation-tick(), 1500ms) / 1500ms) - self.width : 0px;
|
||||
width: parent.width * (root.update-progress < 0 ? 0.25 : root.update-progress);
|
||||
height: parent.height;
|
||||
background: #334fd1;
|
||||
}
|
||||
}
|
||||
CompactButton { x: root.compact-language ? 163px : 130px; y: 309px; width: root.compact-language ? 34px : 70px; primary: true; text: UiStrings.download; enabled: !root.update-downloading && !root.update-security-blocked; clicked => { root.start-update(); } }
|
||||
CompactButton { x: root.compact-language ? 204px : 207px; y: 309px; width: root.compact-language ? 34px : 56px; text: UiStrings.later; clicked => { root.cancel-update(); } }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ export struct NetworkStatsRow {
|
||||
export global StreamStrings {
|
||||
in-out property <string> select-display: "Select Display";
|
||||
in-out property <string> send-shortcut: "Send Shortcut";
|
||||
in-out property <string> privacy-enable: "Enable privacy screen";
|
||||
in-out property <string> privacy-disable: "Turn privacy off / recover";
|
||||
in-out property <string> control-mouse: "Control Mouse";
|
||||
in-out property <string> release-mouse: "Release Mouse";
|
||||
in-out property <string> audio: "Audio Capture";
|
||||
@@ -47,11 +45,6 @@ export global StreamStrings {
|
||||
in-out property <string> stats-connection-mode: "Mode";
|
||||
}
|
||||
|
||||
struct ControlBarTooltip {
|
||||
text: string,
|
||||
anchor-x: length,
|
||||
}
|
||||
|
||||
// The legacy ImGui control bar uses 24px buttons with Font Awesome rendered
|
||||
// at half of the 32px atlas size. Keep this local to the stream window: the
|
||||
// shared IconButton is intentionally larger and is used by the other windows.
|
||||
@@ -61,27 +54,19 @@ component ControlBarButton inherits Rectangle {
|
||||
in property <string> tooltip: "";
|
||||
in property <bool> selected: false;
|
||||
in property <bool> slashed: false;
|
||||
in property <bool> action-enabled: true;
|
||||
// Capability and temporary command availability have different visuals.
|
||||
in property <bool> muted: false;
|
||||
in property <length> icon-size: 16px;
|
||||
out property <bool> has-hover: touch.has-hover;
|
||||
out property <ControlBarTooltip> tooltip-info: {
|
||||
text: root.visible && touch.has-hover && !touch.pressed ? root.tooltip : "",
|
||||
anchor-x: root.x + root.width / 2,
|
||||
};
|
||||
callback clicked;
|
||||
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: root.muted ? #eef0f3 : touch.pressed && root.action-enabled ? #0f87fa
|
||||
background: touch.pressed ? #0f87fa
|
||||
: touch.has-hover ? #4296fa
|
||||
: root.selected ? #4296fa : #4296fa66;
|
||||
border-radius: 3px;
|
||||
|
||||
Text {
|
||||
text: root.icon;
|
||||
color: root.muted ? #8a8f96 : #24272d;
|
||||
color: #24272d;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: root.icon-size;
|
||||
@@ -120,8 +105,7 @@ component ControlBarButton inherits Rectangle {
|
||||
}
|
||||
|
||||
touch := TouchArea {
|
||||
// Keep disabled controls hit-testable so clicks cannot reach the peer.
|
||||
clicked => { if root.action-enabled { root.clicked(); } }
|
||||
clicked => { root.clicked(); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -150,16 +134,11 @@ export component StreamWindow inherits Window {
|
||||
in property <[string]> displays;
|
||||
in-out property <int> selected-display: 0;
|
||||
in property <bool> mouse-control-enabled: true;
|
||||
in property <bool> privacy-can-toggle: false;
|
||||
in property <bool> privacy-active: false;
|
||||
in property <bool> privacy-off: false;
|
||||
in property <bool> privacy-unsupported: false;
|
||||
// Privacy feedback stays on its toolbar button and hover tooltip.
|
||||
in property <string> privacy-status-text: "Privacy screen";
|
||||
// Numeric values mirror crossdesk::RemoteCursorShape and Slint's MouseCursor.
|
||||
in property <bool> remote-cursor-active: false;
|
||||
in property <int> remote-cursor-shape: 0;
|
||||
in property <bool> audio-enabled: true;
|
||||
in property <bool> srtp-enabled: false;
|
||||
in property <bool> fullscreen-enabled: false;
|
||||
in property <bool> stats-visible: false;
|
||||
in property <[NetworkStatsRow]> stats-rows;
|
||||
@@ -175,7 +154,6 @@ export component StreamWindow inherits Window {
|
||||
callback switch-display(int);
|
||||
callback send-shortcut(string);
|
||||
callback toggle-mouse-control;
|
||||
callback toggle-privacy;
|
||||
callback toggle-audio;
|
||||
callback select-file;
|
||||
callback close-file-transfer;
|
||||
@@ -198,25 +176,6 @@ export component StreamWindow inherits Window {
|
||||
private property <bool> display-menu-animation-enabled: false;
|
||||
private property <float> display-menu-progress: 0.0;
|
||||
private property <bool> shortcut-menu-open: false;
|
||||
private property <bool> control-tooltip-visible: false;
|
||||
private property <ControlBarTooltip> hovered-control: {
|
||||
if root.window-active && !root.control-dragging
|
||||
&& !root.display-menu-open && !root.shortcut-menu-open {
|
||||
if collapse-button.has-hover { return collapse-button.tooltip-info; }
|
||||
if root.control-expanded {
|
||||
if display-button.has-hover { return display-button.tooltip-info; }
|
||||
if shortcut-button.has-hover { return shortcut-button.tooltip-info; }
|
||||
if mouse-button.has-hover { return mouse-button.tooltip-info; }
|
||||
if privacy-button.has-hover { return privacy-button.tooltip-info; }
|
||||
if audio-button.has-hover { return audio-button.tooltip-info; }
|
||||
if file-button.has-hover { return file-button.tooltip-info; }
|
||||
if stats-button.has-hover { return stats-button.tooltip-info; }
|
||||
if fullscreen-button.has-hover { return fullscreen-button.tooltip-info; }
|
||||
if close-button.has-hover { return close-button.tooltip-info; }
|
||||
}
|
||||
}
|
||||
return { text: "", anchor-x: 0px };
|
||||
}
|
||||
// A popup can remove itself from the scene in its clicked callback before
|
||||
// the video input layer observes the same pointer release. Remember that
|
||||
// the press belonged to local UI so that release cannot reach the peer.
|
||||
@@ -256,20 +215,6 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
changed hovered-control => {
|
||||
root.control-tooltip-visible = false;
|
||||
control-tooltip-timer.running = root.hovered-control.text != "";
|
||||
if root.hovered-control.text != "" { control-tooltip-timer.restart(); }
|
||||
}
|
||||
control-tooltip-timer := Timer {
|
||||
interval: 450ms;
|
||||
running: false;
|
||||
triggered => {
|
||||
self.running = false;
|
||||
root.control-tooltip-visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
is-local-control-area(pointer-x, pointer-y) => {
|
||||
let over-control = pointer-x >= control.x
|
||||
&& pointer-x <= control.x + control.width
|
||||
@@ -287,7 +232,7 @@ export component StreamWindow inherits Window {
|
||||
let shortcut-menu-y = control.y + 40px;
|
||||
let over-shortcut-menu = root.shortcut-menu-open
|
||||
&& pointer-x >= shortcut-menu-x
|
||||
&& pointer-x <= shortcut-menu-x + 180px
|
||||
&& pointer-x <= shortcut-menu-x + 150px
|
||||
&& pointer-y >= shortcut-menu-y
|
||||
&& pointer-y <= shortcut-menu-y + 68px;
|
||||
return over-control || over-display-menu || over-shortcut-menu;
|
||||
@@ -505,8 +450,20 @@ export component StreamWindow inherits Window {
|
||||
width: parent.width - 27px;
|
||||
height: parent.height;
|
||||
background: transparent;
|
||||
Text {
|
||||
if root.srtp-enabled: Text {
|
||||
x: 0px;
|
||||
width: 12px;
|
||||
height: parent.height;
|
||||
text: FontAwesomeIcons.shield-halved;
|
||||
color: #30343b;
|
||||
font-family: "Font Awesome 6 Free";
|
||||
font-weight: 900;
|
||||
font-size: ImGuiFontStyle.base;
|
||||
horizontal-alignment: center;
|
||||
vertical-alignment: center;
|
||||
}
|
||||
Text {
|
||||
x: root.srtp-enabled ? 16px : 0px;
|
||||
width: parent.width - self.x;
|
||||
height: parent.height;
|
||||
text: tab.title;
|
||||
@@ -682,7 +639,7 @@ export component StreamWindow inherits Window {
|
||||
control := Rectangle {
|
||||
x: root.control-dragging ? root.control-drag-x : root.control-docked-left ? 0px : parent.width - self.width;
|
||||
y: min(max(0px, root.control-pos-y), parent.height - self.height);
|
||||
width: root.control-expanded ? min(332px, parent.width) : 20px;
|
||||
width: root.control-expanded ? min(300px, parent.width) : 20px;
|
||||
height: root.stats-visible && root.control-expanded ? min(180px, parent.height) : 38px;
|
||||
background: white;
|
||||
border-width: 1px;
|
||||
@@ -700,8 +657,7 @@ export component StreamWindow inherits Window {
|
||||
background: ImGuiLineStyle.control-separator;
|
||||
}
|
||||
|
||||
display-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
if root.control-expanded: display-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 9px : 42px; y: 7px;
|
||||
icon: FontAwesomeIcons.display;
|
||||
badge: root.selected-display + 1;
|
||||
@@ -717,8 +673,7 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
}
|
||||
shortcut-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
if root.control-expanded: shortcut-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 41px : 74px; y: 7px;
|
||||
icon: FontAwesomeIcons.keyboard; tooltip: StreamStrings.send-shortcut;
|
||||
clicked => {
|
||||
@@ -729,53 +684,47 @@ export component StreamWindow inherits Window {
|
||||
root.display-menu-open = false;
|
||||
}
|
||||
}
|
||||
mouse-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
if root.control-expanded: mouse-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 73px : 106px; y: 7px;
|
||||
icon: FontAwesomeIcons.computer-mouse; slashed: !root.mouse-control-enabled;
|
||||
tooltip: root.mouse-control-enabled ? StreamStrings.release-mouse : StreamStrings.control-mouse;
|
||||
clicked => { root.toggle-mouse-control(); }
|
||||
}
|
||||
audio-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: root.control-docked-left ? 137px : 170px; y: 7px;
|
||||
if root.control-expanded: audio-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 105px : 138px; y: 7px;
|
||||
icon: FontAwesomeIcons.volume-high; slashed: !root.audio-enabled;
|
||||
tooltip: root.audio-enabled ? StreamStrings.mute : StreamStrings.audio;
|
||||
clicked => { root.toggle-audio(); }
|
||||
}
|
||||
file-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: root.control-docked-left ? 169px : 202px; y: 7px;
|
||||
if root.control-expanded: file-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 137px : 170px; y: 7px;
|
||||
icon: FontAwesomeIcons.folder-open; tooltip: StreamStrings.select-file;
|
||||
clicked => { root.select-file(); }
|
||||
}
|
||||
stats-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: root.control-docked-left ? 201px : 234px; y: 7px;
|
||||
if root.control-expanded: stats-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 169px : 202px; y: 7px;
|
||||
icon: FontAwesomeIcons.signal; selected: root.stats-visible;
|
||||
tooltip: root.stats-visible ? StreamStrings.hide-stats : StreamStrings.show-stats;
|
||||
clicked => { root.toggle-stats(); }
|
||||
}
|
||||
fullscreen-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: root.control-docked-left ? 233px : 266px; y: 7px;
|
||||
if root.control-expanded: fullscreen-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 201px : 234px; y: 7px;
|
||||
icon: root.fullscreen-enabled ? FontAwesomeIcons.compress : FontAwesomeIcons.expand;
|
||||
tooltip: root.fullscreen-enabled ? StreamStrings.exit-fullscreen : StreamStrings.fullscreen;
|
||||
clicked => { root.toggle-fullscreen(); }
|
||||
}
|
||||
close-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: root.control-docked-left ? 265px : 298px; y: 7px;
|
||||
if root.control-expanded: close-button := ControlBarButton {
|
||||
x: root.control-docked-left ? 233px : 266px; y: 7px;
|
||||
icon: FontAwesomeIcons.xmark; tooltip: StreamStrings.disconnect;
|
||||
clicked => { root.disconnect(); }
|
||||
}
|
||||
|
||||
if root.control-expanded && root.control-docked-left: Rectangle {
|
||||
x: 299px; y: 12px; width: 2px; height: 15px;
|
||||
x: 267px; y: 12px; width: 2px; height: 15px;
|
||||
background: ImGuiLineStyle.control-separator;
|
||||
}
|
||||
collapse-button := ControlBarButton {
|
||||
x: root.control-expanded ? (root.control-docked-left ? 307px : 10px) : 2px;
|
||||
x: root.control-expanded ? (root.control-docked-left ? 275px : 10px) : 2px;
|
||||
y: 7px;
|
||||
width: 15px;
|
||||
icon: root.control-expanded
|
||||
@@ -785,19 +734,6 @@ export component StreamWindow inherits Window {
|
||||
clicked => { root.control-expanded = !root.control-expanded; }
|
||||
}
|
||||
|
||||
privacy-button := ControlBarButton {
|
||||
visible: root.control-expanded;
|
||||
x: root.control-docked-left ? 105px : 138px; y: 7px;
|
||||
icon: FontAwesomeIcons.user-secret;
|
||||
slashed: root.privacy-off && !root.privacy-unsupported;
|
||||
muted: root.privacy-unsupported;
|
||||
action-enabled: root.privacy-can-toggle;
|
||||
tooltip: root.privacy-status-text
|
||||
+ (root.privacy-can-toggle ? "\n" + (root.privacy-active
|
||||
? StreamStrings.privacy-disable : StreamStrings.privacy-enable) : "");
|
||||
clicked => { root.toggle-privacy(); }
|
||||
}
|
||||
|
||||
if root.stats-visible && root.control-expanded: stats-table := Rectangle {
|
||||
x: 14px;
|
||||
y: 38px;
|
||||
@@ -1007,7 +943,7 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
|
||||
if root.shortcut-menu-open: Rectangle {
|
||||
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 180px; height: 68px;
|
||||
x: control.x + (root.control-docked-left ? 41px : 74px); y: control.y + 40px; width: 150px; height: 68px;
|
||||
background: white; border-width: 1px; border-color: ImGuiLineStyle.border; border-radius: 5px; z: 11;
|
||||
VerticalLayout {
|
||||
padding: 4px; spacing: 1px;
|
||||
@@ -1026,29 +962,6 @@ export component StreamWindow inherits Window {
|
||||
}
|
||||
}
|
||||
|
||||
// Measure unwrapped text so short hints stay compact. Render outside
|
||||
// the clipped control bar, capping longer hints before wrapping.
|
||||
control-tooltip-width-probe := Text {
|
||||
visible: false;
|
||||
text: root.hovered-control.text;
|
||||
font-size: ImGuiFontStyle.body;
|
||||
}
|
||||
if root.hovered-control.text != "" && root.control-tooltip-visible: Rectangle {
|
||||
width: min(240px, video.width - 8px,
|
||||
control-tooltip-width-probe.preferred-width + 16px);
|
||||
x: min(max(4px, control.x + root.hovered-control.anchor-x - self.width / 2),
|
||||
video.width - self.width - 4px);
|
||||
height: control-tip.preferred-height + 12px;
|
||||
y: control.y + control.height + self.height + 8px <= video.height
|
||||
? control.y + control.height + 4px : max(4px, control.y - self.height - 4px);
|
||||
background: #30343bf5; border-radius: 5px; z: 12;
|
||||
control-tip := Text {
|
||||
x: 8px; y: 6px; width: parent.width - 16px;
|
||||
text: root.hovered-control.text;
|
||||
color: white; font-size: ImGuiFontStyle.body; wrap: word-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
// Transfer progress overlay at bottom-left.
|
||||
if root.file-transfer-visible: Rectangle {
|
||||
x: 18px; y: parent.height - min(210px, parent.height * 0.35); width: min(430px, parent.width * 0.6); height: min(190px, parent.height * 0.32);
|
||||
|
||||
@@ -54,9 +54,6 @@ inline int ApplyMainWindowStrings(
|
||||
strings.set_language(Text(localization::language[language]));
|
||||
strings.set_video_quality(Text(localization::video_quality[language]));
|
||||
strings.set_frame_rate(Text(localization::video_frame_rate[language]));
|
||||
strings.set_capture_method(Text(localization::screen_capture_method[language]));
|
||||
strings.set_capture_method_auto(
|
||||
Text(localization::screen_capture_method_auto[language]));
|
||||
strings.set_adaptation_policy(
|
||||
Text(localization::video_adaptation_policy[language]));
|
||||
strings.set_priority_frame_rate(
|
||||
@@ -69,11 +66,11 @@ inline int ApplyMainWindowStrings(
|
||||
strings.set_hardware_codec(
|
||||
Text(localization::enable_hardware_video_codec[language]));
|
||||
strings.set_turn_relay(Text(localization::enable_turn[language]));
|
||||
strings.set_srtp(Text(localization::enable_srtp[language]));
|
||||
strings.set_self_hosted(
|
||||
Text(localization::self_hosted_server_config[language]));
|
||||
strings.set_autostart(Text(localization::enable_autostart[language]));
|
||||
strings.set_daemon(Text(localization::enable_daemon[language]));
|
||||
strings.set_privacy_on_connect(Text(localization::privacy_on_connect[language]));
|
||||
strings.set_file_save_path(
|
||||
Text(localization::file_transfer_save_path[language]));
|
||||
strings.set_default_desktop(Text(localization::default_desktop[language]));
|
||||
@@ -141,8 +138,6 @@ inline int ApplyStreamWindowStrings(
|
||||
auto& strings = window->template global<ui::StreamStrings>();
|
||||
strings.set_select_display(Text(localization::select_display[language]));
|
||||
strings.set_send_shortcut(Text(localization::send_shortcut[language]));
|
||||
strings.set_privacy_enable(Text(localization::privacy_enable[language]));
|
||||
strings.set_privacy_disable(Text(localization::privacy_disable[language]));
|
||||
strings.set_control_mouse(Text(localization::control_mouse[language]));
|
||||
strings.set_release_mouse(Text(localization::release_mouse[language]));
|
||||
strings.set_audio(Text(localization::audio_capture[language]));
|
||||
@@ -172,4 +167,4 @@ inline int ApplyStreamWindowStrings(
|
||||
} // namespace crossdesk::ui_localization
|
||||
|
||||
|
||||
#endif
|
||||
#endif
|
||||
@@ -268,6 +268,25 @@ int GuiApplication::SettingWindow() {
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset);
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::Text(
|
||||
"%s",
|
||||
localization::enable_srtp[localization_language_index_].c_str());
|
||||
ImGui::SameLine();
|
||||
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f);
|
||||
} else {
|
||||
ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f);
|
||||
}
|
||||
|
||||
ImGui::Checkbox("##enable_srtp", &enable_srtp_);
|
||||
}
|
||||
|
||||
ImGui::Separator();
|
||||
|
||||
{
|
||||
settings_items_offset += settings_items_padding;
|
||||
ImGui::SetCursorPosY(settings_items_offset + 1);
|
||||
@@ -550,6 +569,14 @@ int GuiApplication::SettingWindow() {
|
||||
}
|
||||
enable_turn_last_ = enable_turn_;
|
||||
|
||||
// SRTP
|
||||
if (enable_srtp_) {
|
||||
config_center_->SetSrtp(true);
|
||||
} else {
|
||||
config_center_->SetSrtp(false);
|
||||
}
|
||||
enable_srtp_last_ = enable_srtp_;
|
||||
|
||||
if (enable_self_hosted_) {
|
||||
config_center_->SetSelfHosted(true);
|
||||
} else {
|
||||
|
||||
@@ -136,7 +136,10 @@ int GuiApplication::StreamWindow() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string& tab_label = props->remote_id_;
|
||||
std::string tab_label =
|
||||
enable_srtp_
|
||||
? std::string(ICON_FA_SHIELD_HALVED) + " " + props->remote_id_
|
||||
: props->remote_id_;
|
||||
if (ImGui::BeginTabItem(tab_label.c_str(), &props->tab_opened_)) {
|
||||
props->tab_selected_ = true;
|
||||
ImGui::SetWindowFontScale(0.6f);
|
||||
@@ -305,4 +308,4 @@ int GuiApplication::StreamWindow() {
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
} // namespace crossdesk
|
||||
@@ -3,13 +3,6 @@
|
||||
namespace crossdesk {
|
||||
|
||||
void GuiRuntime::ResetRemoteServiceStatus(RemoteSession& props) {
|
||||
{
|
||||
std::lock_guard lock(props.privacy_status_mutex_);
|
||||
props.privacy_status_ = {};
|
||||
props.privacy_status_received_ = false;
|
||||
props.privacy_command_pending_ = false;
|
||||
props.privacy_status_tick_ = 0;
|
||||
}
|
||||
props.remote_service_status_received_ = false;
|
||||
props.remote_service_available_ = false;
|
||||
props.remote_interactive_stage_.clear();
|
||||
|
||||
@@ -1,244 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-13
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_BACKEND_LINUX_H_
|
||||
#define _PRIVACY_BACKEND_LINUX_H_
|
||||
|
||||
#include <poll.h>
|
||||
#include <spawn.h>
|
||||
#include <sys/eventfd.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
#include "privacy_backend.h"
|
||||
#include "privacy_guard.h"
|
||||
|
||||
extern char** environ;
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
|
||||
class LinuxPrivacyBackend final : public PrivacyBackend {
|
||||
public:
|
||||
LinuxPrivacyBackend() : wake_(eventfd(0, EFD_CLOEXEC | EFD_NONBLOCK)) {}
|
||||
~LinuxPrivacyBackend() override {
|
||||
// EOF also recovers after a crash, SIGKILL, or a normal shutdown. Never
|
||||
// terminate the guard: it must finish restoring the saved display ramps.
|
||||
if (socket_ >= 0) close(socket_);
|
||||
if (wake_ >= 0) close(wake_);
|
||||
if (child_ > 0) {
|
||||
const pid_t child = child_;
|
||||
std::thread([child] {
|
||||
while (waitpid(child, nullptr, 0) < 0 && errno == EINTR) {
|
||||
}
|
||||
}).detach();
|
||||
}
|
||||
}
|
||||
|
||||
PrivacyCapabilities Query() override {
|
||||
if (wake_ < 0)
|
||||
return {false, false, "Cannot create Linux privacy wake event"};
|
||||
if (!StartGuard()) return {false, false, failure_};
|
||||
if (!Send(LinuxPrivacyCommand::query)) return {false, false, failure_};
|
||||
// The first query also serves automatic privacy on the first connection.
|
||||
// Bound process startup so an unavailable helper cannot stall the GUI.
|
||||
if (!discovered_) {
|
||||
pollfd fd{socket_, POLLIN, 0};
|
||||
poll(&fd, 1, 1000);
|
||||
}
|
||||
ReadReplies();
|
||||
return {reply_.supported, reply_.input_supported,
|
||||
reply_.capability_reason[0] ? reply_.capability_reason
|
||||
: "Checking Linux privacy support"};
|
||||
}
|
||||
|
||||
bool Enable(bool block_input, const PrivacyScreenText&,
|
||||
std::string& error) override {
|
||||
failure_.clear();
|
||||
emergency_ = false;
|
||||
if (!Send(block_input ? LinuxPrivacyCommand::enable_blocking
|
||||
: LinuxPrivacyCommand::enable)) {
|
||||
error = failure_;
|
||||
return false;
|
||||
}
|
||||
engaged_ = true;
|
||||
++generation_;
|
||||
last_reply_ = Clock::now();
|
||||
recovered_ = false;
|
||||
reply_.active = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Recover() override {
|
||||
ReadReplies();
|
||||
if (recovered_) {
|
||||
failure_.clear();
|
||||
return;
|
||||
}
|
||||
if (recovering_) return;
|
||||
if (Send(LinuxPrivacyCommand::recover)) recovering_ = true;
|
||||
}
|
||||
bool IsRecovered() const override { return recovered_; }
|
||||
bool RecoveryPending() const override { return recovering_; }
|
||||
|
||||
PrivacyHealth Poll() override {
|
||||
ReadReplies();
|
||||
if (engaged_ && Clock::now() - last_reply_ > std::chrono::seconds(1))
|
||||
failure_ = "Linux privacy guard stopped responding";
|
||||
if (engaged_ &&
|
||||
Clock::now() - last_heartbeat_ >= std::chrono::milliseconds(100)) {
|
||||
Send(LinuxPrivacyCommand::heartbeat);
|
||||
last_heartbeat_ = Clock::now();
|
||||
}
|
||||
const bool emergency = emergency_;
|
||||
emergency_ = false;
|
||||
return {failure_, emergency, reply_.active};
|
||||
}
|
||||
|
||||
void WaitForEvents(unsigned timeout_ms) override {
|
||||
pollfd fds[] = {{wake_, POLLIN, 0}, {socket_, POLLIN, 0}};
|
||||
poll(fds, 2, static_cast<int>(timeout_ms));
|
||||
uint64_t value;
|
||||
if (fds[0].revents & POLLIN) {
|
||||
while (read(wake_, &value, sizeof(value)) == sizeof(value)) {
|
||||
}
|
||||
}
|
||||
}
|
||||
void Wake() override {
|
||||
const uint64_t value = 1;
|
||||
if (wake_ >= 0) {
|
||||
const auto ignored = write(wake_, &value, sizeof(value));
|
||||
(void)ignored;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
bool StartGuard() {
|
||||
if (socket_ >= 0) return true;
|
||||
if (child_ > 0) {
|
||||
const auto result = waitpid(child_, nullptr, WNOHANG);
|
||||
if (result == 0) return false;
|
||||
child_ = -1;
|
||||
}
|
||||
int sockets[2];
|
||||
if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC | SOCK_NONBLOCK, 0,
|
||||
sockets) != 0) {
|
||||
failure_ = "Cannot create Linux privacy guard socket";
|
||||
return false;
|
||||
}
|
||||
posix_spawn_file_actions_t actions;
|
||||
int result = posix_spawn_file_actions_init(&actions);
|
||||
if (result == 0) {
|
||||
// Close the parent end first: it may occupy the guard's destination fd.
|
||||
result = posix_spawn_file_actions_addclose(&actions, sockets[0]);
|
||||
if (result == 0)
|
||||
result = posix_spawn_file_actions_adddup2(&actions, sockets[1],
|
||||
kLinuxPrivacyGuardSocket);
|
||||
if (result == 0 && sockets[1] != kLinuxPrivacyGuardSocket)
|
||||
result = posix_spawn_file_actions_addclose(&actions, sockets[1]);
|
||||
if (result == 0) {
|
||||
char executable[] = "/proc/self/exe";
|
||||
char* args[] = {executable,
|
||||
const_cast<char*>(kLinuxPrivacyGuardArgument), nullptr};
|
||||
result =
|
||||
posix_spawn(&child_, executable, &actions, nullptr, args, environ);
|
||||
}
|
||||
posix_spawn_file_actions_destroy(&actions);
|
||||
}
|
||||
close(sockets[1]);
|
||||
if (result != 0) {
|
||||
close(sockets[0]);
|
||||
child_ = -1;
|
||||
failure_ = std::string("Cannot start Linux privacy guard: ") +
|
||||
std::strerror(result);
|
||||
return false;
|
||||
}
|
||||
socket_ = sockets[0];
|
||||
reply_ = {};
|
||||
discovered_ = false;
|
||||
generation_ = 0;
|
||||
failure_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
void LostGuard() {
|
||||
if (socket_ >= 0) close(socket_);
|
||||
socket_ = -1;
|
||||
reply_ = {};
|
||||
recovering_ = false;
|
||||
failure_ =
|
||||
"Linux privacy guard disconnected; local protection is unavailable";
|
||||
// Do not claim successful recovery after an unexpected guard failure.
|
||||
// A normal guard reports restoration before closing its socket.
|
||||
}
|
||||
bool Send(LinuxPrivacyCommand command) {
|
||||
if (socket_ < 0) return false;
|
||||
if (send(socket_, &command, sizeof(command), MSG_NOSIGNAL) ==
|
||||
sizeof(command))
|
||||
return true;
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) LostGuard();
|
||||
return false;
|
||||
}
|
||||
void ReadReplies() {
|
||||
if (socket_ < 0) return;
|
||||
for (;;) {
|
||||
LinuxPrivacyReply next;
|
||||
const auto bytes = recv(socket_, &next, sizeof(next), 0);
|
||||
if (bytes < 0 &&
|
||||
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
|
||||
return;
|
||||
if (bytes != sizeof(next)) {
|
||||
LostGuard();
|
||||
return;
|
||||
}
|
||||
next.capability_reason[sizeof(next.capability_reason) - 1] = '\0';
|
||||
next.failure[sizeof(next.failure) - 1] = '\0';
|
||||
discovered_ = true;
|
||||
// Ignore queued capability/heartbeat replies from before this enable.
|
||||
// A matching generation can also acknowledge watchdog-driven recovery.
|
||||
if (next.generation != generation_) continue;
|
||||
reply_ = next;
|
||||
last_reply_ = Clock::now();
|
||||
emergency_ = emergency_ || next.emergency;
|
||||
if (next.failure[0]) failure_ = next.failure;
|
||||
recovered_ = next.recovered;
|
||||
if (next.recovered) {
|
||||
recovering_ = false;
|
||||
engaged_ = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int wake_ = -1;
|
||||
int socket_ = -1;
|
||||
pid_t child_ = -1;
|
||||
LinuxPrivacyReply reply_;
|
||||
uint64_t generation_ = 0;
|
||||
bool engaged_ = false;
|
||||
bool discovered_ = false;
|
||||
bool recovering_ = false;
|
||||
bool recovered_ = true;
|
||||
bool emergency_ = false;
|
||||
std::string failure_;
|
||||
Clock::time_point last_heartbeat_{};
|
||||
Clock::time_point last_reply_{};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<PrivacyBackend> CreateLinuxPrivacyBackend() {
|
||||
return std::make_unique<LinuxPrivacyBackend>();
|
||||
}
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,721 +0,0 @@
|
||||
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
|
||||
#include "privacy_guard.h"
|
||||
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/Xatom.h>
|
||||
#include <X11/extensions/XInput.h>
|
||||
#include <X11/extensions/XInput2.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <X11/extensions/Xfixes.h>
|
||||
#include <X11/extensions/Xrandr.h>
|
||||
#include <X11/keysym.h>
|
||||
#include <fcntl.h>
|
||||
#include <poll.h>
|
||||
#include <sys/socket.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <csignal>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
using Gamma = std::unique_ptr<XRRCrtcGamma, decltype(&XRRFreeGamma)>;
|
||||
using Resources =
|
||||
std::unique_ptr<XRRScreenResources, decltype(&XRRFreeScreenResources)>;
|
||||
using CrtcInfo = std::unique_ptr<XRRCrtcInfo, decltype(&XRRFreeCrtcInfo)>;
|
||||
volatile std::sig_atomic_t stopping = 0;
|
||||
int x_error = 0;
|
||||
|
||||
void StopGuard(int) { stopping = 1; }
|
||||
int OnXError(Display*, XErrorEvent* event) {
|
||||
x_error = event->error_code;
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <size_t N>
|
||||
void SetText(char (&dest)[N], const std::string& text) {
|
||||
std::strncpy(dest, text.c_str(), N - 1);
|
||||
dest[N - 1] = '\0';
|
||||
}
|
||||
|
||||
struct Output {
|
||||
Window root = 0;
|
||||
RRCrtc crtc = 0;
|
||||
RRMode mode = 0;
|
||||
int x = 0, y = 0;
|
||||
unsigned width = 0, height = 0;
|
||||
Rotation rotation = 0;
|
||||
std::vector<RROutput> connectors;
|
||||
Gamma saved{nullptr, XRRFreeGamma};
|
||||
|
||||
bool SameLayout(const Output& other) const {
|
||||
return std::tie(root, crtc, mode, x, y, width, height, rotation,
|
||||
connectors) == std::tie(other.root, other.crtc, other.mode,
|
||||
other.x, other.y, other.width,
|
||||
other.height, other.rotation,
|
||||
other.connectors);
|
||||
}
|
||||
};
|
||||
|
||||
struct Device {
|
||||
int id = 0, use = 0, attachment = 0;
|
||||
bool keyboard = false, touch = false;
|
||||
std::string name;
|
||||
|
||||
bool operator==(const Device& other) const {
|
||||
return std::tie(id, use, attachment, keyboard, touch, name) ==
|
||||
std::tie(other.id, other.use, other.attachment, other.keyboard,
|
||||
other.touch, other.name);
|
||||
}
|
||||
};
|
||||
|
||||
bool IsBlack(const XRRCrtcGamma* gamma) {
|
||||
if (!gamma || gamma->size <= 0) return false;
|
||||
for (int i = 0; i < gamma->size; ++i)
|
||||
if (gamma->red[i] || gamma->green[i] || gamma->blue[i]) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// This class lives only in the exec'd guard process. Its Xlib error handler is
|
||||
// isolated from the GUI and capture threads' process-global error handlers.
|
||||
class NativePrivacy {
|
||||
public:
|
||||
~NativePrivacy() {
|
||||
Recover();
|
||||
if (display_) XCloseDisplay(display_);
|
||||
}
|
||||
|
||||
void Query() {
|
||||
reply_.supported = reply_.input_supported = false;
|
||||
std::string error;
|
||||
if (!Initialize(error)) {
|
||||
SetText(reply_.capability_reason, error);
|
||||
return;
|
||||
}
|
||||
std::vector<Output> outputs;
|
||||
std::vector<Device> devices;
|
||||
if (!ReadOutputs(outputs, true, error) || !ReadDevices(devices, error)) {
|
||||
SetText(reply_.capability_reason, error);
|
||||
return;
|
||||
}
|
||||
const bool keyboard = std::any_of(devices.begin(), devices.end(),
|
||||
[](const auto& d) { return d.keyboard; });
|
||||
if (!keyboard) {
|
||||
SetText(reply_.capability_reason,
|
||||
"No local keyboard available for privacy recovery");
|
||||
return;
|
||||
}
|
||||
reply_.supported = true;
|
||||
// XIGrabDevice does not suppress XI 2.2 touch delivery. Do not advertise
|
||||
// input protection for touchscreens without a separate touch grab.
|
||||
reply_.input_supported = std::none_of(
|
||||
devices.begin(), devices.end(), [](const auto& d) { return d.touch; });
|
||||
SetText(reply_.capability_reason,
|
||||
reply_.input_supported
|
||||
? "X11 privacy black screen available; local recovery: "
|
||||
"Ctrl+Alt+Shift+F12"
|
||||
: "X11 privacy black screen available; touchscreen input "
|
||||
"blocking is unavailable");
|
||||
}
|
||||
|
||||
void Enable(bool block_input) {
|
||||
++reply_.generation;
|
||||
reply_.failure[0] = '\0';
|
||||
reply_.emergency = false;
|
||||
if (!reply_.recovered) {
|
||||
Fail("Linux privacy resources are still in use");
|
||||
return;
|
||||
}
|
||||
Query();
|
||||
if (!reply_.supported || (block_input && !reply_.input_supported)) {
|
||||
Fail(reply_.capability_reason);
|
||||
return;
|
||||
}
|
||||
std::string error;
|
||||
keys_.clear();
|
||||
if (!Acquire(error) || !ReadOutputs(outputs_, true, error) ||
|
||||
!ReadDevices(devices_, error) || !ProtectInput(block_input, error)) {
|
||||
Fail(error);
|
||||
return;
|
||||
}
|
||||
// No display ramp is changed until every original ramp has been saved.
|
||||
// Gamma is applied at scanout, after XGetImage/DRM framebuffer capture.
|
||||
// There is deliberately no cover window to contaminate remote frames.
|
||||
x_error = 0;
|
||||
for (const auto& output : outputs_) {
|
||||
Gamma black(XRRAllocGamma(output.saved->size), XRRFreeGamma);
|
||||
if (!black) {
|
||||
Fail("Cannot allocate Linux privacy display ramp");
|
||||
return;
|
||||
}
|
||||
std::fill_n(black->red, black->size, 0);
|
||||
std::fill_n(black->green, black->size, 0);
|
||||
std::fill_n(black->blue, black->size, 0);
|
||||
XRRSetCrtcGamma(display_, output.crtc, black.get());
|
||||
}
|
||||
for (int i = 0; i < ScreenCount(display_); ++i) {
|
||||
XFixesHideCursor(display_, RootWindow(display_, i));
|
||||
hidden_roots_.push_back(RootWindow(display_, i));
|
||||
}
|
||||
XSync(display_, False);
|
||||
if (x_error) {
|
||||
Fail("Cannot blank Linux displays or hide the local cursor");
|
||||
return;
|
||||
}
|
||||
// Active slave grabs temporarily detach devices. Snapshot after grabbing,
|
||||
// and monitor subsequent hardware/hierarchy changes during protection.
|
||||
if (!ReadDevices(devices_, error)) {
|
||||
Fail(error);
|
||||
return;
|
||||
}
|
||||
reply_.active = true;
|
||||
Check();
|
||||
}
|
||||
|
||||
void Recover() {
|
||||
reply_.active = false;
|
||||
if (!display_) {
|
||||
reply_.recovered = true;
|
||||
return;
|
||||
}
|
||||
for (int id : grabbed_) XIUngrabDevice(display_, id, CurrentTime);
|
||||
grabbed_.clear();
|
||||
for (Window root : hidden_roots_) XFixesShowCursor(display_, root);
|
||||
hidden_roots_.clear();
|
||||
// Keep failed restorations for retry. Removed CRTCs no longer own a local
|
||||
// output; all still-present CRTCs must acknowledge the original ramp.
|
||||
for (auto it = outputs_.begin(); it != outputs_.end();) {
|
||||
x_error = 0;
|
||||
Resources resources(XRRGetScreenResourcesCurrent(display_, it->root),
|
||||
XRRFreeScreenResources);
|
||||
if (!resources) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
const bool exists =
|
||||
std::find(resources->crtcs, resources->crtcs + resources->ncrtc,
|
||||
it->crtc) != resources->crtcs + resources->ncrtc;
|
||||
if (exists && it->saved) {
|
||||
XRRSetCrtcGamma(display_, it->crtc, it->saved.get());
|
||||
XSync(display_, False);
|
||||
if (x_error) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
it = outputs_.erase(it);
|
||||
}
|
||||
keys_.clear();
|
||||
if (outputs_.empty() && owner_) {
|
||||
XDestroyWindow(display_, owner_);
|
||||
owner_ = 0;
|
||||
}
|
||||
XSync(display_, False);
|
||||
reply_.recovered = outputs_.empty() && !owner_;
|
||||
}
|
||||
|
||||
void Fail(const std::string& reason) {
|
||||
SetText(reply_.failure, reason);
|
||||
Recover();
|
||||
}
|
||||
|
||||
void Poll() {
|
||||
if (!display_) return;
|
||||
while (XPending(display_)) {
|
||||
XEvent event{};
|
||||
XNextEvent(display_, &event);
|
||||
if (event.type != GenericEvent || event.xcookie.extension != xi_opcode_ ||
|
||||
!XGetEventData(display_, &event.xcookie))
|
||||
continue;
|
||||
if (reply_.active) {
|
||||
const int type = event.xcookie.evtype;
|
||||
if (type == XI_KeyPress || type == XI_KeyRelease) {
|
||||
const auto* key = static_cast<XIDeviceEvent*>(event.xcookie.data);
|
||||
Key(key->sourceid, key->detail, type == XI_KeyPress);
|
||||
} else if (!blocking_ &&
|
||||
(type == XI_RawKeyPress || type == XI_RawKeyRelease)) {
|
||||
const auto* key = static_cast<XIRawEvent*>(event.xcookie.data);
|
||||
Key(key->sourceid, key->detail, type == XI_RawKeyPress);
|
||||
}
|
||||
}
|
||||
XFreeEventData(display_, &event.xcookie);
|
||||
}
|
||||
if (reply_.active &&
|
||||
Clock::now() - checked_ >= std::chrono::milliseconds(100))
|
||||
Check();
|
||||
if (!reply_.active && !reply_.recovered) Recover();
|
||||
}
|
||||
|
||||
const LinuxPrivacyReply& Reply() const { return reply_; }
|
||||
int Connection() const { return display_ ? ConnectionNumber(display_) : -1; }
|
||||
void AcknowledgeEmergency() { reply_.emergency = false; }
|
||||
|
||||
private:
|
||||
bool Initialize(std::string& error) {
|
||||
if (initialized_) return true;
|
||||
const char* session = std::getenv("XDG_SESSION_TYPE");
|
||||
const char* wayland = std::getenv("WAYLAND_DISPLAY");
|
||||
if ((session && std::strcmp(session, "wayland") == 0) ||
|
||||
(wayland && *wayland)) {
|
||||
error =
|
||||
"Linux privacy screen requires an X11 session; Wayland is "
|
||||
"unsupported";
|
||||
return false;
|
||||
}
|
||||
const char* address = std::getenv("DISPLAY");
|
||||
if (!address ||
|
||||
(*address != ':' && std::strncmp(address, "unix:", 5) != 0)) {
|
||||
error = "Linux privacy screen requires a local X11 display";
|
||||
return false;
|
||||
}
|
||||
if (!display_) display_ = XOpenDisplay(nullptr);
|
||||
if (!display_) {
|
||||
error = "Cannot open the local X11 display";
|
||||
return false;
|
||||
}
|
||||
int event = 0, code = 0, opcode = 0;
|
||||
int major = 1, minor = 3;
|
||||
if (XQueryExtension(display_, "XWAYLAND", &opcode, &event, &code)) {
|
||||
error = "XWayland cannot protect native Wayland displays";
|
||||
return false;
|
||||
}
|
||||
if (!XRRQueryExtension(display_, &event, &code) ||
|
||||
!XRRQueryVersion(display_, &major, &minor) || major < 1 ||
|
||||
(major == 1 && minor < 3)) {
|
||||
error = "Linux privacy screen requires RandR 1.3 display gamma control";
|
||||
return false;
|
||||
}
|
||||
major = 4;
|
||||
minor = 0;
|
||||
if (!XFixesQueryExtension(display_, &event, &code) ||
|
||||
!XFixesQueryVersion(display_, &major, &minor) || major < 4) {
|
||||
error = "Linux privacy screen requires XFixes 4 cursor hiding";
|
||||
return false;
|
||||
}
|
||||
major = 2;
|
||||
minor = 2;
|
||||
if (!XQueryExtension(display_, "XInputExtension", &xi_opcode_, &event,
|
||||
&code) ||
|
||||
XIQueryVersion(display_, &major, &minor) != Success || major < 2) {
|
||||
error = "Linux privacy screen requires XInput 2 local input monitoring";
|
||||
return false;
|
||||
}
|
||||
xtest_property_ = XInternAtom(display_, "XTEST Device", True);
|
||||
if (!xtest_property_ ||
|
||||
!XTestQueryExtension(display_, &event, &code, &major, &minor)) {
|
||||
error = "Cannot identify X11 remote input devices";
|
||||
return false;
|
||||
}
|
||||
selection_ = XInternAtom(display_, "_CROSSDESK_PRIVACY_GUARD", False);
|
||||
std::array<unsigned char, XIMaskLen(XI_LASTEVENT)> bits{};
|
||||
XISetMask(bits.data(), XI_RawKeyPress);
|
||||
XISetMask(bits.data(), XI_RawKeyRelease);
|
||||
XIEventMask mask{XIAllDevices, static_cast<int>(bits.size()), bits.data()};
|
||||
x_error = 0;
|
||||
for (int i = 0; i < ScreenCount(display_); ++i)
|
||||
XISelectEvents(display_, RootWindow(display_, i), &mask, 1);
|
||||
XSync(display_, False);
|
||||
if (x_error) {
|
||||
error = "Cannot monitor the local privacy recovery shortcut";
|
||||
return false;
|
||||
}
|
||||
initialized_ = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadOutputs(std::vector<Output>& outputs, bool save,
|
||||
std::string& error) {
|
||||
outputs.clear();
|
||||
x_error = 0;
|
||||
for (int screen = 0; screen < ScreenCount(display_); ++screen) {
|
||||
const Window root = RootWindow(display_, screen);
|
||||
Resources resources(XRRGetScreenResourcesCurrent(display_, root),
|
||||
XRRFreeScreenResources);
|
||||
if (!resources) {
|
||||
error = "Cannot enumerate Linux display outputs";
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < resources->ncrtc; ++i) {
|
||||
CrtcInfo info(
|
||||
XRRGetCrtcInfo(display_, resources.get(), resources->crtcs[i]),
|
||||
XRRFreeCrtcInfo);
|
||||
if (!info) {
|
||||
error = "Cannot inspect Linux display controller";
|
||||
return false;
|
||||
}
|
||||
if (!info->mode || !info->noutput) continue;
|
||||
Output output;
|
||||
output.root = root;
|
||||
output.crtc = resources->crtcs[i];
|
||||
output.mode = info->mode;
|
||||
output.x = info->x;
|
||||
output.y = info->y;
|
||||
output.width = info->width;
|
||||
output.height = info->height;
|
||||
output.rotation = info->rotation;
|
||||
output.connectors.assign(info->outputs, info->outputs + info->noutput);
|
||||
std::sort(output.connectors.begin(), output.connectors.end());
|
||||
if (save) {
|
||||
output.saved.reset(XRRGetCrtcGamma(display_, output.crtc));
|
||||
if (!output.saved || output.saved->size <= 0 ||
|
||||
IsBlack(output.saved.get())) {
|
||||
error =
|
||||
"Display gamma control is unavailable or the display is "
|
||||
"already blank";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
outputs.push_back(std::move(output));
|
||||
}
|
||||
}
|
||||
XSync(display_, False);
|
||||
if (x_error || outputs.empty()) {
|
||||
error = "No controllable Linux display outputs";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReadDevices(std::vector<Device>& devices, std::string& error) {
|
||||
devices.clear();
|
||||
x_error = 0;
|
||||
int count = 0;
|
||||
XIDeviceInfo* info = XIQueryDevice(display_, XIAllDevices, &count);
|
||||
if (!info) {
|
||||
error = "Cannot enumerate local X11 input devices";
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const auto& d = info[i];
|
||||
if (!d.enabled || d.use == XIMasterPointer || d.use == XIMasterKeyboard)
|
||||
continue;
|
||||
Atom type = None;
|
||||
int format = 0;
|
||||
unsigned long items = 0, remaining = 0;
|
||||
unsigned char* value = nullptr;
|
||||
const int result =
|
||||
XIGetProperty(display_, d.deviceid, xtest_property_, 0, 1, False,
|
||||
XA_INTEGER, &type, &format, &items, &remaining, &value);
|
||||
const bool injected = result == Success && type == XA_INTEGER &&
|
||||
format == 8 && items == 1 && value && value[0] == 1;
|
||||
if (value) XFree(value);
|
||||
if (injected) continue;
|
||||
Device device;
|
||||
device.id = d.deviceid;
|
||||
device.use = d.use;
|
||||
device.attachment = d.attachment;
|
||||
device.name = d.name ? d.name : "";
|
||||
for (int c = 0; c < d.num_classes; ++c) {
|
||||
device.keyboard |= d.classes[c]->type == XIKeyClass;
|
||||
device.touch |= d.classes[c]->type == XITouchClass;
|
||||
}
|
||||
devices.push_back(std::move(device));
|
||||
}
|
||||
XIFreeDeviceInfo(info);
|
||||
XSync(display_, False);
|
||||
if (x_error) {
|
||||
error = "Cannot verify local X11 input devices";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Acquire(std::string& error) {
|
||||
// A second instance must not save the first instance's black ramps as its
|
||||
// originals. Selection ownership and creation are atomic on this server.
|
||||
XGrabServer(display_);
|
||||
if (XGetSelectionOwner(display_, selection_) == None) {
|
||||
owner_ = XCreateSimpleWindow(display_, DefaultRootWindow(display_), 0, 0,
|
||||
1, 1, 0, 0, 0);
|
||||
XSetSelectionOwner(display_, selection_, owner_, CurrentTime);
|
||||
}
|
||||
XUngrabServer(display_);
|
||||
XSync(display_, False);
|
||||
if (!owner_ || XGetSelectionOwner(display_, selection_) != owner_) {
|
||||
error = "Another CrossDesk instance owns Linux privacy protection";
|
||||
return false;
|
||||
}
|
||||
reply_.recovered = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ProtectInput(bool block_input, std::string& error) {
|
||||
blocking_ = block_input;
|
||||
if (!block_input) return true;
|
||||
for (const auto& device : devices_) {
|
||||
if (device.touch) {
|
||||
error = "Touchscreen privacy input blocking is unavailable";
|
||||
return false;
|
||||
}
|
||||
if (device.keyboard) {
|
||||
// Preserve held local modifiers for the emergency chord independently
|
||||
// of the releases sent to the master keyboard below.
|
||||
XDevice* keyboard = XOpenDevice(display_, device.id);
|
||||
XDeviceState* state =
|
||||
keyboard ? XQueryDeviceState(display_, keyboard) : nullptr;
|
||||
if (!state) {
|
||||
if (keyboard) XCloseDevice(display_, keyboard);
|
||||
error = "Cannot inspect held local keyboard input";
|
||||
return false;
|
||||
}
|
||||
XInputClass* input = state->data;
|
||||
for (int i = 0; i < state->num_classes; ++i) {
|
||||
if (input->c_class == KeyClass) {
|
||||
const auto* keys = reinterpret_cast<XKeyState*>(input);
|
||||
for (int key = 0; key < 256; ++key)
|
||||
keys_[device.id][key] = keys->keys[key / 8] & (1 << (key % 8));
|
||||
}
|
||||
input = reinterpret_cast<XInputClass*>(
|
||||
reinterpret_cast<char*>(input) + input->length);
|
||||
}
|
||||
XFreeDeviceState(state);
|
||||
XCloseDevice(display_, keyboard);
|
||||
}
|
||||
std::array<unsigned char, XIMaskLen(XI_LASTEVENT)> bits{};
|
||||
for (int type : {XI_KeyPress, XI_KeyRelease, XI_ButtonPress,
|
||||
XI_ButtonRelease, XI_Motion})
|
||||
XISetMask(bits.data(), type);
|
||||
XIEventMask mask{device.id, static_cast<int>(bits.size()), bits.data()};
|
||||
const int result = XIGrabDevice(
|
||||
display_, device.id, DefaultRootWindow(display_), CurrentTime, None,
|
||||
GrabModeAsync, GrabModeAsync, False, &mask);
|
||||
if (result != GrabSuccess) {
|
||||
error = "Cannot grab every local X11 input device";
|
||||
return false;
|
||||
}
|
||||
grabbed_.push_back(device.id);
|
||||
}
|
||||
return ReleaseHeldInput(error);
|
||||
}
|
||||
|
||||
bool ReleaseHeldInput(std::string& error) {
|
||||
// Slave grabs float physical devices, but their previous presses can
|
||||
// remain on the master. Release them after all grabs are installed so a
|
||||
// held local Ctrl/button cannot alter the remote controller's next input.
|
||||
int original_pointer = 0, count = 0;
|
||||
XIGetClientPointer(display_, None, &original_pointer);
|
||||
XIDeviceInfo* masters = XIQueryDevice(display_, XIAllMasterDevices, &count);
|
||||
if (!masters) {
|
||||
error = "Cannot inspect held X11 input";
|
||||
return false;
|
||||
}
|
||||
x_error = 0;
|
||||
bool ok = true;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (masters[i].use != XIMasterPointer || !masters[i].enabled) continue;
|
||||
XISetClientPointer(display_, None, masters[i].deviceid);
|
||||
char keys[32]{};
|
||||
XQueryKeymap(display_, keys);
|
||||
for (int key = 8; key < 256; ++key)
|
||||
if (keys[key / 8] & (1 << (key % 8)))
|
||||
ok &= XTestFakeKeyEvent(display_, key, False, CurrentTime) != 0;
|
||||
Window root = None, child = None;
|
||||
double root_x = 0, root_y = 0, win_x = 0, win_y = 0;
|
||||
XIButtonState buttons{};
|
||||
XIModifierState modifiers{};
|
||||
XIGroupState group{};
|
||||
XIQueryPointer(display_, masters[i].deviceid, DefaultRootWindow(display_),
|
||||
&root, &child, &root_x, &root_y, &win_x, &win_y, &buttons,
|
||||
&modifiers, &group);
|
||||
for (int button = 1; button < buttons.mask_len * 8; ++button)
|
||||
if (XIMaskIsSet(buttons.mask, button))
|
||||
ok &= XTestFakeButtonEvent(display_, button, False, CurrentTime) != 0;
|
||||
if (buttons.mask) XFree(buttons.mask);
|
||||
XSync(display_, False);
|
||||
XQueryKeymap(display_, keys);
|
||||
for (char pressed : keys) ok &= pressed == 0;
|
||||
buttons = {};
|
||||
XIQueryPointer(display_, masters[i].deviceid, DefaultRootWindow(display_),
|
||||
&root, &child, &root_x, &root_y, &win_x, &win_y, &buttons,
|
||||
&modifiers, &group);
|
||||
for (int byte = 0; byte < buttons.mask_len; ++byte)
|
||||
ok &= buttons.mask[byte] == 0;
|
||||
if (buttons.mask) XFree(buttons.mask);
|
||||
}
|
||||
XIFreeDeviceInfo(masters);
|
||||
if (original_pointer) XISetClientPointer(display_, None, original_pointer);
|
||||
XSync(display_, False);
|
||||
if (!ok || x_error) {
|
||||
error = "Cannot release held X11 input for privacy";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Key(int source, int keycode, bool down) {
|
||||
if (keycode < 0 || keycode >= 256 ||
|
||||
std::none_of(devices_.begin(), devices_.end(), [source](const auto& d) {
|
||||
return d.id == source && d.keyboard;
|
||||
}))
|
||||
return;
|
||||
keys_[source][keycode] = down;
|
||||
bool ctrl = false, alt = false, shift = false;
|
||||
for (const auto& [id, keys] : keys_) {
|
||||
for (int code = 0; code < 256; ++code) {
|
||||
if (!keys[code]) continue;
|
||||
const KeySym symbol = XkbKeycodeToKeysym(display_, code, 0, 0);
|
||||
ctrl |= symbol == XK_Control_L || symbol == XK_Control_R;
|
||||
alt |= symbol == XK_Alt_L || symbol == XK_Alt_R;
|
||||
shift |= symbol == XK_Shift_L || symbol == XK_Shift_R;
|
||||
}
|
||||
}
|
||||
if (down && ctrl && alt && shift &&
|
||||
XkbKeycodeToKeysym(display_, keycode, 0, 0) == XK_F12) {
|
||||
reply_.emergency = true;
|
||||
Recover();
|
||||
}
|
||||
}
|
||||
|
||||
void Check() {
|
||||
checked_ = Clock::now();
|
||||
std::string error;
|
||||
std::vector<Output> current;
|
||||
std::vector<Device> devices;
|
||||
if (!ReadOutputs(current, false, error) || !ReadDevices(devices, error)) {
|
||||
Fail(error);
|
||||
return;
|
||||
}
|
||||
if (current.size() != outputs_.size() ||
|
||||
!std::equal(
|
||||
current.begin(), current.end(), outputs_.begin(),
|
||||
[](const auto& a, const auto& b) { return a.SameLayout(b); })) {
|
||||
Fail("Display topology changed; Linux privacy has been turned off");
|
||||
return;
|
||||
}
|
||||
if (devices != devices_) {
|
||||
Fail("Local input devices changed; Linux privacy has been turned off");
|
||||
return;
|
||||
}
|
||||
x_error = 0;
|
||||
for (const auto& output : outputs_) {
|
||||
Gamma ramp(XRRGetCrtcGamma(display_, output.crtc), XRRFreeGamma);
|
||||
if (!IsBlack(ramp.get())) {
|
||||
Fail(
|
||||
"Display gamma protection was changed; Linux privacy is "
|
||||
"unavailable");
|
||||
return;
|
||||
}
|
||||
}
|
||||
XSync(display_, False);
|
||||
if (x_error || XGetSelectionOwner(display_, selection_) != owner_)
|
||||
Fail("Cannot verify Linux privacy protection");
|
||||
}
|
||||
|
||||
Display* display_ = nullptr;
|
||||
bool initialized_ = false;
|
||||
bool blocking_ = false;
|
||||
int xi_opcode_ = 0;
|
||||
Atom xtest_property_ = None, selection_ = None;
|
||||
Window owner_ = 0;
|
||||
std::vector<Output> outputs_;
|
||||
std::vector<Device> devices_;
|
||||
std::vector<int> grabbed_;
|
||||
std::vector<Window> hidden_roots_;
|
||||
std::map<int, std::array<bool, 256>> keys_;
|
||||
LinuxPrivacyReply reply_;
|
||||
Clock::time_point checked_{};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
int RunLinuxPrivacyGuard() {
|
||||
const int socket = kLinuxPrivacyGuardSocket;
|
||||
int type = 0;
|
||||
socklen_t length = sizeof(type);
|
||||
if (getsockopt(socket, SOL_SOCKET, SO_TYPE, &type, &length) != 0 ||
|
||||
type != SOCK_SEQPACKET)
|
||||
return 1;
|
||||
fcntl(socket, F_SETFD, FD_CLOEXEC);
|
||||
fcntl(socket, F_SETFL, O_NONBLOCK);
|
||||
struct sigaction action {};
|
||||
action.sa_handler = StopGuard;
|
||||
sigemptyset(&action.sa_mask);
|
||||
sigaction(SIGTERM, &action, nullptr);
|
||||
sigaction(SIGINT, &action, nullptr);
|
||||
sigaction(SIGHUP, &action, nullptr);
|
||||
XSetErrorHandler(OnXError);
|
||||
NativePrivacy privacy;
|
||||
auto heartbeat = Clock::now();
|
||||
bool connected = true;
|
||||
bool dirty = false;
|
||||
while (connected && !stopping) {
|
||||
pollfd fds[] = {{socket, POLLIN, 0}, {privacy.Connection(), POLLIN, 0}};
|
||||
poll(fds, 2, privacy.Reply().active ? 25 : 250);
|
||||
if (fds[0].revents & (POLLERR | POLLHUP | POLLNVAL)) break;
|
||||
for (int i = 0; i < 64; ++i) {
|
||||
LinuxPrivacyCommand command;
|
||||
const auto bytes = recv(socket, &command, sizeof(command), 0);
|
||||
if (bytes < 0 &&
|
||||
(errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR))
|
||||
break;
|
||||
if (bytes != sizeof(command)) {
|
||||
connected = false;
|
||||
break;
|
||||
}
|
||||
heartbeat = Clock::now();
|
||||
switch (command) {
|
||||
case LinuxPrivacyCommand::query:
|
||||
if (!privacy.Reply().active) privacy.Query();
|
||||
break;
|
||||
case LinuxPrivacyCommand::enable:
|
||||
privacy.Enable(false);
|
||||
break;
|
||||
case LinuxPrivacyCommand::enable_blocking:
|
||||
privacy.Enable(true);
|
||||
break;
|
||||
case LinuxPrivacyCommand::recover:
|
||||
privacy.Recover();
|
||||
break;
|
||||
case LinuxPrivacyCommand::heartbeat:
|
||||
break;
|
||||
default:
|
||||
connected = false;
|
||||
break;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
if (!connected || stopping) break;
|
||||
const auto before = privacy.Reply();
|
||||
privacy.Poll();
|
||||
if (privacy.Reply().active &&
|
||||
Clock::now() - heartbeat > std::chrono::seconds(3))
|
||||
privacy.Fail(
|
||||
"Privacy controller stopped responding; local desktop and input "
|
||||
"restored");
|
||||
const auto& after = privacy.Reply();
|
||||
dirty |= before.active != after.active ||
|
||||
before.recovered != after.recovered ||
|
||||
before.emergency != after.emergency ||
|
||||
std::strcmp(before.failure, after.failure) != 0;
|
||||
if (dirty) {
|
||||
const auto& reply = privacy.Reply();
|
||||
if (send(socket, &reply, sizeof(reply), MSG_NOSIGNAL) == sizeof(reply)) {
|
||||
dirty = false;
|
||||
privacy.AcknowledgeEmergency();
|
||||
} else if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Keep trying if the X server temporarily rejected restoration, even after
|
||||
// the parent exited. Disconnecting releases XI grabs and cursor hiding too.
|
||||
do {
|
||||
privacy.Recover();
|
||||
if (!privacy.Reply().recovered) poll(nullptr, 0, 250);
|
||||
} while (!privacy.Reply().recovered);
|
||||
const auto& reply = privacy.Reply();
|
||||
const auto ignored = send(socket, &reply, sizeof(reply), MSG_NOSIGNAL);
|
||||
(void)ignored;
|
||||
close(socket);
|
||||
return 0;
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-13
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_GUARD_H_
|
||||
#define _PRIVACY_GUARD_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
inline constexpr char kLinuxPrivacyGuardArgument[] = "--linux-privacy-guard";
|
||||
inline constexpr int kLinuxPrivacyGuardSocket = 3;
|
||||
|
||||
// Private protocol over an inherited SOCK_SEQPACKET socket. No listening
|
||||
// socket, shell, elevated privileges or separately installed helper is needed.
|
||||
enum class LinuxPrivacyCommand : uint8_t {
|
||||
query,
|
||||
enable,
|
||||
enable_blocking,
|
||||
recover,
|
||||
heartbeat,
|
||||
};
|
||||
|
||||
struct LinuxPrivacyReply {
|
||||
uint64_t generation = 0;
|
||||
bool supported = false;
|
||||
bool input_supported = false;
|
||||
bool active = false;
|
||||
bool recovered = true;
|
||||
bool emergency = false;
|
||||
char capability_reason[256]{};
|
||||
char failure[256]{};
|
||||
};
|
||||
|
||||
int RunLinuxPrivacyGuard();
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "platform.h"
|
||||
#include "privacy_controller.h"
|
||||
#include "rd_log.h"
|
||||
#if defined(CROSSDESK_HAS_DRM) && CROSSDESK_HAS_DRM
|
||||
#include "screen_capturer_drm.h"
|
||||
@@ -65,13 +64,6 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
return;
|
||||
}
|
||||
invalid_stream_id_logged_.store(false, std::memory_order_relaxed);
|
||||
{
|
||||
std::lock_guard lock(privacy_mutex_);
|
||||
if (privacy_ && !capture_paused_ && !capture_announced_) {
|
||||
capture_announced_ = true;
|
||||
privacy_->CaptureChanged(true);
|
||||
}
|
||||
}
|
||||
if (callback_orig_) {
|
||||
callback_orig_(data, size, width, height, mapped_stream_id.c_str(),
|
||||
native_frame);
|
||||
@@ -153,7 +145,6 @@ int ScreenCapturerLinux::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::Destroy() {
|
||||
Stop();
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
impl_.reset();
|
||||
@@ -178,11 +169,6 @@ int ScreenCapturerLinux::Start(bool show_cursor) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard lock(privacy_mutex_);
|
||||
capture_paused_ = false;
|
||||
}
|
||||
|
||||
// X11 output names, DRM connectors and Wayland stream handles may all change
|
||||
// after an HDMI hotplug, so rebuild the current backend before each session.
|
||||
const int refresh_ret = RefreshCurrentBackend();
|
||||
@@ -240,11 +226,6 @@ int ScreenCapturerLinux::Stop() {
|
||||
return 0;
|
||||
}
|
||||
const int ret = impl_->Stop();
|
||||
{
|
||||
std::lock_guard lock(privacy_mutex_);
|
||||
capture_announced_ = false;
|
||||
if (privacy_) privacy_->CaptureChanged(false);
|
||||
}
|
||||
UpdateAliasesFromBackend(impl_.get());
|
||||
return ret;
|
||||
}
|
||||
@@ -253,25 +234,13 @@ int ScreenCapturerLinux::Pause(int monitor_index) {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
const int ret = impl_->Pause(monitor_index);
|
||||
{
|
||||
// An in-flight frame must not announce running again after a pause.
|
||||
std::lock_guard lock(privacy_mutex_);
|
||||
capture_paused_ = true;
|
||||
capture_announced_ = false;
|
||||
if (privacy_) privacy_->CaptureChanged(false);
|
||||
}
|
||||
return ret;
|
||||
return impl_->Pause(monitor_index);
|
||||
}
|
||||
|
||||
int ScreenCapturerLinux::Resume(int monitor_index) {
|
||||
if (!impl_) {
|
||||
return -1;
|
||||
}
|
||||
{
|
||||
std::lock_guard lock(privacy_mutex_);
|
||||
capture_paused_ = false;
|
||||
}
|
||||
return impl_->Resume(monitor_index);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ class ScreenCapturerLinux : public ScreenCapturer {
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
@@ -54,10 +53,6 @@ class ScreenCapturerLinux : public ScreenCapturer {
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
PrivacyController* privacy_ = nullptr;
|
||||
std::mutex privacy_mutex_;
|
||||
bool capture_announced_ = false;
|
||||
bool capture_paused_ = false;
|
||||
BackendType backend_ = BackendType::kNone;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data callback_;
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "runtime/cursor_position.h"
|
||||
#include "../../privacy/privacy_cursor_mac.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
@@ -162,9 +161,7 @@ bool CursorStateProvider::Sample(const std::vector<DisplayInfo>& displays,
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
NSCursor* cursor = NSCursor.currentSystemCursor;
|
||||
const bool privacy_hidden = IsMacPrivacyCursorHidden();
|
||||
const bool visible = CGCursorIsVisible() || privacy_hidden;
|
||||
if (!cursor && privacy_hidden) cursor = NSCursor.arrowCursor;
|
||||
const bool visible = CGCursorIsVisible();
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
state->seq = 0;
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
#include <unordered_map>
|
||||
|
||||
#include "keyboard_converter.h"
|
||||
#include "../mac_injected_event.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
@@ -12,6 +11,7 @@ namespace crossdesk {
|
||||
static OnKeyAction g_on_key_action = nullptr;
|
||||
static void* g_user_ptr = nullptr;
|
||||
static std::unordered_map<int, int> g_unmapped_keycode_to_vk;
|
||||
constexpr int64_t kCrossDeskInjectedKeyboardEvent = 0x43524f5353444553;
|
||||
|
||||
static int VkCodeFromUnicode(UniChar ch) {
|
||||
if (ch >= 'a' && ch <= 'z') {
|
||||
@@ -106,7 +106,7 @@ CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
|
||||
CGEventRef event, void* userInfo) {
|
||||
(void)proxy;
|
||||
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) ==
|
||||
kCrossDeskInjectedEvent) {
|
||||
kCrossDeskInjectedKeyboardEvent) {
|
||||
return event;
|
||||
}
|
||||
if (!g_on_key_action) {
|
||||
@@ -309,7 +309,7 @@ int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
|
||||
CGEventSetFlags(event, ToCGEventFlags(injected_flags));
|
||||
CGEventSetIntegerValueField(event, kCGEventSourceUserData,
|
||||
kCrossDeskInjectedEvent);
|
||||
kCrossDeskInjectedKeyboardEvent);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
|
||||
@@ -323,7 +323,7 @@ int PlatformKeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down,
|
||||
return -1;
|
||||
}
|
||||
CGEventSetIntegerValueField(fn_release_event, kCGEventSourceUserData,
|
||||
kCrossDeskInjectedEvent);
|
||||
kCrossDeskInjectedKeyboardEvent);
|
||||
CGEventPost(kCGHIDEventTap, fn_release_event);
|
||||
CFRelease(fn_release_event);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-13
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _MAC_INJECTED_EVENT_H_
|
||||
#define _MAC_INJECTED_EVENT_H_
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
// Shared by the keyboard/mouse injectors and the privacy input tap.
|
||||
inline constexpr int64_t kCrossDeskInjectedEvent = 0x43524f5353444553;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "mouse_controller.h"
|
||||
#include "../mac_injected_event.h"
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
@@ -211,8 +210,6 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
}
|
||||
|
||||
if (mouse_event) {
|
||||
CGEventSetIntegerValueField(mouse_event, kCGEventSourceUserData,
|
||||
kCrossDeskInjectedEvent);
|
||||
CGEventPost(kCGHIDEventTap, mouse_event);
|
||||
CFRelease(mouse_event);
|
||||
}
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <cstdlib>
|
||||
#include <libproc.h>
|
||||
#include <mutex>
|
||||
#include <utility>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "privacy_backend.h"
|
||||
#include "privacy_capture_state.h"
|
||||
#include "privacy_cover_view.h"
|
||||
#include "privacy_cursor_mac.h"
|
||||
#include "rd_log.h"
|
||||
#include "../input/mac_injected_event.h"
|
||||
|
||||
// These windows never take keyboard focus. Remote clicks pass through them;
|
||||
// physical input is intercepted independently on the controller's run loop.
|
||||
@interface CrossDeskPrivacyPanel : NSPanel
|
||||
@end
|
||||
@implementation CrossDeskPrivacyPanel
|
||||
- (BOOL)canBecomeKeyWindow { return NO; }
|
||||
- (BOOL)canBecomeMainWindow { return NO; }
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
using Clock = std::chrono::steady_clock;
|
||||
std::mutex tasks_mutex;
|
||||
std::deque<std::function<void()>> tasks;
|
||||
std::mutex capture_mutex;
|
||||
MacPrivacyCaptureState capture_state;
|
||||
|
||||
void OnMain(std::function<void()> task) {
|
||||
{
|
||||
std::lock_guard lock(tasks_mutex);
|
||||
tasks.push_back(std::move(task));
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ FlushMacPrivacyTasks(); });
|
||||
}
|
||||
|
||||
void PublishCovers(std::vector<uint32_t> windows, uint32_t cursor_window = 0) {
|
||||
{
|
||||
std::lock_guard lock(capture_mutex);
|
||||
capture_state.excluded_windows = std::move(windows);
|
||||
capture_state.cursor_window = cursor_window;
|
||||
++capture_state.revision;
|
||||
}
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[NSNotificationCenter.defaultCenter postNotificationName:@"CrossDeskPrivacyCoversChanged"
|
||||
object:nil];
|
||||
});
|
||||
}
|
||||
|
||||
struct Monitor {
|
||||
CGDirectDisplayID id = 0;
|
||||
CGRect bounds{}; // CoreGraphics desktop coordinates (points).
|
||||
NSRect frame{}; // AppKit desktop coordinates (bottom left).
|
||||
CrossDeskPrivacyPanel* __strong cover = nil;
|
||||
};
|
||||
struct WindowState {
|
||||
std::mutex mutex;
|
||||
std::vector<Monitor> monitors;
|
||||
bool recovered = true;
|
||||
bool recovering = false;
|
||||
bool covers_shown = false;
|
||||
bool check_queued = false;
|
||||
MacPrivacyCursor cursor;
|
||||
std::string failure;
|
||||
Clock::time_point checked = Clock::now();
|
||||
};
|
||||
|
||||
CrossDeskPrivacyPanel* MakePanel(NSRect frame, NSInteger level) {
|
||||
auto* panel = [[CrossDeskPrivacyPanel alloc]
|
||||
initWithContentRect:frame
|
||||
styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel
|
||||
backing:NSBackingStoreBuffered defer:NO];
|
||||
panel.releasedWhenClosed = NO;
|
||||
panel.hidesOnDeactivate = NO;
|
||||
panel.opaque = YES;
|
||||
panel.backgroundColor = NSColor.blackColor;
|
||||
panel.hasShadow = NO;
|
||||
panel.ignoresMouseEvents = YES;
|
||||
panel.level = level;
|
||||
panel.animationBehavior = NSWindowAnimationBehaviorNone;
|
||||
panel.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary |
|
||||
NSWindowCollectionBehaviorStationary |
|
||||
NSWindowCollectionBehaviorIgnoresCycle;
|
||||
// Exclusion is explicit in ScreenCaptureKit. SharingNone alone does not
|
||||
// exclude windows on recent macOS releases, and can hide them from discovery.
|
||||
panel.sharingType = NSWindowSharingReadOnly;
|
||||
return panel;
|
||||
}
|
||||
|
||||
bool IsSystemCursorPlane(NSDictionary* info, CGRect bounds) {
|
||||
const auto layer = [info[(__bridge NSString*)kCGWindowLayer] intValue];
|
||||
// WindowServer's cursor plane also carries small recording indicators.
|
||||
// Validate both its geometry and executable identity before ignoring it.
|
||||
if (layer != CGWindowLevelForKey(kCGCursorWindowLevelKey) ||
|
||||
bounds.size.width <= 0 || bounds.size.height <= 0 ||
|
||||
bounds.size.width > 512 || bounds.size.height > 512)
|
||||
return false;
|
||||
char executable[PROC_PIDPATHINFO_MAXSIZE]{};
|
||||
const auto pid = [info[(__bridge NSString*)kCGWindowOwnerPID] intValue];
|
||||
static const std::string system_executable = [] {
|
||||
char resolved[PROC_PIDPATHINFO_MAXSIZE]{};
|
||||
return realpath("/System/Library/PrivateFrameworks/SkyLight.framework/Resources/WindowServer",
|
||||
resolved) ? std::string(resolved) : std::string();
|
||||
}();
|
||||
return proc_pidpath(pid, executable, sizeof(executable)) > 0 &&
|
||||
!system_executable.empty() && system_executable == executable;
|
||||
}
|
||||
|
||||
std::string CoverageFailure(const std::vector<Monitor>& monitors) {
|
||||
uint32_t count = 0;
|
||||
if (CGGetActiveDisplayList(0, nullptr, &count) != kCGErrorSuccess || !count)
|
||||
return "No active macOS desktop; privacy screen unavailable";
|
||||
std::vector<CGDirectDisplayID> displays(count);
|
||||
if (CGGetActiveDisplayList(count, displays.data(), &count) != kCGErrorSuccess ||
|
||||
count != monitors.size())
|
||||
return "Display topology changed; turn privacy off to recover";
|
||||
for (const auto& m : monitors) {
|
||||
if (std::find(displays.begin(), displays.end(), m.id) == displays.end() ||
|
||||
!CGRectEqualToRect(CGDisplayBounds(m.id), m.bounds) ||
|
||||
!m.cover.visible || !m.cover.isOnActiveSpace ||
|
||||
!NSEqualRects(m.cover.frame, m.frame) || m.cover.alphaValue != 1.0)
|
||||
return "Privacy cover or display geometry changed; privacy screen unavailable";
|
||||
}
|
||||
NSDictionary* session = CFBridgingRelease(CGSessionCopyCurrentDictionary());
|
||||
if (!session || ![session[(__bridge NSString*)kCGSessionOnConsoleKey] boolValue])
|
||||
return "macOS session is not on console; privacy screen unavailable";
|
||||
|
||||
NSArray* windows = CFBridgingRelease(CGWindowListCopyWindowInfo(
|
||||
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
|
||||
kCGNullWindowID));
|
||||
if (!windows) return "Unable to verify macOS privacy window order";
|
||||
for (const auto& m : monitors) {
|
||||
bool found = false;
|
||||
for (NSDictionary* info in windows) {
|
||||
const auto window = [info[(__bridge NSString*)kCGWindowNumber] unsignedIntValue];
|
||||
if (window == m.cover.windowNumber) { found = true; break; }
|
||||
if ([info[(__bridge NSString*)kCGWindowAlpha] doubleValue] <= 0) continue;
|
||||
CGRect bounds{};
|
||||
if (!CGRectMakeWithDictionaryRepresentation(
|
||||
(__bridge CFDictionaryRef)info[(__bridge NSString*)kCGWindowBounds], &bounds))
|
||||
return "Unable to verify a window above the privacy cover";
|
||||
if (CGRectIntersectsRect(bounds, m.bounds)) {
|
||||
if (IsSystemCursorPlane(info, bounds)) continue;
|
||||
LOG_WARN("macOS privacy overlap: window={}, pid={}, layer={}, alpha={}, "
|
||||
"bounds=({}, {}, {}, {}), cover={}, cover_level={}",
|
||||
window,
|
||||
[info[(__bridge NSString*)kCGWindowOwnerPID] intValue],
|
||||
[info[(__bridge NSString*)kCGWindowLayer] intValue],
|
||||
[info[(__bridge NSString*)kCGWindowAlpha] doubleValue],
|
||||
bounds.origin.x, bounds.origin.y, bounds.size.width, bounds.size.height,
|
||||
m.cover.windowNumber, m.cover.level);
|
||||
return "A system or application window overlaps the privacy cover; privacy screen unavailable";
|
||||
}
|
||||
}
|
||||
if (!found) return "Privacy cover disappeared; privacy screen unavailable";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
class MacPrivacyBackend final : public PrivacyBackend {
|
||||
public:
|
||||
MacPrivacyBackend() : state_(std::make_shared<WindowState>()) {
|
||||
loop_ = CFRunLoopGetCurrent();
|
||||
CFRetain(loop_);
|
||||
CFRunLoopSourceContext context{};
|
||||
wake_source_ = CFRunLoopSourceCreate(nullptr, 0, &context);
|
||||
CFRunLoopAddSource(loop_, wake_source_, kCFRunLoopDefaultMode);
|
||||
}
|
||||
~MacPrivacyBackend() override {
|
||||
Recover();
|
||||
CFRunLoopRemoveSource(loop_, wake_source_, kCFRunLoopDefaultMode);
|
||||
CFRelease(wake_source_);
|
||||
CFRelease(loop_);
|
||||
}
|
||||
PrivacyCapabilities Query() override {
|
||||
if (@available(macOS 14.0, *)) {
|
||||
if (!CGPreflightScreenCaptureAccess())
|
||||
return {false, false, "Grant Screen Recording permission to use privacy screen"};
|
||||
if (!AXIsProcessTrusted())
|
||||
return {false, false, "Grant Accessibility permission to protect local input and enable local recovery"};
|
||||
if (!MacPrivacyCursor::Supported())
|
||||
return {false, false, "Background cursor control unavailable on this macOS version"};
|
||||
return {true, true, "macOS privacy screen available (ScreenCaptureKit)"};
|
||||
}
|
||||
return {false, false, "Privacy screen requires macOS 14 or newer"};
|
||||
}
|
||||
bool Enable(bool block_input, const PrivacyScreenText& text, std::string& error) override {
|
||||
const auto caps = Query();
|
||||
if (!caps.overlay) { error = caps.reason; return false; }
|
||||
block_input_ = block_input;
|
||||
emergency_ = false;
|
||||
tap_failed_ = false;
|
||||
CGEventMask mask = CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) |
|
||||
CGEventMaskBit(kCGEventFlagsChanged) | CGEventMaskBit(kCGEventLeftMouseDown) |
|
||||
CGEventMaskBit(kCGEventLeftMouseUp) | CGEventMaskBit(kCGEventRightMouseDown) |
|
||||
CGEventMaskBit(kCGEventRightMouseUp) | CGEventMaskBit(kCGEventMouseMoved) |
|
||||
CGEventMaskBit(kCGEventLeftMouseDragged) | CGEventMaskBit(kCGEventRightMouseDragged) |
|
||||
CGEventMaskBit(kCGEventOtherMouseDown) | CGEventMaskBit(kCGEventOtherMouseUp) |
|
||||
CGEventMaskBit(kCGEventOtherMouseDragged) | CGEventMaskBit(kCGEventScrollWheel) |
|
||||
CGEventMaskBit(14); // NX_SYSDEFINED: media/brightness keys.
|
||||
tap_ = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap,
|
||||
kCGEventTapOptionDefault, mask, Input, this);
|
||||
if (!tap_) { error = "Cannot install macOS privacy input tap"; return false; }
|
||||
tap_source_ = CFMachPortCreateRunLoopSource(nullptr, tap_, 0);
|
||||
if (!tap_source_) { error = "Cannot create macOS privacy input run loop"; return false; }
|
||||
CFRunLoopAddSource(loop_, tap_source_, kCFRunLoopDefaultMode);
|
||||
CGEventTapEnable(tap_, true);
|
||||
{
|
||||
std::lock_guard lock(state_->mutex);
|
||||
state_->recovered = false;
|
||||
state_->failure.clear();
|
||||
state_->checked = Clock::now();
|
||||
}
|
||||
auto state = state_;
|
||||
OnMain([state, text] {
|
||||
@autoreleasepool {
|
||||
std::lock_guard lock(state->mutex);
|
||||
if (!state->cursor.Prepare()) {
|
||||
state->failure = "Cannot prepare the privacy cursor";
|
||||
return;
|
||||
}
|
||||
std::vector<uint32_t> covers;
|
||||
for (NSScreen* screen in NSScreen.screens) {
|
||||
Monitor m;
|
||||
m.id = [screen.deviceDescription[@"NSScreenNumber"] unsignedIntValue];
|
||||
m.bounds = CGDisplayBounds(m.id);
|
||||
m.frame = screen.frame;
|
||||
NSView* content = CreateMacPrivacyCoverView(
|
||||
NSMakeRect(0, 0, m.frame.size.width, m.frame.size.height),
|
||||
[NSString stringWithUTF8String:text.unlock_hint.c_str()]);
|
||||
if (!content) {
|
||||
state->failure = "Could not load the CrossDesk privacy screen logo";
|
||||
break;
|
||||
}
|
||||
m.cover = MakePanel(m.frame, CGWindowLevelForKey(kCGScreenSaverWindowLevelKey) + 1);
|
||||
m.cover.contentView = content;
|
||||
// Register transparent windows first. Make them opaque only after
|
||||
// SCK has acknowledged exclusion, avoiding a black remote frame.
|
||||
m.cover.alphaValue = 0;
|
||||
[m.cover orderFrontRegardless];
|
||||
covers.push_back(static_cast<uint32_t>(m.cover.windowNumber));
|
||||
state->monitors.push_back(std::move(m));
|
||||
}
|
||||
PublishCovers(std::move(covers), state->cursor.WindowId());
|
||||
if (state->monitors.empty()) state->failure = "No active macOS desktop";
|
||||
state->checked = Clock::now();
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
void Recover() override {
|
||||
if (tap_) { CGEventTapEnable(tap_, false); CFMachPortInvalidate(tap_); }
|
||||
if (tap_source_) {
|
||||
CFRunLoopRemoveSource(loop_, tap_source_, kCFRunLoopDefaultMode);
|
||||
CFRelease(tap_source_); tap_source_ = nullptr;
|
||||
}
|
||||
if (tap_) { CFRelease(tap_); tap_ = nullptr; }
|
||||
{
|
||||
std::lock_guard lock(state_->mutex);
|
||||
if (state_->recovered || state_->recovering) return;
|
||||
state_->recovering = true;
|
||||
}
|
||||
auto state = state_;
|
||||
OnMain([state] {
|
||||
std::lock_guard lock(state->mutex);
|
||||
for (auto& m : state->monitors) {
|
||||
[m.cover orderOut:nil]; [m.cover close]; m.cover = nil;
|
||||
}
|
||||
state->monitors.clear();
|
||||
state->covers_shown = false;
|
||||
state->cursor.Restore();
|
||||
PublishCovers({});
|
||||
state->failure.clear();
|
||||
state->recovering = false;
|
||||
state->recovered = true;
|
||||
});
|
||||
}
|
||||
bool IsRecovered() const override {
|
||||
std::lock_guard lock(state_->mutex);
|
||||
return state_->recovered && !tap_;
|
||||
}
|
||||
bool RecoveryPending() const override {
|
||||
std::lock_guard lock(state_->mutex);
|
||||
return state_->recovering;
|
||||
}
|
||||
PrivacyHealth Poll() override {
|
||||
if (emergency_) { emergency_ = false; return {{}, true}; }
|
||||
if (tap_ && (tap_failed_ || !CGEventTapIsEnabled(tap_) || !AXIsProcessTrusted()))
|
||||
return {"Local input protection interrupted; privacy screen unavailable", false};
|
||||
auto state = state_;
|
||||
std::lock_guard lock(state->mutex);
|
||||
if (state->recovered || state->recovering) return {};
|
||||
if (!state->failure.empty()) return {state->failure, false};
|
||||
const auto age = Clock::now() - state->checked;
|
||||
if (age > std::chrono::seconds(1))
|
||||
return {"macOS privacy window checks stopped responding; privacy screen unavailable", false};
|
||||
if (!state->check_queued && age > std::chrono::milliseconds(100)) {
|
||||
state->check_queued = true;
|
||||
OnMain([state] {
|
||||
std::lock_guard lock(state->mutex);
|
||||
if (!state->recovered && !state->recovering && state->failure.empty()) {
|
||||
const auto capture = GetMacPrivacyCaptureState();
|
||||
if (!state->covers_shown && !state->monitors.empty() &&
|
||||
capture.revision == capture.applied_revision) {
|
||||
if (!state->cursor.Hide()) {
|
||||
state->failure = "Cannot hide the local privacy cursor";
|
||||
state->checked = Clock::now();
|
||||
state->check_queued = false;
|
||||
return;
|
||||
}
|
||||
for (auto& m : state->monitors) {
|
||||
m.cover.alphaValue = 1;
|
||||
[m.cover displayIfNeeded];
|
||||
[m.cover orderFrontRegardless];
|
||||
}
|
||||
state->covers_shown = true;
|
||||
}
|
||||
if (state->covers_shown) {
|
||||
state->failure = state->cursor.Healthy() ? CoverageFailure(state->monitors)
|
||||
: "Cannot keep the local privacy cursor hidden";
|
||||
}
|
||||
}
|
||||
state->checked = Clock::now();
|
||||
state->check_queued = false;
|
||||
});
|
||||
}
|
||||
const auto capture = GetMacPrivacyCaptureState();
|
||||
return {{}, false, state->covers_shown &&
|
||||
capture.revision == capture.applied_revision};
|
||||
}
|
||||
void WaitForEvents(unsigned timeout_ms) override {
|
||||
@autoreleasepool {
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout_ms / 1000.0, true);
|
||||
}
|
||||
}
|
||||
void Wake() override { CFRunLoopSourceSignal(wake_source_); CFRunLoopWakeUp(loop_); }
|
||||
private:
|
||||
static CGEventRef Input(CGEventTapProxy, CGEventType type, CGEventRef event, void* context) {
|
||||
auto* self = static_cast<MacPrivacyBackend*>(context);
|
||||
if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) {
|
||||
self->tap_failed_ = true;
|
||||
CGEventTapEnable(self->tap_, true); // Keep the local recovery chord usable.
|
||||
return event;
|
||||
}
|
||||
if (!event) return event;
|
||||
if (CGEventGetIntegerValueField(event, kCGEventSourceUserData) == kCrossDeskInjectedEvent &&
|
||||
CGEventGetIntegerValueField(event, kCGEventSourceUnixProcessID) == getpid()) return event;
|
||||
const auto flags = CGEventGetFlags(event);
|
||||
const auto chord = kCGEventFlagMaskControl | kCGEventFlagMaskAlternate | kCGEventFlagMaskShift;
|
||||
if (type == kCGEventKeyDown && (flags & chord) == chord &&
|
||||
CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode) == 53) {
|
||||
self->emergency_ = true;
|
||||
return nullptr;
|
||||
}
|
||||
return self->block_input_ ? nullptr : event;
|
||||
}
|
||||
std::shared_ptr<WindowState> state_;
|
||||
CFRunLoopRef loop_ = nullptr;
|
||||
CFRunLoopSourceRef wake_source_ = nullptr;
|
||||
CFMachPortRef tap_ = nullptr;
|
||||
CFRunLoopSourceRef tap_source_ = nullptr;
|
||||
bool block_input_ = false;
|
||||
bool emergency_ = false;
|
||||
bool tap_failed_ = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
void FlushMacPrivacyTasks() {
|
||||
if (![NSThread isMainThread]) return;
|
||||
std::deque<std::function<void()>> pending;
|
||||
{
|
||||
std::lock_guard lock(tasks_mutex);
|
||||
pending.swap(tasks);
|
||||
}
|
||||
@autoreleasepool { for (auto& task : pending) task(); }
|
||||
}
|
||||
MacPrivacyCaptureState GetMacPrivacyCaptureState() {
|
||||
std::lock_guard lock(capture_mutex);
|
||||
return capture_state;
|
||||
}
|
||||
void AcknowledgeMacPrivacyCapture(uint64_t revision) {
|
||||
std::lock_guard lock(capture_mutex);
|
||||
if (revision == capture_state.revision) capture_state.applied_revision = revision;
|
||||
}
|
||||
std::unique_ptr<PrivacyBackend> CreateMacPrivacyBackend() {
|
||||
return std::make_unique<MacPrivacyBackend>();
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -1,23 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-13
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
#ifndef _PRIVACY_CAPTURE_STATE_H_
|
||||
#define _PRIVACY_CAPTURE_STATE_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
struct MacPrivacyCaptureState {
|
||||
uint64_t revision = 0;
|
||||
uint64_t applied_revision = 0;
|
||||
std::vector<uint32_t> excluded_windows;
|
||||
uint32_t cursor_window = 0;
|
||||
};
|
||||
MacPrivacyCaptureState GetMacPrivacyCaptureState();
|
||||
void AcknowledgeMacPrivacyCapture(uint64_t revision);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,17 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-13
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_COVER_VIEW_H_
|
||||
#define _PRIVACY_COVER_VIEW_H_
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
namespace crossdesk {
|
||||
// Returns nil if the CrossDesk icon resource cannot be loaded.
|
||||
NSView* CreateMacPrivacyCoverView(NSRect frame, NSString* unlock_hint);
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,106 +0,0 @@
|
||||
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
|
||||
|
||||
#include "privacy_cover_view.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace {
|
||||
NSImage* LoadPrivacyLogo() {
|
||||
// The macOS installer currently packages the icon as "crossedesk.icns".
|
||||
for (NSString* name in @[@"crossdesk", @"crossedesk"]) {
|
||||
NSString* path = [NSBundle.mainBundle pathForResource:name ofType:@"icns"];
|
||||
if (path) {
|
||||
NSImage* image = [[NSImage alloc] initWithContentsOfFile:path];
|
||||
if (image.valid) return image;
|
||||
}
|
||||
}
|
||||
// Support unbundled development builds, including launches from outside
|
||||
// the repository. No source-tree path is embedded in the executable.
|
||||
NSString* resource = @"apps/desktop/resources/macos/crossdesk.icns";
|
||||
NSString* directory = NSBundle.mainBundle.executablePath.stringByDeletingLastPathComponent;
|
||||
NSMutableArray<NSString*>* paths = [NSMutableArray arrayWithObject:
|
||||
[NSFileManager.defaultManager.currentDirectoryPath stringByAppendingPathComponent:resource]];
|
||||
for (int depth = 0; directory.length && depth <= 4; ++depth) {
|
||||
[paths addObject:[directory stringByAppendingPathComponent:resource]];
|
||||
directory = directory.stringByDeletingLastPathComponent;
|
||||
}
|
||||
for (NSString* path in paths) {
|
||||
NSImage* image = [[NSImage alloc] initWithContentsOfFile:path.stringByStandardizingPath];
|
||||
if (image.valid) return image;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSAttributedString* Text(NSString* value, CGFloat size, NSFontWeight weight, NSColor* color) {
|
||||
NSMutableParagraphStyle* paragraph = [[NSMutableParagraphStyle alloc] init];
|
||||
paragraph.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
return [[NSAttributedString alloc] initWithString:value attributes:@{
|
||||
NSFontAttributeName: [NSFont systemFontOfSize:size weight:weight],
|
||||
NSForegroundColorAttributeName: color,
|
||||
NSParagraphStyleAttributeName: paragraph}];
|
||||
}
|
||||
constexpr NSStringDrawingOptions kTextOptions =
|
||||
NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading;
|
||||
|
||||
CGFloat TextHeight(NSAttributedString* text, CGFloat width) {
|
||||
return std::ceil([text boundingRectWithSize:NSMakeSize(width, CGFLOAT_MAX)
|
||||
options:kTextOptions].size.height);
|
||||
}
|
||||
}
|
||||
|
||||
@interface CrossDeskPrivacyCoverView : NSView
|
||||
@property(nonatomic, strong) NSImage* logo;
|
||||
@property(nonatomic, copy) NSString* unlockHint;
|
||||
@end
|
||||
|
||||
@implementation CrossDeskPrivacyCoverView
|
||||
- (BOOL)isOpaque { return YES; }
|
||||
- (BOOL)isFlipped { return YES; }
|
||||
- (void)drawRect:(NSRect)dirtyRect {
|
||||
[NSColor.blackColor setFill];
|
||||
NSRectFill(self.bounds);
|
||||
const CGFloat width = NSWidth(self.bounds), height = NSHeight(self.bounds);
|
||||
if (width <= 0 || height <= 0) return;
|
||||
// Match the Windows cover's sizes, colors and bottom-left alignment.
|
||||
// AppKit coordinates are points; Retina backing scale is applied by AppKit.
|
||||
const CGFloat scale = std::min({CGFloat(1), width / 640, height / 200});
|
||||
const CGFloat margin = 32 * scale, text_width = 398 * scale;
|
||||
NSColor* foreground = [NSColor colorWithSRGBRed:245.0 / 255 green:247.0 / 255
|
||||
blue:250.0 / 255 alpha:1];
|
||||
NSColor* muted = [NSColor colorWithSRGBRed:181.0 / 255 green:190.0 / 255
|
||||
blue:204.0 / 255 alpha:1];
|
||||
NSAttributedString* brand = Text(@"CrossDesk", 14 * scale, NSFontWeightSemibold, foreground);
|
||||
NSAttributedString* hint = Text(self.unlockHint, 16 * scale, NSFontWeightRegular, muted);
|
||||
NSAttributedString* shortcut = Text(@"Control + Option + Shift + Esc", 24 * scale,
|
||||
NSFontWeightSemibold, foreground);
|
||||
const CGFloat logo_size = std::ceil(brand.size.width);
|
||||
const CGFloat brand_height = TextHeight(brand, logo_size);
|
||||
const CGFloat hint_height = TextHeight(hint, text_width);
|
||||
const CGFloat shortcut_height = TextHeight(shortcut, text_width);
|
||||
const CGFloat bottom = height - margin;
|
||||
const CGFloat logo_top = bottom - brand_height - 6 * scale - logo_size;
|
||||
[self.logo drawInRect:NSMakeRect(margin, logo_top, logo_size, logo_size)
|
||||
fromRect:NSZeroRect operation:NSCompositingOperationSourceOver
|
||||
fraction:1 respectFlipped:YES hints:@{NSImageHintInterpolation: @(NSImageInterpolationHigh)}];
|
||||
[brand drawWithRect:NSMakeRect(margin, bottom - brand_height, logo_size, brand_height)
|
||||
options:kTextOptions];
|
||||
const CGFloat text_left = margin + logo_size + 24 * scale;
|
||||
[hint drawWithRect:NSMakeRect(text_left, bottom - shortcut_height - 12 * scale - hint_height,
|
||||
text_width, hint_height) options:kTextOptions];
|
||||
[shortcut drawWithRect:NSMakeRect(text_left, bottom - shortcut_height,
|
||||
text_width, shortcut_height) options:kTextOptions];
|
||||
}
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
NSView* CreateMacPrivacyCoverView(NSRect frame, NSString* unlock_hint) {
|
||||
NSImage* logo = LoadPrivacyLogo();
|
||||
if (!logo) return nil;
|
||||
auto* view = [[CrossDeskPrivacyCoverView alloc] initWithFrame:frame];
|
||||
view.logo = logo;
|
||||
view.unlockHint = unlock_hint ?: @"To turn off the privacy screen, press";
|
||||
view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
return view;
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-13
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_CURSOR_MAC_H_
|
||||
#define _PRIVACY_CURSOR_MAC_H_
|
||||
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
// Main-thread resource. Hides only for the lifetime of a local privacy cover;
|
||||
// does not take focus, move the pointer, or suppress remote input.
|
||||
class MacPrivacyCursor {
|
||||
public:
|
||||
MacPrivacyCursor();
|
||||
~MacPrivacyCursor();
|
||||
static bool Supported();
|
||||
bool Prepare();
|
||||
bool Hide();
|
||||
bool Healthy() const;
|
||||
uint32_t WindowId() const;
|
||||
void Restore();
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
// Local hiding must not be reported as an invisible cursor to controllers.
|
||||
bool IsMacPrivacyCursorHidden();
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -1,157 +0,0 @@
|
||||
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
|
||||
#include "privacy_cursor_mac.h"
|
||||
|
||||
#import <AppKit/AppKit.h>
|
||||
#import <ApplicationServices/ApplicationServices.h>
|
||||
#include <atomic>
|
||||
#include <dlfcn.h>
|
||||
|
||||
@interface CrossDeskPrivacyCursorPanel : NSPanel
|
||||
@end
|
||||
@implementation CrossDeskPrivacyCursorPanel
|
||||
- (BOOL)canBecomeKeyWindow { return NO; }
|
||||
- (BOOL)canBecomeMainWindow { return NO; }
|
||||
@end
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
std::atomic<bool> hidden_by_privacy{false};
|
||||
struct CursorApi {
|
||||
using Connection = int (*)();
|
||||
using Copy = CGError (*)(int, int, CFStringRef, CFTypeRef*);
|
||||
using Set = CGError (*)(int, int, CFStringRef, CFTypeRef);
|
||||
Connection connection = reinterpret_cast<Connection>(dlsym(RTLD_DEFAULT, "CGSMainConnectionID"));
|
||||
Copy copy = reinterpret_cast<Copy>(dlsym(RTLD_DEFAULT, "CGSCopyConnectionProperty"));
|
||||
Set set = reinterpret_cast<Set>(dlsym(RTLD_DEFAULT, "CGSSetConnectionProperty"));
|
||||
};
|
||||
const CursorApi& Api() { static const CursorApi api; return api; }
|
||||
CFStringRef const kBackgroundCursor = CFSTR("SetsCursorInBackground");
|
||||
}
|
||||
|
||||
struct MacPrivacyCursor::Impl {
|
||||
int connection = 0;
|
||||
bool previous_background = false;
|
||||
bool background_changed = false;
|
||||
unsigned hide_count = 0;
|
||||
unsigned visible_ticks = 0;
|
||||
bool healthy = true;
|
||||
NSTimer* __strong timer = nil;
|
||||
CrossDeskPrivacyCursorPanel* __strong panel = nil;
|
||||
NSImageView* __strong view = nil;
|
||||
|
||||
void UpdateImage() {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
NSCursor* cursor = NSCursor.currentSystemCursor ?: NSCursor.arrowCursor;
|
||||
#pragma clang diagnostic pop
|
||||
NSImage* image = cursor.image;
|
||||
if (!image || image.size.width <= 0 || image.size.height <= 0) return;
|
||||
const NSPoint point = NSEvent.mouseLocation;
|
||||
const NSSize size = image.size;
|
||||
[panel setFrame:NSMakeRect(point.x - cursor.hotSpot.x,
|
||||
point.y - size.height + cursor.hotSpot.y, size.width, size.height) display:NO];
|
||||
view.image = image;
|
||||
[panel displayIfNeeded];
|
||||
}
|
||||
|
||||
bool KeepHidden() {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if (hide_count && !CGCursorIsVisible()) { visible_ticks = 0; return true; }
|
||||
#pragma clang diagnostic pop
|
||||
if (++visible_ticks > 6 ||
|
||||
CGDisplayHideCursor(kCGDirectMainDisplay) != kCGErrorSuccess) return false;
|
||||
++hide_count;
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
MacPrivacyCursor::MacPrivacyCursor() : impl_(std::make_unique<Impl>()) {}
|
||||
MacPrivacyCursor::~MacPrivacyCursor() { Restore(); }
|
||||
bool MacPrivacyCursor::Supported() {
|
||||
return Api().connection && Api().copy && Api().set;
|
||||
}
|
||||
bool MacPrivacyCursor::Prepare() {
|
||||
if (impl_->panel) return true;
|
||||
if (!Supported()) return false;
|
||||
// A cursor sprite below the local cover is captured only for clients that
|
||||
// request an embedded cursor. Desktop clients exclude it and use metadata.
|
||||
impl_->panel = [[CrossDeskPrivacyCursorPanel alloc] initWithContentRect:NSMakeRect(0, 0, 32, 32)
|
||||
styleMask:NSWindowStyleMaskBorderless | NSWindowStyleMaskNonactivatingPanel
|
||||
backing:NSBackingStoreBuffered defer:NO];
|
||||
auto* panel = impl_->panel;
|
||||
panel.releasedWhenClosed = NO;
|
||||
panel.hidesOnDeactivate = NO;
|
||||
panel.opaque = NO;
|
||||
panel.backgroundColor = NSColor.clearColor;
|
||||
panel.hasShadow = NO;
|
||||
panel.ignoresMouseEvents = YES;
|
||||
panel.level = CGWindowLevelForKey(kCGScreenSaverWindowLevelKey);
|
||||
panel.animationBehavior = NSWindowAnimationBehaviorNone;
|
||||
panel.collectionBehavior = NSWindowCollectionBehaviorCanJoinAllSpaces |
|
||||
NSWindowCollectionBehaviorFullScreenAuxiliary | NSWindowCollectionBehaviorStationary |
|
||||
NSWindowCollectionBehaviorIgnoresCycle;
|
||||
panel.sharingType = NSWindowSharingReadOnly;
|
||||
impl_->view = [[NSImageView alloc] initWithFrame:panel.contentView.bounds];
|
||||
impl_->view.imageScaling = NSImageScaleNone;
|
||||
impl_->view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
panel.contentView = impl_->view;
|
||||
panel.alphaValue = 0;
|
||||
impl_->UpdateImage();
|
||||
[panel orderFrontRegardless];
|
||||
return panel != nil && panel.windowNumber > 0;
|
||||
}
|
||||
bool MacPrivacyCursor::Hide() {
|
||||
if (impl_->timer) return true;
|
||||
if (!Prepare()) return false;
|
||||
impl_->connection = Api().connection();
|
||||
impl_->previous_background = false;
|
||||
CFTypeRef previous = nullptr;
|
||||
if (Api().copy(impl_->connection, impl_->connection, kBackgroundCursor, &previous) == kCGErrorSuccess && previous)
|
||||
impl_->previous_background = CFEqual(previous, kCFBooleanTrue);
|
||||
if (previous) CFRelease(previous);
|
||||
// This connection-scoped WindowServer property is also used by Deskflow.
|
||||
// Resolve it dynamically: an unavailable API disables optional privacy only.
|
||||
if (Api().set(impl_->connection, impl_->connection, kBackgroundCursor, kCFBooleanTrue) != kCGErrorSuccess)
|
||||
return false;
|
||||
impl_->background_changed = true;
|
||||
impl_->healthy = true;
|
||||
impl_->visible_ticks = 0;
|
||||
hidden_by_privacy = true;
|
||||
if (!impl_->KeepHidden()) { Restore(); return false; }
|
||||
impl_->UpdateImage();
|
||||
impl_->panel.alphaValue = 1;
|
||||
auto* state = impl_.get();
|
||||
impl_->timer = [NSTimer timerWithTimeInterval:1.0 / 60 repeats:YES block:^(NSTimer*) {
|
||||
if (state->healthy) state->healthy = state->KeepHidden();
|
||||
state->UpdateImage();
|
||||
}];
|
||||
[[NSRunLoop mainRunLoop] addTimer:impl_->timer forMode:NSRunLoopCommonModes];
|
||||
return true;
|
||||
}
|
||||
bool MacPrivacyCursor::Healthy() const { return impl_->timer && impl_->healthy; }
|
||||
uint32_t MacPrivacyCursor::WindowId() const {
|
||||
return impl_->panel ? static_cast<uint32_t>(impl_->panel.windowNumber) : 0;
|
||||
}
|
||||
void MacPrivacyCursor::Restore() {
|
||||
[impl_->timer invalidate];
|
||||
impl_->timer = nil;
|
||||
[impl_->panel orderOut:nil];
|
||||
[impl_->panel close];
|
||||
impl_->panel = nil;
|
||||
impl_->view = nil;
|
||||
// Balance only our own successful hide calls, including re-hides after
|
||||
// WindowServer changes cursor ownership when a remote click activates an app.
|
||||
while (impl_->hide_count) {
|
||||
CGDisplayShowCursor(kCGDirectMainDisplay);
|
||||
--impl_->hide_count;
|
||||
}
|
||||
hidden_by_privacy = false;
|
||||
if (impl_->background_changed) {
|
||||
Api().set(impl_->connection, impl_->connection, kBackgroundCursor,
|
||||
impl_->previous_background ? kCFBooleanTrue : kCFBooleanFalse);
|
||||
impl_->background_changed = false;
|
||||
}
|
||||
}
|
||||
bool IsMacPrivacyCursorHidden() { return hidden_by_privacy.load(); }
|
||||
}
|
||||
@@ -7,12 +7,6 @@ namespace crossdesk {
|
||||
ScreenCapturerSck::ScreenCapturerSck() {}
|
||||
ScreenCapturerSck::~ScreenCapturerSck() {}
|
||||
|
||||
void ScreenCapturerSck::SetPrivacyController(PrivacyController* privacy) {
|
||||
privacy_ = privacy;
|
||||
if (screen_capturer_sck_impl_)
|
||||
screen_capturer_sck_impl_->SetPrivacyController(privacy);
|
||||
}
|
||||
|
||||
int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) {
|
||||
if (cb) {
|
||||
on_data_ = cb;
|
||||
@@ -22,7 +16,6 @@ int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) {
|
||||
}
|
||||
|
||||
screen_capturer_sck_impl_ = CreateScreenCapturerSck();
|
||||
screen_capturer_sck_impl_->SetPrivacyController(privacy_);
|
||||
const int ret = screen_capturer_sck_impl_->Init(fps, on_data_);
|
||||
if (ret != 0) {
|
||||
screen_capturer_sck_impl_.reset();
|
||||
|
||||
@@ -25,7 +25,6 @@ class ScreenCapturerSck : public ScreenCapturer {
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
void SetPrivacyController(PrivacyController* privacy) override;
|
||||
int Destroy() override;
|
||||
int Start(bool show_cursor) override;
|
||||
int Stop() override;
|
||||
@@ -58,7 +57,6 @@ class ScreenCapturerSck : public ScreenCapturer {
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> screen_capturer_sck_impl_;
|
||||
PrivacyController* privacy_ = nullptr;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
#endif
|
||||
@@ -26,9 +26,6 @@
|
||||
#include "display_info.h"
|
||||
#include <display_stream_id.h>
|
||||
#include "rd_log.h"
|
||||
#include "privacy_controller.h"
|
||||
#include "../privacy/privacy_capture_state.h"
|
||||
#include <algorithm>
|
||||
|
||||
using namespace crossdesk;
|
||||
|
||||
@@ -113,18 +110,13 @@ API_AVAILABLE(macos(14.0))
|
||||
|
||||
- (instancetype)initWithCapturer:(ScreenCapturerSckImpl *)capturer;
|
||||
|
||||
- (void)onShareableContentCreated:(SCShareableContent *)content generation:(uint64_t)generation
|
||||
revision:(uint64_t)revision;
|
||||
- (void)onShareableContentCreated:(SCShareableContent *)content;
|
||||
|
||||
// Called just before the capturer is destroyed. This avoids a dangling pointer,
|
||||
// and prevents any new calls into a deleted capturer. If any method-call on the
|
||||
// capturer is currently running on a different thread, this blocks until it
|
||||
// completes.
|
||||
- (void)releaseCapturer;
|
||||
- (void)privacyCoversChanged:(NSNotification*)notification;
|
||||
- (void)configurationFinished:(SCStream*)stream generation:(uint64_t)generation
|
||||
revision:(uint64_t)revision complete:(BOOL)complete
|
||||
configuration:(SCStreamConfiguration*)configuration error:(NSError*)error;
|
||||
@end
|
||||
|
||||
class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
@@ -137,7 +129,6 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
|
||||
public:
|
||||
int Init(const int fps, cb_desktop_data cb) override;
|
||||
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
|
||||
|
||||
int Start(bool show_cursor) override;
|
||||
|
||||
@@ -165,30 +156,11 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer {
|
||||
bool capture_requested_ = false;
|
||||
bool invalid_stream_id_logged_ = false;
|
||||
bool native_output_logged_ = false;
|
||||
PrivacyController* privacy_ = nullptr;
|
||||
uint64_t configuration_generation_ = 0;
|
||||
bool configuration_in_flight_ = false;
|
||||
bool reconfigure_pending_ = false;
|
||||
SCStreamConfiguration* __strong applied_configuration_ = nil;
|
||||
|
||||
public:
|
||||
// Called by SckHelper when shareable content is returned by ScreenCaptureKit. `content` will be
|
||||
// nil if an error occurred. May run on an arbitrary thread.
|
||||
void OnShareableContentCreated(SCShareableContent *content, uint64_t generation,
|
||||
uint64_t revision);
|
||||
void ConfigurationFinished(SCStream* source, uint64_t generation, uint64_t revision,
|
||||
bool complete, SCStreamConfiguration* configuration,
|
||||
NSError* error);
|
||||
void RetryPrivacyFilter();
|
||||
void PrivacyCoversChanged() { StartOrReconfigureCapturer(); }
|
||||
void CaptureStopped(SCStream* source, NSError* error) {
|
||||
{
|
||||
std::lock_guard lock(lock_);
|
||||
if (!capture_requested_ || source != stream_) return;
|
||||
}
|
||||
LOG_ERROR("ScreenCaptureKit stream stopped: {}", NSErrorToString(error));
|
||||
if (privacy_) privacy_->Fail("ScreenCaptureKit stream stopped");
|
||||
}
|
||||
void OnShareableContentCreated(SCShareableContent *content);
|
||||
// Called by SckHelper to notify of a newly captured frame. May run on an arbitrary thread.
|
||||
// void OnNewIOSurface(IOSurfaceRef io_surface, CFDictionaryRef attachment);
|
||||
void OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer, CFDictionaryRef attachment);
|
||||
@@ -417,7 +389,6 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) {
|
||||
capture_requested_ = true;
|
||||
invalid_stream_id_logged_ = false;
|
||||
}
|
||||
if (privacy_) privacy_->CaptureChanged(true);
|
||||
StartOrReconfigureCapturer();
|
||||
return 0;
|
||||
}
|
||||
@@ -475,7 +446,6 @@ int ScreenCapturerSckImpl::ResetToInitialMonitor() {
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Destroy() {
|
||||
if (privacy_) privacy_->CaptureChanged(false);
|
||||
SckHelper *helper_to_release = nil;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
@@ -485,9 +455,6 @@ int ScreenCapturerSckImpl::Destroy() {
|
||||
stream_ = nil;
|
||||
}
|
||||
capture_requested_ = false;
|
||||
configuration_in_flight_ = reconfigure_pending_ = false;
|
||||
applied_configuration_ = nil;
|
||||
++configuration_generation_;
|
||||
current_display_ = 0;
|
||||
current_stream_id_.clear();
|
||||
permanent_error_ = false;
|
||||
@@ -502,12 +469,8 @@ int ScreenCapturerSckImpl::Destroy() {
|
||||
}
|
||||
|
||||
int ScreenCapturerSckImpl::Stop() {
|
||||
if (privacy_) privacy_->CaptureChanged(false);
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
capture_requested_ = false;
|
||||
configuration_in_flight_ = reconfigure_pending_ = false;
|
||||
applied_configuration_ = nil;
|
||||
++configuration_generation_;
|
||||
if (stream_) {
|
||||
LOG_INFO("Stopping stream");
|
||||
[stream_ stopCaptureWithCompletionHandler:nil];
|
||||
@@ -518,36 +481,24 @@ int ScreenCapturerSckImpl::Stop() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *content,
|
||||
uint64_t generation, uint64_t revision) {
|
||||
void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *content) {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!capture_requested_ || generation != configuration_generation_) {
|
||||
if (!capture_requested_) {
|
||||
LOG_INFO("Ignoring stale ScreenCaptureKit display refresh after stop");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// The window list belongs to the privacy revision present when the request
|
||||
// was issued. A newer cover must be enumerated by a new request.
|
||||
if (GetMacPrivacyCaptureState().revision != revision) {
|
||||
StartOrReconfigureCapturer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
LOG_ERROR("getShareableContent failed");
|
||||
if (privacy_) privacy_->Fail("Could not refresh ScreenCaptureKit privacy content");
|
||||
std::lock_guard lock(lock_);
|
||||
if (!stream_) permanent_error_ = true;
|
||||
permanent_error_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!content.displays || content.displays.count == 0) {
|
||||
LOG_ERROR("getShareableContent returned no displays");
|
||||
if (privacy_) privacy_->Fail("No shareable displays available for privacy");
|
||||
std::lock_guard lock(lock_);
|
||||
if (!stream_) permanent_error_ = true;
|
||||
permanent_error_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -555,7 +506,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
bool show_cursor = false;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!capture_requested_ || generation != configuration_generation_) return;
|
||||
if (!capture_requested_) return;
|
||||
|
||||
int logical_index = current_monitor_index_;
|
||||
if (logical_index < 0 ||
|
||||
@@ -625,40 +576,8 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
return;
|
||||
}
|
||||
|
||||
const auto privacy_capture = GetMacPrivacyCaptureState();
|
||||
// The cursor sprite is included only when this stream embeds the cursor.
|
||||
// Other controllers receive the unchanged cursor metadata separately.
|
||||
auto excluded_ids = privacy_capture.excluded_windows;
|
||||
if (!show_cursor && privacy_capture.cursor_window)
|
||||
excluded_ids.push_back(privacy_capture.cursor_window);
|
||||
NSMutableArray<SCWindow*>* excluded = [NSMutableArray array];
|
||||
for (SCWindow* window in content.windows) {
|
||||
if (std::find(excluded_ids.begin(), excluded_ids.end(), window.windowID) != excluded_ids.end())
|
||||
[excluded addObject:window];
|
||||
}
|
||||
// Newly registered transparent covers may need another enumeration. Never
|
||||
// acknowledge a partial exclusion or remove an active cover's filter.
|
||||
const bool privacy_filter_complete = excluded.count == excluded_ids.size();
|
||||
LOG_INFO("macOS privacy filter: generation={}, revision={}, requested={}, excluded={}",
|
||||
generation, privacy_capture.revision, excluded_ids.size(),
|
||||
excluded.count);
|
||||
if (!privacy_filter_complete) {
|
||||
bool have_stream;
|
||||
{ std::lock_guard lock(lock_); have_stream = stream_ != nil; }
|
||||
if (privacy_capture.applied_revision == privacy_capture.revision) {
|
||||
if (privacy_) privacy_->Fail("ScreenCaptureKit could not enumerate every privacy cover");
|
||||
return; // Preserve the existing exclusion until recovery removes covers.
|
||||
}
|
||||
if (have_stream) {
|
||||
RetryPrivacyFilter();
|
||||
return; // Transparent covers are still being registered by WindowServer.
|
||||
}
|
||||
// Start ordinary capture even if a newly created, still-transparent cover
|
||||
// is not enumerable yet. Retry exclusion after start; never acknowledge it.
|
||||
[excluded removeAllObjects];
|
||||
}
|
||||
SCContentFilter *filter = [[SCContentFilter alloc] initWithDisplay:captured_display
|
||||
excludingWindows:excluded];
|
||||
excludingWindows:@[]];
|
||||
if (!filter) {
|
||||
LOG_ERROR("Failed to create SCContentFilter");
|
||||
permanent_error_ = true;
|
||||
@@ -679,39 +598,13 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
config.captureResolution = SCCaptureResolutionAutomatic;
|
||||
config.minimumFrameInterval = CMTimeMake(1, fps_);
|
||||
|
||||
std::unique_lock<std::mutex> lock(lock_);
|
||||
if (!capture_requested_ || generation != configuration_generation_) return;
|
||||
if (configuration_in_flight_) { reconfigure_pending_ = true; return; }
|
||||
if (GetMacPrivacyCaptureState().revision != revision) {
|
||||
lock.unlock();
|
||||
RetryPrivacyFilter();
|
||||
return;
|
||||
}
|
||||
configuration_in_flight_ = true;
|
||||
std::lock_guard<std::mutex> lock(lock_);
|
||||
if (!capture_requested_) return;
|
||||
|
||||
if (stream_) {
|
||||
SckHelper* helper = helper_;
|
||||
SCStream* source = stream_;
|
||||
auto finished = ^(NSError* error) {
|
||||
[helper configurationFinished:source generation:generation revision:revision
|
||||
complete:privacy_filter_complete configuration:config error:error];
|
||||
};
|
||||
// Apply configuration before its filter and acknowledge the final update.
|
||||
// Privacy-only toggles do not need a configuration change.
|
||||
const bool same_config = applied_configuration_ &&
|
||||
applied_configuration_.width == config.width &&
|
||||
applied_configuration_.height == config.height &&
|
||||
applied_configuration_.showsCursor == config.showsCursor &&
|
||||
applied_configuration_.pixelFormat == config.pixelFormat &&
|
||||
CMTimeCompare(applied_configuration_.minimumFrameInterval, config.minimumFrameInterval) == 0;
|
||||
if (same_config) {
|
||||
[source updateContentFilter:filter completionHandler:finished];
|
||||
} else {
|
||||
[source updateConfiguration:config completionHandler:^(NSError* error) {
|
||||
if (error) finished(error);
|
||||
else [source updateContentFilter:filter completionHandler:finished];
|
||||
}];
|
||||
}
|
||||
LOG_INFO("Updating stream configuration");
|
||||
[stream_ updateContentFilter:filter completionHandler:nil];
|
||||
[stream_ updateConfiguration:config completionHandler:nil];
|
||||
} else {
|
||||
stream_ = [[SCStream alloc] initWithFilter:filter configuration:config delegate:helper_];
|
||||
|
||||
@@ -725,62 +618,28 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten
|
||||
error:&add_stream_output_error];
|
||||
|
||||
if (!add_stream_output_result) {
|
||||
configuration_in_flight_ = false;
|
||||
stream_ = nil;
|
||||
LOG_ERROR("addStreamOutput failed: {}", NSErrorToString(add_stream_output_error));
|
||||
permanent_error_ = true;
|
||||
return;
|
||||
}
|
||||
|
||||
SckHelper* helper = helper_;
|
||||
SCStream* source = stream_;
|
||||
auto handler = ^(NSError *error) {
|
||||
[helper configurationFinished:source generation:generation revision:revision
|
||||
complete:privacy_filter_complete configuration:config error:error];
|
||||
if (error) {
|
||||
// It should be safe to access `this` here, because the C++ destructor
|
||||
// calls stopCaptureWithCompletionHandler on the stream, which cancels
|
||||
// this handler.
|
||||
permanent_error_ = true;
|
||||
LOG_ERROR("startCaptureWithCompletionHandler failed: {}", NSErrorToString(error));
|
||||
} else {
|
||||
LOG_INFO("Capture started");
|
||||
}
|
||||
};
|
||||
|
||||
[stream_ startCaptureWithCompletionHandler:handler];
|
||||
}
|
||||
}
|
||||
|
||||
void ScreenCapturerSckImpl::RetryPrivacyFilter() {
|
||||
SckHelper* helper;
|
||||
{
|
||||
std::lock_guard lock(lock_);
|
||||
if (!capture_requested_) return;
|
||||
helper = helper_;
|
||||
}
|
||||
// The retained helper safely ignores this callback after Destroy().
|
||||
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC),
|
||||
dispatch_get_main_queue(), ^{ [helper privacyCoversChanged:nil]; });
|
||||
}
|
||||
|
||||
void ScreenCapturerSckImpl::ConfigurationFinished(SCStream* source, uint64_t generation,
|
||||
uint64_t revision, bool complete,
|
||||
SCStreamConfiguration* configuration,
|
||||
NSError* error) {
|
||||
bool refresh = false;
|
||||
{
|
||||
std::lock_guard lock(lock_);
|
||||
if (!capture_requested_ || source != stream_) return;
|
||||
configuration_in_flight_ = false;
|
||||
const auto current = GetMacPrivacyCaptureState();
|
||||
refresh = reconfigure_pending_ || current.revision != revision;
|
||||
reconfigure_pending_ = false;
|
||||
if (!error) {
|
||||
applied_configuration_ = configuration;
|
||||
if (complete && !refresh && generation == configuration_generation_)
|
||||
AcknowledgeMacPrivacyCapture(revision);
|
||||
refresh = refresh || !complete;
|
||||
}
|
||||
LOG_INFO("macOS privacy filter completed: generation={}, revision={}, complete={}, refresh={}, error={}",
|
||||
generation, revision, complete, refresh, NSErrorToString(error));
|
||||
}
|
||||
if (error && privacy_)
|
||||
privacy_->Fail("ScreenCaptureKit privacy filter: " + NSErrorToString(error));
|
||||
if (refresh) RetryPrivacyFilter();
|
||||
}
|
||||
|
||||
void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer,
|
||||
CFDictionaryRef attachment) {
|
||||
(void)attachment;
|
||||
@@ -868,18 +727,7 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
}
|
||||
}
|
||||
|
||||
SckHelper *local_helper;
|
||||
uint64_t generation;
|
||||
uint64_t revision;
|
||||
{
|
||||
std::lock_guard lock(lock_);
|
||||
if (!capture_requested_) return;
|
||||
if (configuration_in_flight_) { reconfigure_pending_ = true; return; }
|
||||
reconfigure_pending_ = false;
|
||||
local_helper = helper_;
|
||||
generation = ++configuration_generation_;
|
||||
revision = GetMacPrivacyCaptureState().revision;
|
||||
}
|
||||
SckHelper *local_helper = helper_;
|
||||
if (!local_helper) {
|
||||
LOG_ERROR("Cannot reconfigure capturer: helper is null");
|
||||
return;
|
||||
@@ -888,14 +736,12 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
auto handler = ^(SCShareableContent *content, NSError *error) {
|
||||
if (error) {
|
||||
LOG_ERROR("getShareableContent failed: {}", NSErrorToString(error));
|
||||
[local_helper onShareableContentCreated:nil generation:generation revision:revision];
|
||||
[local_helper onShareableContentCreated:nil];
|
||||
return;
|
||||
}
|
||||
[local_helper onShareableContentCreated:content generation:generation revision:revision];
|
||||
[local_helper onShareableContentCreated:content];
|
||||
};
|
||||
[SCShareableContent getShareableContentExcludingDesktopWindows:NO
|
||||
onScreenWindowsOnly:NO
|
||||
completionHandler:handler];
|
||||
[SCShareableContent getShareableContentWithCompletionHandler:handler];
|
||||
}
|
||||
|
||||
@implementation SckHelper {
|
||||
@@ -909,28 +755,14 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_capturer = capturer;
|
||||
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(privacyCoversChanged:)
|
||||
name:@"CrossDeskPrivacyCoversChanged" object:nil];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)privacyCoversChanged:(NSNotification*)notification {
|
||||
std::lock_guard<std::mutex> lock(_capturer_lock);
|
||||
if (_capturer) _capturer->PrivacyCoversChanged();
|
||||
}
|
||||
- (void)configurationFinished:(SCStream*)stream generation:(uint64_t)generation
|
||||
revision:(uint64_t)revision complete:(BOOL)complete
|
||||
configuration:(SCStreamConfiguration*)configuration error:(NSError*)error {
|
||||
std::lock_guard<std::mutex> lock(_capturer_lock);
|
||||
if (_capturer)
|
||||
_capturer->ConfigurationFinished(stream, generation, revision, complete, configuration, error);
|
||||
}
|
||||
- (void)onShareableContentCreated:(SCShareableContent *)content generation:(uint64_t)generation
|
||||
revision:(uint64_t)revision {
|
||||
- (void)onShareableContentCreated:(SCShareableContent *)content {
|
||||
std::lock_guard<std::mutex> lock(_capturer_lock);
|
||||
if (_capturer) {
|
||||
_capturer->OnShareableContentCreated(content, generation, revision);
|
||||
_capturer->OnShareableContentCreated(content);
|
||||
} else {
|
||||
LOG_ERROR("Invalid capturer");
|
||||
}
|
||||
@@ -965,16 +797,10 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() {
|
||||
}
|
||||
|
||||
- (void)releaseCapturer {
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||||
std::lock_guard<std::mutex> lock(_capturer_lock);
|
||||
_capturer = nullptr;
|
||||
}
|
||||
|
||||
- (void)stream:(SCStream*)stream didStopWithError:(NSError*)error {
|
||||
std::lock_guard<std::mutex> lock(_capturer_lock);
|
||||
if (_capturer) _capturer->CaptureStopped(stream, error);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
std::unique_ptr<ScreenCapturer> ScreenCapturerSck::CreateScreenCapturerSck() {
|
||||
|
||||
@@ -114,9 +114,6 @@ void GuiRuntime::HandleWindowsServiceIntegration() {
|
||||
|
||||
bool force_broadcast = false;
|
||||
if (pending_windows_service_sas_.exchange(false, std::memory_order_relaxed)) {
|
||||
if (privacy_.Engaged()) {
|
||||
privacy_.Fail("Security shortcut requested; privacy screen will turn off");
|
||||
}
|
||||
const std::string response =
|
||||
QueryCrossDeskService("sas", kWindowsServiceSasTimeoutMs);
|
||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||
|
||||
@@ -1,20 +1,16 @@
|
||||
#include "mouse_controller.h"
|
||||
|
||||
#include <Windows.h>
|
||||
#include <remote_action.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <Windows.h>
|
||||
|
||||
#include "../../windows_thread_dpi.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
PlatformMouseController::PlatformMouseController() {}
|
||||
|
||||
PlatformMouseController::~PlatformMouseController() { Destroy(); }
|
||||
PlatformMouseController::~PlatformMouseController() {}
|
||||
|
||||
int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) {
|
||||
display_info_list_ = display_info_list;
|
||||
@@ -22,27 +18,10 @@ int PlatformMouseController::Init(std::vector<DisplayInfo> display_info_list) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int PlatformMouseController::Destroy() { return ReleasePressedButtons(); }
|
||||
|
||||
int PlatformMouseController::ReleasePressedButtons() {
|
||||
const unsigned pressed = pressed_buttons_.load();
|
||||
if (!pressed) return 0;
|
||||
INPUT input{};
|
||||
input.type = INPUT_MOUSE;
|
||||
input.mi.dwExtraInfo = kInjectedMouseInputMarker;
|
||||
if (pressed & 1) input.mi.dwFlags |= MOUSEEVENTF_LEFTUP;
|
||||
if (pressed & 2) input.mi.dwFlags |= MOUSEEVENTF_RIGHTUP;
|
||||
if (pressed & 4) input.mi.dwFlags |= MOUSEEVENTF_MIDDLEUP;
|
||||
if (SendInput(1, &input, sizeof(INPUT)) != 1) {
|
||||
LOG_WARN("Release remote mouse buttons failed, error={}", GetLastError());
|
||||
return -1;
|
||||
}
|
||||
pressed_buttons_.fetch_and(~pressed);
|
||||
return 0;
|
||||
}
|
||||
int PlatformMouseController::Destroy() { return 0; }
|
||||
|
||||
int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
int display_index) {
|
||||
int display_index) {
|
||||
if (display_index < 0 ||
|
||||
display_index >= static_cast<int>(display_info_list_.size())) {
|
||||
LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}",
|
||||
@@ -51,82 +30,68 @@ int PlatformMouseController::SendMouseCommand(RemoteAction remote_action,
|
||||
}
|
||||
|
||||
INPUT ip = {0};
|
||||
ScopedWindowsPhysicalCoordinates physical_coordinates;
|
||||
|
||||
if (remote_action.type != ControlType::mouse) return 0;
|
||||
if (!std::isfinite(remote_action.m.x) || !std::isfinite(remote_action.m.y))
|
||||
return -1;
|
||||
ip.type = INPUT_MOUSE;
|
||||
ip.mi.dx = (LONG)(std::clamp(remote_action.m.x, 0.0f, 1.0f) *
|
||||
(display_info_list_[display_index].width - 1)) +
|
||||
display_info_list_[display_index].left;
|
||||
ip.mi.dy = (LONG)(std::clamp(remote_action.m.y, 0.0f, 1.0f) *
|
||||
(display_info_list_[display_index].height - 1)) +
|
||||
display_info_list_[display_index].top;
|
||||
if (remote_action.type == ControlType::mouse) {
|
||||
ip.type = INPUT_MOUSE;
|
||||
ip.mi.dx =
|
||||
(LONG)(remote_action.m.x * display_info_list_[display_index].width) +
|
||||
display_info_list_[display_index].left;
|
||||
ip.mi.dy =
|
||||
(LONG)(remote_action.m.y * display_info_list_[display_index].height) +
|
||||
display_info_list_[display_index].top;
|
||||
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN;
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTUP;
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN;
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEUP;
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_WHEEL;
|
||||
ip.mi.mouseData = remote_action.m.s * 120;
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_HWHEEL;
|
||||
ip.mi.mouseData = remote_action.m.s * 120;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
switch (remote_action.m.flag) {
|
||||
case MouseFlag::left_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::left_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_LEFTUP | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::right_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::right_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_RIGHTUP | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::middle_down:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEDOWN | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::middle_up:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MIDDLEUP | MOUSEEVENTF_ABSOLUTE;
|
||||
break;
|
||||
case MouseFlag::wheel_vertical:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_WHEEL;
|
||||
ip.mi.mouseData = remote_action.m.s * 120;
|
||||
break;
|
||||
case MouseFlag::wheel_horizontal:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_HWHEEL;
|
||||
ip.mi.mouseData = remote_action.m.s * 120;
|
||||
break;
|
||||
default:
|
||||
ip.mi.dwFlags = MOUSEEVENTF_MOVE;
|
||||
break;
|
||||
}
|
||||
|
||||
const int virtual_left = GetSystemMetrics(SM_XVIRTUALSCREEN);
|
||||
const int virtual_top = GetSystemMetrics(SM_YVIRTUALSCREEN);
|
||||
const int virtual_width = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
const int virtual_height = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
if (virtual_width <= 0 || virtual_height <= 0) {
|
||||
LOG_WARN("Remote mouse skipped: invalid virtual desktop geometry");
|
||||
return -1;
|
||||
ip.mi.time = 0;
|
||||
|
||||
if (!SetCursorPos(ip.mi.dx, ip.mi.dy)) {
|
||||
LOG_WARN("SetCursorPos failed for mouse x={}, y={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, static_cast<int>(remote_action.m.flag),
|
||||
GetLastError());
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (ip.mi.dwFlags != MOUSEEVENTF_MOVE) {
|
||||
UINT sent = SendInput(1, &ip, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN(
|
||||
"SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), GetLastError());
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Pixel centres in the virtual desktop, including negative monitor origins.
|
||||
// SetCursorPos cannot carry dwExtraInfo through the privacy mouse hook.
|
||||
ip.mi.dx = static_cast<LONG>(
|
||||
(static_cast<int64_t>(ip.mi.dx - virtual_left) * 65536 + 32768) /
|
||||
virtual_width);
|
||||
ip.mi.dy = static_cast<LONG>(
|
||||
(static_cast<int64_t>(ip.mi.dy - virtual_top) * 65536 + 32768) /
|
||||
virtual_height);
|
||||
ip.mi.dwFlags |=
|
||||
MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK;
|
||||
ip.mi.dwExtraInfo = kInjectedMouseInputMarker;
|
||||
const UINT sent = SendInput(1, &ip, sizeof(INPUT));
|
||||
if (sent != 1) {
|
||||
LOG_WARN("SendInput failed for mouse x={}, y={}, wheel={}, flag={}, err={}",
|
||||
ip.mi.dx, ip.mi.dy, remote_action.m.s,
|
||||
static_cast<int>(remote_action.m.flag), GetLastError());
|
||||
return -1;
|
||||
}
|
||||
if (ip.mi.dwFlags & MOUSEEVENTF_LEFTDOWN) pressed_buttons_.fetch_or(1);
|
||||
if (ip.mi.dwFlags & MOUSEEVENTF_RIGHTDOWN) pressed_buttons_.fetch_or(2);
|
||||
if (ip.mi.dwFlags & MOUSEEVENTF_MIDDLEDOWN) pressed_buttons_.fetch_or(4);
|
||||
if (ip.mi.dwFlags & MOUSEEVENTF_LEFTUP) pressed_buttons_.fetch_and(~1u);
|
||||
if (ip.mi.dwFlags & MOUSEEVENTF_RIGHTUP) pressed_buttons_.fetch_and(~2u);
|
||||
if (ip.mi.dwFlags & MOUSEEVENTF_MIDDLEUP) pressed_buttons_.fetch_and(~4u);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
#define _MOUSE_CONTROLLER_H_
|
||||
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
|
||||
#include <remote_action.h>
|
||||
|
||||
@@ -26,11 +25,9 @@ class PlatformMouseController final : public MouseController {
|
||||
virtual int Init(std::vector<DisplayInfo> display_info_list);
|
||||
virtual int Destroy();
|
||||
virtual int SendMouseCommand(RemoteAction remote_action, int display_index);
|
||||
int ReleasePressedButtons();
|
||||
|
||||
private:
|
||||
std::vector<DisplayInfo> display_info_list_;
|
||||
std::atomic<unsigned> pressed_buttons_{0};
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
|
||||
@@ -9,7 +9,6 @@ namespace crossdesk {
|
||||
// keyboard input so the controller hook can ignore only its own injections
|
||||
// while still accepting input from accessibility tools or remote sessions.
|
||||
inline constexpr std::uintptr_t kInjectedKeyboardInputMarker = 0x4353444B;
|
||||
inline constexpr std::uintptr_t kInjectedMouseInputMarker = 0x4353444D;
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
|
||||
@@ -1,293 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <Windows.h>
|
||||
#include <dwmapi.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cwchar>
|
||||
#include <vector>
|
||||
|
||||
#include "../../windows_thread_dpi.h"
|
||||
#include "../privacy_band_ipc.h"
|
||||
#include "privacy_cover_renderer.h"
|
||||
|
||||
namespace {
|
||||
using namespace crossdesk;
|
||||
constexpr wchar_t kWindowClass[] = L"CrossDesk.HighBandPrivacy";
|
||||
constexpr DWORD kCoverStyles = WS_EX_TOPMOST | WS_EX_NOACTIVATE |
|
||||
WS_EX_TOOLWINDOW | WS_EX_LAYERED |
|
||||
WS_EX_TRANSPARENT;
|
||||
HMODULE module = nullptr;
|
||||
|
||||
bool SameRect(const RECT& a, const RECT& b) {
|
||||
return a.left == b.left && a.top == b.top && a.right == b.right &&
|
||||
a.bottom == b.bottom;
|
||||
}
|
||||
|
||||
class Windows {
|
||||
public:
|
||||
explicit Windows(PrivacyBandShared* shared) : shared_(shared) {}
|
||||
~Windows() {
|
||||
for (const auto& window : windows_)
|
||||
DestroyWindow(reinterpret_cast<HWND>(window.hwnd));
|
||||
if (atom_) UnregisterClassW(kWindowClass, module);
|
||||
}
|
||||
bool Fail(const wchar_t* operation, DWORD code = GetLastError()) {
|
||||
wcsncpy_s(shared_->operation, operation, _TRUNCATE);
|
||||
InterlockedExchange(&shared_->error, code ? code : ERROR_GEN_FAILURE);
|
||||
return false;
|
||||
}
|
||||
bool Initialize() {
|
||||
const HMODULE user = GetModuleHandleW(L"user32.dll");
|
||||
create_ = reinterpret_cast<CreatePrivacyWindowInBand>(
|
||||
GetProcAddress(user, "CreateWindowInBand"));
|
||||
get_band_ = reinterpret_cast<GetPrivacyWindowBand>(
|
||||
GetProcAddress(user, "GetWindowBand"));
|
||||
if (!create_ || !get_band_)
|
||||
return Fail(L"Resolve high-band APIs", ERROR_PROC_NOT_FOUND);
|
||||
WNDCLASSEXW cls{sizeof(cls)};
|
||||
cls.hInstance = module;
|
||||
cls.lpfnWndProc = WindowProc;
|
||||
cls.lpszClassName = kWindowClass;
|
||||
cls.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
|
||||
atom_ = RegisterClassExW(&cls);
|
||||
return atom_ ? Reconcile() : Fail(L"Register high-band window class");
|
||||
}
|
||||
bool Reconcile() {
|
||||
std::vector<RECT> monitors;
|
||||
if (!EnumDisplayMonitors(
|
||||
nullptr, nullptr,
|
||||
[](HMONITOR, HDC, LPRECT rect, LPARAM data) -> BOOL {
|
||||
auto& result = *reinterpret_cast<std::vector<RECT>*>(data);
|
||||
if (result.size() >= kPrivacyMaxMonitors) return FALSE;
|
||||
result.push_back(*rect);
|
||||
return TRUE;
|
||||
},
|
||||
reinterpret_cast<LPARAM>(&monitors)) ||
|
||||
monitors.empty())
|
||||
return Fail(L"Enumerate high-band displays");
|
||||
bool changed = monitors.size() != windows_.size();
|
||||
for (const auto& rect : monitors) {
|
||||
const auto found =
|
||||
std::find_if(windows_.begin(), windows_.end(),
|
||||
[&](const auto& w) { return SameRect(w.rect, rect); });
|
||||
if (found != windows_.end()) continue;
|
||||
changed = true;
|
||||
HWND hwnd = create_(kCoverStyles, atom_, L"CrossDesk Privacy", WS_POPUP,
|
||||
rect.left, rect.top, rect.right - rect.left,
|
||||
rect.bottom - rect.top, nullptr, nullptr, module,
|
||||
this, kPrivacyWindowBand);
|
||||
if (!hwnd) return Fail(L"CreateWindowInBand ZBID_ABOVELOCK_UX");
|
||||
windows_.push_back({reinterpret_cast<uint64_t>(hwnd), rect});
|
||||
if (!SetLayeredWindowAttributes(hwnd, 0, 255, LWA_ALPHA))
|
||||
return Fail(L"Make high-band cover opaque");
|
||||
if (!SetWindowDisplayAffinity(hwnd, 0x11))
|
||||
return Fail(L"Exclude high-band cover from capture");
|
||||
const BOOL yes = TRUE;
|
||||
const HRESULT peek = DwmSetWindowAttribute(hwnd, DWMWA_EXCLUDED_FROM_PEEK,
|
||||
&yes, sizeof(yes));
|
||||
if (FAILED(peek)) return Fail(L"Exclude high-band cover from Peek", peek);
|
||||
if (!SetWindowPos(hwnd, nullptr, rect.left, rect.top,
|
||||
rect.right - rect.left, rect.bottom - rect.top,
|
||||
SWP_NOZORDER | SWP_NOACTIVATE | SWP_SHOWWINDOW) ||
|
||||
!RedrawWindow(hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW))
|
||||
return Fail(L"Present high-band cover");
|
||||
}
|
||||
if (changed) {
|
||||
const HRESULT result = DwmFlush();
|
||||
if (FAILED(result))
|
||||
return Fail(L"Flush high-band cover presentation", result);
|
||||
// New areas are covered before old windows are removed.
|
||||
for (auto it = windows_.begin(); it != windows_.end();) {
|
||||
if (std::none_of(
|
||||
monitors.begin(), monitors.end(),
|
||||
[&](const auto& rect) { return SameRect(rect, it->rect); })) {
|
||||
if (!DestroyWindow(reinterpret_cast<HWND>(it->hwnd)))
|
||||
return Fail(L"Destroy obsolete high-band cover");
|
||||
it = windows_.erase(it);
|
||||
} else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (!Validate()) return false;
|
||||
if (changed) {
|
||||
InterlockedIncrement(&shared_->sequence);
|
||||
shared_->count = static_cast<uint32_t>(windows_.size());
|
||||
std::copy(windows_.begin(), windows_.end(), shared_->windows);
|
||||
InterlockedIncrement(&shared_->sequence);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool Validate() {
|
||||
if (paint_failed_) return false;
|
||||
for (const auto& w : windows_) {
|
||||
HWND hwnd = reinterpret_cast<HWND>(w.hwnd);
|
||||
DWORD band = 0, affinity = 0, flags = 0, cloaked = 0;
|
||||
BYTE alpha = 0;
|
||||
RECT rect{};
|
||||
if ((GetWindowLongPtrW(hwnd, GWL_EXSTYLE) & kCoverStyles) !=
|
||||
kCoverStyles ||
|
||||
!get_band_(hwnd, &band) || band != kPrivacyWindowBand ||
|
||||
!GetWindowDisplayAffinity(hwnd, &affinity) || affinity != 0x11 ||
|
||||
!GetLayeredWindowAttributes(hwnd, nullptr, &alpha, &flags) ||
|
||||
alpha != 255 || flags != LWA_ALPHA || !IsWindowVisible(hwnd) ||
|
||||
!GetWindowRect(hwnd, &rect) || !SameRect(rect, w.rect) ||
|
||||
FAILED(DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked,
|
||||
sizeof(cloaked))) ||
|
||||
cloaked)
|
||||
return Fail(L"High-band cover lost style/band/affinity/visibility",
|
||||
ERROR_INVALID_STATE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool changed = false;
|
||||
|
||||
private:
|
||||
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT message, WPARAM wp,
|
||||
LPARAM lp) {
|
||||
if (message == WM_NCCREATE) {
|
||||
auto create = reinterpret_cast<CREATESTRUCTW*>(lp);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(create->lpCreateParams));
|
||||
}
|
||||
auto self =
|
||||
reinterpret_cast<Windows*>(GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
if (message == WM_MOUSEACTIVATE) return MA_NOACTIVATE;
|
||||
if (message == WM_NCHITTEST) return HTTRANSPARENT;
|
||||
if (message == WM_SETCURSOR) {
|
||||
SetCursor(nullptr);
|
||||
return TRUE;
|
||||
}
|
||||
if (message == WM_CLOSE) return 0;
|
||||
if ((message == WM_DISPLAYCHANGE || message == WM_DPICHANGED) && self) {
|
||||
self->changed = true;
|
||||
InvalidateRect(hwnd, nullptr, TRUE);
|
||||
return 0;
|
||||
}
|
||||
if (message == WM_PAINT) {
|
||||
PAINTSTRUCT paint{};
|
||||
HDC dc = BeginPaint(hwnd, &paint);
|
||||
RECT rect{};
|
||||
GetClientRect(hwnd, &rect);
|
||||
if (self && !DrawPrivacyCover(dc, rect, PrivacyWindowDpi(hwnd), module,
|
||||
self->shared_->unlock_hint)) {
|
||||
self->paint_failed_ = true;
|
||||
self->Fail(L"Paint privacy logo and unlock instructions");
|
||||
}
|
||||
EndPaint(hwnd, &paint);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, message, wp, lp);
|
||||
}
|
||||
PrivacyBandShared* shared_;
|
||||
bool paint_failed_ = false;
|
||||
std::vector<PrivacyBandWindow> windows_;
|
||||
ATOM atom_ = 0;
|
||||
CreatePrivacyWindowInBand create_ = nullptr;
|
||||
GetPrivacyWindowBand get_band_ = nullptr;
|
||||
};
|
||||
|
||||
DWORD WINAPI Run(void*) {
|
||||
wchar_t bootstrap[256]{};
|
||||
if (!GetEnvironmentVariableW(kPrivacyBandBootstrap, bootstrap, 256))
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
SetEnvironmentVariableW(kPrivacyBandBootstrap, nullptr);
|
||||
std::array<HANDLE, 4> handles{};
|
||||
wchar_t* item = bootstrap;
|
||||
for (size_t i = 0; i < handles.size(); ++i) {
|
||||
wchar_t* end = nullptr;
|
||||
const auto value = wcstoull(item, &end, 10);
|
||||
if (!value || end == item ||
|
||||
(i + 1 == handles.size() ? *end != 0 : *end != L','))
|
||||
return ERROR_INVALID_PARAMETER;
|
||||
handles[i] = reinterpret_cast<HANDLE>(value);
|
||||
item = end + 1;
|
||||
}
|
||||
HANDLE parent = handles[0], stop = handles[1], ready = handles[2],
|
||||
mapping = handles[3];
|
||||
auto shared = static_cast<PrivacyBandShared*>(MapViewOfFile(
|
||||
mapping, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
|
||||
if (!shared) return GetLastError();
|
||||
if (shared->magic != kPrivacyBandMagic ||
|
||||
shared->version != kPrivacyBandVersion ||
|
||||
shared->size != sizeof(PrivacyBandShared) ||
|
||||
shared->broker_pid != GetCurrentProcessId() ||
|
||||
shared->parent_pid != GetProcessId(parent) ||
|
||||
WaitForSingleObject(parent, 0) != WAIT_TIMEOUT) {
|
||||
UnmapViewOfFile(shared);
|
||||
return ERROR_INVALID_DATA;
|
||||
}
|
||||
DWORD exit_code = ERROR_SUCCESS;
|
||||
{
|
||||
ScopedWindowsPhysicalCoordinates dpi;
|
||||
Windows windows(shared);
|
||||
try {
|
||||
bool healthy = dpi.active() && windows.Initialize();
|
||||
const bool initialized = healthy;
|
||||
if (!dpi.active()) windows.Fail(L"Set high-band thread DPI context");
|
||||
InterlockedExchange64(&shared->heartbeat, GetTickCount64());
|
||||
if (healthy) {
|
||||
InterlockedExchange(&shared->error, ERROR_SUCCESS);
|
||||
InterlockedExchange(&shared->active, 1);
|
||||
}
|
||||
SetEvent(ready);
|
||||
auto last_check = GetTickCount64();
|
||||
HANDLE watched[] = {parent, stop};
|
||||
// Once enabled, retain surviving covers on a validation/layout fault.
|
||||
// The parent pauses the session after reading the latched error. Only
|
||||
// an explicit stop or parent death tears the remaining windows down.
|
||||
while (initialized) {
|
||||
const DWORD wait = MsgWaitForMultipleObjectsEx(
|
||||
2, watched, 100, QS_ALLINPUT, MWMO_INPUTAVAILABLE);
|
||||
if (wait == WAIT_OBJECT_0 || wait == WAIT_OBJECT_0 + 1) break;
|
||||
if (wait == WAIT_FAILED) {
|
||||
windows.Fail(L"Wait for high-band lifecycle");
|
||||
break;
|
||||
}
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
if (message.message == WM_QUIT) healthy = false;
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
const auto now = GetTickCount64();
|
||||
if (healthy && (windows.changed || now - last_check >= 250)) {
|
||||
windows.changed = false;
|
||||
last_check = now;
|
||||
healthy = windows.Reconcile();
|
||||
}
|
||||
InterlockedExchange64(&shared->heartbeat, now);
|
||||
}
|
||||
} catch (...) {
|
||||
windows.Fail(L"High-band window thread exception",
|
||||
ERROR_UNHANDLED_EXCEPTION);
|
||||
SetEvent(ready);
|
||||
}
|
||||
exit_code = InterlockedCompareExchange(&shared->error, 0, 0);
|
||||
InterlockedExchange(&shared->active, 0);
|
||||
}
|
||||
UnmapViewOfFile(shared);
|
||||
for (HANDLE handle : handles) CloseHandle(handle);
|
||||
// This module runs only in the dedicated process created for this session.
|
||||
// Exit outside DllMain/loader lock, after HWND destruction on their UI
|
||||
// thread.
|
||||
ExitProcess(exit_code);
|
||||
return exit_code;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, LPVOID) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
module = instance;
|
||||
// Keep thread notifications enabled: this module uses the static CRT.
|
||||
HANDLE thread = CreateThread(nullptr, 0, Run, nullptr, 0, nullptr);
|
||||
if (!thread) return FALSE;
|
||||
CloseHandle(thread);
|
||||
}
|
||||
return TRUE;
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "privacy_cover_renderer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr size_t kInstructionLines = 2;
|
||||
constexpr size_t kBrandLine = kInstructionLines;
|
||||
|
||||
struct PaintResources {
|
||||
HDC dc;
|
||||
int saved;
|
||||
HICON logo = nullptr;
|
||||
std::array<HFONT, kInstructionLines + 1> fonts{};
|
||||
~PaintResources() {
|
||||
// Deselect every owned GDI object before releasing it.
|
||||
if (saved) RestoreDC(dc, saved);
|
||||
for (auto font : fonts)
|
||||
if (font) DeleteObject(font);
|
||||
if (logo) DestroyIcon(logo);
|
||||
}
|
||||
};
|
||||
|
||||
bool Select(HDC dc, HGDIOBJ object) {
|
||||
const auto previous = SelectObject(dc, object);
|
||||
return previous && previous != HGDI_ERROR;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
|
||||
const wchar_t* unlock_hint) {
|
||||
if (!FillRect(dc, &rect, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH))))
|
||||
return false;
|
||||
const int width = rect.right - rect.left;
|
||||
const int height = rect.bottom - rect.top;
|
||||
if (width <= 0 || height <= 0) return false;
|
||||
// Scale with the monitor DPI, shrinking the whole group on small displays.
|
||||
// All coordinates remain in the owning window's physical-pixel context.
|
||||
const double scale = (std::min)({dpi / 96.0, width / 640.0, height / 200.0});
|
||||
const auto px = [scale](int value) {
|
||||
return (std::max)(1, static_cast<int>(value * scale + 0.5));
|
||||
};
|
||||
const int margin = px(32);
|
||||
const int text_width = px(398);
|
||||
PaintResources resources{dc, SaveDC(dc)};
|
||||
if (!resources.saved || !SetBkMode(dc, TRANSPARENT)) return false;
|
||||
|
||||
struct Line {
|
||||
const wchar_t* text;
|
||||
int size;
|
||||
int weight;
|
||||
COLORREF color;
|
||||
RECT bounds{};
|
||||
};
|
||||
std::array<Line, kInstructionLines + 1> lines{{
|
||||
{unlock_hint, 16, FW_NORMAL, RGB(181, 190, 204)},
|
||||
{L"Ctrl + Alt + Shift + F12", 24, FW_SEMIBOLD, RGB(245, 247, 250)},
|
||||
{L"CrossDesk", 14, FW_SEMIBOLD, RGB(245, 247, 250)},
|
||||
}};
|
||||
constexpr UINT flags = DT_LEFT | DT_WORDBREAK | DT_NOPREFIX;
|
||||
for (size_t i = 0; i < lines.size(); ++i) {
|
||||
auto& line = lines[i];
|
||||
resources.fonts[i] =
|
||||
CreateFontW(-px(line.size), 0, 0, 0, line.weight, FALSE, FALSE, FALSE,
|
||||
DEFAULT_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
|
||||
ANTIALIASED_QUALITY, DEFAULT_PITCH, L"Microsoft YaHei UI");
|
||||
if (!resources.fonts[i] || !Select(dc, resources.fonts[i])) return false;
|
||||
line.bounds = {0, 0, text_width, 0};
|
||||
if (!DrawTextW(dc, line.text, -1, &line.bounds, flags | DT_CALCRECT) ||
|
||||
line.bounds.right > text_width)
|
||||
return false;
|
||||
}
|
||||
const int content_height =
|
||||
static_cast<int>(lines[0].bounds.bottom + lines[1].bounds.bottom) +
|
||||
px(12);
|
||||
// Match the icon width to the name; each side keeps its own height.
|
||||
const int brand_gap = px(6);
|
||||
const int brand_width = static_cast<int>(lines[kBrandLine].bounds.right);
|
||||
const int logo_size = brand_width;
|
||||
const int brand_height =
|
||||
logo_size + brand_gap + static_cast<int>(lines[kBrandLine].bounds.bottom);
|
||||
if (logo_size <= 0 ||
|
||||
(std::max)(brand_height, content_height) + 2 * margin > height ||
|
||||
brand_width + px(24) + text_width + 2 * margin > width)
|
||||
return false;
|
||||
resources.logo = static_cast<HICON>(LoadImageW(
|
||||
module, L"IDI_CROSSDESK_LOGO", IMAGE_ICON, logo_size, logo_size, 0));
|
||||
if (!resources.logo) return false;
|
||||
const int brand_left = rect.left + margin;
|
||||
const int left = brand_left + brand_width + px(24);
|
||||
const int brand_top = rect.bottom - margin - brand_height;
|
||||
int top = rect.bottom - margin - content_height;
|
||||
if (!DrawIconEx(dc, brand_left, brand_top, resources.logo, logo_size,
|
||||
logo_size, 0, nullptr, DI_NORMAL))
|
||||
return false;
|
||||
if (!Select(dc, resources.fonts[kBrandLine])) return false;
|
||||
RECT brand_bounds{brand_left, brand_top + logo_size + brand_gap,
|
||||
brand_left + brand_width, rect.bottom - margin};
|
||||
SetTextColor(dc, lines[kBrandLine].color);
|
||||
if (!DrawTextW(dc, lines[kBrandLine].text, -1, &brand_bounds,
|
||||
DT_CENTER | DT_SINGLELINE | DT_NOPREFIX))
|
||||
return false;
|
||||
for (size_t i = 0; i < kInstructionLines; ++i) {
|
||||
const auto& line = lines[i];
|
||||
if (!Select(dc, resources.fonts[i])) return false;
|
||||
RECT bounds{left, top, left + text_width, top + line.bounds.bottom};
|
||||
SetTextColor(dc, line.color);
|
||||
if (!DrawTextW(dc, line.text, -1, &bounds, flags)) return false;
|
||||
top += line.bounds.bottom + px(12);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,20 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_COVER_RENDERER_H_
|
||||
#define _PRIVACY_COVER_RENDERER_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Paints the local, opaque cover. The caller owns the HWND and its DPI context.
|
||||
bool DrawPrivacyCover(HDC dc, const RECT& rect, UINT dpi, HINSTANCE module,
|
||||
const wchar_t* unlock_hint);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_COVER_RENDERER_H_
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdint>
|
||||
#include <cwchar>
|
||||
#include <limits>
|
||||
|
||||
#include "../privacy_cursor_api.h"
|
||||
|
||||
namespace {
|
||||
HANDLE ParseHandle(const wchar_t* text) {
|
||||
wchar_t* end = nullptr;
|
||||
errno = 0;
|
||||
const auto value = std::wcstoull(text, &end, 10);
|
||||
if (errno || !text[0] || *end || text[0] == L'-' || value == 0 ||
|
||||
value > (std::numeric_limits<uintptr_t>::max)())
|
||||
return nullptr;
|
||||
return reinterpret_cast<HANDLE>(static_cast<uintptr_t>(value));
|
||||
}
|
||||
DWORD FailureCode() {
|
||||
const DWORD code = GetLastError();
|
||||
return code ? code : ERROR_GEN_FAILURE;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int wmain(int argc, wchar_t** argv) {
|
||||
if (argc != 5) return ERROR_INVALID_PARAMETER;
|
||||
HANDLE parent = ParseHandle(argv[1]);
|
||||
HANDLE stop = ParseHandle(argv[2]);
|
||||
HANDLE ready = ParseHandle(argv[3]);
|
||||
HANDLE mapping = ParseHandle(argv[4]);
|
||||
// Observe the inherited process OBJECT, never a reused PID or a process name.
|
||||
if (!parent || !stop || !ready || !mapping ||
|
||||
WaitForSingleObject(parent, 0) != WAIT_TIMEOUT ||
|
||||
WaitForSingleObject(stop, 0) != WAIT_TIMEOUT)
|
||||
return ERROR_INVALID_HANDLE;
|
||||
auto shared = static_cast<crossdesk::PrivacyCursorReady*>(MapViewOfFile(
|
||||
mapping, FILE_MAP_WRITE, 0, 0, sizeof(crossdesk::PrivacyCursorReady)));
|
||||
if (!shared) return static_cast<int>(FailureCode());
|
||||
|
||||
crossdesk::PrivacyCursorApi cursor;
|
||||
DWORD error = ERROR_SUCCESS;
|
||||
if (!cursor.Initialize() || !cursor.Hide()) error = FailureCode();
|
||||
InterlockedExchange(&shared->error, static_cast<LONG>(error));
|
||||
if (!SetEvent(ready)) error = FailureCode();
|
||||
if (!error) {
|
||||
HANDLE watched[] = {parent, stop};
|
||||
for (;;) {
|
||||
const DWORD result =
|
||||
MsgWaitForMultipleObjects(2, watched, FALSE, 250, QS_ALLINPUT);
|
||||
if (result == WAIT_OBJECT_0 || result == WAIT_OBJECT_0 + 1) break;
|
||||
if (result == WAIT_OBJECT_0 + 2) {
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
} else if (result != WAIT_TIMEOUT) {
|
||||
error = FailureCode();
|
||||
break;
|
||||
}
|
||||
if (!cursor.Hide()) {
|
||||
error = FailureCode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also runs when the parent is force-terminated or crashes. The helper does
|
||||
// not own any input hooks or privacy HWNDs and never changes magnification.
|
||||
if (!cursor.RestoreAndClose()) error = FailureCode();
|
||||
UnmapViewOfFile(shared);
|
||||
CloseHandle(mapping);
|
||||
CloseHandle(ready);
|
||||
CloseHandle(stop);
|
||||
CloseHandle(parent);
|
||||
return static_cast<int>(error);
|
||||
}
|
||||
@@ -1,573 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include <ShlObj.h>
|
||||
#include <Windows.h>
|
||||
#include <dwmapi.h>
|
||||
#include <winternl.h>
|
||||
#include <wtsapi32.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
|
||||
#include "../windows_thread_dpi.h"
|
||||
#include "privacy_backend.h"
|
||||
#include "privacy_band_guard.h"
|
||||
#include "privacy_cursor_guard.h"
|
||||
#include "privacy_input_guard.h"
|
||||
#include "privacy_window_visibility.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
#ifndef WDA_EXCLUDEFROMCAPTURE
|
||||
#define WDA_EXCLUDEFROMCAPTURE 0x00000011
|
||||
#endif
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr wchar_t kClassName[] = L"CrossDesk.PrivacyWindow";
|
||||
constexpr UINT kEmergency = WM_APP + 71;
|
||||
constexpr int kHotkeyId = 0x4353;
|
||||
|
||||
std::string Error(const char* operation, DWORD code = GetLastError()) {
|
||||
const std::string result = std::string(operation) +
|
||||
" failed (Windows error " + std::to_string(code) +
|
||||
")";
|
||||
LOG_ERROR("Privacy: {}", result);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool DesktopName(HDESK desktop, wchar_t (&name)[256]) {
|
||||
DWORD needed = 0;
|
||||
return desktop && GetUserObjectInformationW(desktop, UOI_NAME, name,
|
||||
sizeof(name), &needed);
|
||||
}
|
||||
|
||||
bool SameRect(const RECT& a, const RECT& b) {
|
||||
return a.left == b.left && a.top == b.top && a.right == b.right &&
|
||||
a.bottom == b.bottom;
|
||||
}
|
||||
|
||||
struct MonitorEnumeration {
|
||||
std::vector<RECT> rects;
|
||||
bool ok = true;
|
||||
};
|
||||
|
||||
BOOL CALLBACK EnumMonitor(HMONITOR monitor, HDC, LPRECT, LPARAM data) {
|
||||
auto& result = *reinterpret_cast<MonitorEnumeration*>(data);
|
||||
MONITORINFO info{sizeof(info)};
|
||||
if (!GetMonitorInfoW(monitor, &info)) {
|
||||
Error("GetMonitorInfoW");
|
||||
result.ok = false;
|
||||
return FALSE;
|
||||
}
|
||||
result.rects.push_back(info.rcMonitor);
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
class WindowsPrivacyBackend final : public PrivacyBackend {
|
||||
public:
|
||||
WindowsPrivacyBackend() {
|
||||
wake_event_ = CreateEventW(nullptr, FALSE, FALSE, nullptr);
|
||||
if (!wake_event_) wake_error_ = Error("CreateEventW privacy commands");
|
||||
}
|
||||
|
||||
~WindowsPrivacyBackend() override {
|
||||
Recover();
|
||||
if (sink_) DestroyWindow(sink_);
|
||||
if (registered_) UnregisterClassW(kClassName, GetModuleHandleW(nullptr));
|
||||
if (wake_event_) CloseHandle(wake_event_);
|
||||
}
|
||||
|
||||
PrivacyCapabilities Query() override {
|
||||
if (!wake_event_) return {false, false, wake_error_};
|
||||
using RtlGetVersionFn = LONG(WINAPI*)(PRTL_OSVERSIONINFOW);
|
||||
const auto rtl = reinterpret_cast<RtlGetVersionFn>(
|
||||
GetProcAddress(GetModuleHandleW(L"ntdll.dll"), "RtlGetVersion"));
|
||||
RTL_OSVERSIONINFOW version{};
|
||||
version.dwOSVersionInfoSize = sizeof(version);
|
||||
if (!rtl || rtl(&version) != 0 || version.dwMajorVersion < 10 ||
|
||||
version.dwBuildNumber < 19041) {
|
||||
return {false, false, "Requires Windows 10 2004 (build 19041) or later"};
|
||||
}
|
||||
BOOL composed = FALSE;
|
||||
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
|
||||
return {false, false, "Desktop composition is unavailable"};
|
||||
}
|
||||
if (GetSystemMetrics(SM_REMOTESESSION)) {
|
||||
return {false, false, "Privacy screen requires a local console session"};
|
||||
}
|
||||
if (!IsWindowsPrivacyDesktopAvailable()) {
|
||||
return {false, false,
|
||||
"Privacy screen requires the unlocked Default desktop"};
|
||||
}
|
||||
QUERY_USER_NOTIFICATION_STATE shell_state{};
|
||||
if (FAILED(SHQueryUserNotificationState(&shell_state)) ||
|
||||
shell_state == QUNS_RUNNING_D3D_FULL_SCREEN) {
|
||||
return {false, false,
|
||||
"Exclusive fullscreen or unknown display presentation cannot "
|
||||
"guarantee privacy coverage"};
|
||||
}
|
||||
std::string high_band_error;
|
||||
if (!PrivacyBandGuard::Available(high_band_error))
|
||||
return {false, false, high_band_error};
|
||||
return {true, true,
|
||||
"High-band privacy screen available"};
|
||||
}
|
||||
|
||||
bool Enable(bool block_input, const PrivacyScreenText& text,
|
||||
std::string& error) override {
|
||||
const auto caps = Query();
|
||||
if (!caps.overlay) {
|
||||
error = caps.reason;
|
||||
return false;
|
||||
}
|
||||
if (!Initialize(error)) return false;
|
||||
emergency_ = false;
|
||||
health_error_.clear();
|
||||
if (!RegisterHotKey(sink_, kHotkeyId,
|
||||
MOD_CONTROL | MOD_ALT | MOD_SHIFT | MOD_NOREPEAT,
|
||||
VK_F12)) {
|
||||
error = Error("RegisterHotKey Ctrl+Alt+Shift+F12");
|
||||
return false;
|
||||
}
|
||||
hotkey_ = true;
|
||||
if (!cursor_guard_.Start(error)) return false;
|
||||
block_input_ = block_input;
|
||||
if (block_input && !input_guard_.Start(sink_, kEmergency, error))
|
||||
return false;
|
||||
if (!band_guard_.Start(text, error)) return false;
|
||||
if (!ReconcileMonitors(error)) return false;
|
||||
event_owner_ = this;
|
||||
for (DWORD event : {EVENT_OBJECT_SHOW, EVENT_OBJECT_REORDER,
|
||||
EVENT_OBJECT_LOCATIONCHANGE}) {
|
||||
auto hook = SetWinEventHook(event, event, nullptr, WindowEvent, 0, 0,
|
||||
WINEVENT_OUTOFCONTEXT);
|
||||
if (!hook) {
|
||||
error = Error("Watch privacy window coverage changes");
|
||||
return false;
|
||||
}
|
||||
window_events_.push_back(hook);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Recover() override {
|
||||
for (auto& hook : window_events_) {
|
||||
if (UnhookWinEvent(hook))
|
||||
hook = nullptr;
|
||||
else
|
||||
Error("Unhook privacy window events");
|
||||
}
|
||||
window_events_.erase(
|
||||
std::remove(window_events_.begin(), window_events_.end(), nullptr),
|
||||
window_events_.end());
|
||||
if (window_events_.empty()) event_owner_ = nullptr;
|
||||
std::string band_error;
|
||||
band_guard_.Stop(band_error);
|
||||
if (band_guard_.stopped()) windows_.clear();
|
||||
input_guard_.Stop();
|
||||
block_input_ = false;
|
||||
if (hotkey_) {
|
||||
if (UnregisterHotKey(sink_, kHotkeyId) ||
|
||||
GetLastError() == ERROR_HOTKEY_NOT_REGISTERED)
|
||||
hotkey_ = false;
|
||||
else
|
||||
Error("UnregisterHotKey");
|
||||
}
|
||||
// The sink lives across sessions. Do not let an already-posted emergency
|
||||
// command from a closed session cancel a subsequent connection.
|
||||
if (sink_) {
|
||||
MSG discarded{};
|
||||
while (PeekMessageW(&discarded, sink_, WM_HOTKEY, WM_HOTKEY, PM_REMOVE)) {
|
||||
}
|
||||
while (
|
||||
PeekMessageW(&discarded, sink_, kEmergency, kEmergency, PM_REMOVE)) {
|
||||
}
|
||||
}
|
||||
last_check_ = 0;
|
||||
last_health_ = {};
|
||||
last_obstruction_ = nullptr;
|
||||
last_obstruction_log_ = 0;
|
||||
layout_changed_ = false;
|
||||
coverage_dirty_ = false;
|
||||
emergency_ = false;
|
||||
health_error_.clear();
|
||||
std::string cursor_error;
|
||||
cursor_guard_.Stop(cursor_error);
|
||||
}
|
||||
|
||||
bool IsRecovered() const override {
|
||||
return band_guard_.stopped() && windows_.empty() &&
|
||||
window_events_.empty() && input_guard_.stopped() && !hotkey_ &&
|
||||
cursor_guard_.stopped();
|
||||
}
|
||||
|
||||
void Wake() override {
|
||||
if (wake_event_) SetEvent(wake_event_);
|
||||
}
|
||||
|
||||
void WaitForEvents(unsigned timeout_ms) override {
|
||||
const DWORD result = MsgWaitForMultipleObjectsEx(
|
||||
wake_event_ ? 1 : 0, wake_event_ ? &wake_event_ : nullptr, timeout_ms,
|
||||
QS_ALLINPUT, MWMO_INPUTAVAILABLE);
|
||||
if (result == WAIT_FAILED)
|
||||
health_error_ = Error("MsgWaitForMultipleObjectsEx privacy thread");
|
||||
}
|
||||
|
||||
PrivacyHealth Poll() override {
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
if (message.message == WM_QUIT)
|
||||
health_error_ =
|
||||
"Privacy message thread is exiting";
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
if (emergency_) {
|
||||
emergency_ = false;
|
||||
return {{}, true};
|
||||
}
|
||||
if (block_input_) {
|
||||
std::string error;
|
||||
if (!input_guard_.Healthy(error)) return {error, false};
|
||||
}
|
||||
if (windows_.empty()) return {health_error_, false};
|
||||
// Input messages wake the thread immediately; expensive health queries
|
||||
// must retain their own cadence instead of running once per input event.
|
||||
if (!layout_changed_ &&
|
||||
GetTickCount64() - last_check_ < 100) {
|
||||
if (coverage_dirty_ && last_health_.failure.empty()) {
|
||||
coverage_dirty_ = false;
|
||||
last_health_ = CheckCoverage();
|
||||
}
|
||||
return last_health_;
|
||||
}
|
||||
coverage_dirty_ = false;
|
||||
last_check_ = GetTickCount64();
|
||||
last_health_ = CheckHealth();
|
||||
return last_health_;
|
||||
}
|
||||
|
||||
private:
|
||||
PrivacyHealth CheckHealth() {
|
||||
if (!health_error_.empty()) return {health_error_, false};
|
||||
if (!band_guard_.Healthy(health_error_)) return {health_error_, false};
|
||||
if (!IsWindowsPrivacyDesktopAvailable()) {
|
||||
std::string cursor_error;
|
||||
cursor_guard_.Stop(cursor_error);
|
||||
return {
|
||||
"Secure desktop, lock screen or unavailable input desktop; "
|
||||
"privacy screen will turn off",
|
||||
false};
|
||||
}
|
||||
std::string cursor_error;
|
||||
if (!cursor_guard_.Healthy(cursor_error)) return {cursor_error, false};
|
||||
QUERY_USER_NOTIFICATION_STATE shell_state{};
|
||||
if (FAILED(SHQueryUserNotificationState(&shell_state)) ||
|
||||
shell_state == QUNS_RUNNING_D3D_FULL_SCREEN) {
|
||||
return {
|
||||
"Exclusive fullscreen or unknown display presentation",
|
||||
false};
|
||||
}
|
||||
MonitorEnumeration monitors;
|
||||
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
|
||||
reinterpret_cast<LPARAM>(&monitors)) ||
|
||||
!monitors.ok) {
|
||||
return {Error("EnumDisplayMonitors"), false};
|
||||
}
|
||||
const bool changed =
|
||||
monitors.rects.size() != windows_.size() ||
|
||||
std::any_of(monitors.rects.begin(), monitors.rects.end(),
|
||||
[this](const RECT& rect) {
|
||||
return std::none_of(windows_.begin(), windows_.end(),
|
||||
[&](const auto& w) {
|
||||
return SameRect(w.rect, rect);
|
||||
});
|
||||
});
|
||||
if (changed || layout_changed_) {
|
||||
layout_changed_ = false;
|
||||
// Retain old covers until the monitor check completes, then release
|
||||
// privacy so capture can refresh its display mapping independently.
|
||||
std::string error;
|
||||
if (!ReconcileMonitors(error))
|
||||
health_error_ = error;
|
||||
else
|
||||
health_error_ =
|
||||
"Display layout/DPI changed; privacy screen will turn off";
|
||||
}
|
||||
if (!health_error_.empty()) return {health_error_, false};
|
||||
BOOL composed = FALSE;
|
||||
if (FAILED(DwmIsCompositionEnabled(&composed)) || !composed) {
|
||||
return {
|
||||
"Desktop composition stopped; privacy coverage cannot be guaranteed",
|
||||
false};
|
||||
}
|
||||
return CheckCoverage();
|
||||
}
|
||||
|
||||
PrivacyHealth CheckCoverage() {
|
||||
const auto get_band = reinterpret_cast<GetPrivacyWindowBand>(
|
||||
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetWindowBand"));
|
||||
if (!get_band) return {"High-band window API became unavailable", false};
|
||||
for (const auto& w : windows_) {
|
||||
DWORD band = 0;
|
||||
DWORD affinity = 0;
|
||||
BYTE alpha = 0;
|
||||
DWORD layered_flags = 0;
|
||||
RECT rect{};
|
||||
DWORD cloaked = 0;
|
||||
if (!get_band(w.cover, &band) || band != kPrivacyWindowBand ||
|
||||
!IsWindowVisible(w.cover) || !GetWindowRect(w.cover, &rect) ||
|
||||
!SameRect(rect, w.rect) ||
|
||||
!(GetWindowLongPtrW(w.cover, GWL_EXSTYLE) & WS_EX_TOPMOST) ||
|
||||
!GetLayeredWindowAttributes(w.cover, nullptr, &alpha,
|
||||
&layered_flags) ||
|
||||
alpha != 255 || layered_flags != LWA_ALPHA ||
|
||||
!GetWindowDisplayAffinity(w.cover, &affinity) ||
|
||||
affinity != WDA_EXCLUDEFROMCAPTURE ||
|
||||
FAILED(DwmGetWindowAttribute(w.cover, DWMWA_CLOAKED, &cloaked,
|
||||
sizeof(cloaked))) ||
|
||||
cloaked) {
|
||||
return {
|
||||
"Privacy window visibility, geometry or capture affinity was lost",
|
||||
false};
|
||||
}
|
||||
}
|
||||
for (const auto& w : windows_) {
|
||||
// Detect windows in special bands above the cover. A high-band window
|
||||
// still cannot cover another desktop or guarantee exclusive fullscreen.
|
||||
for (HWND above = GetWindow(w.cover, GW_HWNDPREV); above;
|
||||
above = GetWindow(above, GW_HWNDPREV)) {
|
||||
if (!IsWindowVisible(above) || IsIconic(above)) continue;
|
||||
DWORD above_band = 0;
|
||||
if (!get_band(above, &above_band))
|
||||
return {"Could not inspect a window above the high-band cover",
|
||||
false};
|
||||
// HWND list order alone does not express compositor band order.
|
||||
// Only skip the two known ordinary desktop bands, never unknown bands.
|
||||
if (above_band == 0 || above_band == 1) continue;
|
||||
if (std::any_of(windows_.begin(), windows_.end(),
|
||||
[above](const auto& own) {
|
||||
return above == own.cover;
|
||||
}))
|
||||
continue;
|
||||
RECT bounds{}, intersection{};
|
||||
DWORD hidden = 0;
|
||||
if (SUCCEEDED(DwmGetWindowAttribute(above, DWMWA_CLOAKED, &hidden,
|
||||
sizeof(hidden))) &&
|
||||
hidden)
|
||||
continue;
|
||||
if (GetWindowRect(above, &bounds) &&
|
||||
IntersectRect(&intersection, &bounds, &w.rect)) {
|
||||
if (IsNonObstructingPrivacyWindow(above, bounds)) continue;
|
||||
char window_class[128]{};
|
||||
DWORD process_id = 0;
|
||||
GetClassNameA(above, window_class, sizeof(window_class));
|
||||
GetWindowThreadProcessId(above, &process_id);
|
||||
const auto now = GetTickCount64();
|
||||
if (above != last_obstruction_ ||
|
||||
now - last_obstruction_log_ >= 1000) {
|
||||
LOG_WARN(
|
||||
"Potential privacy cover overlap: class={}, process={}, "
|
||||
"band={}, bounds=({}, {})-({}, {}), exstyle=0x{:x}",
|
||||
window_class, process_id, above_band, bounds.left, bounds.top,
|
||||
bounds.right, bounds.bottom,
|
||||
GetWindowLongPtrW(above, GWL_EXSTYLE));
|
||||
last_obstruction_ = above;
|
||||
last_obstruction_log_ = now;
|
||||
}
|
||||
// HWND order cannot prove compositor order, but this window may
|
||||
// display content. Report failure so optional privacy is released.
|
||||
return {
|
||||
"A visible window may overlap privacy coverage",
|
||||
false};
|
||||
}
|
||||
}
|
||||
}
|
||||
return {health_error_, false};
|
||||
}
|
||||
|
||||
struct Window {
|
||||
RECT rect{};
|
||||
HWND cover = nullptr;
|
||||
};
|
||||
|
||||
bool Initialize(std::string& error) {
|
||||
if (sink_) return true;
|
||||
// This dedicated thread owns no existing application windows. Never alter
|
||||
// process-wide or Slint/SDL DPI awareness.
|
||||
if (!dpi_scope_ || !dpi_scope_->active()) {
|
||||
dpi_scope_.reset();
|
||||
dpi_scope_ = std::make_unique<ScopedWindowsPhysicalCoordinates>();
|
||||
}
|
||||
if (!dpi_scope_->active()) {
|
||||
error = Error("SetThreadDpiAwarenessContext");
|
||||
return false;
|
||||
}
|
||||
WNDCLASSEXW cls{sizeof(cls)};
|
||||
cls.lpfnWndProc = WindowProc;
|
||||
cls.hInstance = GetModuleHandleW(nullptr);
|
||||
cls.lpszClassName = kClassName;
|
||||
cls.hCursor = nullptr;
|
||||
cls.hbrBackground = static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH));
|
||||
if (!registered_ && !RegisterClassExW(&cls)) {
|
||||
error = Error("RegisterClassExW");
|
||||
return false;
|
||||
}
|
||||
registered_ = true;
|
||||
sink_ = CreateWindowExW(WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, kClassName,
|
||||
L"", WS_POPUP, 0, 0, 0, 0, nullptr, nullptr,
|
||||
cls.hInstance, this);
|
||||
if (!sink_) {
|
||||
error = Error("CreateWindowExW message sink");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool ReconcileMonitors(std::string& error) {
|
||||
MonitorEnumeration monitors;
|
||||
if (!EnumDisplayMonitors(nullptr, nullptr, EnumMonitor,
|
||||
reinterpret_cast<LPARAM>(&monitors)) ||
|
||||
!monitors.ok || monitors.rects.empty()) {
|
||||
error = Error("EnumDisplayMonitors");
|
||||
return false;
|
||||
}
|
||||
std::vector<PrivacyBandWindow> covers;
|
||||
if (!band_guard_.Windows(covers, error)) return false;
|
||||
for (const RECT& rect : monitors.rects) {
|
||||
auto existing =
|
||||
std::find_if(windows_.begin(), windows_.end(),
|
||||
[&](const auto& w) { return SameRect(w.rect, rect); });
|
||||
if (existing != windows_.end()) continue;
|
||||
const auto cover = std::find_if(covers.begin(), covers.end(),
|
||||
[&](const auto& candidate) {
|
||||
return SameRect(candidate.rect, rect);
|
||||
});
|
||||
if (cover == covers.end()) {
|
||||
error = "High-band broker has not covered every physical monitor";
|
||||
return false;
|
||||
}
|
||||
windows_.push_back({rect, reinterpret_cast<HWND>(cover->hwnd)});
|
||||
LOG_INFO(
|
||||
"Privacy high-band cover ready: bounds=({}, {})-({}, {}), "
|
||||
"affinity=0x11, alpha=255",
|
||||
rect.left, rect.top, rect.right, rect.bottom);
|
||||
}
|
||||
// Cover windows are owned and released by the broker.
|
||||
windows_.erase(std::remove_if(windows_.begin(), windows_.end(),
|
||||
[&](const auto& window) {
|
||||
return std::none_of(monitors.rects.begin(), monitors.rects.end(),
|
||||
[&](const RECT& rect) { return SameRect(window.rect, rect); });
|
||||
}), windows_.end());
|
||||
return true;
|
||||
}
|
||||
|
||||
static LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wp,
|
||||
LPARAM lp) {
|
||||
if (msg == WM_NCCREATE) {
|
||||
const auto create = reinterpret_cast<CREATESTRUCTW*>(lp);
|
||||
SetWindowLongPtrW(hwnd, GWLP_USERDATA,
|
||||
reinterpret_cast<LONG_PTR>(create->lpCreateParams));
|
||||
}
|
||||
auto self = reinterpret_cast<WindowsPrivacyBackend*>(
|
||||
GetWindowLongPtrW(hwnd, GWLP_USERDATA));
|
||||
if (msg == WM_HOTKEY || msg == kEmergency) {
|
||||
if (self && hwnd == self->sink_) self->emergency_ = true;
|
||||
return 0;
|
||||
}
|
||||
if (msg == WM_MOUSEACTIVATE) return MA_NOACTIVATE;
|
||||
if (msg == WM_SETCURSOR) {
|
||||
SetCursor(nullptr);
|
||||
return TRUE;
|
||||
}
|
||||
if (msg == WM_NCHITTEST) return HTTRANSPARENT;
|
||||
if (msg == WM_CLOSE) return 0;
|
||||
if ((msg == WM_DISPLAYCHANGE || msg == WM_DPICHANGED) && self) {
|
||||
self->layout_changed_ = true;
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProcW(hwnd, msg, wp, lp);
|
||||
}
|
||||
|
||||
static void CALLBACK WindowEvent(HWINEVENTHOOK, DWORD, HWND hwnd, LONG object,
|
||||
LONG child, DWORD, DWORD) {
|
||||
auto self = event_owner_;
|
||||
if (!self || !hwnd || object != OBJID_WINDOW || child != CHILDID_SELF ||
|
||||
hwnd == self->sink_ || GetAncestor(hwnd, GA_ROOT) != hwnd)
|
||||
return;
|
||||
for (const auto& w : self->windows_)
|
||||
if (hwnd == w.cover) return;
|
||||
// OUTOFCONTEXT delivers on this window thread. Only mark work here;
|
||||
// monitor/session queries and Z-order changes remain in the control loop.
|
||||
const auto get_band = reinterpret_cast<GetPrivacyWindowBand>(
|
||||
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetWindowBand"));
|
||||
DWORD band = 0;
|
||||
if (get_band && get_band(hwnd, &band) && (band == 0 || band == 1)) return;
|
||||
self->coverage_dirty_ = true;
|
||||
}
|
||||
|
||||
inline static thread_local WindowsPrivacyBackend* event_owner_ = nullptr;
|
||||
std::vector<HWINEVENTHOOK> window_events_;
|
||||
std::vector<Window> windows_;
|
||||
HWND sink_ = nullptr;
|
||||
HANDLE wake_event_ = nullptr;
|
||||
std::string wake_error_;
|
||||
PrivacyInputGuard input_guard_;
|
||||
bool block_input_ = false;
|
||||
PrivacyCursorGuard cursor_guard_;
|
||||
PrivacyBandGuard band_guard_;
|
||||
std::unique_ptr<ScopedWindowsPhysicalCoordinates> dpi_scope_;
|
||||
bool registered_ = false;
|
||||
bool hotkey_ = false;
|
||||
bool emergency_ = false;
|
||||
bool layout_changed_ = false;
|
||||
bool coverage_dirty_ = false;
|
||||
uint64_t last_check_ = 0;
|
||||
PrivacyHealth last_health_;
|
||||
HWND last_obstruction_ = nullptr;
|
||||
uint64_t last_obstruction_log_ = 0;
|
||||
std::string health_error_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsWindowsPrivacyDesktopAvailable() {
|
||||
// LockApp can display the lock screen while OpenInputDesktop still reports
|
||||
// Default. Desktop-name checks alone are NOT a session-unlocked check.
|
||||
DWORD session = 0;
|
||||
if (!ProcessIdToSessionId(GetCurrentProcessId(), &session) ||
|
||||
session != WTSGetActiveConsoleSessionId())
|
||||
return false;
|
||||
LPWSTR buffer = nullptr;
|
||||
DWORD bytes = 0;
|
||||
if (!WTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session,
|
||||
WTSSessionInfoEx, &buffer, &bytes))
|
||||
return false;
|
||||
const auto info = reinterpret_cast<WTSINFOEXW*>(buffer);
|
||||
const bool unlocked =
|
||||
bytes >= sizeof(WTSINFOEXW) && info->Level == 1 &&
|
||||
info->Data.WTSInfoExLevel1.SessionFlags == WTS_SESSIONSTATE_UNLOCK;
|
||||
WTSFreeMemory(buffer);
|
||||
if (!unlocked) return false;
|
||||
HDESK input = OpenInputDesktop(0, FALSE, DESKTOP_READOBJECTS);
|
||||
if (!input) return false;
|
||||
wchar_t input_name[256]{}, thread_name[256]{};
|
||||
const bool ok =
|
||||
DesktopName(input, input_name) &&
|
||||
DesktopName(GetThreadDesktop(GetCurrentThreadId()), thread_name) &&
|
||||
_wcsicmp(input_name, L"Default") == 0 &&
|
||||
_wcsicmp(input_name, thread_name) == 0;
|
||||
CloseDesktop(input);
|
||||
return ok;
|
||||
}
|
||||
|
||||
std::unique_ptr<PrivacyBackend> CreateWindowsPrivacyBackend() {
|
||||
return std::make_unique<WindowsPrivacyBackend>();
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,323 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "privacy_band_guard.h"
|
||||
|
||||
#include <objbase.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <iterator>
|
||||
#include <new>
|
||||
|
||||
#include "privacy_backend.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
struct ScopedHandle {
|
||||
HANDLE value = nullptr;
|
||||
~ScopedHandle() {
|
||||
if (value && value != INVALID_HANDLE_VALUE) CloseHandle(value);
|
||||
}
|
||||
};
|
||||
bool Failed(const char* operation, std::string& error,
|
||||
DWORD code = GetLastError()) {
|
||||
error =
|
||||
std::string(operation) + " (Windows error " + std::to_string(code) + ")";
|
||||
LOG_ERROR("Privacy high-band: {}", error);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::filesystem::path ModuleDirectory() {
|
||||
std::vector<wchar_t> path(32768);
|
||||
const DWORD length = GetModuleFileNameW(nullptr, path.data(), path.size());
|
||||
if (!length || length >= path.size()) return {};
|
||||
return std::filesystem::path(path.data()).parent_path();
|
||||
}
|
||||
|
||||
std::vector<wchar_t> ChildEnvironment(const std::wstring& bootstrap) {
|
||||
std::vector<std::wstring> entries;
|
||||
LPWCH environment = GetEnvironmentStringsW();
|
||||
if (!environment) return {};
|
||||
const std::wstring prefix = std::wstring(kPrivacyBandBootstrap) + L"=";
|
||||
for (const wchar_t* item = environment; *item; item += wcslen(item) + 1) {
|
||||
if (_wcsnicmp(item, prefix.c_str(), prefix.size()) != 0)
|
||||
entries.emplace_back(item);
|
||||
}
|
||||
FreeEnvironmentStringsW(environment);
|
||||
entries.push_back(prefix + bootstrap);
|
||||
std::sort(entries.begin(), entries.end(), [](const auto& a, const auto& b) {
|
||||
return _wcsicmp(a.c_str(), b.c_str()) < 0;
|
||||
});
|
||||
std::vector<wchar_t> result;
|
||||
for (const auto& entry : entries) {
|
||||
result.insert(result.end(), entry.begin(), entry.end());
|
||||
result.push_back(0);
|
||||
}
|
||||
result.push_back(0);
|
||||
return result;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PrivacyBandGuard::~PrivacyBandGuard() {
|
||||
std::string error;
|
||||
if (!Stop(error)) {
|
||||
// Last-resort teardown still targets only this object's dedicated job.
|
||||
if (job_) CloseHandle(job_);
|
||||
job_ = nullptr;
|
||||
Stop(error);
|
||||
}
|
||||
}
|
||||
|
||||
bool PrivacyBandGuard::Available(std::string& error) {
|
||||
#if !defined(_M_X64) && !defined(__x86_64__)
|
||||
error = "High-band privacy currently requires an x64 Windows process";
|
||||
return false;
|
||||
#else
|
||||
const HMODULE user = GetModuleHandleW(L"user32.dll");
|
||||
if (!GetProcAddress(user, "CreateWindowInBand") ||
|
||||
!GetProcAddress(user, "GetWindowBand")) {
|
||||
error = "Windows high-band window APIs are unavailable";
|
||||
return false;
|
||||
}
|
||||
const auto directory = ModuleDirectory();
|
||||
if (directory.empty())
|
||||
return Failed("Resolve privacy module directory", error);
|
||||
const auto dll = directory / kPrivacyBandDll;
|
||||
if (GetFileAttributesW(dll.c_str()) == INVALID_FILE_ATTRIBUTES)
|
||||
return Failed("Missing crossdesk_privacy_window.dll", error);
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
bool PrivacyBandGuard::Start(const PrivacyScreenText& text,
|
||||
std::string& error) {
|
||||
if (process_)
|
||||
return Failed("High-band broker is already running", error, ERROR_BUSY);
|
||||
if (!Available(error)) return false;
|
||||
const auto directory = ModuleDirectory();
|
||||
if (directory.empty())
|
||||
return Failed("Resolve privacy module directory", error);
|
||||
const auto dll = directory / kPrivacyBandDll;
|
||||
wchar_t system[MAX_PATH]{}, temporary[MAX_PATH]{}, guid_text[40]{};
|
||||
GUID guid{};
|
||||
if (!GetSystemDirectoryW(system, MAX_PATH) ||
|
||||
!GetTempPathW(MAX_PATH, temporary) || FAILED(CoCreateGuid(&guid)) ||
|
||||
!StringFromGUID2(guid, guid_text, 40))
|
||||
return Failed("Resolve high-band broker paths", error);
|
||||
broker_directory_ = std::filesystem::path(temporary) /
|
||||
(std::wstring(L"CrossDesk-privacy-") + guid_text);
|
||||
if (!CreateDirectoryW(broker_directory_.c_str(), nullptr))
|
||||
return Failed("Create private broker directory", error);
|
||||
broker_path_ = broker_directory_ / L"RuntimeBroker_crossdesk.exe";
|
||||
if (!CopyFileW((std::filesystem::path(system) / L"RuntimeBroker.exe").c_str(),
|
||||
broker_path_.c_str(), TRUE))
|
||||
return Failed("Copy system RuntimeBroker for this session", error);
|
||||
// Keep both images immutable throughout creation/injection and the session.
|
||||
image_lock_ = CreateFileW(broker_path_.c_str(), GENERIC_READ, FILE_SHARE_READ,
|
||||
nullptr, OPEN_EXISTING, 0, nullptr);
|
||||
dll_lock_ = CreateFileW(dll.c_str(), GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, 0, nullptr);
|
||||
if (image_lock_ == INVALID_HANDLE_VALUE || dll_lock_ == INVALID_HANDLE_VALUE)
|
||||
return Failed("Lock privacy broker images", error);
|
||||
|
||||
SECURITY_ATTRIBUTES inherit{sizeof(inherit), nullptr, TRUE};
|
||||
ScopedHandle parent;
|
||||
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
|
||||
GetCurrentProcess(), &parent.value,
|
||||
SYNCHRONIZE | PROCESS_QUERY_LIMITED_INFORMATION, TRUE,
|
||||
0))
|
||||
return Failed("Duplicate privacy parent process handle", error);
|
||||
stop_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
|
||||
ready_ = CreateEventW(&inherit, TRUE, FALSE, nullptr);
|
||||
mapping_ = CreateFileMappingW(INVALID_HANDLE_VALUE, &inherit, PAGE_READWRITE,
|
||||
0, sizeof(PrivacyBandShared), nullptr);
|
||||
if (!stop_ || !ready_ || !mapping_) {
|
||||
const auto code = GetLastError();
|
||||
return Failed("Create high-band broker IPC", error, code);
|
||||
}
|
||||
shared_ = static_cast<PrivacyBandShared*>(MapViewOfFile(
|
||||
mapping_, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyBandShared)));
|
||||
if (!shared_) {
|
||||
return Failed("Map high-band broker IPC", error);
|
||||
}
|
||||
new (shared_) PrivacyBandShared{};
|
||||
// Immutable after process startup; no cross-process string objects or
|
||||
// concurrent writes while the window thread paints.
|
||||
if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS,
|
||||
text.unlock_hint.c_str(), -1, shared_->unlock_hint,
|
||||
static_cast<int>(std::size(shared_->unlock_hint))))
|
||||
return Failed("Encode privacy screen text", error);
|
||||
shared_->parent_pid = GetCurrentProcessId();
|
||||
HANDLE handles[] = {parent.value, stop_, ready_, mapping_};
|
||||
std::wstring bootstrap;
|
||||
for (HANDLE handle : handles) {
|
||||
if (!bootstrap.empty()) bootstrap += L",";
|
||||
bootstrap += std::to_wstring(reinterpret_cast<uintptr_t>(handle));
|
||||
}
|
||||
auto environment = ChildEnvironment(bootstrap);
|
||||
if (environment.empty()) {
|
||||
return Failed("Build broker environment", error);
|
||||
}
|
||||
|
||||
job_ = CreateJobObjectW(nullptr, nullptr);
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION limits{};
|
||||
limits.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
if (!job_ || !SetInformationJobObject(job_, JobObjectExtendedLimitInformation,
|
||||
&limits, sizeof(limits))) {
|
||||
return Failed("Bind high-band broker lifetime", error);
|
||||
}
|
||||
SIZE_T bytes = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes);
|
||||
std::vector<unsigned char> attributes(bytes);
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.lpAttributeList =
|
||||
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes.data());
|
||||
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0,
|
||||
&bytes)) {
|
||||
return Failed("Initialize broker handle list", error);
|
||||
}
|
||||
bool ok = UpdateProcThreadAttribute(
|
||||
startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
handles, sizeof(handles), nullptr, nullptr) != FALSE;
|
||||
PROCESS_INFORMATION child{};
|
||||
std::wstring command = L"\"" + broker_path_.wstring() + L"\"";
|
||||
if (ok) {
|
||||
ok = CreateProcessW(
|
||||
broker_path_.c_str(), command.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_SUSPENDED | CREATE_NO_WINDOW | CREATE_UNICODE_ENVIRONMENT |
|
||||
EXTENDED_STARTUPINFO_PRESENT,
|
||||
environment.data(), broker_directory_.c_str(),
|
||||
&startup.StartupInfo, &child) != FALSE;
|
||||
}
|
||||
const DWORD create_error = GetLastError();
|
||||
DeleteProcThreadAttributeList(startup.lpAttributeList);
|
||||
if (!ok)
|
||||
return Failed("Create owned RuntimeBroker process", error, create_error);
|
||||
process_ = child.hProcess;
|
||||
shared_->broker_pid = child.dwProcessId;
|
||||
if (!AssignProcessToJobObject(job_, process_)) {
|
||||
const DWORD code = GetLastError();
|
||||
TerminateProcess(process_, code);
|
||||
CloseHandle(child.hThread);
|
||||
return Failed("Assign owned RuntimeBroker to privacy job", error, code);
|
||||
}
|
||||
const auto path = dll.wstring();
|
||||
const auto size = (path.size() + 1) * sizeof(wchar_t);
|
||||
void* remote_path = VirtualAllocEx(process_, nullptr, size,
|
||||
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
SIZE_T written = 0;
|
||||
const auto loader =
|
||||
GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW");
|
||||
ok =
|
||||
remote_path && loader &&
|
||||
WriteProcessMemory(process_, remote_path, path.c_str(), size, &written) &&
|
||||
written == size &&
|
||||
QueueUserAPC(reinterpret_cast<PAPCFUNC>(loader), child.hThread,
|
||||
reinterpret_cast<ULONG_PTR>(remote_path));
|
||||
DWORD injection_error = GetLastError();
|
||||
if (ok && ResumeThread(child.hThread) == DWORD(-1)) {
|
||||
ok = false;
|
||||
injection_error = GetLastError();
|
||||
}
|
||||
CloseHandle(child.hThread);
|
||||
if (!ok)
|
||||
return Failed("Load high-band window module into owned broker", error,
|
||||
injection_error);
|
||||
HANDLE startup_events[] = {ready_, process_};
|
||||
const DWORD result = WaitForMultipleObjects(2, startup_events, FALSE, 5000);
|
||||
if (result != WAIT_OBJECT_0) {
|
||||
DWORD code = ERROR_TIMEOUT;
|
||||
if (result == WAIT_OBJECT_0 + 1) GetExitCodeProcess(process_, &code);
|
||||
return Failed("High-band broker did not become ready", error, code);
|
||||
}
|
||||
std::vector<PrivacyBandWindow> windows;
|
||||
if (!Windows(windows, error)) return false;
|
||||
LOG_INFO("Privacy high-band broker ready: process={}, band={}, monitors={}",
|
||||
process_id(), kPrivacyWindowBand, windows.size());
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrivacyBandGuard::Healthy(std::string& error) {
|
||||
if (!process_ || !shared_)
|
||||
return Failed("High-band broker is not initialized", error,
|
||||
ERROR_INVALID_HANDLE);
|
||||
const LONG code = InterlockedCompareExchange(&shared_->error, 0, 0);
|
||||
if (code != ERROR_SUCCESS) {
|
||||
char operation[256]{};
|
||||
WideCharToMultiByte(CP_UTF8, 0, shared_->operation, -1, operation,
|
||||
sizeof(operation), nullptr, nullptr);
|
||||
return Failed(operation[0] ? operation : "High-band window startup failed",
|
||||
error, code);
|
||||
}
|
||||
if (WaitForSingleObject(process_, 0) != WAIT_TIMEOUT) {
|
||||
DWORD exit_code = ERROR_PROCESS_ABORTED;
|
||||
GetExitCodeProcess(process_, &exit_code);
|
||||
return Failed("High-band window process exited", error, exit_code);
|
||||
}
|
||||
if (!InterlockedCompareExchange(&shared_->active, 0, 0) ||
|
||||
GetTickCount64() - static_cast<ULONGLONG>(InterlockedCompareExchange64(
|
||||
&shared_->heartbeat, 0, 0)) >
|
||||
1500)
|
||||
return Failed("High-band window thread stopped responding", error,
|
||||
ERROR_TIMEOUT);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrivacyBandGuard::Windows(std::vector<PrivacyBandWindow>& windows,
|
||||
std::string& error) {
|
||||
if (!Healthy(error)) return false;
|
||||
for (int attempt = 0; attempt < 4; ++attempt) {
|
||||
const LONG before = InterlockedCompareExchange(&shared_->sequence, 0, 0);
|
||||
if (before & 1) continue;
|
||||
const auto count = shared_->count;
|
||||
if (count == 0 || count > kPrivacyMaxMonitors) break;
|
||||
windows.assign(shared_->windows, shared_->windows + count);
|
||||
if (before == InterlockedCompareExchange(&shared_->sequence, 0, 0)) {
|
||||
for (const auto& window : windows) {
|
||||
DWORD pid = 0;
|
||||
GetWindowThreadProcessId(reinterpret_cast<HWND>(window.hwnd), &pid);
|
||||
if (pid != process_id())
|
||||
return Failed("Invalid high-band window owner", error,
|
||||
ERROR_INVALID_HANDLE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return Failed("High-band monitor snapshot is changing or invalid", error,
|
||||
ERROR_RETRY);
|
||||
}
|
||||
|
||||
bool PrivacyBandGuard::Stop(std::string& error) {
|
||||
bool ok = true;
|
||||
if (process_) {
|
||||
if (stop_) SetEvent(stop_);
|
||||
if (WaitForSingleObject(process_, 1500) != WAIT_OBJECT_0) {
|
||||
// This handle came only from our CreateProcessW call, never discovery.
|
||||
if (!TerminateProcess(process_, ERROR_CANCELLED) ||
|
||||
WaitForSingleObject(process_, 1500) != WAIT_OBJECT_0)
|
||||
ok = Failed("Stop owned high-band broker", error);
|
||||
}
|
||||
if (!ok) return false;
|
||||
CloseHandle(process_);
|
||||
process_ = nullptr;
|
||||
}
|
||||
if (job_) CloseHandle(job_);
|
||||
job_ = nullptr;
|
||||
if (shared_) UnmapViewOfFile(shared_);
|
||||
shared_ = nullptr;
|
||||
for (HANDLE handle : {stop_, ready_, mapping_, image_lock_, dll_lock_})
|
||||
if (handle && handle != INVALID_HANDLE_VALUE) CloseHandle(handle);
|
||||
stop_ = ready_ = mapping_ = image_lock_ = dll_lock_ = nullptr;
|
||||
if (!broker_path_.empty()) DeleteFileW(broker_path_.c_str());
|
||||
if (!broker_directory_.empty()) RemoveDirectoryW(broker_directory_.c_str());
|
||||
broker_path_.clear();
|
||||
broker_directory_.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,51 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_BAND_GUARD_H_
|
||||
#define _PRIVACY_BAND_GUARD_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "privacy_band_ipc.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct PrivacyScreenText;
|
||||
|
||||
// Lifecycle and snapshots are owned by the privacy control thread. The broker
|
||||
// owns all high-band HWND operations. This class never injects an existing
|
||||
// process and never terminates a process found by name or PID.
|
||||
class PrivacyBandGuard {
|
||||
public:
|
||||
~PrivacyBandGuard();
|
||||
static bool Available(std::string& error);
|
||||
bool Start(const PrivacyScreenText& text, std::string& error);
|
||||
bool Stop(std::string& error);
|
||||
bool Healthy(std::string& error);
|
||||
bool Windows(std::vector<PrivacyBandWindow>& windows, std::string& error);
|
||||
bool stopped() const { return !process_; }
|
||||
DWORD process_id() const { return process_ ? GetProcessId(process_) : 0; }
|
||||
|
||||
private:
|
||||
HANDLE process_ = nullptr;
|
||||
HANDLE job_ = nullptr;
|
||||
HANDLE stop_ = nullptr;
|
||||
HANDLE ready_ = nullptr;
|
||||
HANDLE mapping_ = nullptr;
|
||||
HANDLE image_lock_ = nullptr;
|
||||
HANDLE dll_lock_ = nullptr;
|
||||
PrivacyBandShared* shared_ = nullptr;
|
||||
std::filesystem::path broker_path_;
|
||||
std::filesystem::path broker_directory_;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_BAND_GUARD_H_
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_BAND_IPC_H_
|
||||
#define _PRIVACY_BAND_IPC_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
constexpr uint32_t kPrivacyBandMagic = 0x43534231;
|
||||
constexpr uint32_t kPrivacyBandVersion = 2;
|
||||
constexpr DWORD kPrivacyWindowBand = 18; // ZBID_ABOVELOCK_UX, current desktop.
|
||||
constexpr size_t kPrivacyMaxMonitors = 64;
|
||||
constexpr wchar_t kPrivacyBandBootstrap[] = L"CROSSDESK_PRIVACY_BAND_BOOTSTRAP";
|
||||
constexpr wchar_t kPrivacyBandDll[] = L"crossdesk_privacy_window.dll";
|
||||
|
||||
struct PrivacyBandWindow {
|
||||
uint64_t hwnd = 0;
|
||||
RECT rect{};
|
||||
};
|
||||
|
||||
// Unnamed mapping and synchronization/process handles are inherited only by
|
||||
// the newly created broker. No global window-name/PID discovery or named IPC.
|
||||
struct PrivacyBandShared {
|
||||
uint32_t magic = kPrivacyBandMagic;
|
||||
uint32_t version = kPrivacyBandVersion;
|
||||
uint32_t size = sizeof(PrivacyBandShared);
|
||||
DWORD parent_pid = 0;
|
||||
DWORD broker_pid = 0;
|
||||
volatile LONG sequence = 0;
|
||||
volatile LONG error = ERROR_IO_PENDING;
|
||||
volatile LONG active = 0;
|
||||
alignas(8) volatile LONG64 heartbeat = 0;
|
||||
wchar_t operation[128]{};
|
||||
wchar_t unlock_hint[128]{};
|
||||
uint32_t count = 0;
|
||||
PrivacyBandWindow windows[kPrivacyMaxMonitors]{};
|
||||
};
|
||||
|
||||
using CreatePrivacyWindowInBand = HWND(WINAPI*)(DWORD, ATOM, LPCWSTR, DWORD,
|
||||
int, int, int, int, HWND, HMENU,
|
||||
HINSTANCE, LPVOID, DWORD);
|
||||
using GetPrivacyWindowBand = BOOL(WINAPI*)(HWND, DWORD*);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_BAND_IPC_H_
|
||||
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_CURSOR_API_H_
|
||||
#define _PRIVACY_CURSOR_API_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// A separate cursor plane can remain visible above a capture-excluded HWND.
|
||||
// Magnification's documented cursor API hides that plane across input queues;
|
||||
// it does not change cursor schemes, shapes, positions, focus or DPI awareness.
|
||||
class PrivacyCursorApi {
|
||||
public:
|
||||
PrivacyCursorApi() = default;
|
||||
PrivacyCursorApi(const PrivacyCursorApi&) = delete;
|
||||
PrivacyCursorApi& operator=(const PrivacyCursorApi&) = delete;
|
||||
~PrivacyCursorApi() { RestoreAndClose(); }
|
||||
bool Initialize() {
|
||||
module_ = LoadLibraryExW(L"Magnification.dll", nullptr,
|
||||
LOAD_LIBRARY_SEARCH_SYSTEM32);
|
||||
if (!module_) return false;
|
||||
const auto initialize = reinterpret_cast<InitializeFn>(
|
||||
GetProcAddress(module_, "MagInitialize"));
|
||||
uninitialize_ = reinterpret_cast<InitializeFn>(
|
||||
GetProcAddress(module_, "MagUninitialize"));
|
||||
show_ = reinterpret_cast<ShowFn>(
|
||||
GetProcAddress(module_, "MagShowSystemCursor"));
|
||||
if (!initialize || !uninitialize_ || !show_) {
|
||||
SetLastError(ERROR_PROC_NOT_FOUND);
|
||||
return false;
|
||||
}
|
||||
initialized_ = initialize() != FALSE;
|
||||
return initialized_;
|
||||
}
|
||||
bool Hide() { return initialized_ && show_(FALSE); }
|
||||
bool RestoreAndClose() {
|
||||
bool ok = true;
|
||||
if (initialized_) {
|
||||
ok = show_(TRUE) != FALSE;
|
||||
ok = (uninitialize_() != FALSE) && ok;
|
||||
initialized_ = false;
|
||||
}
|
||||
if (module_) {
|
||||
FreeLibrary(module_);
|
||||
module_ = nullptr;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
private:
|
||||
using InitializeFn = BOOL(WINAPI*)();
|
||||
using ShowFn = BOOL(WINAPI*)(BOOL);
|
||||
HMODULE module_ = nullptr;
|
||||
InitializeFn uninitialize_ = nullptr;
|
||||
ShowFn show_ = nullptr;
|
||||
bool initialized_ = false;
|
||||
};
|
||||
|
||||
struct PrivacyCursorReady {
|
||||
volatile LONG error;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_CURSOR_API_H_
|
||||
@@ -1,158 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "privacy_cursor_guard.h"
|
||||
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
|
||||
#include "privacy_cursor_api.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
struct Handle {
|
||||
HANDLE value = nullptr;
|
||||
~Handle() {
|
||||
if (value) CloseHandle(value);
|
||||
}
|
||||
};
|
||||
bool Failed(const char* operation, std::string& error,
|
||||
DWORD code = GetLastError()) {
|
||||
error = std::string(operation) + " failed (Windows error " +
|
||||
std::to_string(code) + ")";
|
||||
LOG_ERROR("Privacy cursor: {}", error);
|
||||
return false;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
PrivacyCursorGuard::~PrivacyCursorGuard() {
|
||||
std::string error;
|
||||
Stop(error);
|
||||
// The inherited parent handle still lets a slow helper restore on exit.
|
||||
if (process_) CloseHandle(process_);
|
||||
if (stop_event_) CloseHandle(stop_event_);
|
||||
}
|
||||
|
||||
bool PrivacyCursorGuard::Start(std::string& error) {
|
||||
if (process_ || restore_pending_)
|
||||
return Failed("Cursor guard already active", error, ERROR_BUSY);
|
||||
helper_failure_reported_ = false;
|
||||
std::vector<wchar_t> module(32768);
|
||||
const DWORD count = GetModuleFileNameW(nullptr, module.data(),
|
||||
static_cast<DWORD>(module.size()));
|
||||
if (!count || count >= module.size())
|
||||
return Failed("GetModuleFileNameW", error);
|
||||
const auto helper = std::filesystem::path(module.data()).parent_path() /
|
||||
L"crossdesk_privacy_cursor_helper.exe";
|
||||
SECURITY_ATTRIBUTES security{sizeof(security), nullptr, TRUE};
|
||||
Handle parent;
|
||||
if (!DuplicateHandle(GetCurrentProcess(), GetCurrentProcess(),
|
||||
GetCurrentProcess(), &parent.value, SYNCHRONIZE, TRUE,
|
||||
0))
|
||||
return Failed("Duplicate parent process handle", error);
|
||||
Handle stop{CreateEventW(&security, TRUE, FALSE, nullptr)};
|
||||
Handle ready{CreateEventW(&security, TRUE, FALSE, nullptr)};
|
||||
Handle mapping{CreateFileMappingW(INVALID_HANDLE_VALUE, &security,
|
||||
PAGE_READWRITE, 0,
|
||||
sizeof(PrivacyCursorReady), nullptr)};
|
||||
if (!stop.value || !ready.value || !mapping.value)
|
||||
return Failed("Create cursor guard IPC", error);
|
||||
auto shared = static_cast<PrivacyCursorReady*>(MapViewOfFile(
|
||||
mapping.value, FILE_MAP_ALL_ACCESS, 0, 0, sizeof(PrivacyCursorReady)));
|
||||
if (!shared) return Failed("Map cursor guard readiness", error);
|
||||
InterlockedExchange(&shared->error, ERROR_IO_PENDING);
|
||||
|
||||
SIZE_T bytes = 0;
|
||||
InitializeProcThreadAttributeList(nullptr, 1, 0, &bytes);
|
||||
std::vector<unsigned char> attributes(bytes);
|
||||
STARTUPINFOEXW startup{};
|
||||
startup.StartupInfo.cb = sizeof(startup);
|
||||
startup.lpAttributeList =
|
||||
reinterpret_cast<LPPROC_THREAD_ATTRIBUTE_LIST>(attributes.data());
|
||||
if (!InitializeProcThreadAttributeList(startup.lpAttributeList, 1, 0,
|
||||
&bytes)) {
|
||||
const DWORD code = GetLastError();
|
||||
UnmapViewOfFile(shared);
|
||||
return Failed("Initialize cursor guard handle list", error, code);
|
||||
}
|
||||
HANDLE inherited[] = {parent.value, stop.value, ready.value, mapping.value};
|
||||
bool ok = UpdateProcThreadAttribute(
|
||||
startup.lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_HANDLE_LIST,
|
||||
inherited, sizeof(inherited), nullptr, nullptr) != FALSE;
|
||||
PROCESS_INFORMATION child{};
|
||||
if (ok) {
|
||||
std::wstring command = L"\"" + helper.wstring() + L"\"";
|
||||
for (HANDLE handle : inherited)
|
||||
command += L" " + std::to_wstring(reinterpret_cast<uintptr_t>(handle));
|
||||
ok =
|
||||
CreateProcessW(helper.c_str(), command.data(), nullptr, nullptr, TRUE,
|
||||
CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT |
|
||||
CREATE_BREAKAWAY_FROM_JOB,
|
||||
nullptr, nullptr, &startup.StartupInfo, &child) != FALSE;
|
||||
}
|
||||
const DWORD start_error = GetLastError();
|
||||
DeleteProcThreadAttributeList(startup.lpAttributeList);
|
||||
if (!ok) {
|
||||
UnmapViewOfFile(shared);
|
||||
return Failed("Start crossdesk_privacy_cursor_helper.exe", error,
|
||||
start_error);
|
||||
}
|
||||
CloseHandle(child.hThread);
|
||||
process_ = child.hProcess;
|
||||
stop_event_ = stop.value;
|
||||
stop.value = nullptr;
|
||||
HANDLE startup_events[] = {ready.value, process_};
|
||||
const DWORD result = WaitForMultipleObjects(2, startup_events, FALSE, 2000);
|
||||
const DWORD ready_error =
|
||||
static_cast<DWORD>(InterlockedCompareExchange(&shared->error, 0, 0));
|
||||
UnmapViewOfFile(shared);
|
||||
if (result != WAIT_OBJECT_0 || ready_error != ERROR_SUCCESS) {
|
||||
std::string cleanup;
|
||||
Stop(cleanup);
|
||||
return Failed(
|
||||
"Hide system cursor", error,
|
||||
ready_error == ERROR_IO_PENDING ? ERROR_TIMEOUT : ready_error);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrivacyCursorGuard::RestoreAfterFailure(std::string& error) {
|
||||
PrivacyCursorApi cursor;
|
||||
if (!cursor.Initialize() || !cursor.RestoreAndClose()) {
|
||||
restore_pending_ = true;
|
||||
return Failed("Restore system cursor after helper failure", error);
|
||||
}
|
||||
restore_pending_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PrivacyCursorGuard::Healthy(std::string& error) {
|
||||
if (process_ && WaitForSingleObject(process_, 0) == WAIT_TIMEOUT) return true;
|
||||
if (restore_pending_ || (process_ && !helper_failure_reported_))
|
||||
RestoreAfterFailure(error);
|
||||
helper_failure_reported_ = true;
|
||||
error = "System cursor helper exited; privacy screen will turn off";
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PrivacyCursorGuard::Stop(std::string& error) {
|
||||
if (!process_) return !restore_pending_ || RestoreAfterFailure(error);
|
||||
if (!SetEvent(stop_event_)) return Failed("Stop cursor helper", error);
|
||||
if (WaitForSingleObject(process_, 2000) != WAIT_OBJECT_0) {
|
||||
return Failed("Wait for cursor helper restoration", error, ERROR_TIMEOUT);
|
||||
}
|
||||
DWORD code = ERROR_GEN_FAILURE;
|
||||
GetExitCodeProcess(process_, &code);
|
||||
CloseHandle(process_);
|
||||
process_ = nullptr;
|
||||
CloseHandle(stop_event_);
|
||||
stop_event_ = nullptr;
|
||||
if (code != ERROR_SUCCESS) return RestoreAfterFailure(error);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,39 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_CURSOR_GUARD_H_
|
||||
#define _PRIVACY_CURSOR_GUARD_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Called only on the privacy message thread. The child owns cursor suppression
|
||||
// and restores it when either this guard stops it or its parent process exits.
|
||||
class PrivacyCursorGuard {
|
||||
public:
|
||||
PrivacyCursorGuard() = default;
|
||||
PrivacyCursorGuard(const PrivacyCursorGuard&) = delete;
|
||||
PrivacyCursorGuard& operator=(const PrivacyCursorGuard&) = delete;
|
||||
~PrivacyCursorGuard();
|
||||
bool Start(std::string& error);
|
||||
bool Healthy(std::string& error);
|
||||
bool Stop(std::string& error);
|
||||
bool stopped() const { return !process_ && !restore_pending_; }
|
||||
|
||||
private:
|
||||
bool RestoreAfterFailure(std::string& error);
|
||||
HANDLE process_ = nullptr;
|
||||
HANDLE stop_event_ = nullptr;
|
||||
bool restore_pending_ = false;
|
||||
bool helper_failure_reported_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_CURSOR_GUARD_H_
|
||||
@@ -1,304 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#include "privacy_input_guard.h"
|
||||
|
||||
#include "rd_log.h"
|
||||
#include "windows_input_marker.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
PrivacyInputGuard::~PrivacyInputGuard() { Stop(); }
|
||||
|
||||
void PrivacyInputGuard::Fault(const std::string& operation, DWORD code) {
|
||||
{
|
||||
std::lock_guard lock(error_mutex_);
|
||||
error_ = operation + " (Windows error " + std::to_string(code) + ")";
|
||||
LOG_ERROR("Privacy input: {}", error_);
|
||||
}
|
||||
healthy_.store(false);
|
||||
// Wake the owner's message loop; recovery is never performed in a hook.
|
||||
PostMessageW(owner_, WM_NULL, 0, 0);
|
||||
}
|
||||
|
||||
bool PrivacyInputGuard::Start(HWND owner, UINT exit_message,
|
||||
std::string& error) {
|
||||
if (thread_.joinable()) {
|
||||
error = "Privacy input thread is already running";
|
||||
return false;
|
||||
}
|
||||
owner_ = owner;
|
||||
exit_message_ = exit_message;
|
||||
healthy_.store(false);
|
||||
{
|
||||
std::lock_guard lock(error_mutex_);
|
||||
error_.clear();
|
||||
}
|
||||
stop_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
ready_ = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (!stop_ || !ready_) {
|
||||
Fault("Create privacy input events failed", GetLastError());
|
||||
Healthy(error);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
thread_ = std::thread([this] { Run(); });
|
||||
} catch (const std::exception& e) {
|
||||
error = std::string("Start privacy input thread failed: ") + e.what();
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
if (WaitForSingleObject(ready_, 2000) != WAIT_OBJECT_0) {
|
||||
error = "Privacy input thread startup timed out";
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
if (!Healthy(error)) {
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PrivacyInputGuard::Stop() {
|
||||
if (stop_) SetEvent(stop_);
|
||||
if (thread_.joinable()) thread_.join();
|
||||
if (stop_) CloseHandle(stop_);
|
||||
if (ready_) CloseHandle(ready_);
|
||||
stop_ = ready_ = nullptr;
|
||||
owner_ = nullptr;
|
||||
}
|
||||
|
||||
bool PrivacyInputGuard::Healthy(std::string& error) const {
|
||||
if (healthy_.load() && running_.load() &&
|
||||
GetTickCount64() - last_pump_.load() < 1500)
|
||||
return true;
|
||||
std::lock_guard lock(error_mutex_);
|
||||
error = error_.empty() ? "Privacy input message thread stopped responding"
|
||||
: error_;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PrivacyInputGuard::Renew(HHOOK& hook, int kind, HOOKPROC procedure) {
|
||||
HHOOK replacement =
|
||||
SetWindowsHookExW(kind, procedure, GetModuleHandleW(nullptr), 0);
|
||||
if (!replacement) {
|
||||
Fault("Renew privacy input hook failed", GetLastError());
|
||||
return false;
|
||||
}
|
||||
HHOOK previous = hook;
|
||||
hook = replacement;
|
||||
if (UnhookWindowsHookEx(previous)) return true;
|
||||
const DWORD code = GetLastError();
|
||||
if (code != ERROR_INVALID_HOOK_HANDLE) retired_.push_back(previous);
|
||||
Fault("Privacy input hook was lost or could not be retired", code);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool PrivacyInputGuard::PumpMessages() {
|
||||
MSG message{};
|
||||
while (PeekMessageW(&message, nullptr, 0, 0, PM_REMOVE)) {
|
||||
if (message.message == WM_QUIT) return false;
|
||||
TranslateMessage(&message);
|
||||
DispatchMessageW(&message);
|
||||
}
|
||||
last_pump_.store(GetTickCount64());
|
||||
return WaitForSingleObject(stop_, 0) == WAIT_TIMEOUT;
|
||||
}
|
||||
|
||||
bool PrivacyInputGuard::ReleasePressedInputs() {
|
||||
// Both hooks must already be installed: otherwise a new local press can
|
||||
// slip between this snapshot and the transition to blocked input.
|
||||
std::array<bool, 256> pressed{};
|
||||
for (int key = 1; key < 256; ++key)
|
||||
pressed[key] = (GetAsyncKeyState(key) & 0x8000) != 0;
|
||||
// Keep held modifiers available to the physical emergency-exit chord.
|
||||
// Our injected releases intentionally do not clear this hook-side state.
|
||||
physical_keys_ = pressed;
|
||||
|
||||
std::vector<INPUT> releases;
|
||||
for (int key = 1; key < 256; ++key) {
|
||||
if (!pressed[key]) continue;
|
||||
// Generic modifier VKs alias their left/right variants. Release each
|
||||
// side once; use the generic VK only if Windows reports no specific side.
|
||||
if ((key == VK_SHIFT && (pressed[VK_LSHIFT] || pressed[VK_RSHIFT])) ||
|
||||
(key == VK_CONTROL && (pressed[VK_LCONTROL] || pressed[VK_RCONTROL])) ||
|
||||
(key == VK_MENU && (pressed[VK_LMENU] || pressed[VK_RMENU])))
|
||||
continue;
|
||||
INPUT input{};
|
||||
input.type = INPUT_MOUSE;
|
||||
input.mi.dwExtraInfo = kInjectedMouseInputMarker;
|
||||
switch (key) {
|
||||
// GetAsyncKeyState reports physical mouse buttons, as do these flags,
|
||||
// including when Windows swaps the primary and secondary buttons.
|
||||
case VK_LBUTTON:
|
||||
input.mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
||||
break;
|
||||
case VK_RBUTTON:
|
||||
input.mi.dwFlags = MOUSEEVENTF_RIGHTUP;
|
||||
break;
|
||||
case VK_MBUTTON:
|
||||
input.mi.dwFlags = MOUSEEVENTF_MIDDLEUP;
|
||||
break;
|
||||
case VK_XBUTTON1:
|
||||
case VK_XBUTTON2:
|
||||
input.mi.dwFlags = MOUSEEVENTF_XUP;
|
||||
input.mi.mouseData = key == VK_XBUTTON1 ? XBUTTON1 : XBUTTON2;
|
||||
break;
|
||||
default: {
|
||||
input = {};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = static_cast<WORD>(key);
|
||||
const UINT scan = MapVirtualKeyW(key, MAPVK_VK_TO_VSC_EX);
|
||||
input.ki.wScan = static_cast<WORD>(scan & 0xff);
|
||||
input.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
if ((scan & 0xff00) == 0xe000)
|
||||
input.ki.dwFlags |= KEYEVENTF_EXTENDEDKEY;
|
||||
input.ki.dwExtraInfo = kInjectedKeyboardInputMarker;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG_INFO("Privacy input handoff: releasing held input vk=0x{:02X}", key);
|
||||
releases.push_back(input);
|
||||
}
|
||||
if (!releases.empty()) {
|
||||
const UINT count = static_cast<UINT>(releases.size());
|
||||
SetLastError(ERROR_SUCCESS);
|
||||
const UINT sent = SendInput(count, releases.data(), sizeof(INPUT));
|
||||
if (sent != count) {
|
||||
Fault("Release held privacy inputs failed (sent " + std::to_string(sent) +
|
||||
"/" + std::to_string(count) + ")",
|
||||
GetLastError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// SendInput acknowledges insertion, not delivery. Pump this hook thread
|
||||
// until the tagged releases have passed through and Windows reports no
|
||||
// pressed inputs. The caller cannot publish input_blocked/ready earlier.
|
||||
const auto deadline = GetTickCount64() + 1000;
|
||||
for (;;) {
|
||||
if (!PumpMessages()) {
|
||||
Fault("Privacy input handoff cancelled", ERROR_CANCELLED);
|
||||
return false;
|
||||
}
|
||||
int held = 0;
|
||||
for (int key = 1; key < 256; ++key) {
|
||||
if (GetAsyncKeyState(key) & 0x8000) {
|
||||
held = key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!held) {
|
||||
LOG_INFO("Privacy input handoff complete: released {} held inputs",
|
||||
releases.size());
|
||||
return true;
|
||||
}
|
||||
if (GetTickCount64() >= deadline) {
|
||||
Fault("Privacy input release timed out, still pressed vk=" +
|
||||
std::to_string(held),
|
||||
ERROR_TIMEOUT);
|
||||
return false;
|
||||
}
|
||||
if (MsgWaitForMultipleObjectsEx(1, &stop_, 10, QS_ALLINPUT,
|
||||
MWMO_INPUTAVAILABLE) == WAIT_FAILED) {
|
||||
Fault("Wait for privacy input releases failed", GetLastError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PrivacyInputGuard::Run() {
|
||||
current_ = this;
|
||||
running_.store(true);
|
||||
try {
|
||||
keyboard_ = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardHook,
|
||||
GetModuleHandleW(nullptr), 0);
|
||||
if (!keyboard_)
|
||||
Fault("Install privacy keyboard hook failed", GetLastError());
|
||||
if (keyboard_) {
|
||||
mouse_ =
|
||||
SetWindowsHookExW(WH_MOUSE_LL, MouseHook, GetModuleHandleW(nullptr), 0);
|
||||
if (!mouse_) Fault("Install privacy mouse hook failed", GetLastError());
|
||||
}
|
||||
healthy_.store(keyboard_ && mouse_ && ReleasePressedInputs());
|
||||
last_pump_.store(GetTickCount64());
|
||||
SetEvent(ready_);
|
||||
auto last_refresh = GetTickCount64();
|
||||
LOG_INFO("Privacy input message thread started: thread={}",
|
||||
GetCurrentThreadId());
|
||||
while (keyboard_ && mouse_ &&
|
||||
WaitForSingleObject(stop_, 0) == WAIT_TIMEOUT) {
|
||||
if (!PumpMessages()) break;
|
||||
const auto now = GetTickCount64();
|
||||
if (healthy_.load() && now - last_refresh >= 1000) {
|
||||
last_refresh = now;
|
||||
// Install replacements before retiring old hooks, with no input gap.
|
||||
if (Renew(keyboard_, WH_KEYBOARD_LL, KeyboardHook))
|
||||
Renew(mouse_, WH_MOUSE_LL, MouseHook);
|
||||
}
|
||||
if (MsgWaitForMultipleObjectsEx(1, &stop_, 100, QS_ALLINPUT,
|
||||
MWMO_INPUTAVAILABLE) == WAIT_FAILED) {
|
||||
Fault("Wait for privacy input messages failed", GetLastError());
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
Fault(std::string("Privacy input thread failed: ") + e.what(),
|
||||
ERROR_GEN_FAILURE);
|
||||
SetEvent(ready_);
|
||||
}
|
||||
healthy_.store(false);
|
||||
auto unhook = [this](HHOOK hook) {
|
||||
if (hook && !UnhookWindowsHookEx(hook) &&
|
||||
GetLastError() != ERROR_INVALID_HOOK_HANDLE)
|
||||
Fault("Release privacy input hook failed", GetLastError());
|
||||
};
|
||||
unhook(keyboard_);
|
||||
unhook(mouse_);
|
||||
for (auto hook : retired_) unhook(hook);
|
||||
keyboard_ = mouse_ = nullptr;
|
||||
retired_.clear();
|
||||
physical_keys_.fill(false);
|
||||
current_ = nullptr;
|
||||
running_.store(false);
|
||||
PostMessageW(owner_, WM_NULL, 0, 0);
|
||||
// Windows also removes hooks installed by a thread when that thread exits.
|
||||
LOG_INFO("Privacy input message thread stopped");
|
||||
}
|
||||
|
||||
LRESULT CALLBACK PrivacyInputGuard::KeyboardHook(int code, WPARAM wp,
|
||||
LPARAM lp) {
|
||||
auto self = current_;
|
||||
if (code < 0 || !self) return CallNextHookEx(nullptr, code, wp, lp);
|
||||
const auto& key = *reinterpret_cast<KBDLLHOOKSTRUCT*>(lp);
|
||||
if ((key.flags & LLKHF_INJECTED) &&
|
||||
key.dwExtraInfo == kInjectedKeyboardInputMarker)
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
if (!(key.flags & LLKHF_INJECTED) && key.vkCode < 256) {
|
||||
self->physical_keys_[key.vkCode] = !(key.flags & LLKHF_UP);
|
||||
const auto& keys = self->physical_keys_;
|
||||
if (key.vkCode == VK_F12 && !(key.flags & LLKHF_UP) &&
|
||||
(keys[VK_LCONTROL] || keys[VK_RCONTROL]) &&
|
||||
(keys[VK_LMENU] || keys[VK_RMENU]) &&
|
||||
(keys[VK_LSHIFT] || keys[VK_RSHIFT]))
|
||||
PostMessageW(self->owner_, self->exit_message_, 0, 0);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
LRESULT CALLBACK PrivacyInputGuard::MouseHook(int code, WPARAM wp, LPARAM lp) {
|
||||
auto self = current_;
|
||||
if (code < 0 || !self) return CallNextHookEx(nullptr, code, wp, lp);
|
||||
const auto& mouse = *reinterpret_cast<MSLLHOOKSTRUCT*>(lp);
|
||||
if ((mouse.flags & LLMHF_INJECTED) &&
|
||||
mouse.dwExtraInfo == kInjectedMouseInputMarker)
|
||||
return CallNextHookEx(nullptr, code, wp, lp);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_INPUT_GUARD_H_
|
||||
#define _PRIVACY_INPUT_GUARD_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// The owner controls lifecycle; the dedicated message thread owns LL hooks.
|
||||
// Rendering and session queries never run on this thread.
|
||||
class PrivacyInputGuard {
|
||||
public:
|
||||
~PrivacyInputGuard();
|
||||
bool Start(HWND owner, UINT exit_message, std::string& error);
|
||||
void Stop();
|
||||
bool Healthy(std::string& error) const;
|
||||
bool stopped() const { return !thread_.joinable(); }
|
||||
|
||||
private:
|
||||
void Run();
|
||||
bool ReleasePressedInputs();
|
||||
bool PumpMessages();
|
||||
void Fault(const std::string& operation, DWORD code);
|
||||
bool Renew(HHOOK& hook, int kind, HOOKPROC procedure);
|
||||
static LRESULT CALLBACK KeyboardHook(int code, WPARAM wp, LPARAM lp);
|
||||
static LRESULT CALLBACK MouseHook(int code, WPARAM wp, LPARAM lp);
|
||||
|
||||
std::thread thread_;
|
||||
HANDLE stop_ = nullptr;
|
||||
HANDLE ready_ = nullptr;
|
||||
HWND owner_ = nullptr;
|
||||
UINT exit_message_ = 0;
|
||||
std::atomic<bool> healthy_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<ULONGLONG> last_pump_{0};
|
||||
mutable std::mutex error_mutex_;
|
||||
std::string error_;
|
||||
HHOOK keyboard_ = nullptr;
|
||||
HHOOK mouse_ = nullptr;
|
||||
std::vector<HHOOK> retired_;
|
||||
std::array<bool, 256> physical_keys_{};
|
||||
inline static thread_local PrivacyInputGuard* current_ = nullptr;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_INPUT_GUARD_H_
|
||||
@@ -1,42 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_WINDOW_VISIBILITY_H_
|
||||
#define _PRIVACY_WINDOW_VISIBILITY_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Called for a visible foreign window whose full bounds intersect a cover.
|
||||
// Inspect it on every check: an OSD helper can grow into a content window.
|
||||
inline bool IsNonObstructingPrivacyWindow(HWND window, const RECT& bounds) {
|
||||
const auto styles = GetWindowLongPtrW(window, GWL_EXSTYLE);
|
||||
if (!(styles & WS_EX_LAYERED)) return false;
|
||||
|
||||
BYTE alpha = 0;
|
||||
DWORD flags = 0;
|
||||
// WS_EX_TRANSPARENT alone does not imply invisible pixels. Likewise,
|
||||
// UpdateLayeredWindow windows can reject this query; failure proves nothing.
|
||||
if (GetLayeredWindowAttributes(window, nullptr, &alpha, &flags) &&
|
||||
(flags & LWA_ALPHA) && alpha == 0)
|
||||
return true;
|
||||
|
||||
constexpr LONG_PTR helper_styles =
|
||||
WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TRANSPARENT;
|
||||
const auto width = static_cast<int64_t>(bounds.right) - bounds.left;
|
||||
const auto height = static_cast<int64_t>(bounds.bottom) - bounds.top;
|
||||
// Single-pixel, nonactivating OSD placeholders cannot display meaningful
|
||||
// desktop content. Use the whole window, never its clipped intersection;
|
||||
// larger OSDs and windows with unknown opacity still require coverage checks.
|
||||
return (styles & helper_styles) == helper_styles && width == 1 && height == 1;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_WINDOW_VISIBILITY_H_
|
||||
@@ -1,12 +1,11 @@
|
||||
#include "screen_capturer_dxgi.h"
|
||||
|
||||
#include <display_stream_id.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <display_stream_id.h>
|
||||
#include "libyuv.h"
|
||||
#include "rd_log.h"
|
||||
|
||||
@@ -219,13 +218,11 @@ void ScreenCapturerDxgi::EnumerateDisplays() {
|
||||
mi.rcMonitor.left, mi.rcMonitor.top,
|
||||
mi.rcMonitor.right, mi.rcMonitor.bottom);
|
||||
// primary first
|
||||
if (is_primary) {
|
||||
if (is_primary)
|
||||
display_info_list_.insert(display_info_list_.begin(), info);
|
||||
outputs_.insert(outputs_.begin(), output);
|
||||
} else {
|
||||
else
|
||||
display_info_list_.push_back(info);
|
||||
outputs_.push_back(output);
|
||||
}
|
||||
outputs_.push_back(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -249,11 +246,6 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
||||
}
|
||||
|
||||
staging_.Reset();
|
||||
DXGI_OUTDUPL_DESC desc{};
|
||||
duplication_->GetDesc(&desc);
|
||||
rotation_ = desc.Rotation;
|
||||
LOG_INFO("DXGI: duplication ready, monitor={}, rotation={}", monitor_index,
|
||||
static_cast<int>(rotation_));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -284,7 +276,6 @@ bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
++duplication_generation_;
|
||||
staging_.Reset();
|
||||
if (duplication_) {
|
||||
duplication_->ReleaseFrame();
|
||||
@@ -293,27 +284,16 @@ void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||
}
|
||||
|
||||
void ScreenCapturerDxgi::CaptureLoop() {
|
||||
const int timeout_ms = (std::max)(1, 1000 / (std::max)(1, fps_));
|
||||
bool cached_frame_valid = false;
|
||||
int cached_monitor = -1;
|
||||
uint64_t cached_generation = 0;
|
||||
std::vector<uint8_t> rotated_frame;
|
||||
const int timeout_ms = 33;
|
||||
auto last_duplication_retry =
|
||||
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
|
||||
while (running_) {
|
||||
if (paused_) {
|
||||
cached_frame_valid = false;
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Duplication/staging textures belong to one monitor. Do not release or
|
||||
// replace them in SwitchTo while AcquireNextFrame/Map is using them.
|
||||
std::unique_lock capture_lock(switch_mutex_);
|
||||
if (paused_ || !running_) continue;
|
||||
if (!duplication_) {
|
||||
cached_frame_valid = false;
|
||||
capture_lock.unlock();
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now - last_duplication_retry >= std::chrono::milliseconds(500)) {
|
||||
last_duplication_retry = now;
|
||||
@@ -323,31 +303,15 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
const int frame_monitor = monitor_index_.load();
|
||||
DXGI_OUTDUPL_FRAME_INFO frame_info{};
|
||||
Microsoft::WRL::ComPtr<IDXGIResource> desktop_resource;
|
||||
HRESULT hr = duplication_->AcquireNextFrame(
|
||||
timeout_ms, &frame_info, desktop_resource.GetAddressOf());
|
||||
if (hr == DXGI_ERROR_WAIT_TIMEOUT) {
|
||||
// DXGI explicitly reports an unchanged desktop. Re-emit only a frame
|
||||
// successfully acquired for this monitor to keep delivering static frames.
|
||||
if (cached_frame_valid && cached_generation == duplication_generation_ &&
|
||||
cached_monitor == monitor_index_.load() && callback_ && nv12_frame_) {
|
||||
const auto stream_id = MakeDisplayStreamId(cached_monitor);
|
||||
capture_lock.unlock();
|
||||
callback_(nv12_frame_, nv12_width_ * nv12_height_ * 3 / 2, nv12_width_,
|
||||
nv12_height_, stream_id.c_str(), nullptr);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// Never replay a cached image after an acquisition/conversion failure or
|
||||
// a backend rebuild. Normal error/reset handling releases optional privacy.
|
||||
cached_frame_valid = false;
|
||||
if (FAILED(hr)) {
|
||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||
capture_lock.unlock();
|
||||
if (callback_)
|
||||
callback_(nullptr, ScreenCapturer::kBackendReset, 0, 0, "", nullptr);
|
||||
RecreateDuplicationForCurrentMonitor();
|
||||
continue;
|
||||
}
|
||||
@@ -392,42 +356,9 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
continue;
|
||||
}
|
||||
|
||||
auto pixels = static_cast<const uint8_t*>(mapped.pData);
|
||||
int stride = static_cast<int>(mapped.RowPitch);
|
||||
int logical_width = static_cast<int>(src_desc.Width);
|
||||
int logical_height = static_cast<int>(src_desc.Height);
|
||||
libyuv::RotationMode rotation = libyuv::kRotate0;
|
||||
switch (rotation_) {
|
||||
case DXGI_MODE_ROTATION_ROTATE90:
|
||||
rotation = libyuv::kRotate90;
|
||||
break;
|
||||
case DXGI_MODE_ROTATION_ROTATE180:
|
||||
rotation = libyuv::kRotate180;
|
||||
break;
|
||||
case DXGI_MODE_ROTATION_ROTATE270:
|
||||
rotation = libyuv::kRotate270;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (rotation != libyuv::kRotate0) {
|
||||
const bool swap_axes = rotation != libyuv::kRotate180;
|
||||
const int rotated_width = swap_axes ? logical_height : logical_width;
|
||||
rotated_frame.resize(static_cast<size_t>(logical_width) * logical_height *
|
||||
4);
|
||||
if (libyuv::ARGBRotate(pixels, stride, rotated_frame.data(),
|
||||
rotated_width * 4, logical_width, logical_height,
|
||||
rotation) != 0) {
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
duplication_->ReleaseFrame();
|
||||
continue;
|
||||
}
|
||||
pixels = rotated_frame.data();
|
||||
stride = rotated_width * 4;
|
||||
if (swap_axes) std::swap(logical_width, logical_height);
|
||||
}
|
||||
int even_width = logical_width & ~1;
|
||||
int even_height = logical_height & ~1;
|
||||
int even_height = static_cast<int>(src_desc.Height) & ~1;
|
||||
if (even_width <= 0 || even_height <= 0) {
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
duplication_->ReleaseFrame();
|
||||
@@ -443,29 +374,25 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
||||
nv12_height_ = even_height;
|
||||
}
|
||||
|
||||
const int converted =
|
||||
libyuv::ARGBToNV12(pixels, stride, nv12_frame_, even_width,
|
||||
nv12_frame_ + even_width * even_height, even_width,
|
||||
even_width, even_height);
|
||||
libyuv::ARGBToNV12(static_cast<const uint8_t*>(mapped.pData),
|
||||
static_cast<int>(mapped.RowPitch), nv12_frame_,
|
||||
even_width, nv12_frame_ + even_width * even_height,
|
||||
even_width, even_width, even_height);
|
||||
|
||||
cached_generation = duplication_generation_;
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
duplication_->ReleaseFrame();
|
||||
capture_lock.unlock();
|
||||
|
||||
if (converted == 0 && frame_monitor == monitor_index_.load() && callback_) {
|
||||
int idx = frame_monitor;
|
||||
if (callback_) {
|
||||
int idx = monitor_index_.load();
|
||||
if (idx >= 0 && idx < static_cast<int>(display_info_list_.size())) {
|
||||
const std::string stream_id = MakeDisplayStreamId(idx);
|
||||
callback_(nv12_frame_, nv12_size, even_width, even_height,
|
||||
stream_id.c_str(), nullptr);
|
||||
cached_monitor = idx;
|
||||
cached_frame_valid = true;
|
||||
} else {
|
||||
LOG_ERROR("DXGI: CaptureLoop invalid monitor_index {} (list size {})",
|
||||
idx, display_info_list_.size());
|
||||
}
|
||||
}
|
||||
|
||||
d3d_context_->Unmap(staging_.Get(), 0);
|
||||
duplication_->ReleaseFrame();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -63,8 +63,6 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
||||
Microsoft::WRL::ComPtr<ID3D11DeviceContext> d3d_context_;
|
||||
Microsoft::WRL::ComPtr<IDXGIOutputDuplication> duplication_;
|
||||
Microsoft::WRL::ComPtr<ID3D11Texture2D> staging_;
|
||||
DXGI_MODE_ROTATION rotation_ = DXGI_MODE_ROTATION_IDENTITY;
|
||||
uint64_t duplication_generation_ = 0;
|
||||
|
||||
std::atomic<bool> running_{false};
|
||||
std::atomic<bool> paused_{false};
|
||||
|
||||
@@ -351,10 +351,6 @@ int ScreenCapturerWgc::ResetToInitialMonitor() {
|
||||
}
|
||||
void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
||||
int id) {
|
||||
if (!frame.data && frame.width == 0 && frame.height == 0) {
|
||||
if (on_data_) on_data_(nullptr, ScreenCapturer::kBackendReset, 0, 0, "", nullptr);
|
||||
return;
|
||||
}
|
||||
if (!running_ || !on_data_) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -127,13 +127,6 @@ class WgcPluginCapturer final : public ScreenCapturer {
|
||||
DestroyFn destroy_fn_ = nullptr;
|
||||
};
|
||||
|
||||
const char* CaptureBackendName(const ScreenCapturer* capturer) {
|
||||
if (dynamic_cast<const ScreenCapturerDxgi*>(capturer)) return "DXGI";
|
||||
if (dynamic_cast<const WgcPluginCapturer*>(capturer)) return "WGC";
|
||||
if (dynamic_cast<const ScreenCapturerGdi*>(capturer)) return "GDI";
|
||||
return "unavailable";
|
||||
}
|
||||
|
||||
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
|
||||
bool show_cursor,
|
||||
const std::string& stage,
|
||||
@@ -337,13 +330,7 @@ bool QuerySecureDesktopHelperFrame(
|
||||
ScreenCapturerWin::ScreenCapturerWin() {}
|
||||
ScreenCapturerWin::~ScreenCapturerWin() { Destroy(); }
|
||||
|
||||
void ScreenCapturerWin::NotifyPrivacyCapture(bool running) {
|
||||
if (privacy_)
|
||||
privacy_->CaptureChanged(running);
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
NotifyPrivacyCapture(false);
|
||||
fps_ = fps;
|
||||
cb_orig_ = cb;
|
||||
native_output_logged_.store(false, std::memory_order_relaxed);
|
||||
@@ -357,10 +344,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
cb_ = [this](unsigned char* data, int size, int w, int h,
|
||||
const char* reported_stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame) {
|
||||
if (size == ScreenCapturer::kBackendReset) {
|
||||
if (privacy_) privacy_->Fail("Capture restarted; privacy screen will turn off");
|
||||
return;
|
||||
}
|
||||
if (secure_desktop_capture_active_.load(std::memory_order_relaxed)) {
|
||||
return;
|
||||
}
|
||||
@@ -381,9 +364,6 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
mapped_stream_id.c_str(), canonical_displays_.size(),
|
||||
monitor_index_.load(std::memory_order_relaxed));
|
||||
}
|
||||
if (privacy_ && privacy_->Engaged() && !IsWindowsPrivacyDesktopAvailable()) {
|
||||
privacy_->Fail("Secure or unavailable desktop; privacy screen will turn off");
|
||||
}
|
||||
if (mapped_stream_id.empty()) {
|
||||
if (!invalid_stream_id_logged_.exchange(true,
|
||||
std::memory_order_relaxed)) {
|
||||
@@ -413,34 +393,50 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
||||
native_frame);
|
||||
};
|
||||
|
||||
impl_.reset();
|
||||
int ret = -1;
|
||||
auto try_init = [&](std::unique_ptr<ScreenCapturer> candidate) {
|
||||
ret = candidate ? candidate->Init(fps_, cb_) : -1;
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Windows capturer: {} init failed (ret={})",
|
||||
CaptureBackendName(candidate.get()), ret);
|
||||
return false;
|
||||
}
|
||||
impl_ = std::move(candidate);
|
||||
return true;
|
||||
};
|
||||
// Short-circuiting also keeps the unused WGC plugin unloaded.
|
||||
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
|
||||
try_init(std::make_unique<ScreenCapturerDxgi>())) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
|
||||
try_init(WgcPluginCapturer::Create())) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
|
||||
try_init(std::make_unique<ScreenCapturerGdi>()))) {
|
||||
LOG_INFO("Windows capturer: using {}", CaptureBackendName(impl_.get()));
|
||||
|
||||
impl_ = WgcPluginCapturer::Create();
|
||||
impl_is_wgc_plugin_ = (impl_ != nullptr);
|
||||
ret = impl_ ? impl_->Init(fps_, cb_) : -1;
|
||||
if (ret == 0) {
|
||||
LOG_INFO("Windows capturer: using WGC plugin");
|
||||
BuildCanonicalFromImpl();
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
initial_monitor_index_ = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Windows capturer: selected capture method {} failed, ret={}",
|
||||
static_cast<int>(capture_method_), ret);
|
||||
LOG_WARN("Windows capturer: WGC plugin init failed (ret={}), try DXGI", ret);
|
||||
impl_.reset();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
|
||||
impl_ = std::make_unique<ScreenCapturerDxgi>();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
LOG_INFO("Windows capturer: using DXGI Desktop Duplication");
|
||||
BuildCanonicalFromImpl();
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
initial_monitor_index_ = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_WARN("Windows capturer: DXGI init failed (ret={}), fallback to GDI", ret);
|
||||
impl_.reset();
|
||||
|
||||
impl_ = std::make_unique<ScreenCapturerGdi>();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
ret = impl_->Init(fps_, cb_);
|
||||
if (ret == 0) {
|
||||
LOG_INFO("Windows capturer: using GDI BitBlt");
|
||||
BuildCanonicalFromImpl();
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
initial_monitor_index_ = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
LOG_ERROR("Windows capturer: all implementations failed, ret={}", ret);
|
||||
impl_.reset();
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -450,6 +446,7 @@ int ScreenCapturerWin::Destroy() {
|
||||
if (impl_) {
|
||||
impl_->Destroy();
|
||||
impl_.reset();
|
||||
impl_is_wgc_plugin_ = false;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||
@@ -462,13 +459,7 @@ int ScreenCapturerWin::Destroy() {
|
||||
|
||||
void ScreenCapturerWin::EmitCapturedFrame(
|
||||
unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame,
|
||||
bool from_secure_desktop) {
|
||||
// Secure-desktop capture takes priority; remove privacy without dropping
|
||||
// the helper frame or delaying the remote session.
|
||||
if (from_secure_desktop && privacy_ && privacy_->Engaged()) {
|
||||
privacy_->Fail("Secure desktop helper frame arrived; privacy screen will turn off");
|
||||
}
|
||||
const char* stream_id, const MiniRtcNativeVideoFrame* native_frame) {
|
||||
if (!cb_orig_) {
|
||||
return;
|
||||
}
|
||||
@@ -508,43 +499,11 @@ void ScreenCapturerWin::EmitCapturedFrame(
|
||||
owned_frame->Release();
|
||||
}
|
||||
|
||||
void ScreenCapturerWin::RestoreMonitor(int monitor_index) {
|
||||
RebuildAliasesFromImpl();
|
||||
if (monitor_index > 0 && impl_->SwitchTo(monitor_index) != 0) {
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::TryStartBackend(
|
||||
std::unique_ptr<ScreenCapturer> candidate, int monitor_index) {
|
||||
if (!candidate) {
|
||||
LOG_WARN("Windows capturer: WGC plugin unavailable");
|
||||
return false;
|
||||
}
|
||||
const char* name = CaptureBackendName(candidate.get());
|
||||
int ret = candidate->Init(fps_, cb_);
|
||||
if (ret == 0)
|
||||
ret = candidate->Start(show_cursor_.load(std::memory_order_relaxed));
|
||||
if (ret != 0) {
|
||||
LOG_WARN("Windows capturer: {} initialization/start failed (ret={})", name,
|
||||
ret);
|
||||
candidate->Destroy();
|
||||
return false;
|
||||
}
|
||||
// Commit only after the replacement is running. Failed candidates release
|
||||
// their resources without replacing the current backend.
|
||||
impl_ = std::move(candidate);
|
||||
RestoreMonitor(monitor_index);
|
||||
LOG_INFO("Windows capturer: started {}", name);
|
||||
return true;
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
if (!impl_) return -1;
|
||||
if (running_.load(std::memory_order_relaxed)) {
|
||||
return 0;
|
||||
}
|
||||
NotifyPrivacyCapture(false);
|
||||
|
||||
show_cursor_.store(show_cursor, std::memory_order_relaxed);
|
||||
paused_.store(false, std::memory_order_relaxed);
|
||||
@@ -566,23 +525,40 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
}
|
||||
}
|
||||
if (ret != 0) {
|
||||
if (capture_method_ != ScreenCaptureMethod::Auto) {
|
||||
LOG_ERROR("Windows capturer: selected backend {} failed to start (ret={})",
|
||||
CaptureBackendName(impl_.get()), ret);
|
||||
return ret;
|
||||
}
|
||||
LOG_WARN("Windows capturer: refresh/start failed (ret={}), trying fallback",
|
||||
ret);
|
||||
|
||||
auto try_init_start = [&](std::unique_ptr<ScreenCapturer> cand) -> bool {
|
||||
int r = cand->Init(fps_, cb_);
|
||||
if (r != 0) return false;
|
||||
int s = cand->Start(show_cursor);
|
||||
if (s == 0) {
|
||||
impl_ = std::move(cand);
|
||||
impl_is_wgc_plugin_ = false;
|
||||
RebuildAliasesFromImpl();
|
||||
if (requested_monitor > 0 &&
|
||||
impl_->SwitchTo(requested_monitor) != 0) {
|
||||
monitor_index_.store(0, std::memory_order_relaxed);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
bool fallback_started = false;
|
||||
if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get()) &&
|
||||
TryStartBackend(WgcPluginCapturer::Create(), requested_monitor)) {
|
||||
fallback_started = true;
|
||||
}
|
||||
if (!fallback_started && (dynamic_cast<WgcPluginCapturer*>(impl_.get()) ||
|
||||
dynamic_cast<ScreenCapturerDxgi*>(impl_.get()))) {
|
||||
fallback_started = TryStartBackend(std::make_unique<ScreenCapturerGdi>(),
|
||||
requested_monitor);
|
||||
if (impl_is_wgc_plugin_) {
|
||||
if (try_init_start(std::make_unique<ScreenCapturerDxgi>())) {
|
||||
LOG_INFO("Windows capturer: fallback to DXGI");
|
||||
fallback_started = true;
|
||||
} else if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
|
||||
LOG_INFO("Windows capturer: fallback to GDI");
|
||||
fallback_started = true;
|
||||
}
|
||||
} else if (dynamic_cast<ScreenCapturerDxgi*>(impl_.get())) {
|
||||
if (try_init_start(std::make_unique<ScreenCapturerGdi>())) {
|
||||
LOG_INFO("Windows capturer: fallback to GDI");
|
||||
fallback_started = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fallback_started) {
|
||||
@@ -592,7 +568,6 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
}
|
||||
|
||||
running_.store(true, std::memory_order_relaxed);
|
||||
NotifyPrivacyCapture(true);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
std::memory_order_relaxed);
|
||||
@@ -606,7 +581,6 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
||||
}
|
||||
|
||||
int ScreenCapturerWin::Stop() {
|
||||
NotifyPrivacyCapture(false);
|
||||
running_.store(false, std::memory_order_relaxed);
|
||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||
post_secure_desktop_waiting_for_frame_.store(false,
|
||||
@@ -639,8 +613,6 @@ int ScreenCapturerWin::SwitchTo(int monitor_index) {
|
||||
const int ret = impl_->SwitchTo(monitor_index);
|
||||
if (ret == 0) {
|
||||
monitor_index_.store(monitor_index, std::memory_order_relaxed);
|
||||
} else if (privacy_) {
|
||||
privacy_->Fail("Display switch failed; privacy screen will turn off");
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
@@ -763,19 +735,53 @@ void ScreenCapturerWin::StopSecureCaptureThread() {
|
||||
}
|
||||
|
||||
bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
||||
NotifyPrivacyCapture(false);
|
||||
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) {
|
||||
RestoreMonitor(current_monitor);
|
||||
restore_monitor();
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -789,16 +795,20 @@ bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
||||
ret = impl_->Start(show_cursor);
|
||||
}
|
||||
if (ret == 0) {
|
||||
RestoreMonitor(current_monitor);
|
||||
restore_monitor();
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((IsBackendEnabled(ScreenCaptureMethod::Dxgi) &&
|
||||
TryStartBackend(std::make_unique<ScreenCapturerDxgi>(), current_monitor)) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Wgc) &&
|
||||
TryStartBackend(WgcPluginCapturer::Create(), current_monitor)) ||
|
||||
(IsBackendEnabled(ScreenCaptureMethod::Gdi) &&
|
||||
TryStartBackend(std::make_unique<ScreenCapturerGdi>(), current_monitor))) {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1113,12 +1123,6 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
std::vector<uint8_t> secure_frame;
|
||||
|
||||
while (running_.load(std::memory_order_relaxed)) {
|
||||
// Privacy covers belong to the ordinary desktop. Drop privacy on a
|
||||
// secure desktop, while continuing the normal helper capture path.
|
||||
if (privacy_ && privacy_->Engaged()) {
|
||||
if (!IsWindowsPrivacyDesktopAvailable())
|
||||
privacy_->Fail("Secure desktop or lock screen; privacy screen will turn off. Privacy cannot cover Windows security UI.");
|
||||
}
|
||||
if (paused_.load(std::memory_order_relaxed)) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
continue;
|
||||
@@ -1291,7 +1295,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||
if (frame_delivered && !secure_frame.empty()) {
|
||||
EmitCapturedFrame(secure_frame.data(),
|
||||
static_cast<int>(secure_frame.size()), captured_width,
|
||||
captured_height, display_name.c_str(), nullptr, true);
|
||||
captured_height, display_name.c_str());
|
||||
}
|
||||
|
||||
if (!frame_delivered && !frame_pending) {
|
||||
|
||||
@@ -20,8 +20,6 @@
|
||||
#include <vector>
|
||||
|
||||
#include "screen_capturer.h"
|
||||
#include "screen_capture_method.h"
|
||||
#include "privacy_controller.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
@@ -45,19 +43,10 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
int ResetToInitialMonitor() override;
|
||||
|
||||
std::vector<DisplayInfo> GetDisplayInfoList() override;
|
||||
void SetPrivacyController(PrivacyController* privacy) override { privacy_ = privacy; }
|
||||
// Set before Init(), while capture is stopped.
|
||||
void SetCaptureMethod(ScreenCaptureMethod method) { capture_method_ = method; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<ScreenCapturer> impl_;
|
||||
ScreenCaptureMethod capture_method_ = ScreenCaptureMethod::Auto;
|
||||
bool IsBackendEnabled(ScreenCaptureMethod method) const {
|
||||
return capture_method_ == ScreenCaptureMethod::Auto ||
|
||||
capture_method_ == method;
|
||||
}
|
||||
PrivacyController* privacy_ = nullptr;
|
||||
void NotifyPrivacyCapture(bool running);
|
||||
bool impl_is_wgc_plugin_ = false;
|
||||
int fps_ = 60;
|
||||
cb_desktop_data cb_;
|
||||
cb_desktop_data cb_orig_;
|
||||
@@ -97,13 +86,9 @@ class ScreenCapturerWin : public ScreenCapturer {
|
||||
|
||||
void BuildCanonicalFromImpl();
|
||||
void RebuildAliasesFromImpl();
|
||||
void RestoreMonitor(int monitor_index);
|
||||
bool TryStartBackend(std::unique_ptr<ScreenCapturer> candidate,
|
||||
int monitor_index);
|
||||
void EmitCapturedFrame(unsigned char* data, int size, int width, int height,
|
||||
const char* stream_id,
|
||||
const MiniRtcNativeVideoFrame* native_frame = nullptr,
|
||||
bool from_secure_desktop = false);
|
||||
const MiniRtcNativeVideoFrame* native_frame = nullptr);
|
||||
void StopSecureCaptureThread();
|
||||
bool RestartCaptureBackendAfterSecureDesktop();
|
||||
void SecureDesktopCaptureLoop();
|
||||
|
||||
@@ -339,7 +339,6 @@ void WgcSessionImpl::OnClosed(
|
||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||
winrt::Windows::Foundation::IInspectable const&) {
|
||||
std::lock_guard locker(lock_);
|
||||
if (observer_) observer_->OnFrame({}, id_);
|
||||
const bool was_running = is_running_;
|
||||
const bool was_paused = is_paused_;
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
#include "windows_installer.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <shobjidl.h>
|
||||
#include <winhttp.h>
|
||||
#include <wrl/client.h>
|
||||
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
namespace {
|
||||
|
||||
constexpr uint64_t kMaxInstallerSize = 1024ull * 1024 * 1024;
|
||||
// Stable identity used only for Windows Attachment Services policy/UI.
|
||||
constexpr GUID kAttachmentClient = {
|
||||
0x72d17a6b,
|
||||
0xeac1,
|
||||
0x4816,
|
||||
{0x91, 0x40, 0x33, 0x1b, 0xa7, 0x76, 0x22, 0xcd}};
|
||||
|
||||
void CheckWin32(BOOL success, const char* operation) {
|
||||
if (!success) {
|
||||
throw std::system_error(static_cast<int>(GetLastError()),
|
||||
std::system_category(), operation);
|
||||
}
|
||||
}
|
||||
|
||||
struct InternetCloser {
|
||||
void operator()(HINTERNET handle) const { WinHttpCloseHandle(handle); }
|
||||
};
|
||||
using InternetHandle = std::unique_ptr<void, InternetCloser>;
|
||||
|
||||
struct PartialFile {
|
||||
std::filesystem::path path;
|
||||
~PartialFile() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path, error);
|
||||
}
|
||||
};
|
||||
|
||||
bool IsWindowsExecutable(const std::filesystem::path& path) {
|
||||
std::ifstream input(path, std::ios::binary);
|
||||
std::array<unsigned char, 64> header{};
|
||||
input.read(reinterpret_cast<char*>(header.data()), header.size());
|
||||
if (!input || header[0] != 'M' || header[1] != 'Z') return false;
|
||||
const uint32_t pe_offset =
|
||||
uint32_t(header[60]) | (uint32_t(header[61]) << 8) |
|
||||
(uint32_t(header[62]) << 16) | (uint32_t(header[63]) << 24);
|
||||
if (pe_offset < header.size()) return false;
|
||||
input.seekg(pe_offset);
|
||||
std::array<char, 4> signature{};
|
||||
input.read(signature.data(), signature.size());
|
||||
return input && signature == std::array<char, 4>{'P', 'E', 0, 0};
|
||||
}
|
||||
|
||||
uint64_t ContentLength(HINTERNET request) {
|
||||
std::array<wchar_t, 32> value{};
|
||||
DWORD bytes = static_cast<DWORD>(sizeof(value));
|
||||
if (!WinHttpQueryHeaders(request, WINHTTP_QUERY_CONTENT_LENGTH,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, value.data(), &bytes,
|
||||
WINHTTP_NO_HEADER_INDEX)) {
|
||||
if (GetLastError() == ERROR_WINHTTP_HEADER_NOT_FOUND) return 0;
|
||||
CheckWin32(FALSE, "WinHttpQueryHeaders(Content-Length)");
|
||||
}
|
||||
const std::wstring length(value.data());
|
||||
if (length.empty() ||
|
||||
length.find_first_not_of(L"0123456789") != std::wstring::npos) {
|
||||
throw std::runtime_error("Invalid installer content length");
|
||||
}
|
||||
return std::stoull(length);
|
||||
}
|
||||
|
||||
class ComApartment {
|
||||
public:
|
||||
ComApartment() : status_(CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED)) {}
|
||||
~ComApartment() {
|
||||
if (SUCCEEDED(status_)) CoUninitialize();
|
||||
}
|
||||
bool Ready() const {
|
||||
return SUCCEEDED(status_) || status_ == RPC_E_CHANGED_MODE;
|
||||
}
|
||||
|
||||
private:
|
||||
HRESULT status_;
|
||||
};
|
||||
|
||||
bool HandleAttachment(const std::filesystem::path& installer,
|
||||
const std::string& source, bool execute) {
|
||||
ComApartment apartment;
|
||||
if (!apartment.Ready()) {
|
||||
LOG_WARN("Cannot initialize Windows Attachment Services");
|
||||
return false;
|
||||
}
|
||||
Microsoft::WRL::ComPtr<IAttachmentExecute> attachment;
|
||||
HRESULT result =
|
||||
CoCreateInstance(CLSID_AttachmentServices, nullptr, CLSCTX_INPROC_SERVER,
|
||||
IID_PPV_ARGS(&attachment));
|
||||
const std::wstring source_url(source.begin(), source.end());
|
||||
if (SUCCEEDED(result)) result = attachment->SetClientGuid(kAttachmentClient);
|
||||
if (SUCCEEDED(result))
|
||||
result = attachment->SetClientTitle(L"CrossDesk Update");
|
||||
if (SUCCEEDED(result)) result = attachment->SetLocalPath(installer.c_str());
|
||||
if (SUCCEEDED(result)) result = attachment->SetSource(source_url.c_str());
|
||||
if (SUCCEEDED(result)) {
|
||||
if (execute) {
|
||||
HANDLE process = nullptr;
|
||||
// Execute applies attachment policy, scanning, and any required prompts.
|
||||
// The installer's manifest, rather than this process, requests elevation.
|
||||
result = attachment->Execute(GetActiveWindow(), L"open", &process);
|
||||
if (process) CloseHandle(process);
|
||||
} else {
|
||||
// Save records the source zone and invokes installed trust/AV services.
|
||||
result = attachment->Save();
|
||||
}
|
||||
}
|
||||
if (FAILED(result)) {
|
||||
LOG_WARN("Windows Attachment Services {} failed/blocked, HRESULT=0x{:08X}",
|
||||
execute ? "execute" : "save", static_cast<uint32_t>(result));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool DownloadWindowsInstaller(
|
||||
const InstallerDownload& download, const std::filesystem::path& destination,
|
||||
const std::function<bool(uint64_t, uint64_t)>& progress) {
|
||||
auto partial_path = destination;
|
||||
partial_path += ".part";
|
||||
PartialFile partial{partial_path};
|
||||
try {
|
||||
if (!progress(0, 0)) return false;
|
||||
const std::string agent = "CrossDesk/" CROSSDESK_VERSION " WindowsUpdater";
|
||||
const std::wstring user_agent(agent.begin(), agent.end());
|
||||
InternetHandle session(
|
||||
WinHttpOpen(user_agent.c_str(), WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY,
|
||||
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0));
|
||||
CheckWin32(session != nullptr, "WinHttpOpen");
|
||||
CheckWin32(WinHttpSetTimeouts(session.get(), 5000, 5000, 5000, 5000),
|
||||
"WinHttpSetTimeouts");
|
||||
InternetHandle connection(WinHttpConnect(session.get(),
|
||||
L"downloads.crossdesk.cn",
|
||||
INTERNET_DEFAULT_HTTPS_PORT, 0));
|
||||
CheckWin32(connection != nullptr, "WinHttpConnect");
|
||||
const std::wstring path(download.path.begin(), download.path.end());
|
||||
InternetHandle request(WinHttpOpenRequest(
|
||||
connection.get(), L"GET", path.c_str(), nullptr, WINHTTP_NO_REFERER,
|
||||
WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE));
|
||||
CheckWin32(request != nullptr, "WinHttpOpenRequest");
|
||||
DWORD disabled = WINHTTP_DISABLE_REDIRECTS;
|
||||
CheckWin32(WinHttpSetOption(request.get(), WINHTTP_OPTION_DISABLE_FEATURE,
|
||||
&disabled, sizeof(disabled)),
|
||||
"Disable redirects");
|
||||
// Use Windows certificate validation and configured proxies as-is. Do not
|
||||
// ignore TLS errors, spoof a browser, or retry through alternative hosts.
|
||||
CheckWin32(WinHttpSendRequest(request.get(), WINHTTP_NO_ADDITIONAL_HEADERS,
|
||||
0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0),
|
||||
"WinHttpSendRequest");
|
||||
CheckWin32(WinHttpReceiveResponse(request.get(), nullptr),
|
||||
"WinHttpReceiveResponse");
|
||||
DWORD status = 0;
|
||||
DWORD status_size = sizeof(status);
|
||||
CheckWin32(WinHttpQueryHeaders(
|
||||
request.get(),
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
WINHTTP_HEADER_NAME_BY_INDEX, &status, &status_size,
|
||||
WINHTTP_NO_HEADER_INDEX),
|
||||
"WinHttpQueryHeaders(status)");
|
||||
if (status != 200) {
|
||||
LOG_WARN("Windows installer download returned HTTP {}", status);
|
||||
return false;
|
||||
}
|
||||
const uint64_t total = ContentLength(request.get());
|
||||
if (total > kMaxInstallerSize || !progress(0, total)) return false;
|
||||
std::ofstream output(partial.path, std::ios::binary | std::ios::trunc);
|
||||
if (!output)
|
||||
throw std::runtime_error("Cannot create installer download file");
|
||||
uint64_t received = 0;
|
||||
std::array<unsigned char, 64 * 1024> buffer{};
|
||||
while (progress(received, total)) {
|
||||
DWORD count = 0;
|
||||
CheckWin32(WinHttpReadData(request.get(), buffer.data(),
|
||||
static_cast<DWORD>(buffer.size()), &count),
|
||||
"WinHttpReadData");
|
||||
if (count == 0) break;
|
||||
received += count;
|
||||
if (received > kMaxInstallerSize || (total > 0 && received > total))
|
||||
return false;
|
||||
output.write(reinterpret_cast<const char*>(buffer.data()), count);
|
||||
if (!output)
|
||||
throw std::runtime_error("Cannot write installer download file");
|
||||
}
|
||||
output.close();
|
||||
if (output.fail() || received == 0 || (total > 0 && received != total) ||
|
||||
!progress(received, total) || !IsWindowsExecutable(partial.path))
|
||||
return false;
|
||||
LOG_INFO("Windows installer downloaded, bytes={}", received);
|
||||
std::filesystem::rename(partial.path, destination);
|
||||
return true;
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Windows installer download failed: {}", error.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool CheckWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source) {
|
||||
return HandleAttachment(installer, source, false);
|
||||
}
|
||||
|
||||
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source) {
|
||||
return HandleAttachment(installer, source, true);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_INSTALLER_H_
|
||||
#define _WINDOWS_INSTALLER_H_
|
||||
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
|
||||
#include "installer_download.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
bool DownloadWindowsInstaller(
|
||||
const InstallerDownload& download, const std::filesystem::path& destination,
|
||||
const std::function<bool(uint64_t, uint64_t)>& progress);
|
||||
|
||||
// Attachment Services records the Internet source and applies Windows policy
|
||||
// and installed antivirus checks. Failure must never fall back to direct
|
||||
// launch.
|
||||
bool CheckWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source);
|
||||
bool LaunchWindowsInstaller(const std::filesystem::path& installer,
|
||||
const std::string& source);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,53 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_THREAD_DPI_H_
|
||||
#define _WINDOWS_THREAD_DPI_H_
|
||||
|
||||
#include <Windows.h>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Keep newer DPI entry points out of the import table: privacy capability
|
||||
// checks must still be able to report unsupported on pre-2004 Windows.
|
||||
class ScopedWindowsPhysicalCoordinates {
|
||||
public:
|
||||
using SetContext = DPI_AWARENESS_CONTEXT(WINAPI*)(DPI_AWARENESS_CONTEXT);
|
||||
ScopedWindowsPhysicalCoordinates() {
|
||||
setter_ = reinterpret_cast<SetContext>(GetProcAddress(
|
||||
GetModuleHandleW(L"user32.dll"), "SetThreadDpiAwarenessContext"));
|
||||
if (setter_) {
|
||||
previous_ = setter_(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
// Ordinary remote input retains compatibility with Windows 10 1607.
|
||||
// The privacy backend itself requires build 19041 before using this.
|
||||
if (!previous_)
|
||||
previous_ = setter_(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE);
|
||||
}
|
||||
}
|
||||
~ScopedWindowsPhysicalCoordinates() {
|
||||
if (setter_ && previous_) setter_(previous_);
|
||||
}
|
||||
ScopedWindowsPhysicalCoordinates(const ScopedWindowsPhysicalCoordinates&) =
|
||||
delete;
|
||||
ScopedWindowsPhysicalCoordinates& operator=(
|
||||
const ScopedWindowsPhysicalCoordinates&) = delete;
|
||||
bool active() const { return previous_ != nullptr; }
|
||||
|
||||
private:
|
||||
SetContext setter_ = nullptr;
|
||||
DPI_AWARENESS_CONTEXT previous_ = nullptr;
|
||||
};
|
||||
|
||||
inline UINT PrivacyWindowDpi(HWND hwnd) {
|
||||
using GetDpi = UINT(WINAPI*)(HWND);
|
||||
static const auto get_dpi = reinterpret_cast<GetDpi>(
|
||||
GetProcAddress(GetModuleHandleW(L"user32.dll"), "GetDpiForWindow"));
|
||||
return get_dpi ? get_dpi(hwnd) : 96;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _WINDOWS_THREAD_DPI_H_
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "windows_updater.h"
|
||||
|
||||
#include <shlobj.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
|
||||
#include "installer_download.h"
|
||||
#include "rd_log.h"
|
||||
#include "windows_installer.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
WindowsUpdater::~WindowsUpdater() {
|
||||
Cancel();
|
||||
if (worker_.joinable()) worker_.join();
|
||||
RemoveDownload();
|
||||
}
|
||||
|
||||
void WindowsUpdater::RemoveDownload() {
|
||||
// A running NSIS installer still needs its source file. Leave that one in
|
||||
// the user's update cache; clean up cancelled/failed attempts.
|
||||
if (installer_.empty() || installer_launched_) return;
|
||||
std::error_code error;
|
||||
std::filesystem::remove(installer_, error);
|
||||
std::filesystem::remove(installer_.parent_path(), error);
|
||||
}
|
||||
|
||||
void WindowsUpdater::Start(const nlohmann::json& metadata) {
|
||||
const State state = GetState();
|
||||
if (state == State::Downloading || state == State::Checking ||
|
||||
state == State::Launching || state == State::SecurityBlocked)
|
||||
return;
|
||||
if (state == State::Ready || state == State::LaunchFailed ||
|
||||
state == State::Launched) {
|
||||
cancelled_ = false;
|
||||
Launch();
|
||||
return;
|
||||
}
|
||||
if (worker_.joinable()) worker_.join();
|
||||
RemoveDownload();
|
||||
installer_.clear();
|
||||
installer_launched_ = false;
|
||||
cancelled_ = false;
|
||||
downloaded_ = 0;
|
||||
total_ = 0;
|
||||
const auto download = GetWindowsInstallerDownload(metadata);
|
||||
if (!download) {
|
||||
LOG_WARN("No valid Windows installer for the advertised version");
|
||||
state_ = State::Failed;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
std::random_device random;
|
||||
PWSTR local_app_data = nullptr;
|
||||
const HRESULT path_result = SHGetKnownFolderPath(
|
||||
FOLDERID_LocalAppData, KF_FLAG_CREATE, nullptr, &local_app_data);
|
||||
if (FAILED(path_result))
|
||||
throw std::runtime_error("Cannot locate LocalAppData");
|
||||
const std::filesystem::path data_path(local_app_data);
|
||||
CoTaskMemFree(local_app_data);
|
||||
const auto updates = data_path / L"CrossDesk" / L"Updates";
|
||||
std::filesystem::create_directories(updates);
|
||||
for (int attempt = 0; attempt < 10; ++attempt) {
|
||||
const auto directory = updates / ("download-" + std::to_string(random()) +
|
||||
"-" + std::to_string(random()));
|
||||
if (std::filesystem::create_directory(directory)) {
|
||||
installer_ = directory / download->filename;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (installer_.empty())
|
||||
throw std::runtime_error("Cannot create update directory");
|
||||
source_url_ = download->origin + download->path;
|
||||
state_ = State::Downloading;
|
||||
worker_ = std::thread([this, download = *download] {
|
||||
const bool success = DownloadWindowsInstaller(
|
||||
download, installer_, [this](uint64_t received, uint64_t total) {
|
||||
downloaded_ = received;
|
||||
total_ = total;
|
||||
return !cancelled_.load();
|
||||
});
|
||||
if (success && !cancelled_) {
|
||||
state_ = State::Checking;
|
||||
state_ = CheckWindowsInstaller(installer_, source_url_)
|
||||
? State::Ready
|
||||
: State::SecurityBlocked;
|
||||
} else {
|
||||
state_ = State::Failed;
|
||||
}
|
||||
if (cancelled_) state_ = State::Idle;
|
||||
});
|
||||
} catch (const std::exception& error) {
|
||||
LOG_WARN("Cannot start Windows update: {}", error.what());
|
||||
state_ = State::Failed;
|
||||
}
|
||||
}
|
||||
|
||||
void WindowsUpdater::Cancel() {
|
||||
cancelled_ = true;
|
||||
if (GetState() == State::Ready) state_ = State::Idle;
|
||||
}
|
||||
|
||||
void WindowsUpdater::Launch() {
|
||||
if (cancelled_) return;
|
||||
const State state = GetState();
|
||||
if (state != State::Ready && state != State::LaunchFailed &&
|
||||
state != State::Launched)
|
||||
return;
|
||||
if (worker_.joinable()) worker_.join();
|
||||
state_ = State::Launching;
|
||||
if (!LaunchWindowsInstaller(installer_, source_url_)) {
|
||||
state_ = State::LaunchFailed;
|
||||
return;
|
||||
}
|
||||
installer_launched_ = true;
|
||||
state_ = State::Launched;
|
||||
// NSIS already prompts to close CrossDesk and stops the installed service.
|
||||
// Keep this process alive if the user subsequently cancels installation.
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _WINDOWS_UPDATER_H_
|
||||
#define _WINDOWS_UPDATER_H_
|
||||
|
||||
#include <atomic>
|
||||
#include <filesystem>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class WindowsUpdater {
|
||||
public:
|
||||
enum class State {
|
||||
Idle,
|
||||
Downloading,
|
||||
Checking,
|
||||
Ready,
|
||||
Launching,
|
||||
Failed,
|
||||
SecurityBlocked,
|
||||
LaunchFailed,
|
||||
Launched
|
||||
};
|
||||
|
||||
~WindowsUpdater();
|
||||
void Start(const nlohmann::json& metadata);
|
||||
void Cancel();
|
||||
void Launch();
|
||||
State GetState() const { return state_.load(); }
|
||||
uint64_t Downloaded() const { return downloaded_.load(); }
|
||||
uint64_t Total() const { return total_.load(); }
|
||||
|
||||
private:
|
||||
void RemoveDownload();
|
||||
|
||||
std::atomic<State> state_{State::Idle};
|
||||
std::atomic<bool> cancelled_{false};
|
||||
std::atomic<uint64_t> downloaded_{0};
|
||||
std::atomic<uint64_t> total_{0};
|
||||
std::thread worker_;
|
||||
std::filesystem::path installer_;
|
||||
std::string source_url_;
|
||||
bool installer_launched_ = false;
|
||||
};
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-10
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _PRIVACY_BACKEND_H_
|
||||
#define _PRIVACY_BACKEND_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct PrivacyScreenText {
|
||||
std::string unlock_hint = "To turn off the privacy screen, press";
|
||||
};
|
||||
|
||||
struct PrivacyCapabilities {
|
||||
bool overlay = false;
|
||||
bool block_input = false;
|
||||
std::string reason;
|
||||
};
|
||||
|
||||
struct PrivacyHealth {
|
||||
std::string failure;
|
||||
bool emergency_exit = false;
|
||||
bool ready = true;
|
||||
};
|
||||
|
||||
// Lifecycle, message pumping and native resource operations run on the
|
||||
// controller's message thread. Const metadata queries are serialized with
|
||||
// those operations by the controller mutex and must not call native UI APIs.
|
||||
// AppKit work is queued to the main thread without waiting under this mutex.
|
||||
class PrivacyBackend {
|
||||
public:
|
||||
virtual ~PrivacyBackend() = default;
|
||||
virtual PrivacyCapabilities Query() = 0;
|
||||
virtual bool Enable(bool block_input, const PrivacyScreenText& text,
|
||||
std::string& error) = 0;
|
||||
virtual void Recover() = 0;
|
||||
virtual bool IsRecovered() const = 0;
|
||||
virtual bool RecoveryPending() const { return false; }
|
||||
virtual PrivacyHealth Poll() = 0;
|
||||
// Wait on both native window messages and controller commands.
|
||||
// Wake is the only backend operation callable from another thread.
|
||||
virtual void WaitForEvents(unsigned timeout_ms) = 0;
|
||||
virtual void Wake() = 0;
|
||||
};
|
||||
|
||||
#ifdef _WIN32
|
||||
std::unique_ptr<PrivacyBackend> CreateWindowsPrivacyBackend();
|
||||
bool IsWindowsPrivacyDesktopAvailable();
|
||||
#endif
|
||||
#ifdef __APPLE__
|
||||
std::unique_ptr<PrivacyBackend> CreateMacPrivacyBackend();
|
||||
// Flush queued AppKit cleanup after joining the controller at app shutdown.
|
||||
void FlushMacPrivacyTasks();
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
std::unique_ptr<PrivacyBackend> CreateLinuxPrivacyBackend();
|
||||
#endif
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif // _PRIVACY_BACKEND_H_
|
||||
@@ -1,210 +0,0 @@
|
||||
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
|
||||
#include "privacy_controller.h"
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include "rd_log.h"
|
||||
|
||||
namespace crossdesk {
|
||||
uint64_t PrivacyController::Now() {
|
||||
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||
std::chrono::steady_clock::now().time_since_epoch()).count();
|
||||
}
|
||||
PrivacyController::PrivacyController(Factory factory) {
|
||||
status_.state = PrivacyState::unsupported;
|
||||
std::strcpy(status_.reason, "Privacy screen is not available on this platform");
|
||||
#ifdef _WIN32
|
||||
if (!factory) factory = CreateWindowsPrivacyBackend;
|
||||
#elif defined(__APPLE__)
|
||||
if (!factory) factory = CreateMacPrivacyBackend;
|
||||
#elif defined(__linux__)
|
||||
if (!factory) factory = CreateLinuxPrivacyBackend;
|
||||
#endif
|
||||
thread_ = std::thread([this, factory] { Run(factory); });
|
||||
}
|
||||
PrivacyController::~PrivacyController() { Shutdown(); }
|
||||
void PrivacyController::WakeLocked() {
|
||||
if (backend_) backend_->Wake();
|
||||
wake_.notify_all();
|
||||
}
|
||||
void PrivacyController::Shutdown() {
|
||||
{
|
||||
std::lock_guard lock(mutex_);
|
||||
quit_ = true;
|
||||
WakeLocked();
|
||||
}
|
||||
if (thread_.joinable()) thread_.join();
|
||||
#ifdef __APPLE__
|
||||
FlushMacPrivacyTasks();
|
||||
#endif
|
||||
}
|
||||
PrivacyStatus PrivacyController::Snapshot() const {
|
||||
std::lock_guard lock(mutex_);
|
||||
return status_;
|
||||
}
|
||||
void PrivacyController::SetText(const PrivacyScreenText& text) {
|
||||
std::lock_guard lock(mutex_);
|
||||
text_ = text;
|
||||
}
|
||||
void PrivacyController::SetState(PrivacyState state, const std::string& reason) {
|
||||
status_.state = state;
|
||||
status_.remote_paused = false; // Wire compatibility; privacy never pauses a session.
|
||||
++status_.revision;
|
||||
std::strncpy(status_.reason, reason.c_str(), sizeof(status_.reason) - 1);
|
||||
status_.reason[sizeof(status_.reason) - 1] = '\0';
|
||||
LOG_INFO("Privacy: state={}, overlay={}, input_blocked={}, reason={}",
|
||||
static_cast<int>(state), status_.overlay_active, status_.input_blocked, reason);
|
||||
}
|
||||
void PrivacyController::Enable(bool block_input) {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (quit_ || engaged_.load() || disable_pending_) return;
|
||||
if (!status_.supported || (block_input && !status_.input_block_supported)) {
|
||||
SetState(PrivacyState::unsupported, "Privacy screen is unavailable");
|
||||
return;
|
||||
}
|
||||
engaged_.store(true);
|
||||
block_requested_ = block_input;
|
||||
automatic_enable_ = false;
|
||||
enable_pending_ = true;
|
||||
failure_.clear();
|
||||
started_ = Now();
|
||||
SetState(PrivacyState::starting, "Enabling privacy screen");
|
||||
WakeLocked();
|
||||
}
|
||||
void PrivacyController::EnableOnConnection() {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (quit_ || (engaged_.load() && !disable_pending_)) return;
|
||||
// A reconnect may arrive before asynchronous AppKit recovery has completed.
|
||||
engaged_.store(true);
|
||||
block_requested_ = true;
|
||||
automatic_enable_ = true;
|
||||
enable_pending_ = true;
|
||||
failure_.clear();
|
||||
started_ = Now();
|
||||
SetState(PrivacyState::starting, "Enabling privacy on connection");
|
||||
WakeLocked();
|
||||
}
|
||||
void PrivacyController::Disable() {
|
||||
std::lock_guard lock(mutex_);
|
||||
if (quit_) return;
|
||||
enable_pending_ = false;
|
||||
failure_.clear();
|
||||
disable_pending_ = true;
|
||||
SetState(PrivacyState::stopping, "Restoring local desktop and input");
|
||||
WakeLocked();
|
||||
}
|
||||
void PrivacyController::Disconnected() { Disable(); }
|
||||
void PrivacyController::FailLocked(const std::string& reason) {
|
||||
if (!engaged_.load() || disable_pending_) return;
|
||||
failure_ = reason;
|
||||
enable_pending_ = false;
|
||||
disable_pending_ = true;
|
||||
SetState(PrivacyState::failed, reason);
|
||||
WakeLocked();
|
||||
}
|
||||
void PrivacyController::Fail(const std::string& reason) {
|
||||
std::lock_guard lock(mutex_);
|
||||
FailLocked(reason);
|
||||
}
|
||||
void PrivacyController::CaptureChanged(bool running) {
|
||||
std::lock_guard lock(mutex_);
|
||||
// Windows announces a stopped capture while preparing the first session.
|
||||
// Keep automatic privacy pending until capture starts, within its deadline.
|
||||
// Once capture was running or backend enable began, a stop remains a fault.
|
||||
const bool waiting_for_initial_capture =
|
||||
automatic_enable_ && enable_pending_ && !capture_running_;
|
||||
capture_running_ = running;
|
||||
if (!running && !waiting_for_initial_capture)
|
||||
FailLocked("Capture changed; privacy screen is being turned off");
|
||||
WakeLocked();
|
||||
}
|
||||
void PrivacyController::Run(Factory factory) {
|
||||
std::unique_lock lock(mutex_);
|
||||
try {
|
||||
if (factory) backend_ = factory();
|
||||
while (!quit_) {
|
||||
if (last_capability_check_ == 0 ||
|
||||
(!engaged_.load() && Now() - last_capability_check_ >= 2000)) {
|
||||
last_capability_check_ = Now();
|
||||
const auto caps = backend_ ? backend_->Query() : PrivacyCapabilities{};
|
||||
const bool changed = status_.supported != caps.overlay ||
|
||||
status_.input_block_supported != caps.block_input;
|
||||
status_.supported = caps.overlay;
|
||||
status_.input_block_supported = caps.block_input;
|
||||
if (!engaged_.load() && (changed || status_.revision == 0))
|
||||
SetState(caps.overlay ? PrivacyState::off : PrivacyState::unsupported, caps.reason);
|
||||
}
|
||||
if (disable_pending_) {
|
||||
if (backend_) backend_->Recover();
|
||||
if (backend_ && backend_->RecoveryPending()) {
|
||||
lock.unlock();
|
||||
backend_->WaitForEvents(25);
|
||||
lock.lock();
|
||||
continue;
|
||||
}
|
||||
disable_pending_ = false;
|
||||
if (backend_ && !backend_->IsRecovered()) {
|
||||
enable_pending_ = false;
|
||||
disable_pending_ = true; // Retry native cleanup without affecting the session.
|
||||
SetState(PrivacyState::failed, "Could not release every privacy resource");
|
||||
} else {
|
||||
status_.overlay_active = status_.input_blocked = false;
|
||||
if (!enable_pending_) {
|
||||
engaged_.store(false);
|
||||
SetState(failure_.empty() ? PrivacyState::off : PrivacyState::failed,
|
||||
failure_.empty() ? "Privacy screen is off" : failure_);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (enable_pending_) {
|
||||
const auto caps = backend_ ? backend_->Query() : PrivacyCapabilities{};
|
||||
status_.supported = caps.overlay;
|
||||
status_.input_block_supported = caps.block_input;
|
||||
if (!caps.overlay || (block_requested_ && !caps.block_input)) {
|
||||
FailLocked("Privacy screen unavailable: " + caps.reason);
|
||||
} else if (!capture_running_) {
|
||||
if (!automatic_enable_ || Now() - started_ > 10000)
|
||||
FailLocked("No desktop capture available for privacy screen");
|
||||
} else {
|
||||
enable_pending_ = false;
|
||||
std::string error;
|
||||
if (!backend_->Enable(block_requested_, text_, error)) {
|
||||
FailLocked(error);
|
||||
} else {
|
||||
status_.overlay_active = true;
|
||||
status_.input_blocked = block_requested_;
|
||||
started_ = Now();
|
||||
}
|
||||
}
|
||||
}
|
||||
const auto health = backend_ ? backend_->Poll() : PrivacyHealth{};
|
||||
if (engaged_.load() && !disable_pending_) {
|
||||
if (health.emergency_exit) {
|
||||
failure_.clear();
|
||||
enable_pending_ = false;
|
||||
disable_pending_ = true;
|
||||
SetState(PrivacyState::stopping, "Local shortcut: restoring desktop and input");
|
||||
} else if (!health.failure.empty()) {
|
||||
FailLocked(health.failure);
|
||||
} else if (!enable_pending_ && status_.state == PrivacyState::starting) {
|
||||
if (health.ready)
|
||||
SetState(PrivacyState::on, "Privacy screen is on");
|
||||
else if (Now() - started_ > 3000)
|
||||
FailLocked("Privacy screen did not become ready");
|
||||
}
|
||||
}
|
||||
if (backend_) {
|
||||
const unsigned timeout = engaged_.load() || disable_pending_ ? 25 : 2000;
|
||||
lock.unlock();
|
||||
backend_->WaitForEvents(timeout);
|
||||
lock.lock();
|
||||
} else {
|
||||
wake_.wait(lock, [this] { return quit_ || enable_pending_ || disable_pending_; });
|
||||
}
|
||||
}
|
||||
} catch (const std::exception& e) {
|
||||
if (!lock.owns_lock()) lock.lock();
|
||||
SetState(PrivacyState::failed, std::string("Privacy component failed: ") + e.what());
|
||||
}
|
||||
if (backend_) { backend_->Recover(); backend_.reset(); }
|
||||
}
|
||||
} // namespace crossdesk
|
||||
@@ -1,59 +0,0 @@
|
||||
/* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved. */
|
||||
#ifndef _PRIVACY_CONTROLLER_H_
|
||||
#define _PRIVACY_CONTROLLER_H_
|
||||
|
||||
#include <remote_action.h>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
#include <functional>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include "privacy_backend.h"
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Privacy is optional: it must never gate desktop video, audio or remote input.
|
||||
// A failed backend is recovered automatically while the session keeps running.
|
||||
class PrivacyController {
|
||||
public:
|
||||
using Factory = std::function<std::unique_ptr<PrivacyBackend>()>;
|
||||
explicit PrivacyController(Factory factory = {});
|
||||
~PrivacyController();
|
||||
PrivacyController(const PrivacyController&) = delete;
|
||||
PrivacyController& operator=(const PrivacyController&) = delete;
|
||||
PrivacyStatus Snapshot() const;
|
||||
void SetText(const PrivacyScreenText& text);
|
||||
void Enable(bool block_input);
|
||||
void EnableOnConnection();
|
||||
void Disable();
|
||||
void Disconnected();
|
||||
void Shutdown();
|
||||
void Fail(const std::string& reason);
|
||||
bool Engaged() const { return engaged_.load(); }
|
||||
void CaptureChanged(bool running);
|
||||
|
||||
private:
|
||||
void Run(Factory factory);
|
||||
void WakeLocked();
|
||||
void SetState(PrivacyState state, const std::string& reason);
|
||||
void FailLocked(const std::string& reason);
|
||||
static uint64_t Now();
|
||||
mutable std::mutex mutex_;
|
||||
std::condition_variable wake_;
|
||||
std::thread thread_;
|
||||
std::unique_ptr<PrivacyBackend> backend_;
|
||||
PrivacyStatus status_{};
|
||||
PrivacyScreenText text_;
|
||||
std::atomic<bool> engaged_{false};
|
||||
bool quit_ = false;
|
||||
bool enable_pending_ = false;
|
||||
bool disable_pending_ = false;
|
||||
bool block_requested_ = false;
|
||||
bool automatic_enable_ = false;
|
||||
bool capture_running_ = false;
|
||||
std::string failure_;
|
||||
uint64_t started_ = 0;
|
||||
uint64_t last_capability_check_ = 0;
|
||||
};
|
||||
} // namespace crossdesk
|
||||
#endif
|
||||
@@ -1,22 +0,0 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-11
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _SCREEN_CAPTURE_METHOD_H_
|
||||
#define _SCREEN_CAPTURE_METHOD_H_
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
// Persisted values; keep these in the same order as the settings choices.
|
||||
enum class ScreenCaptureMethod { Auto = 0, Dxgi = 1, Wgc = 2, Gdi = 3 };
|
||||
|
||||
inline bool IsValidScreenCaptureMethod(long value) {
|
||||
return value >= static_cast<long>(ScreenCaptureMethod::Auto) &&
|
||||
value <= static_cast<long>(ScreenCaptureMethod::Gdi);
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -14,13 +14,8 @@
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
class PrivacyController;
|
||||
|
||||
class ScreenCapturer {
|
||||
public:
|
||||
// Windows backends emit a control-only callback BEFORE automatic recovery.
|
||||
// No video buffer accompanies this notification; wrappers must consume it.
|
||||
static constexpr int kBackendReset = -1;
|
||||
// |stream_id| is a logical MiniRTC stream ID (DisplayN), not a platform
|
||||
// display name or physical handle. |native_frame| is borrowed for the
|
||||
// duration of the callback; retain its owner before using it asynchronously.
|
||||
@@ -34,7 +29,6 @@ class ScreenCapturer {
|
||||
|
||||
public:
|
||||
virtual int Init(const int fps, cb_desktop_data cb) = 0;
|
||||
virtual void SetPrivacyController(PrivacyController*) {}
|
||||
virtual int Destroy() = 0;
|
||||
virtual int Start(bool show_cursor) = 0;
|
||||
virtual int Stop() = 0;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#include "installer_download.h"
|
||||
|
||||
namespace crossdesk {
|
||||
std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
||||
const nlohmann::json& metadata) {
|
||||
if (!metadata.is_object()) return std::nullopt;
|
||||
const auto version_it = metadata.find(
|
||||
metadata.contains("latest_version") ? "latest_version" : "version");
|
||||
if (version_it == metadata.end() || !version_it->is_string()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::string version = version_it->get<std::string>();
|
||||
if (!version.empty() && version.front() == 'v') version.erase(0, 1);
|
||||
if (version.empty() ||
|
||||
version.find_first_not_of(
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-") !=
|
||||
std::string::npos) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const auto downloads = metadata.find("downloads");
|
||||
if (downloads == metadata.end() || !downloads->is_object())
|
||||
return std::nullopt;
|
||||
const auto installer = downloads->find("windows-x64");
|
||||
if (installer == downloads->end() || !installer->is_object())
|
||||
return std::nullopt;
|
||||
const auto url = installer->find("url");
|
||||
const auto filename = installer->find("filename");
|
||||
InstallerDownload result{"https://downloads.crossdesk.cn", "",
|
||||
"crossdesk-win-x64-v" + version + ".exe"};
|
||||
result.path = "/" + result.filename;
|
||||
if (url == installer->end() || !url->is_string() ||
|
||||
url->get<std::string>() != result.origin + result.path ||
|
||||
(filename != installer->end() &&
|
||||
(!filename->is_string() ||
|
||||
filename->get<std::string>() != result.filename))) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace crossdesk
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* @Author: DI JUNKUN
|
||||
* @Date: 2026-09-09
|
||||
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
|
||||
*/
|
||||
|
||||
#ifndef _INSTALLER_DOWNLOAD_H_
|
||||
#define _INSTALLER_DOWNLOAD_H_
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace crossdesk {
|
||||
|
||||
struct InstallerDownload {
|
||||
std::string origin;
|
||||
std::string path;
|
||||
std::string filename;
|
||||
};
|
||||
|
||||
// Only accept the official Windows installer for the advertised version.
|
||||
std::optional<InstallerDownload> GetWindowsInstallerDownload(
|
||||
const nlohmann::json& metadata);
|
||||
|
||||
} // namespace crossdesk
|
||||
|
||||
#endif
|
||||
@@ -1,78 +0,0 @@
|
||||
-- Run from the repository root: xmake lua apps/desktop/tests/build_define_scope_test.lua windows true
|
||||
-- Inspect the real Xmake target graph without configuring/downloading packages.
|
||||
function main(plat, portable)
|
||||
import("core.project.config")
|
||||
import("core.project.project")
|
||||
config.load()
|
||||
config.set("plat", plat or "windows", {force = true})
|
||||
config.set("arch", plat == "macosx" and "arm64" or "x64", {force = true})
|
||||
config.set("CROSSDESK_PORTABLE", portable == "true", {force = true})
|
||||
config.set("CROSSDESK_VERSION", "v1.5.3-2-20260913", {force = true})
|
||||
local targets = project.targets()
|
||||
local function flatten(values)
|
||||
local result = {}
|
||||
local function append(value)
|
||||
if type(value) == "table" then
|
||||
for _, entry in ipairs(value) do append(entry) end
|
||||
elseif value ~= nil then
|
||||
table.insert(result, value)
|
||||
end
|
||||
end
|
||||
append(values)
|
||||
return result
|
||||
end
|
||||
local function contains(values, expected)
|
||||
return table.contains(flatten(values), expected)
|
||||
end
|
||||
local function effective_defines(target)
|
||||
return table.join(table.wrap(target:get("defines")),
|
||||
flatten(target:get_from("defines", "dep::*")))
|
||||
end
|
||||
|
||||
for _, name in ipairs({"minirtc", "media", "common", "crossdesk_wire", "rd_log"}) do
|
||||
for _, define in ipairs(effective_defines(targets[name])) do
|
||||
assert(not define:startswith("CROSSDESK_VERSION"), name .. " inherits version macros")
|
||||
assert(not define:startswith("CROSSDESK_PORTABLE"), name .. " inherits portable macros")
|
||||
end
|
||||
end
|
||||
for _, name in ipairs({"gui", "crossdesk", "path_manager"}) do
|
||||
assert(contains(effective_defines(targets[name]), "CROSSDESK_PORTABLE=1") ==
|
||||
(portable == "true"), name .. " has inconsistent portable layout/configuration")
|
||||
end
|
||||
|
||||
local gui = targets.gui
|
||||
local version_files = {
|
||||
["apps/desktop/src/gui/application/gui_application.cpp"] = true,
|
||||
["apps/desktop/src/gui/runtime/peer_event_handler.cpp"] = true
|
||||
}
|
||||
local seen = {}
|
||||
for _, file in ipairs(gui:sourcefiles()) do
|
||||
assert(not seen[file], "duplicate GUI source: " .. file)
|
||||
seen[file] = true
|
||||
local defines = (gui:fileconfig(file) or {}).defines
|
||||
assert(contains(defines, 'CROSSDESK_VERSION="v1.5.3-2-20260913"') ==
|
||||
(version_files[file] == true), "unexpected version scope: " .. file)
|
||||
end
|
||||
for file in pairs(version_files) do
|
||||
assert(seen[file], "missing version consumer: " .. file)
|
||||
end
|
||||
|
||||
if plat == "windows" then
|
||||
local resources = {
|
||||
crossdesk = portable == "true" and "crossdesk_portable" or "crossdesk",
|
||||
crossdesk_service = "crossdesk_service",
|
||||
crossdesk_session_helper = "crossdesk_session_helper",
|
||||
crossdesk_privacy_window = "crossdesk_privacy_window",
|
||||
wgc_plugin = "wgc_plugin"
|
||||
}
|
||||
for name, resource in pairs(resources) do
|
||||
local defines = targets[name]:fileconfig(
|
||||
"apps/desktop/resources/windows/" .. resource .. ".rc").defines
|
||||
assert(contains(defines, 'CROSSDESK_VERSION_STRING="v1.5.3-2-20260913"'))
|
||||
assert(contains(defines, "CROSSDESK_VERSION_NUMERIC=1,5,3,0"))
|
||||
assert(contains(defines, "CROSSDESK_PORTABLE=1") ==
|
||||
(name == "crossdesk_session_helper" and portable == "true"))
|
||||
end
|
||||
end
|
||||
print("Build define scopes passed: %s portable=%s", plat, portable)
|
||||
end
|
||||
@@ -56,6 +56,8 @@ int main() {
|
||||
bool ok = true;
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"INPUT ip = {0};");
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"SetCursorPos failed");
|
||||
ok &= ExpectContains("mouse_controller.cpp", mouse_controller,
|
||||
"SendInput failed for mouse");
|
||||
return ok ? 0 : 1;
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
# Portable tests for artifact handling and signature validation decisions.
|
||||
# Get-AuthenticodeSignature and SignTool are simulated; CI validates real files.
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$scripts = Join-Path $PSScriptRoot '../scripts/windows'
|
||||
. "$scripts/signpath-common.ps1"
|
||||
$artifactScript = Join-Path $scripts 'signpath-artifact.ps1'
|
||||
$testDirectory = Join-Path ([IO.Path]::GetTempPath()) "crossdesk-signing-test-$([guid]::NewGuid())"
|
||||
New-Item -ItemType Directory -Path $testDirectory | Out-Null
|
||||
$checks = 0
|
||||
|
||||
function Assert-True([bool]$Condition, [string]$Message) {
|
||||
if (!$Condition) { throw $Message }
|
||||
$script:checks++
|
||||
}
|
||||
|
||||
function Assert-Throws([scriptblock]$Action, [string]$Expected) {
|
||||
try { & $Action } catch {
|
||||
if ($_.Exception.Message -notlike "*$Expected*") { throw }
|
||||
$script:checks++
|
||||
return
|
||||
}
|
||||
throw "Expected an error containing: $Expected"
|
||||
}
|
||||
|
||||
function New-TestZip([string]$Path, [string[]]$Names) {
|
||||
$zip = [IO.Compression.ZipFile]::Open($Path, [IO.Compression.ZipArchiveMode]::Create)
|
||||
try {
|
||||
foreach ($name in $Names) {
|
||||
$writer = [IO.StreamWriter]::new($zip.CreateEntry($name).Open())
|
||||
try { $writer.Write("signed $name") } finally { $writer.Dispose() }
|
||||
}
|
||||
} finally { $zip.Dispose() }
|
||||
}
|
||||
|
||||
$mockStatus = 'NotTrusted'
|
||||
$mockBadFile = ''
|
||||
$mockCertificate = [pscustomobject]@{ Subject = "CN=Test certificate for 'CrossDesk [OSS]'" }
|
||||
$mockTimestamp = $null
|
||||
function Get-AuthenticodeSignature {
|
||||
param([string]$LiteralPath)
|
||||
$status = if ([IO.Path]::GetFileName($LiteralPath) -eq $mockBadFile) { 'HashMismatch' } else { $mockStatus }
|
||||
return [pscustomobject]@{
|
||||
Status = $status
|
||||
SignerCertificate = $mockCertificate
|
||||
TimeStamperCertificate = $mockTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$files = @(Get-CrossDeskSigningFiles)
|
||||
$build = Join-Path $testDirectory 'build'
|
||||
New-Item -ItemType Directory -Path $build | Out-Null
|
||||
foreach ($name in $files) { Set-Content -LiteralPath (Join-Path $build $name) -Value "original $name" }
|
||||
Set-Content -LiteralPath (Join-Path $build 'third-party.dll') -Value 'upstream'
|
||||
$unsigned = Join-Path $testDirectory 'unsigned.zip'
|
||||
& $artifactScript -Operation PrepareBinaries -Directory $build -Artifact $unsigned
|
||||
$zip = [IO.Compression.ZipFile]::OpenRead($unsigned)
|
||||
try {
|
||||
$entries = @($zip.Entries | ForEach-Object { $_.FullName })
|
||||
Assert-True ($entries.Count -eq 4 -and @(Compare-Object $files $entries).Count -eq 0) 'Signing input must include only the four project binaries.'
|
||||
} finally { $zip.Dispose() }
|
||||
|
||||
[xml]$config = Get-Content -Raw (Join-Path $scripts 'signpath-binaries.xml')
|
||||
$configured = @($config.SelectNodes('//*[local-name()="include"]') | ForEach-Object { $_.path })
|
||||
Assert-True (@(Compare-Object $files $configured).Count -eq 0) 'SignPath XML must cover exactly the files in the submitted ZIP.'
|
||||
|
||||
Remove-Item -LiteralPath (Join-Path $build 'wgc_plugin.dll')
|
||||
Assert-Throws { & $artifactScript -Operation PrepareBinaries -Directory $build -Artifact (Join-Path $testDirectory 'missing.zip') } 'Missing CrossDesk binary'
|
||||
Set-Content -LiteralPath (Join-Path $build 'wgc_plugin.dll') -Value 'original wgc_plugin.dll'
|
||||
|
||||
$signed = Join-Path $testDirectory 'signed.zip'
|
||||
New-TestZip $signed $files
|
||||
$mockBadFile = 'wgc_plugin.dll'
|
||||
Assert-Throws { & $artifactScript -Operation RestoreBinaries -Directory $build -Artifact $signed -SigningPolicy test-signing } 'HashMismatch'
|
||||
foreach ($name in $files) {
|
||||
Assert-True ((Get-Content -Raw (Join-Path $build $name)).Trim() -eq "original $name") 'Failed verification must not replace any output.'
|
||||
}
|
||||
$mockBadFile = ''
|
||||
& $artifactScript -Operation RestoreBinaries -Directory $build -Artifact $signed -SigningPolicy test-signing
|
||||
foreach ($name in $files) {
|
||||
Assert-True ((Get-Content -Raw (Join-Path $build $name)) -eq "signed $name") 'Verified signed files must replace the unsigned build output.'
|
||||
}
|
||||
Assert-True ((Get-Content -Raw (Join-Path $build 'third-party.dll')).Trim() -eq 'upstream') 'Third-party libraries must remain untouched.'
|
||||
|
||||
$badArchives = @(
|
||||
@($files[0], $files[1], $files[2]),
|
||||
@($files[0], $files[1], $files[2], '../wgc_plugin.dll'),
|
||||
@($files[0], $files[1], $files[2], $files[0]),
|
||||
@($files[0], $files[1], $files[2], $files[3], 'third-party.dll')
|
||||
)
|
||||
for ($i = 0; $i -lt $badArchives.Count; $i++) {
|
||||
$badZip = Join-Path $testDirectory "bad-$i.zip"
|
||||
New-TestZip $badZip $badArchives[$i]
|
||||
Assert-Throws { & $artifactScript -Operation RestoreBinaries -Directory $build -Artifact $badZip -SigningPolicy test-signing } 'Signed archive must contain exactly'
|
||||
}
|
||||
|
||||
$file = Join-Path $build 'crossdesk.exe'
|
||||
foreach ($mockStatus in @('NotSigned', 'HashMismatch', 'UnknownError')) {
|
||||
Assert-Throws { Assert-CrossDeskSignature $file test-signing } 'Invalid test signature'
|
||||
}
|
||||
$mockStatus = 'Valid'
|
||||
$mockCertificate = $null
|
||||
Assert-Throws { Assert-CrossDeskSignature $file test-signing } 'no Authenticode signing certificate'
|
||||
$mockCertificate = [pscustomobject]@{ Subject = 'CN=SignPath Foundation' }
|
||||
$mockStatus = 'NotTrusted'
|
||||
Assert-Throws { Assert-CrossDeskSignature $file release-signing } 'Release signature is not trusted'
|
||||
$mockStatus = 'Valid'
|
||||
Assert-Throws { Assert-CrossDeskSignature $file release-signing } 'no timestamp'
|
||||
$mockTimestamp = [pscustomobject]@{ Subject = 'CN=Timestamp authority' }
|
||||
|
||||
# Only the real SignTool process is replaced; exercise the actual exit-code check.
|
||||
$fakeSignTool = Join-Path $testDirectory 'signtool.ps1'
|
||||
function Find-CrossDeskSignTool { return $fakeSignTool }
|
||||
Set-Content $fakeSignTool '$global:LASTEXITCODE = 1'
|
||||
Assert-Throws { Assert-CrossDeskSignature $file release-signing } 'SignTool verification failed'
|
||||
Set-Content $fakeSignTool '$global:LASTEXITCODE = 0'
|
||||
Assert-CrossDeskSignature $file release-signing
|
||||
$checks++
|
||||
|
||||
Write-Host "Passed $checks Windows signing checks."
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $testDirectory -Recurse -Force
|
||||
}
|
||||
@@ -29,6 +29,30 @@ function setup_options_and_dependencies()
|
||||
set_description("Build CrossDesk as a portable package that stores data beside the executable")
|
||||
option_end()
|
||||
|
||||
-- Preserve the existing codebase/toolchain contract. Slint targets opt in to
|
||||
-- C++20 locally because its generated C++ API requires it.
|
||||
local crossdesk_version = get_config("CROSSDESK_VERSION") or "0.0.0"
|
||||
local version_base = crossdesk_version:gsub("^v", ""):match("^(%d[%d%.]*)") or "0.0.0"
|
||||
local version_parts = {}
|
||||
for part in version_base:gmatch("%d+") do
|
||||
local value = tonumber(part) or 0
|
||||
if value > 65535 then
|
||||
value = 0
|
||||
end
|
||||
table.insert(version_parts, value)
|
||||
if #version_parts == 4 then
|
||||
break
|
||||
end
|
||||
end
|
||||
while #version_parts < 4 do
|
||||
table.insert(version_parts, 0)
|
||||
end
|
||||
|
||||
add_defines("CROSSDESK_VERSION_STRING=\"" .. crossdesk_version .. "\"")
|
||||
if is_os("windows") then
|
||||
add_defines("CROSSDESK_VERSION_NUMERIC=" .. table.concat(version_parts, ","))
|
||||
end
|
||||
|
||||
-- set_policy("build.warning", true)
|
||||
-- set_warnings("all", "extra")
|
||||
-- add_cxxflags("/W4", "/WX")
|
||||
@@ -37,6 +61,10 @@ function setup_options_and_dependencies()
|
||||
add_defines("USE_CUDA=" .. (is_config("USE_CUDA", true) and "1" or "0"))
|
||||
add_defines("USE_WAYLAND=" .. (is_config("USE_WAYLAND", true) and "1" or "0"))
|
||||
add_defines("USE_DRM=" .. (is_config("USE_DRM", true) and "1" or "0"))
|
||||
if is_config("CROSSDESK_PORTABLE", true) then
|
||||
add_defines("CROSSDESK_PORTABLE=1")
|
||||
end
|
||||
|
||||
if is_mode("debug") then
|
||||
add_defines("CROSSDESK_DEBUG")
|
||||
end
|
||||
|
||||
@@ -3,30 +3,6 @@ function setup_targets()
|
||||
|
||||
includes("deps/submodules", "deps/thirdparty")
|
||||
|
||||
local crossdesk_version = get_config("CROSSDESK_VERSION") or "0.0.0"
|
||||
local version_define = "CROSSDESK_VERSION=\"" .. crossdesk_version .. "\""
|
||||
local windows_resource_defines = {
|
||||
"CROSSDESK_VERSION_STRING=\"" .. crossdesk_version .. "\""
|
||||
}
|
||||
local version_base = crossdesk_version:gsub("^v", ""):match("^(%d[%d%.]*)") or "0.0.0"
|
||||
local version_parts = {}
|
||||
for part in version_base:gmatch("%d+") do
|
||||
local value = tonumber(part) or 0
|
||||
table.insert(version_parts, value <= 65535 and value or 0)
|
||||
if #version_parts == 4 then
|
||||
break
|
||||
end
|
||||
end
|
||||
while #version_parts < 4 do
|
||||
table.insert(version_parts, 0)
|
||||
end
|
||||
table.insert(windows_resource_defines,
|
||||
"CROSSDESK_VERSION_NUMERIC=" .. table.concat(version_parts, ","))
|
||||
local windows_helper_resource_defines = table.join(windows_resource_defines)
|
||||
if is_config("CROSSDESK_PORTABLE", true) then
|
||||
table.insert(windows_helper_resource_defines, "CROSSDESK_PORTABLE=1")
|
||||
end
|
||||
|
||||
local function copy_slint_runtime(target)
|
||||
if not target:is_plat("windows") then
|
||||
return
|
||||
@@ -74,9 +50,6 @@ function setup_targets()
|
||||
|
||||
target("path_manager")
|
||||
set_kind("object")
|
||||
if is_config("CROSSDESK_PORTABLE", true) then
|
||||
add_defines("CROSSDESK_PORTABLE=1")
|
||||
end
|
||||
add_deps("rd_log")
|
||||
add_files("apps/desktop/src/path_manager/*.cpp")
|
||||
if is_os("windows") then
|
||||
@@ -183,48 +156,12 @@ function setup_targets()
|
||||
add_frameworks("Security", "CoreFoundation")
|
||||
end
|
||||
|
||||
target("privacy")
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "crossdesk_wire")
|
||||
add_files("apps/desktop/src/privacy/privacy_controller.cpp")
|
||||
add_includedirs("apps/desktop/src/privacy", {public = true})
|
||||
if is_os("windows") then
|
||||
add_files("apps/desktop/src/platform/windows/privacy/*.cpp")
|
||||
add_deps("crossdesk_privacy_cursor_helper")
|
||||
add_deps("crossdesk_privacy_window", {inherit = false})
|
||||
add_includedirs("apps/desktop/src/platform/windows/input")
|
||||
add_syslinks("Wtsapi32", "Ole32", {public = true})
|
||||
elseif is_os("macosx") then
|
||||
add_files("apps/desktop/src/platform/macos/privacy/*.mm")
|
||||
add_mxxflags("-fobjc-arc")
|
||||
add_frameworks("AppKit", "ApplicationServices", {public = true})
|
||||
elseif is_os("linux") then
|
||||
add_files("apps/desktop/src/platform/linux/privacy/*.cpp")
|
||||
add_syslinks("Xi", {public = true})
|
||||
end
|
||||
|
||||
if is_os("windows") then
|
||||
target("crossdesk_privacy_window")
|
||||
set_kind("shared")
|
||||
set_runtimes("MT")
|
||||
add_files("apps/desktop/src/platform/windows/privacy/band_module/main.cpp")
|
||||
add_files("apps/desktop/src/platform/windows/privacy/band_module/privacy_cover_renderer.cpp")
|
||||
add_files("apps/desktop/resources/windows/crossdesk_privacy_window.rc",
|
||||
{defines = windows_resource_defines})
|
||||
add_syslinks("user32", "gdi32", "dwmapi")
|
||||
|
||||
target("crossdesk_privacy_cursor_helper")
|
||||
set_kind("binary")
|
||||
add_files("apps/desktop/src/platform/windows/privacy/cursor_helper/main.cpp")
|
||||
end
|
||||
|
||||
target("screen_capturer")
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "common", "crossdesk_wire")
|
||||
add_includedirs("apps/desktop/src/screen_capturer", {public = true})
|
||||
add_includedirs("deps/submodules/minirtc/src/api", {public = true})
|
||||
if is_os("windows") then
|
||||
add_deps("privacy")
|
||||
add_packages("libyuv")
|
||||
add_files("apps/desktop/src/screen_capturer/captured_nv12_frame.cpp")
|
||||
add_files("apps/desktop/src/platform/windows/screen_capturer/screen_capturer_dxgi.cpp",
|
||||
@@ -234,13 +171,10 @@ function setup_targets()
|
||||
add_includedirs("apps/desktop/src/platform/windows/screen_capturer",
|
||||
"apps/desktop/src/platform/windows/service")
|
||||
elseif is_os("macosx") then
|
||||
add_deps("privacy")
|
||||
add_mxxflags("-fobjc-arc")
|
||||
add_files("apps/desktop/src/platform/macos/screen_capturer/*.cpp",
|
||||
"apps/desktop/src/platform/macos/screen_capturer/*.mm")
|
||||
add_includedirs("apps/desktop/src/platform/macos/screen_capturer")
|
||||
elseif is_os("linux") then
|
||||
add_deps("privacy")
|
||||
add_packages("libyuv")
|
||||
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_linux.cpp")
|
||||
add_files("apps/desktop/src/platform/linux/screen_capturer/screen_capturer_x11.cpp")
|
||||
@@ -336,8 +270,7 @@ function setup_targets()
|
||||
set_kind("object")
|
||||
add_deps("rd_log", "autostart")
|
||||
add_files("apps/desktop/src/config_center/*.cpp")
|
||||
add_includedirs("apps/desktop/src", "apps/desktop/src/config_center",
|
||||
{public = true})
|
||||
add_includedirs("apps/desktop/src/config_center", {public = true})
|
||||
|
||||
target("assets")
|
||||
set_kind("headeronly")
|
||||
@@ -349,8 +282,14 @@ function setup_targets()
|
||||
target("version_checker")
|
||||
set_kind("object")
|
||||
add_packages("cpp-httplib")
|
||||
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
|
||||
add_deps("rd_log")
|
||||
add_files("apps/desktop/src/version_checker/*.cpp")
|
||||
if is_os("windows") then
|
||||
add_files("apps/desktop/src/platform/windows/windows_updater.cpp",
|
||||
"apps/desktop/src/platform/windows/windows_installer.cpp")
|
||||
add_links("winhttp", "ole32", "uuid")
|
||||
end
|
||||
add_includedirs("apps/desktop/src/version_checker", {public = true})
|
||||
if is_os("macosx") then
|
||||
add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN")
|
||||
@@ -380,15 +319,11 @@ function setup_targets()
|
||||
add_packages("slint", {public = true})
|
||||
add_packages("libyuv", "tinyfiledialogs")
|
||||
add_rules("slint")
|
||||
if is_config("CROSSDESK_PORTABLE", true) then
|
||||
-- RuntimeState changes layout in portable builds. Consumers of GUI
|
||||
-- headers must see the same definition, without affecting its deps.
|
||||
add_defines("CROSSDESK_PORTABLE=1", {public = true})
|
||||
end
|
||||
add_defines("CROSSDESK_VERSION=\"" .. (get_config("CROSSDESK_VERSION") or "Unknown") .. "\"")
|
||||
add_deps("rd_log", "common", "assets", "config_center", "minirtc",
|
||||
"path_manager", "screen_capturer", "speaker_capturer",
|
||||
"device_controller", "thumbnail", "version_checker", "tools",
|
||||
"crossdesk_wire", "privacy")
|
||||
"crossdesk_wire")
|
||||
add_files("apps/desktop/src/gui/render.cpp", "apps/desktop/src/gui/application/gui_application.cpp",
|
||||
"apps/desktop/src/gui/rendering/*.cpp",
|
||||
"apps/desktop/src/gui/runtime/*.cpp",
|
||||
@@ -396,11 +331,6 @@ function setup_targets()
|
||||
"apps/desktop/src/gui/features/devices/*.cpp", "apps/desktop/src/gui/features/input/*.cpp",
|
||||
"apps/desktop/src/gui/features/clipboard/*.cpp", "apps/desktop/src/gui/features/file_transfer/*.cpp",
|
||||
"apps/desktop/src/gui/features/settings/*.cpp", "apps/desktop/src/gui/ui/crossdesk_ui.slint")
|
||||
-- Only these translation units consume the application version. Keep
|
||||
-- version changes out of the flags for media and the rest of the GUI.
|
||||
add_files("apps/desktop/src/gui/application/gui_application.cpp",
|
||||
"apps/desktop/src/gui/runtime/peer_event_handler.cpp",
|
||||
{defines = version_define})
|
||||
add_includedirs("apps/desktop/src", "apps/desktop/src/gui", {public = true})
|
||||
add_includedirs("apps/desktop/src/platform/common/input")
|
||||
if is_os("windows") then
|
||||
@@ -452,8 +382,7 @@ function setup_targets()
|
||||
add_files("apps/desktop/src/platform/windows/screen_capturer/screen_capturer_wgc.cpp",
|
||||
"apps/desktop/src/platform/windows/screen_capturer/wgc_session_impl.cpp",
|
||||
"apps/desktop/src/platform/windows/screen_capturer/wgc_plugin_entry.cpp")
|
||||
add_files("apps/desktop/resources/windows/wgc_plugin.rc",
|
||||
{defines = windows_resource_defines})
|
||||
add_files("apps/desktop/resources/windows/wgc_plugin.rc")
|
||||
add_includedirs("apps/desktop/src/common", "apps/desktop/src/screen_capturer",
|
||||
"apps/desktop/src/platform/windows/screen_capturer",
|
||||
"deps/submodules/minirtc/src/api")
|
||||
@@ -464,8 +393,7 @@ function setup_targets()
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_files("apps/desktop/src/platform/windows/service/main.cpp",
|
||||
"apps/desktop/src/platform/windows/service/service_host.cpp")
|
||||
add_files("apps/desktop/resources/windows/crossdesk_service.rc",
|
||||
{defines = windows_resource_defines})
|
||||
add_files("apps/desktop/resources/windows/crossdesk_service.rc")
|
||||
add_includedirs("apps/desktop/src/platform/windows/service")
|
||||
|
||||
target("crossdesk_session_helper")
|
||||
@@ -474,8 +402,7 @@ function setup_targets()
|
||||
add_deps("rd_log", "path_manager")
|
||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||
add_files("apps/desktop/src/platform/windows/service/session_helper_main.cpp")
|
||||
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc",
|
||||
{defines = windows_helper_resource_defines})
|
||||
add_files("apps/desktop/resources/windows/crossdesk_session_helper.rc")
|
||||
add_includedirs("apps/desktop/src/common",
|
||||
"apps/desktop/src/platform/windows/service")
|
||||
add_includedirs("apps/desktop/src/platform/windows/input")
|
||||
@@ -492,7 +419,7 @@ function setup_targets()
|
||||
add_includedirs("apps/desktop/src/platform/windows/service")
|
||||
add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv")
|
||||
add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper")
|
||||
add_files(crossdesk_windows_resource, {defines = windows_resource_defines})
|
||||
add_files(crossdesk_windows_resource)
|
||||
elseif is_os("macosx") then
|
||||
add_files("apps/desktop/src/platform/common/posix_daemon_backend.cpp",
|
||||
"apps/desktop/src/platform/macos/daemon_backend.cpp")
|
||||
|
||||
@@ -99,7 +99,8 @@ typedef NS_ENUM(NSInteger, CrossDeskVideoAdaptationPolicy) {
|
||||
@property(nonatomic, weak, nullable) id<CrossDeskRTCBridgeDelegate> delegate;
|
||||
|
||||
- (void)configureWithSignalHost:(NSString *)host
|
||||
signalPort:(NSInteger)signalPort;
|
||||
signalPort:(NSInteger)signalPort
|
||||
enableSRTP:(BOOL)enableSRTP;
|
||||
- (void)setHardwareAccelerationEnabled:(BOOL)enabled;
|
||||
- (void)setVideoAdaptationPolicy:(CrossDeskVideoAdaptationPolicy)policy;
|
||||
- (void)requestPresenceForRemoteIDs:(NSArray<NSString *> *)remoteIDs
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user