mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-07-01 11:25:56 +08:00
Compare commits
15 Commits
82c0cbbad4
..
v1.3.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f3959e6c6 | |||
| 5ff6b601c7 | |||
| 4895ac9c23 | |||
| f121aa47f7 | |||
| 00a8d59284 | |||
| a30489e05b | |||
| dfbeb3ed20 | |||
| 2eed1c974e | |||
| 63a79a90ac | |||
| a61e74d97b | |||
| 54438a4aa1 | |||
| 7682ad63e4 | |||
| 06c53fdc9c | |||
| 665f4e684c | |||
| 52b894fe0e |
@@ -250,10 +250,34 @@ jobs:
|
|||||||
- name: Package Portable
|
- name: Package Portable
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
run: |
|
run: |
|
||||||
|
$buildDir = "${{ github.workspace }}\build\windows\x64\release"
|
||||||
$portableDir = "${{ github.workspace }}\portable"
|
$portableDir = "${{ github.workspace }}\portable"
|
||||||
New-Item -ItemType Directory -Force -Path $portableDir
|
New-Item -ItemType Directory -Force -Path $portableDir
|
||||||
Copy-Item "${{ github.workspace }}\build\windows\x64\release\crossdesk.exe" "$portableDir\CrossDesk.exe"
|
|
||||||
Copy-Item "${{ github.workspace }}\build\windows\x64\release\*.dll" $portableDir -Force
|
$portableFiles = @(
|
||||||
|
@("crossdesk.exe", "CrossDesk.exe"),
|
||||||
|
@("crossdesk_service.exe", "crossdesk_service.exe"),
|
||||||
|
@("crossdesk_session_helper.exe", "crossdesk_session_helper.exe")
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($file in $portableFiles) {
|
||||||
|
$source = Join-Path $buildDir $file[0]
|
||||||
|
$destination = Join-Path $portableDir $file[1]
|
||||||
|
if (!(Test-Path $source)) {
|
||||||
|
throw "Missing portable package file: $source"
|
||||||
|
}
|
||||||
|
Copy-Item $source $destination -Force
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item (Join-Path $buildDir "*.dll") $portableDir -Force
|
||||||
|
|
||||||
|
foreach ($file in $portableFiles) {
|
||||||
|
$packagedFile = Join-Path $portableDir $file[1]
|
||||||
|
if (!(Test-Path $packagedFile)) {
|
||||||
|
throw "Portable package is missing: $packagedFile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
|
Compress-Archive -Path "$portableDir\*" -DestinationPath "${{ github.workspace }}\crossdesk-win-x64-portable-${{ env.VERSION_NUM }}.zip"
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
|
|||||||
@@ -53,6 +53,29 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
|
|||||||
|
|
||||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||||
|
|
||||||
|
### Windows 服务(CrossDesk Service)
|
||||||
|
CrossDesk 在 Windows 平台提供本地辅助服务 **CrossDesk Service**,服务名为 `CrossDeskService`。该服务用于锁屏、登录界面和安全桌面等受保护场景下的远程控制增强能力,包括:
|
||||||
|
- 上报远端当前是否处于锁屏、登录、凭据输入或安全桌面状态;
|
||||||
|
- 支持从控制端发送 `Ctrl+Alt+Del`(SAS);
|
||||||
|
- 在锁屏、登录和安全桌面阶段转发键盘、鼠标输入。
|
||||||
|
|
||||||
|
Windows 安装包会自动打包 `crossdesk_service.exe` 和 `crossdesk_session_helper.exe`,并在安装时注册为按需启动的 Windows 服务。CrossDesk 客户端启动时会尝试启动已安装的服务;当本机没有 CrossDesk 客户端进程运行时,服务会自动退出。卸载客户端时会同步停止并移除该服务。
|
||||||
|
|
||||||
|
如果是手动编译或手动部署 Windows 版本,请确保 `CrossDesk.exe`、`crossdesk_service.exe` 和 `crossdesk_session_helper.exe` 位于同一目录。安装或卸载服务需要使用管理员权限打开 PowerShell:
|
||||||
|
```
|
||||||
|
# 安装
|
||||||
|
.\CrossDesk.exe --service-install
|
||||||
|
# 启动
|
||||||
|
.\CrossDesk.exe --service-start
|
||||||
|
# 查看状态
|
||||||
|
.\CrossDesk.exe --service-status
|
||||||
|
.\CrossDesk.exe --service-stop
|
||||||
|
# 卸载
|
||||||
|
.\CrossDesk.exe --service-uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
如果远端 Windows 服务未安装、未启动或暂时不可用,基础远程桌面连接仍可使用,但锁屏、登录界面和安全桌面阶段的控制能力会受限,客户端会提示“远端Windows服务不可用”。
|
||||||
|
|
||||||
## 如何编译
|
## 如何编译
|
||||||
|
|
||||||
依赖:
|
依赖:
|
||||||
|
|||||||
@@ -56,6 +56,31 @@ Enter the **Remote Device ID** and **Password**, then click Connect to access th
|
|||||||
|
|
||||||
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
<img width="645" height="300" alt="_cgi-bin_mmwebwx-bin_webwxgetmsgimg__ MsgID=932911462648581698 skey=@crypt_1f5153b1_b550ca7462b5009ce03c991cca2a92a7 mmweb_appid=wx_webfilehelper" src="https://github.com/user-attachments/assets/a5109e6f-752c-4654-9f4e-7e161bddf43e" />
|
||||||
|
|
||||||
|
### Windows Service (CrossDesk Service)
|
||||||
|
CrossDesk provides a local helper service on Windows named **CrossDesk Service**. Its service name is `CrossDeskService`. The service improves remote control in protected Windows states such as the lock screen, sign-in UI, credential UI, and secure desktop. It provides:
|
||||||
|
- Remote status reporting for lock screen, sign-in, credential, and secure desktop states.
|
||||||
|
- Remote `Ctrl+Alt+Del` (SAS) delivery.
|
||||||
|
- Keyboard and mouse input forwarding while the remote Windows device is on the lock screen, sign-in UI, or secure desktop.
|
||||||
|
|
||||||
|
The Windows installer bundles `crossdesk_service.exe` and `crossdesk_session_helper.exe`, then registers the service as an on-demand Windows service during installation. When the CrossDesk client starts, it tries to start the installed service automatically. When no CrossDesk client process is running on the machine, the service exits automatically. Uninstalling the client also stops and removes the service.
|
||||||
|
|
||||||
|
For manual Windows builds or deployments, make sure `CrossDesk.exe`, `crossdesk_service.exe`, and `crossdesk_session_helper.exe` are placed in the same directory. Open PowerShell with administrator privileges to install or uninstall the service:
|
||||||
|
```
|
||||||
|
# install
|
||||||
|
.\CrossDesk.exe --service-install
|
||||||
|
# start
|
||||||
|
.\CrossDesk.exe --service-start
|
||||||
|
# check status
|
||||||
|
.\CrossDesk.exe --service-status
|
||||||
|
.\CrossDesk.exe --service-ping
|
||||||
|
# stop
|
||||||
|
.\CrossDesk.exe --service-stop
|
||||||
|
# uninstall
|
||||||
|
.\CrossDesk.exe --service-uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
If the remote Windows service is not installed, not running, or temporarily unavailable, the basic remote desktop connection still works, but remote control on the lock screen, sign-in UI, and secure desktop is limited. The client will show “Remote Windows service unavailable”.
|
||||||
|
|
||||||
## How to build
|
## How to build
|
||||||
|
|
||||||
Requirements:
|
Requirements:
|
||||||
|
|||||||
+66
-50
@@ -15,6 +15,7 @@ APP_BUNDLE="${APP_NAME_UPPER}.app"
|
|||||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||||
|
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||||
|
|
||||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||||
@@ -73,67 +74,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
|||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||||
|
find "${APP_BUNDLE}" -name '._*' -delete
|
||||||
|
|
||||||
echo ".app created successfully."
|
echo ".app created successfully."
|
||||||
|
|
||||||
|
mkdir -p build_pkg_scripts
|
||||||
|
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||||
|
chmod +x build_pkg_scripts/postinstall
|
||||||
|
|
||||||
|
mkdir -p build_pkg_resources
|
||||||
|
cat > build_pkg_resources/welcome.html <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||||
|
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||||
|
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||||
|
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||||
|
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
|
||||||
echo "building pkg..."
|
echo "building pkg..."
|
||||||
pkgbuild \
|
pkgbuild \
|
||||||
--identifier "${IDENTIFIER}" \
|
--identifier "${IDENTIFIER}" \
|
||||||
--version "${APP_VERSION}" \
|
--version "${APP_VERSION}" \
|
||||||
--install-location "/Applications" \
|
--install-location "/Applications" \
|
||||||
--component "${APP_BUNDLE}" \
|
--component "${APP_BUNDLE}" \
|
||||||
|
--scripts build_pkg_scripts \
|
||||||
build_pkg_temp/${APP_NAME}-component.pkg
|
build_pkg_temp/${APP_NAME}-component.pkg
|
||||||
|
|
||||||
mkdir -p build_pkg_scripts
|
cat > build_pkg_temp/Distribution <<EOF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
cat > build_pkg_scripts/postinstall <<'EOF'
|
<installer-gui-script minSpecVersion="1">
|
||||||
#!/bin/bash
|
<title>${INSTALLER_TITLE}</title>
|
||||||
set -e
|
<welcome file="welcome.html" mime-type="text/html"/>
|
||||||
|
<options customize="never" require-scripts="false" hostArchitectures="arm64"/>
|
||||||
IDENTIFIER="cn.crossdesk.app"
|
<choices-outline>
|
||||||
|
<line choice="default">
|
||||||
# 获取当前登录用户
|
<line choice="${IDENTIFIER}"/>
|
||||||
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
|
</line>
|
||||||
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
|
</choices-outline>
|
||||||
|
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||||
# 清除应用的权限授权,以便重新授权
|
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||||
# 使用 tccutil 重置录屏权限和辅助功能权限
|
<pkg-ref id="${IDENTIFIER}"/>
|
||||||
if command -v tccutil >/dev/null 2>&1; then
|
</choice>
|
||||||
# 重置录屏权限
|
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||||
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
|
</installer-gui-script>
|
||||||
# 重置辅助功能权限
|
|
||||||
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置摄像头权限(如果需要)
|
|
||||||
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置麦克风权限(如果需要)
|
|
||||||
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 为所有用户清除权限(可选,如果需要)
|
|
||||||
# 遍历所有用户目录并清除权限
|
|
||||||
for USER_DIR in /Users/*; do
|
|
||||||
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
|
|
||||||
USER_NAME=$(basename "$USER_DIR")
|
|
||||||
# 跳过系统用户
|
|
||||||
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
|
|
||||||
# 删除 TCC 数据库中的相关条目(需要管理员权限)
|
|
||||||
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
|
|
||||||
if [ -f "$TCC_DB" ]; then
|
|
||||||
# 使用 sqlite3 删除相关权限记录(如果可用)
|
|
||||||
if command -v sqlite3 >/dev/null 2>&1; then
|
|
||||||
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x build_pkg_scripts/postinstall
|
|
||||||
|
|
||||||
productbuild \
|
productbuild \
|
||||||
--package build_pkg_temp/${APP_NAME}-component.pkg \
|
--distribution build_pkg_temp/Distribution \
|
||||||
|
--package-path build_pkg_temp \
|
||||||
|
--resources build_pkg_resources \
|
||||||
"${PKG_NAME}"
|
"${PKG_NAME}"
|
||||||
|
|
||||||
echo "PKG package created: ${PKG_NAME}"
|
echo "PKG package created: ${PKG_NAME}"
|
||||||
@@ -171,8 +187,8 @@ APPLESCRIPT
|
|||||||
fi
|
fi
|
||||||
echo "Set icon finished"
|
echo "Set icon finished"
|
||||||
|
|
||||||
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
|
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||||
|
|
||||||
echo "PKG package created successfully."
|
echo "PKG package created successfully."
|
||||||
echo "package ${APP_BUNDLE}"
|
echo "package ${APP_BUNDLE}"
|
||||||
echo "installer ${PKG_NAME}"
|
echo "installer ${PKG_NAME}"
|
||||||
|
|||||||
+66
-50
@@ -15,6 +15,7 @@ APP_BUNDLE="${APP_NAME_UPPER}.app"
|
|||||||
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
CONTENTS_DIR="${APP_BUNDLE}/Contents"
|
||||||
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
MACOS_DIR="${CONTENTS_DIR}/MacOS"
|
||||||
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
RESOURCES_DIR="${CONTENTS_DIR}/Resources"
|
||||||
|
INSTALLER_TITLE="${APP_NAME_UPPER} ${APP_VERSION}"
|
||||||
|
|
||||||
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
PKG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.pkg"
|
||||||
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
DMG_NAME="${APP_NAME}-${PLATFORM}-${ARCH}-${APP_VERSION}.dmg"
|
||||||
@@ -73,67 +74,82 @@ cat > "${CONTENTS_DIR}/Info.plist" <<EOF
|
|||||||
</plist>
|
</plist>
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
|
xattr -cr "${APP_BUNDLE}" 2>/dev/null || true
|
||||||
|
find "${APP_BUNDLE}" -name '._*' -delete
|
||||||
|
|
||||||
echo ".app created successfully."
|
echo ".app created successfully."
|
||||||
|
|
||||||
|
mkdir -p build_pkg_scripts
|
||||||
|
cp scripts/macosx/tcc_postinstall.sh build_pkg_scripts/postinstall
|
||||||
|
chmod +x build_pkg_scripts/postinstall
|
||||||
|
|
||||||
|
mkdir -p build_pkg_resources
|
||||||
|
cat > build_pkg_resources/welcome.html <<EOF
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Helvetica Neue", sans-serif;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #222;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>欢迎安装 ${INSTALLER_TITLE}</h1>
|
||||||
|
<p>CrossDesk 将安装到“应用程序”文件夹。</p>
|
||||||
|
<p>首次启动时,CrossDesk 会引导你在系统设置中授予必要权限,包括辅助功能、录屏与系统录音等。</p>
|
||||||
|
<p>为避免旧版本授权残留造成状态误判,安装后可能需要重新授权。</p>
|
||||||
|
<p>安装完成后,请从“应用程序”文件夹启动 CrossDesk。</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
EOF
|
||||||
|
|
||||||
echo "building pkg..."
|
echo "building pkg..."
|
||||||
pkgbuild \
|
pkgbuild \
|
||||||
--identifier "${IDENTIFIER}" \
|
--identifier "${IDENTIFIER}" \
|
||||||
--version "${APP_VERSION}" \
|
--version "${APP_VERSION}" \
|
||||||
--install-location "/Applications" \
|
--install-location "/Applications" \
|
||||||
--component "${APP_BUNDLE}" \
|
--component "${APP_BUNDLE}" \
|
||||||
|
--scripts build_pkg_scripts \
|
||||||
build_pkg_temp/${APP_NAME}-component.pkg
|
build_pkg_temp/${APP_NAME}-component.pkg
|
||||||
|
|
||||||
mkdir -p build_pkg_scripts
|
cat > build_pkg_temp/Distribution <<EOF
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
cat > build_pkg_scripts/postinstall <<'EOF'
|
<installer-gui-script minSpecVersion="1">
|
||||||
#!/bin/bash
|
<title>${INSTALLER_TITLE}</title>
|
||||||
set -e
|
<welcome file="welcome.html" mime-type="text/html"/>
|
||||||
|
<options customize="never" require-scripts="false" hostArchitectures="x86_64"/>
|
||||||
IDENTIFIER="cn.crossdesk.app"
|
<choices-outline>
|
||||||
|
<line choice="default">
|
||||||
# 获取当前登录用户
|
<line choice="${IDENTIFIER}"/>
|
||||||
USER_HOME=$( /usr/bin/stat -f "%Su" /dev/console )
|
</line>
|
||||||
HOME_DIR=$( /usr/bin/dscl . -read /Users/$USER_HOME NFSHomeDirectory | awk '{print $2}' )
|
</choices-outline>
|
||||||
|
<choice id="default" title="${INSTALLER_TITLE}"/>
|
||||||
# 清除应用的权限授权,以便重新授权
|
<choice id="${IDENTIFIER}" title="${INSTALLER_TITLE}" visible="false">
|
||||||
# 使用 tccutil 重置录屏权限和辅助功能权限
|
<pkg-ref id="${IDENTIFIER}"/>
|
||||||
if command -v tccutil >/dev/null 2>&1; then
|
</choice>
|
||||||
# 重置录屏权限
|
<pkg-ref id="${IDENTIFIER}" version="${APP_VERSION}" onConclusion="none">crossdesk-component.pkg</pkg-ref>
|
||||||
tccutil reset ScreenCapture "$IDENTIFIER" 2>/dev/null || true
|
</installer-gui-script>
|
||||||
# 重置辅助功能权限
|
|
||||||
tccutil reset Accessibility "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置摄像头权限(如果需要)
|
|
||||||
tccutil reset Camera "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
# 重置麦克风权限(如果需要)
|
|
||||||
tccutil reset Microphone "$IDENTIFIER" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# 为所有用户清除权限(可选,如果需要)
|
|
||||||
# 遍历所有用户目录并清除权限
|
|
||||||
for USER_DIR in /Users/*; do
|
|
||||||
if [ -d "$USER_DIR" ] && [ "$USER_DIR" != "/Users/Shared" ]; then
|
|
||||||
USER_NAME=$(basename "$USER_DIR")
|
|
||||||
# 跳过系统用户
|
|
||||||
if [ "$USER_NAME" != "Shared" ] && [ -d "$USER_DIR/Library" ]; then
|
|
||||||
# 删除 TCC 数据库中的相关条目(需要管理员权限)
|
|
||||||
TCC_DB="$USER_DIR/Library/Application Support/com.apple.TCC/TCC.db"
|
|
||||||
if [ -f "$TCC_DB" ]; then
|
|
||||||
# 使用 sqlite3 删除相关权限记录(如果可用)
|
|
||||||
if command -v sqlite3 >/dev/null 2>&1; then
|
|
||||||
sqlite3 "$TCC_DB" "DELETE FROM access WHERE client='$IDENTIFIER' AND service IN ('kTCCServiceScreenCapture', 'kTCCServiceAccessibility');" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
exit 0
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x build_pkg_scripts/postinstall
|
|
||||||
|
|
||||||
productbuild \
|
productbuild \
|
||||||
--package build_pkg_temp/${APP_NAME}-component.pkg \
|
--distribution build_pkg_temp/Distribution \
|
||||||
|
--package-path build_pkg_temp \
|
||||||
|
--resources build_pkg_resources \
|
||||||
"${PKG_NAME}"
|
"${PKG_NAME}"
|
||||||
|
|
||||||
echo "PKG package created: ${PKG_NAME}"
|
echo "PKG package created: ${PKG_NAME}"
|
||||||
@@ -171,8 +187,8 @@ APPLESCRIPT
|
|||||||
fi
|
fi
|
||||||
echo "Set icon finished"
|
echo "Set icon finished"
|
||||||
|
|
||||||
rm -rf build_pkg_temp build_pkg_scripts ${APP_BUNDLE}
|
rm -rf build_pkg_temp build_pkg_scripts build_pkg_resources ${APP_BUNDLE}
|
||||||
|
|
||||||
echo "PKG package created successfully."
|
echo "PKG package created successfully."
|
||||||
echo "package ${APP_BUNDLE}"
|
echo "package ${APP_BUNDLE}"
|
||||||
echo "installer ${PKG_NAME}"
|
echo "installer ${PKG_NAME}"
|
||||||
|
|||||||
@@ -0,0 +1,112 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -e
|
||||||
|
|
||||||
|
APP_IDENTIFIER="cn.crossdesk.app"
|
||||||
|
|
||||||
|
# Keep known historical identifiers here. tccutil only resets identifiers that
|
||||||
|
# Launch Services can currently resolve, so path/db cleanup below remains a
|
||||||
|
# best-effort fallback for stale entries from unsigned or removed builds.
|
||||||
|
BUNDLE_IDENTIFIERS=(
|
||||||
|
"cn.crossdesk.app"
|
||||||
|
"cn.crossdesk.CrossDesk"
|
||||||
|
"com.crossdesk.app"
|
||||||
|
"com.crossdesk.CrossDesk"
|
||||||
|
"com.kunkundi.crossdesk"
|
||||||
|
"com.kunkundi.CrossDesk"
|
||||||
|
)
|
||||||
|
|
||||||
|
TCC_SERVICES=(
|
||||||
|
"ScreenCapture"
|
||||||
|
"Accessibility"
|
||||||
|
"Microphone"
|
||||||
|
"AudioCapture"
|
||||||
|
)
|
||||||
|
|
||||||
|
run_tccutil() {
|
||||||
|
local user_name="$1"
|
||||||
|
local user_id="$2"
|
||||||
|
local service="$3"
|
||||||
|
local bundle_id="$4"
|
||||||
|
|
||||||
|
if [ -n "$user_name" ] && [ -n "$user_id" ]; then
|
||||||
|
/bin/launchctl asuser "$user_id" \
|
||||||
|
/usr/bin/sudo -u "$user_name" \
|
||||||
|
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||||
|
else
|
||||||
|
/usr/bin/tccutil reset "$service" "$bundle_id" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_bundle_tcc() {
|
||||||
|
local user_name="$1"
|
||||||
|
local user_id="$2"
|
||||||
|
local bundle_id
|
||||||
|
local service
|
||||||
|
|
||||||
|
for bundle_id in "${BUNDLE_IDENTIFIERS[@]}"; do
|
||||||
|
if run_tccutil "$user_name" "$user_id" "All" "$bundle_id"; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
for service in "${TCC_SERVICES[@]}"; do
|
||||||
|
run_tccutil "$user_name" "$user_id" "$service" "$bundle_id" || true
|
||||||
|
done
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_tcc_db() {
|
||||||
|
local db_path="$1"
|
||||||
|
|
||||||
|
if [ ! -f "$db_path" ] || ! command -v sqlite3 >/dev/null 2>&1; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
/usr/bin/sqlite3 "$db_path" <<'SQL' >/dev/null 2>&1 || true
|
||||||
|
DELETE FROM access
|
||||||
|
WHERE service IN (
|
||||||
|
'kTCCServiceScreenCapture',
|
||||||
|
'kTCCServiceAccessibility',
|
||||||
|
'kTCCServiceMicrophone',
|
||||||
|
'kTCCServiceAudioCapture'
|
||||||
|
)
|
||||||
|
AND (
|
||||||
|
client IN (
|
||||||
|
'cn.crossdesk.app',
|
||||||
|
'cn.crossdesk.CrossDesk',
|
||||||
|
'com.crossdesk.app',
|
||||||
|
'com.crossdesk.CrossDesk',
|
||||||
|
'com.kunkundi.crossdesk',
|
||||||
|
'com.kunkundi.CrossDesk'
|
||||||
|
)
|
||||||
|
OR lower(client) LIKE '%crossdesk%'
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup_user_tcc_db() {
|
||||||
|
local user_name="$1"
|
||||||
|
local home_dir
|
||||||
|
|
||||||
|
home_dir=$(/usr/bin/dscl . -read "/Users/${user_name}" NFSHomeDirectory 2>/dev/null |
|
||||||
|
/usr/bin/awk '{print $2}')
|
||||||
|
if [ -z "$home_dir" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_tcc_db "${home_dir}/Library/Application Support/com.apple.TCC/TCC.db"
|
||||||
|
}
|
||||||
|
|
||||||
|
CONSOLE_USER=$(/usr/bin/stat -f "%Su" /dev/console 2>/dev/null || true)
|
||||||
|
if [ -n "$CONSOLE_USER" ] &&
|
||||||
|
[ "$CONSOLE_USER" != "root" ] &&
|
||||||
|
[ "$CONSOLE_USER" != "loginwindow" ]; then
|
||||||
|
CONSOLE_UID=$(/usr/bin/id -u "$CONSOLE_USER" 2>/dev/null || true)
|
||||||
|
reset_bundle_tcc "$CONSOLE_USER" "$CONSOLE_UID"
|
||||||
|
cleanup_user_tcc_db "$CONSOLE_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Also clear any system/root-scoped decisions as a harmless fallback.
|
||||||
|
reset_bundle_tcc "" ""
|
||||||
|
cleanup_tcc_db "/Library/Application Support/com.apple.TCC/TCC.db"
|
||||||
|
|
||||||
|
exit 0
|
||||||
@@ -19,155 +19,176 @@ struct TranslationRow {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Single source of truth for all UI strings.
|
// Single source of truth for all UI strings.
|
||||||
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
#define CROSSDESK_LOCALIZATION_ALL(X) \
|
||||||
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \
|
||||||
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \
|
||||||
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \
|
||||||
u8"Скопировано в буфер обмена") \
|
u8"Скопировано в буфер обмена") \
|
||||||
X(password, u8"密码", "Password", u8"Пароль") \
|
X(password, u8"密码", "Password", u8"Пароль") \
|
||||||
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
|
X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \
|
||||||
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
X(remote_desktop, u8"远程桌面", "Remote Desktop", \
|
||||||
u8"Удаленный рабочий стол") \
|
u8"Удаленный рабочий стол") \
|
||||||
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \
|
||||||
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
X(connect, u8"连接", "Connect", u8"Подключиться") \
|
||||||
X(recent_connections, u8"近期连接", "Recent Connections", \
|
X(recent_connections, u8"近期连接", "Recent Connections", \
|
||||||
u8"Недавние подключения") \
|
u8"Недавние подключения") \
|
||||||
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \
|
||||||
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
|
X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \
|
||||||
X(show_net_traffic_stats, u8"显示流量统计", "Show Net Traffic Stats", \
|
X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \
|
||||||
u8"Показать статистику трафика") \
|
u8"Развернуть панель управления") \
|
||||||
X(hide_net_traffic_stats, u8"隐藏流量统计", "Hide Net Traffic Stats", \
|
X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \
|
||||||
u8"Скрыть статистику трафика") \
|
u8"Свернуть панель управления") \
|
||||||
X(video, u8"视频", "Video", u8"Видео") \
|
X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \
|
||||||
X(audio, u8"音频", "Audio", u8"Аудио") \
|
X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \
|
||||||
X(data, u8"数据", "Data", u8"Данные") \
|
u8"Показать статистику трафика") \
|
||||||
X(total, u8"总计", "Total", u8"Итого") \
|
X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \
|
||||||
X(in, u8"输入", "In", u8"Вход") \
|
u8"Скрыть статистику трафика") \
|
||||||
X(out, u8"输出", "Out", u8"Выход") \
|
X(video, u8"视频", "Video", u8"Видео") \
|
||||||
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
|
X(audio, u8"音频", "Audio", u8"Аудио") \
|
||||||
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
|
X(data, u8"数据", "Data", u8"Данные") \
|
||||||
u8"Выйти из полноэкранного режима") \
|
X(total, u8"总计", "Total", u8"Итого") \
|
||||||
X(control_mouse, u8"控制", "Control", u8"Управление") \
|
X(in, u8"输入", "In", u8"Вход") \
|
||||||
X(release_mouse, u8"释放", "Release", u8"Освободить") \
|
X(out, u8"输出", "Out", u8"Выход") \
|
||||||
X(audio_capture, u8"声音", "Audio", u8"Звук") \
|
X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \
|
||||||
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \
|
||||||
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
u8"Выйти из полноэкранного режима") \
|
||||||
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \
|
||||||
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \
|
||||||
X(remote_password_box_visible, u8"远端密码框已出现", \
|
X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \
|
||||||
"Remote password box visible", u8"Окно ввода пароля видно") \
|
X(mute, u8" 静音", " Mute", u8"Без звука") \
|
||||||
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \
|
||||||
"Remote lock screen visible, send SAS", \
|
X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \
|
||||||
u8"Видна блокировка, отправьте SAS") \
|
X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \
|
||||||
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
X(remote_password_box_visible, u8"远端密码框已出现", \
|
||||||
"Remote secure desktop active", \
|
"Remote password box visible", u8"Окно ввода пароля видно") \
|
||||||
u8"Активен защищенный рабочий стол") \
|
X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \
|
||||||
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
"Remote lock screen visible, send SAS", \
|
||||||
"Remote Windows service unavailable", \
|
u8"Видна блокировка, отправьте SAS") \
|
||||||
u8"Служба Windows на удаленной стороне недоступна") \
|
X(remote_secure_desktop_active, u8"远端已进入安全桌面", \
|
||||||
X(remote_unlock_requires_secure_desktop, \
|
"Remote secure desktop active", u8"Активен защищенный рабочий стол") \
|
||||||
u8"当前仍需要安全桌面专用采集/输入", \
|
X(remote_service_unavailable, u8"远端Windows服务不可用", \
|
||||||
"Secure desktop capture/input is still required", \
|
"Remote Windows service unavailable", \
|
||||||
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего стола") \
|
u8"Служба Windows на удаленной стороне недоступна") \
|
||||||
X(settings, u8"设置", "Settings", u8"Настройки") \
|
X(windows_service_setup_title, u8"安装 CrossDesk Service", \
|
||||||
X(language, u8"语言:", "Language:", u8"Язык:") \
|
"Install CrossDesk Service", u8"Установить CrossDesk Service") \
|
||||||
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
X(windows_service_setup_message, \
|
||||||
X(video_frame_rate, u8"画面采集帧率:", \
|
u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \
|
||||||
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
"The portable version needs the local Windows service for full control on the lock screen, sign-in UI, and secure desktop. The service is not installed. Click Install and approve the system prompt.", \
|
||||||
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \
|
||||||
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
X(install_windows_service, u8"安装", "Install", \
|
||||||
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
u8"Установить службу") \
|
||||||
X(video_encode_format, u8"视频编码格式:", \
|
X(installing_windows_service, u8"正在安装服务...", "Installing service...", \
|
||||||
"Video Encode Format:", u8"Формат кодека видео:") \
|
u8"Установка службы...") \
|
||||||
X(av1, u8"AV1", "AV1", "AV1") \
|
X(windows_service_install_success, u8"服务已安装并启动", \
|
||||||
X(h264, u8"H.264", "H.264", "H.264") \
|
"Service installed and started", u8"Служба установлена и запущена") \
|
||||||
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
|
X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \
|
||||||
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
|
"Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \
|
||||||
X(enable_turn, u8"启用中继服务:", \
|
u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \
|
||||||
"Enable TURN Service:", u8"Включить TURN-сервис:") \
|
X(remote_unlock_requires_secure_desktop, \
|
||||||
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
|
u8"当前仍需要安全桌面专用采集/输入", \
|
||||||
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
|
"Secure desktop capture/input is still required", \
|
||||||
u8"Конфигурация self-hosted") \
|
u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \
|
||||||
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
|
u8"стола") \
|
||||||
u8"Настройки self-hosted") \
|
X(settings, u8"设置", "Settings", u8"Настройки") \
|
||||||
X(self_hosted_server_address, u8"服务器地址:", \
|
X(language, u8"语言:", "Language:", u8"Язык:") \
|
||||||
"Server Address:", u8"Адрес сервера:") \
|
X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \
|
||||||
X(self_hosted_server_port, u8"信令服务端口:", \
|
X(video_frame_rate, u8"画面采集帧率:", \
|
||||||
"Signal Service Port:", u8"Порт сигнального сервиса:") \
|
"Video Capture Frame Rate:", u8"Частота захвата видео:") \
|
||||||
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
|
X(video_quality_high, u8"高", "High", u8"Высокое") \
|
||||||
"Relay Service Port:", u8"Порт реле-сервиса:") \
|
X(video_quality_medium, u8"中", "Medium", u8"Среднее") \
|
||||||
X(ok, u8"确认", "OK", u8"ОК") \
|
X(video_quality_low, u8"低", "Low", u8"Низкое") \
|
||||||
X(cancel, u8"取消", "Cancel", u8"Отмена") \
|
X(video_encode_format, u8"视频编码格式:", \
|
||||||
X(new_password, u8"请输入六位密码:", \
|
"Video Encode Format:", u8"Формат кодека видео:") \
|
||||||
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
|
X(av1, u8"AV1", "AV1", "AV1") \
|
||||||
X(input_password, u8"请输入密码:", \
|
X(h264, u8"H.264", "H.264", "H.264") \
|
||||||
"Please input password:", u8"Введите пароль:") \
|
X(enable_hardware_video_codec, u8"启用硬件编解码器:", \
|
||||||
X(validate_password, u8"验证密码中...", "Validate password ...", \
|
"Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \
|
||||||
u8"Проверка пароля...") \
|
X(enable_turn, u8"启用中继服务:", \
|
||||||
X(reinput_password, u8"请重新输入密码", "Please input password again", \
|
"Enable TURN Service:", u8"Включить TURN-сервис:") \
|
||||||
u8"Повторно введите пароль") \
|
X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \
|
||||||
X(remember_password, u8"记住密码", "Remember password", \
|
X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \
|
||||||
u8"Запомнить пароль") \
|
u8"Конфигурация self-hosted") \
|
||||||
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \
|
||||||
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
u8"Настройки self-hosted") \
|
||||||
u8"Нет подключения к серверу") \
|
X(self_hosted_server_address, u8"服务器地址:", \
|
||||||
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
"Server Address:", u8"Адрес сервера:") \
|
||||||
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
X(self_hosted_server_port, u8"信令服务端口:", \
|
||||||
u8"P2P отключено") \
|
"Signal Service Port:", u8"Порт сигнального сервиса:") \
|
||||||
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \
|
||||||
u8"Подключение P2P...") \
|
"Relay Service Port:", u8"Порт реле-сервиса:") \
|
||||||
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
X(ok, u8"确认", "OK", u8"ОК") \
|
||||||
u8"Получение изображения...") \
|
X(cancel, u8"取消", "Cancel", u8"Отмена") \
|
||||||
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
X(new_password, u8"请输入六位密码:", \
|
||||||
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
"Please input a six-char password:", u8"Введите шестизначный пароль:") \
|
||||||
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
X(input_password, u8"请输入密码:", \
|
||||||
X(about, u8"关于", "About", u8"О программе") \
|
"Please input password:", u8"Введите пароль:") \
|
||||||
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
X(validate_password, u8"验证密码中...", "Validate password ...", \
|
||||||
X(new_version_available, u8"新版本可用", "New Version Available", \
|
u8"Проверка пароля...") \
|
||||||
u8"Доступна новая версия") \
|
X(reinput_password, u8"请重新输入密码", "Please input password again", \
|
||||||
X(version, u8"版本", "Version", u8"Версия") \
|
u8"Повторно введите пароль") \
|
||||||
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
X(remember_password, u8"记住密码", "Remember password", \
|
||||||
X(access_website, u8"访问官网: ", \
|
u8"Запомнить пароль") \
|
||||||
"Access Website: ", u8"Официальный сайт: ") \
|
X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \
|
||||||
X(update, u8"更新", "Update", u8"Обновить") \
|
X(signal_disconnected, u8"未连接服务器", "Disconnected", \
|
||||||
X(confirm_delete_connection, u8"确认删除此连接", \
|
u8"Нет подключения к серверу") \
|
||||||
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \
|
||||||
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \
|
||||||
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
u8"P2P отключено") \
|
||||||
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \
|
||||||
u8"Вступит в силу после перезапуска") \
|
u8"Подключение P2P...") \
|
||||||
X(select_file, u8"选择文件", "Select File", u8"Выбрать файл") \
|
X(receiving_screen, u8"画面接收中...", "Receiving screen...", \
|
||||||
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
|
u8"Получение изображения...") \
|
||||||
u8"Прогресс передачи файлов") \
|
X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \
|
||||||
X(queued, u8"队列中", "Queued", u8"В очереди") \
|
X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \
|
||||||
X(sending, u8"正在传输", "Sending", u8"Передача") \
|
X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \
|
||||||
X(completed, u8"已完成", "Completed", u8"Завершено") \
|
X(about, u8"关于", "About", u8"О программе") \
|
||||||
X(failed, u8"失败", "Failed", u8"Ошибка") \
|
X(notification, u8"通知", "Notification", u8"Уведомление") \
|
||||||
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
|
X(new_version_available, u8"新版本可用", "New Version Available", \
|
||||||
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
|
u8"Доступна новая версия") \
|
||||||
X(connection_status, u8"连接状态:", \
|
X(version, u8"版本", "Version", u8"Версия") \
|
||||||
"Connection Status:", u8"Состояние соединения:") \
|
X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \
|
||||||
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
X(access_website, u8"访问官网: ", \
|
||||||
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
"Access Website: ", u8"Официальный сайт: ") \
|
||||||
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
X(update, u8"更新", "Update", u8"Обновить") \
|
||||||
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
|
X(confirm_delete_connection, u8"确认删除此连接", \
|
||||||
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
|
"Confirm to delete this connection", u8"Удалить это подключение?") \
|
||||||
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \
|
||||||
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \
|
||||||
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \
|
||||||
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
|
u8"Вступит в силу после перезапуска") \
|
||||||
X(online, u8"在线", "Online", u8"Онлайн") \
|
X(select_file, u8"选择文件发送", "Select File to Send", \
|
||||||
X(offline, u8"离线", "Offline", u8"Офлайн") \
|
u8"Выбрать файл для отправки") \
|
||||||
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
|
X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \
|
||||||
X(request_permissions, u8"权限请求", "Request Permissions", \
|
u8"Прогресс передачи файлов") \
|
||||||
u8"Запрос разрешений") \
|
X(queued, u8"队列中", "Queued", u8"В очереди") \
|
||||||
X(screen_recording_permission, u8"屏幕录制权限", \
|
X(sending, u8"正在传输", "Sending", u8"Передача") \
|
||||||
"Screen Recording Permission", u8"Разрешение на запись экрана") \
|
X(completed, u8"已完成", "Completed", u8"Завершено") \
|
||||||
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
|
X(failed, u8"失败", "Failed", u8"Ошибка") \
|
||||||
u8"Разрешение специальных возможностей") \
|
X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \
|
||||||
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \
|
||||||
"The application requires the following permissions:", \
|
X(connection_status, u8"连接状态:", \
|
||||||
u8"Для работы приложения требуются следующие разрешения:") \
|
"Connection Status:", u8"Состояние соединения:") \
|
||||||
|
X(file_transfer_save_path, u8"文件接收保存路径:", \
|
||||||
|
"File Transfer Save Path:", u8"Путь сохранения файлов:") \
|
||||||
|
X(default_desktop, u8"桌面", "Desktop", u8"Рабочий стол") \
|
||||||
|
X(minimize_to_tray, u8"退出时最小化到系统托盘:", \
|
||||||
|
"Minimize on Exit:", u8"Сворачивать в трей при выходе:") \
|
||||||
|
X(resolution, u8"分辨率", "Res", u8"Разрешение") \
|
||||||
|
X(connection_mode, u8"连接模式", "Mode", u8"Режим") \
|
||||||
|
X(connection_mode_direct, u8"直连", "Direct", u8"Прямой") \
|
||||||
|
X(connection_mode_relay, u8"中继", "Relay", u8"Релейный") \
|
||||||
|
X(online, u8"在线", "Online", u8"Онлайн") \
|
||||||
|
X(offline, u8"离线", "Offline", u8"Офлайн") \
|
||||||
|
X(device_offline, u8"设备离线", "Device Offline", u8"Устройство офлайн") \
|
||||||
|
X(request_permissions, u8"权限请求", "Request Permissions", \
|
||||||
|
u8"Запрос разрешений") \
|
||||||
|
X(screen_recording_permission, u8"屏幕录制权限", \
|
||||||
|
"Screen Recording Permission", u8"Разрешение на запись экрана") \
|
||||||
|
X(accessibility_permission, u8"辅助功能权限", "Accessibility Permission", \
|
||||||
|
u8"Разрешение специальных возможностей") \
|
||||||
|
X(permission_required_message, u8"该应用需要授权以下权限:", \
|
||||||
|
"The application requires the following permissions:", \
|
||||||
|
u8"Для работы приложения требуются следующие разрешения:") \
|
||||||
X(exit_program, u8"退出", "Exit", u8"Выход")
|
X(exit_program, u8"退出", "Exit", u8"Выход")
|
||||||
|
|
||||||
inline constexpr TranslationRow kTranslationRows[] = {
|
inline constexpr TranslationRow kTranslationRows[] = {
|
||||||
|
|||||||
+110
-8
@@ -42,6 +42,8 @@
|
|||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
constexpr uint64_t kCaptureResumeKeyFrameGapMs = 500;
|
||||||
|
|
||||||
const ImWchar* GetMultilingualGlyphRanges() {
|
const ImWchar* GetMultilingualGlyphRanges() {
|
||||||
static std::vector<ImWchar> glyph_ranges;
|
static std::vector<ImWchar> glyph_ranges;
|
||||||
if (glyph_ranges.empty()) {
|
if (glyph_ranges.empty()) {
|
||||||
@@ -82,15 +84,22 @@ HICON LoadTrayIcon() {
|
|||||||
|
|
||||||
struct WindowsServiceInteractiveStatus {
|
struct WindowsServiceInteractiveStatus {
|
||||||
bool available = false;
|
bool available = false;
|
||||||
|
bool sas_secure_desktop_grace_active = false;
|
||||||
unsigned int error_code = 0;
|
unsigned int error_code = 0;
|
||||||
std::string interactive_stage;
|
std::string interactive_stage;
|
||||||
std::string error;
|
std::string error;
|
||||||
};
|
};
|
||||||
|
|
||||||
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
|
constexpr uint32_t kWindowsServiceStatusIntervalMs = 1000;
|
||||||
constexpr DWORD kWindowsServiceQueryTimeoutMs = 100;
|
constexpr uint32_t kWindowsServiceSasSecureDesktopGraceMs = 2000;
|
||||||
|
constexpr DWORD kWindowsServiceQueryTimeoutMs = 500;
|
||||||
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
|
constexpr DWORD kWindowsServiceSasTimeoutMs = 500;
|
||||||
|
|
||||||
|
bool IsTransientWindowsServiceStatusError(const std::string& error) {
|
||||||
|
return error == "pipe_unavailable" || error == "pipe_connect_failed" ||
|
||||||
|
error == "pipe_read_failed";
|
||||||
|
}
|
||||||
|
|
||||||
RemoteAction BuildWindowsServiceStatusAction(
|
RemoteAction BuildWindowsServiceStatusAction(
|
||||||
const WindowsServiceInteractiveStatus& status) {
|
const WindowsServiceInteractiveStatus& status) {
|
||||||
RemoteAction action{};
|
RemoteAction action{};
|
||||||
@@ -125,6 +134,8 @@ bool QueryWindowsServiceInteractiveStatus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
status->interactive_stage = json.value("interactive_stage", std::string());
|
status->interactive_stage = json.value("interactive_stage", std::string());
|
||||||
|
status->sas_secure_desktop_grace_active =
|
||||||
|
json.value("sas_secure_desktop_grace_active", false);
|
||||||
|
|
||||||
if (ShouldNormalizeUnlockToUserDesktop(
|
if (ShouldNormalizeUnlockToUserDesktop(
|
||||||
json.value("interactive_lock_screen_visible", false),
|
json.value("interactive_lock_screen_visible", false),
|
||||||
@@ -625,6 +636,12 @@ int Render::LoadSettingsFromCacheFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Render::ScreenCapturerInit() {
|
int Render::ScreenCapturerInit() {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacScreenRecordingPermission()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!screen_capturer_) {
|
if (!screen_capturer_) {
|
||||||
screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create();
|
screen_capturer_ = (ScreenCapturer*)screen_capturer_factory_->Create();
|
||||||
}
|
}
|
||||||
@@ -642,18 +659,37 @@ int Render::ScreenCapturerInit() {
|
|||||||
fps,
|
fps,
|
||||||
[this, fps](unsigned char* data, int size, int width, int height,
|
[this, fps](unsigned char* data, int size, int width, int height,
|
||||||
const char* display_name) -> void {
|
const char* display_name) -> void {
|
||||||
auto now_time = std::chrono::duration_cast<std::chrono::milliseconds>(
|
const auto now_time =
|
||||||
std::chrono::steady_clock::now().time_since_epoch())
|
static_cast<uint64_t>(std::chrono::duration_cast<
|
||||||
.count();
|
std::chrono::milliseconds>(
|
||||||
|
std::chrono::steady_clock::now()
|
||||||
|
.time_since_epoch())
|
||||||
|
.count());
|
||||||
auto duration = now_time - last_frame_time_;
|
auto duration = now_time - last_frame_time_;
|
||||||
if (duration * fps >= 1000) { // ~60 FPS
|
if (duration * fps >= 1000) { // ~60 FPS
|
||||||
|
const std::string stream_id = display_name ? display_name : "";
|
||||||
|
const bool resumed_after_gap =
|
||||||
|
last_frame_time_ != 0 &&
|
||||||
|
duration >= kCaptureResumeKeyFrameGapMs;
|
||||||
|
const bool stream_changed =
|
||||||
|
!last_video_frame_stream_id_.empty() &&
|
||||||
|
last_video_frame_stream_id_ != stream_id;
|
||||||
|
if (resumed_after_gap || stream_changed) {
|
||||||
|
if (RequestVideoKeyFrame(peer_, stream_id.c_str()) == 0) {
|
||||||
|
LOG_INFO(
|
||||||
|
"Request video key frame before sending captured frame, "
|
||||||
|
"stream='{}', gap_ms={}, stream_changed={}",
|
||||||
|
stream_id, duration, stream_changed);
|
||||||
|
}
|
||||||
|
}
|
||||||
XVideoFrame frame;
|
XVideoFrame frame;
|
||||||
frame.data = (const char*)data;
|
frame.data = (const char*)data;
|
||||||
frame.size = size;
|
frame.size = size;
|
||||||
frame.width = width;
|
frame.width = width;
|
||||||
frame.height = height;
|
frame.height = height;
|
||||||
frame.captured_timestamp = GetSystemTimeMicros(peer_);
|
frame.captured_timestamp = GetSystemTimeMicros(peer_);
|
||||||
SendVideoFrame(peer_, &frame, display_name);
|
SendVideoFrame(peer_, &frame, stream_id.c_str());
|
||||||
|
last_video_frame_stream_id_ = stream_id;
|
||||||
last_frame_time_ = now_time;
|
last_frame_time_ = now_time;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -675,6 +711,12 @@ int Render::ScreenCapturerInit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Render::StartScreenCapturer() {
|
int Render::StartScreenCapturer() {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacScreenRecordingPermission()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!screen_capturer_) {
|
if (!screen_capturer_) {
|
||||||
LOG_INFO("Screen capturer instance missing, recreating before start");
|
LOG_INFO("Screen capturer instance missing, recreating before start");
|
||||||
if (0 != ScreenCapturerInit()) {
|
if (0 != ScreenCapturerInit()) {
|
||||||
@@ -739,6 +781,12 @@ int Render::StopSpeakerCapturer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int Render::StartMouseController() {
|
int Render::StartMouseController() {
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacAccessibilityPermission()) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (!device_controller_factory_) {
|
if (!device_controller_factory_) {
|
||||||
LOG_INFO("Device controller factory is nullptr");
|
LOG_INFO("Device controller factory is nullptr");
|
||||||
return -1;
|
return -1;
|
||||||
@@ -796,6 +844,13 @@ int Render::StopMouseController() {
|
|||||||
int Render::StartKeyboardCapturer() {
|
int Render::StartKeyboardCapturer() {
|
||||||
keyboard_capturer_uses_sdl_events_ = false;
|
keyboard_capturer_uses_sdl_events_ = false;
|
||||||
|
|
||||||
|
#ifdef __APPLE__
|
||||||
|
if (!EnsureMacAccessibilityPermission()) {
|
||||||
|
keyboard_capturer_uses_sdl_events_ = true;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
#if defined(__linux__) && !defined(__APPLE__)
|
#if defined(__linux__) && !defined(__APPLE__)
|
||||||
if (IsWaylandSession()) {
|
if (IsWaylandSession()) {
|
||||||
keyboard_capturer_uses_sdl_events_ = true;
|
keyboard_capturer_uses_sdl_events_ = true;
|
||||||
@@ -1540,6 +1595,10 @@ int Render::DrawMainWindow() {
|
|||||||
|
|
||||||
UpdateNotificationWindow();
|
UpdateNotificationWindow();
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
PortableServiceInstallWindow();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
if (show_request_permission_window_) {
|
if (show_request_permission_window_) {
|
||||||
RequestPermissionWindow();
|
RequestPermissionWindow();
|
||||||
@@ -1702,6 +1761,10 @@ int Render::Run() {
|
|||||||
InitializeModules();
|
InitializeModules();
|
||||||
InitializeMainWindow();
|
InitializeMainWindow();
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
CheckPortableWindowsService();
|
||||||
|
#endif
|
||||||
|
|
||||||
const int scaled_video_width_ = 160;
|
const int scaled_video_width_ = 160;
|
||||||
const int scaled_video_height_ = 90;
|
const int scaled_video_height_ = 90;
|
||||||
|
|
||||||
@@ -1923,6 +1986,12 @@ void Render::HandleWindowsServiceIntegration() {
|
|||||||
LOG_WARN("Remote SAS request failed: {}", response);
|
LOG_WARN("Remote SAS request failed: {}", response);
|
||||||
} else {
|
} else {
|
||||||
LOG_INFO("Remote SAS request forwarded to local Windows service");
|
LOG_INFO("Remote SAS request forwarded to local Windows service");
|
||||||
|
optimistic_windows_secure_desktop_until_tick_ =
|
||||||
|
static_cast<uint32_t>(SDL_GetTicks()) +
|
||||||
|
kWindowsServiceSasSecureDesktopGraceMs;
|
||||||
|
local_service_status_received_ = true;
|
||||||
|
local_service_available_ = true;
|
||||||
|
local_interactive_stage_ = "secure-desktop";
|
||||||
}
|
}
|
||||||
last_windows_service_status_tick_ = 0;
|
last_windows_service_status_tick_ = 0;
|
||||||
force_broadcast = true;
|
force_broadcast = true;
|
||||||
@@ -1938,9 +2007,32 @@ void Render::HandleWindowsServiceIntegration() {
|
|||||||
|
|
||||||
WindowsServiceInteractiveStatus status;
|
WindowsServiceInteractiveStatus status;
|
||||||
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
|
const bool status_ok = QueryWindowsServiceInteractiveStatus(&status);
|
||||||
local_service_status_received_ = status_ok;
|
WindowsServiceInteractiveStatus broadcast_status = status;
|
||||||
|
const bool previous_secure_desktop_interaction =
|
||||||
|
IsSecureDesktopInteractionRequired(local_interactive_stage_);
|
||||||
|
const bool optimistic_secure_desktop_active =
|
||||||
|
optimistic_windows_secure_desktop_until_tick_ != 0 &&
|
||||||
|
static_cast<int32_t>(optimistic_windows_secure_desktop_until_tick_ -
|
||||||
|
now) > 0;
|
||||||
|
const bool keep_optimistic_secure_desktop =
|
||||||
|
status_ok && status.available && optimistic_secure_desktop_active &&
|
||||||
|
status.sas_secure_desktop_grace_active &&
|
||||||
|
status.interactive_stage == "user-desktop";
|
||||||
|
local_service_status_received_ =
|
||||||
|
status_ok || previous_secure_desktop_interaction;
|
||||||
local_service_available_ = status.available;
|
local_service_available_ = status.available;
|
||||||
local_interactive_stage_ = status.available ? status.interactive_stage : "";
|
if (status.available) {
|
||||||
|
if (keep_optimistic_secure_desktop) {
|
||||||
|
local_interactive_stage_ = "secure-desktop";
|
||||||
|
broadcast_status.interactive_stage = local_interactive_stage_;
|
||||||
|
} else {
|
||||||
|
local_interactive_stage_ = status.interactive_stage;
|
||||||
|
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||||
|
}
|
||||||
|
} else if (!previous_secure_desktop_interaction) {
|
||||||
|
local_interactive_stage_.clear();
|
||||||
|
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (status_ok) {
|
if (status_ok) {
|
||||||
const bool availability_changed =
|
const bool availability_changed =
|
||||||
@@ -1953,6 +2045,11 @@ void Render::HandleWindowsServiceIntegration() {
|
|||||||
if (status.available) {
|
if (status.available) {
|
||||||
LOG_INFO(
|
LOG_INFO(
|
||||||
"Local Windows service available for secure desktop integration");
|
"Local Windows service available for secure desktop integration");
|
||||||
|
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||||
|
LOG_INFO(
|
||||||
|
"Local Windows service temporarily unavailable, keeping last "
|
||||||
|
"secure desktop state: error={}, code={}",
|
||||||
|
status.error, status.error_code);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARN(
|
LOG_WARN(
|
||||||
"Local Windows service unavailable, secure desktop integration "
|
"Local Windows service unavailable, secure desktop integration "
|
||||||
@@ -1973,7 +2070,7 @@ void Render::HandleWindowsServiceIntegration() {
|
|||||||
last_logged_service_error_code = 0;
|
last_logged_service_error_code = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
RemoteAction remote_action = BuildWindowsServiceStatusAction(status);
|
RemoteAction remote_action = BuildWindowsServiceStatusAction(broadcast_status);
|
||||||
std::string msg = remote_action.to_json();
|
std::string msg = remote_action.to_json();
|
||||||
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
|
int ret = SendReliableDataFrame(peer_, msg.data(), msg.size(),
|
||||||
control_data_label_.c_str());
|
control_data_label_.c_str());
|
||||||
@@ -1992,6 +2089,7 @@ void Render::ResetLocalWindowsServiceState(bool clear_pending_sas) {
|
|||||||
local_service_status_received_ = false;
|
local_service_status_received_ = false;
|
||||||
local_service_available_ = false;
|
local_service_available_ = false;
|
||||||
local_interactive_stage_.clear();
|
local_interactive_stage_.clear();
|
||||||
|
optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@@ -2182,6 +2280,10 @@ void Render::Cleanup() {
|
|||||||
CleanupFactories();
|
CleanupFactories();
|
||||||
CleanupPeers();
|
CleanupPeers();
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
JoinPortableWindowsServiceInstallThread();
|
||||||
|
#endif
|
||||||
|
|
||||||
WaitForThumbnailSaveTasks();
|
WaitForThumbnailSaveTasks();
|
||||||
|
|
||||||
AudioDeviceDestroy();
|
AudioDeviceDestroy();
|
||||||
|
|||||||
@@ -296,6 +296,9 @@ class Render {
|
|||||||
void OpenScreenRecordingPreferences();
|
void OpenScreenRecordingPreferences();
|
||||||
void OpenAccessibilityPreferences();
|
void OpenAccessibilityPreferences();
|
||||||
bool DrawToggleSwitch(const char* id, bool active, bool enabled);
|
bool DrawToggleSwitch(const char* id, bool active, bool enabled);
|
||||||
|
void RefreshMacPermissionStatus(bool force);
|
||||||
|
bool EnsureMacScreenRecordingPermission();
|
||||||
|
bool EnsureMacAccessibilityPermission();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -382,6 +385,19 @@ class Render {
|
|||||||
void HandleWindowsServiceIntegration();
|
void HandleWindowsServiceIntegration();
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
void ResetLocalWindowsServiceState(bool clear_pending_sas);
|
||||||
|
#if CROSSDESK_PORTABLE
|
||||||
|
enum class PortableServiceInstallState {
|
||||||
|
idle,
|
||||||
|
installing,
|
||||||
|
succeeded,
|
||||||
|
failed,
|
||||||
|
};
|
||||||
|
|
||||||
|
void CheckPortableWindowsService();
|
||||||
|
int PortableServiceInstallWindow();
|
||||||
|
void StartPortableWindowsServiceInstall();
|
||||||
|
void JoinPortableWindowsServiceInstallThread();
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -547,6 +563,14 @@ class Render {
|
|||||||
std::string local_interactive_stage_;
|
std::string local_interactive_stage_;
|
||||||
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
uint32_t last_local_secure_input_block_log_tick_ = 0;
|
||||||
uint32_t last_windows_service_status_tick_ = 0;
|
uint32_t last_windows_service_status_tick_ = 0;
|
||||||
|
uint32_t optimistic_windows_secure_desktop_until_tick_ = 0;
|
||||||
|
#if CROSSDESK_PORTABLE
|
||||||
|
bool portable_service_prompt_checked_ = false;
|
||||||
|
bool show_portable_service_install_window_ = false;
|
||||||
|
std::atomic<PortableServiceInstallState> portable_service_install_state_{
|
||||||
|
PortableServiceInstallState::idle};
|
||||||
|
std::thread portable_service_install_thread_;
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// stream window render
|
// stream window render
|
||||||
@@ -671,12 +695,19 @@ class Render {
|
|||||||
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
KeyboardCapturer* keyboard_capturer_ = nullptr;
|
||||||
std::vector<DisplayInfo> display_info_list_;
|
std::vector<DisplayInfo> display_info_list_;
|
||||||
uint64_t last_frame_time_;
|
uint64_t last_frame_time_;
|
||||||
|
std::string last_video_frame_stream_id_;
|
||||||
bool show_new_version_icon_ = false;
|
bool show_new_version_icon_ = false;
|
||||||
bool show_new_version_icon_in_menu_ = true;
|
bool show_new_version_icon_in_menu_ = true;
|
||||||
double new_version_icon_last_trigger_time_ = 0.0;
|
double new_version_icon_last_trigger_time_ = 0.0;
|
||||||
double new_version_icon_render_start_time_ = 0.0;
|
double new_version_icon_render_start_time_ = 0.0;
|
||||||
#ifdef __APPLE__
|
#ifdef __APPLE__
|
||||||
bool show_request_permission_window_ = true;
|
bool show_request_permission_window_ = true;
|
||||||
|
bool mac_permission_status_initialized_ = false;
|
||||||
|
uint32_t mac_permission_last_check_tick_ = 0;
|
||||||
|
bool mac_screen_recording_permission_granted_ = false;
|
||||||
|
bool mac_accessibility_permission_granted_ = false;
|
||||||
|
bool mac_screen_recording_permission_requested_ = false;
|
||||||
|
bool mac_accessibility_permission_requested_ = false;
|
||||||
#endif
|
#endif
|
||||||
char client_id_[10] = "";
|
char client_id_[10] = "";
|
||||||
char client_id_display_[12] = "";
|
char client_id_display_[12] = "";
|
||||||
|
|||||||
@@ -317,6 +317,22 @@ void LogSecureDesktopInputBlocked(uint32_t* last_tick, const char* side,
|
|||||||
"cannot drive the Windows password UI",
|
"cannot drive the Windows password UI",
|
||||||
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
|
side != nullptr ? side : "unknown", stage != nullptr ? stage : "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsTransientSecureDesktopInputFailure(const nlohmann::json& response,
|
||||||
|
const RemoteAction& action) {
|
||||||
|
if (!response.is_object()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.value("error", std::string()) != "send_input_failed") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.value("code", 0u) != ERROR_ACCESS_DENIED) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return action.type == ControlType::keyboard &&
|
||||||
|
action.k.flag == KeyFlag::key_up;
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
@@ -492,7 +508,7 @@ int Render::ProcessKeyboardEvent(const SDL_Event& event) {
|
|||||||
|
|
||||||
int Render::ProcessMouseEvent(const SDL_Event& event) {
|
int Render::ProcessMouseEvent(const SDL_Event& event) {
|
||||||
controlled_remote_id_ = "";
|
controlled_remote_id_ = "";
|
||||||
RemoteAction remote_action;
|
RemoteAction remote_action{};
|
||||||
float cursor_x = last_mouse_event.motion.x;
|
float cursor_x = last_mouse_event.motion.x;
|
||||||
float cursor_y = last_mouse_event.motion.y;
|
float cursor_y = last_mouse_event.motion.y;
|
||||||
|
|
||||||
@@ -1104,7 +1120,6 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
// remote
|
// remote
|
||||||
#if _WIN32
|
#if _WIN32
|
||||||
if (render->local_service_status_received_ &&
|
if (render->local_service_status_received_ &&
|
||||||
render->local_service_available_ &&
|
|
||||||
IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) {
|
IsSecureDesktopInteractionRequired(render->local_interactive_stage_)) {
|
||||||
if (remote_action.type == ControlType::mouse) {
|
if (remote_action.type == ControlType::mouse) {
|
||||||
int absolute_x = 0;
|
int absolute_x = 0;
|
||||||
@@ -1145,6 +1160,14 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size,
|
|||||||
remote_action.k.extended, 1000);
|
remote_action.k.extended, 1000);
|
||||||
auto json = nlohmann::json::parse(response, nullptr, false);
|
auto json = nlohmann::json::parse(response, nullptr, false);
|
||||||
if (json.is_discarded() || !json.value("ok", false)) {
|
if (json.is_discarded() || !json.value("ok", false)) {
|
||||||
|
if (!json.is_discarded() &&
|
||||||
|
IsTransientSecureDesktopInputFailure(json, remote_action)) {
|
||||||
|
LOG_INFO(
|
||||||
|
"Secure desktop keyboard injection transient failure, "
|
||||||
|
"key_code={}, is_down={}, response={}",
|
||||||
|
key_code, is_down, response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
LogSecureDesktopInputBlocked(
|
LogSecureDesktopInputBlocked(
|
||||||
&render->last_local_secure_input_block_log_tick_, "local",
|
&render->last_local_secure_input_block_log_tick_, "local",
|
||||||
render->local_interactive_stage_.c_str());
|
render->local_interactive_stage_.c_str());
|
||||||
|
|||||||
@@ -15,6 +15,22 @@
|
|||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
void ShowControlBarTooltip(const std::string& text) {
|
||||||
|
if (!ImGui::IsItemHovered() || text.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::BeginTooltip();
|
||||||
|
ImGui::SetWindowFontScale(0.5f);
|
||||||
|
ImGui::Text("%s", text.c_str());
|
||||||
|
ImGui::SetWindowFontScale(1.0f);
|
||||||
|
ImGui::EndTooltip();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
int CountDigits(int number) {
|
int CountDigits(int number) {
|
||||||
if (number == 0) return 1;
|
if (number == 0) return 1;
|
||||||
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
return (int)std::floor(std::log10(std::abs(number))) + 1;
|
||||||
@@ -162,6 +178,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
|
|
||||||
ImVec2 btn_min = ImGui::GetItemRectMin();
|
ImVec2 btn_min = ImGui::GetItemRectMin();
|
||||||
ImVec2 btn_size_actual = ImGui::GetItemRectSize();
|
ImVec2 btn_size_actual = ImGui::GetItemRectSize();
|
||||||
|
ShowControlBarTooltip(
|
||||||
|
localization::select_display[localization_language_index_]);
|
||||||
|
|
||||||
props->display_selectable_hovered_ = false;
|
props->display_selectable_hovered_ = false;
|
||||||
if (ImGui::BeginPopup("display")) {
|
if (ImGui::BeginPopup("display")) {
|
||||||
@@ -185,12 +203,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SetWindowFontScale(0.5f);
|
ImGui::SetWindowFontScale(0.35f);
|
||||||
ImVec2 text_size = ImGui::CalcTextSize(
|
ImVec2 text_size = ImGui::CalcTextSize(
|
||||||
std::to_string(props->selected_display_ + 1).c_str());
|
std::to_string(props->selected_display_ + 1).c_str());
|
||||||
ImVec2 text_pos =
|
ImVec2 text_pos =
|
||||||
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.5f,
|
ImVec2(btn_min.x + (btn_size_actual.x - text_size.x) * 0.55f,
|
||||||
btn_min.y + (btn_size_actual.y - text_size.y) * 0.35f);
|
btn_min.y + (btn_size_actual.y - text_size.y) * 0.33f);
|
||||||
ImGui::GetWindowDrawList()->AddText(
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
text_pos, IM_COL32(0, 0, 0, 255),
|
text_pos, IM_COL32(0, 0, 0, 255),
|
||||||
std::to_string(props->selected_display_ + 1).c_str());
|
std::to_string(props->selected_display_ + 1).c_str());
|
||||||
@@ -218,25 +236,14 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) {
|
if (ImGui::Button(shortcut.c_str(), ImVec2(button_width, button_height))) {
|
||||||
ImGui::OpenPopup("shortcut");
|
ImGui::OpenPopup("shortcut");
|
||||||
}
|
}
|
||||||
|
ShowControlBarTooltip(
|
||||||
if (ImGui::IsItemHovered()) {
|
localization::send_shortcut[localization_language_index_]);
|
||||||
ImGui::BeginTooltip();
|
|
||||||
ImGui::SetWindowFontScale(0.5f);
|
|
||||||
ImGui::Text(
|
|
||||||
"%s",
|
|
||||||
localization::send_shortcut[localization_language_index_].c_str());
|
|
||||||
ImGui::SetWindowFontScale(1.0f);
|
|
||||||
ImGui::EndTooltip();
|
|
||||||
}
|
|
||||||
|
|
||||||
props->shortcut_selectable_hovered_ = false;
|
props->shortcut_selectable_hovered_ = false;
|
||||||
if (ImGui::BeginPopup("shortcut")) {
|
if (ImGui::BeginPopup("shortcut")) {
|
||||||
ImGui::SetWindowFontScale(0.5f);
|
ImGui::SetWindowFontScale(0.5f);
|
||||||
std::string sas_label =
|
std::string sas_label = "Ctrl+Alt+Del";
|
||||||
"Ctrl+Alt+Del - " +
|
std::string lock_label = "Win+L";
|
||||||
localization::send_sas[localization_language_index_];
|
|
||||||
std::string lock_label =
|
|
||||||
"Win+L - " + localization::lock_remote[localization_language_index_];
|
|
||||||
if (ImGui::Selectable(sas_label.c_str())) {
|
if (ImGui::Selectable(sas_label.c_str())) {
|
||||||
send_service_command(ServiceCommandFlag::send_sas, "SAS");
|
send_service_command(ServiceCommandFlag::send_sas, "SAS");
|
||||||
}
|
}
|
||||||
@@ -268,6 +275,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
: localization::control_mouse[localization_language_index_];
|
: localization::control_mouse[localization_language_index_];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const bool mouse_button_hovered = ImGui::IsItemHovered();
|
||||||
|
const std::string mouse_tooltip =
|
||||||
|
props->enable_mouse_control_
|
||||||
|
? localization::release_mouse[localization_language_index_]
|
||||||
|
: localization::control_mouse[localization_language_index_];
|
||||||
|
ShowControlBarTooltip(mouse_tooltip);
|
||||||
|
|
||||||
if (!props->enable_mouse_control_) {
|
if (!props->enable_mouse_control_) {
|
||||||
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
|
draw_list->AddLine(ImVec2(disable_mouse_x, disable_mouse_y),
|
||||||
@@ -280,8 +293,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
ImVec2(
|
ImVec2(
|
||||||
mouse_x + button_width - line_padding - line_thickness * 0.7f,
|
mouse_x + button_width - line_padding - line_thickness * 0.7f,
|
||||||
mouse_y + button_height - line_padding + line_thickness * 0.7f),
|
mouse_y + button_height - line_padding + line_thickness * 0.7f),
|
||||||
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
|
mouse_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||||
: IM_COL32(179, 213, 253, 255),
|
: IM_COL32(179, 213, 253, 255),
|
||||||
line_thickness);
|
line_thickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +326,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
props->control_data_label_.c_str());
|
props->control_data_label_.c_str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const bool audio_button_hovered = ImGui::IsItemHovered();
|
||||||
|
const std::string audio_tooltip =
|
||||||
|
props->audio_capture_button_pressed_
|
||||||
|
? localization::mute[localization_language_index_]
|
||||||
|
: localization::audio_capture[localization_language_index_];
|
||||||
|
ShowControlBarTooltip(audio_tooltip);
|
||||||
|
|
||||||
if (!props->audio_capture_button_pressed_) {
|
if (!props->audio_capture_button_pressed_) {
|
||||||
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
|
draw_list->AddLine(ImVec2(disable_audio_x, disable_audio_y),
|
||||||
@@ -325,8 +344,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
ImVec2(
|
ImVec2(
|
||||||
audio_x + button_width - line_padding - line_thickness * 0.7f,
|
audio_x + button_width - line_padding - line_thickness * 0.7f,
|
||||||
audio_y + button_height - line_padding + line_thickness * 0.7f),
|
audio_y + button_height - line_padding + line_thickness * 0.7f),
|
||||||
ImGui::IsItemHovered() ? IM_COL32(66, 150, 250, 255)
|
audio_button_hovered ? IM_COL32(66, 150, 250, 255)
|
||||||
: IM_COL32(179, 213, 253, 255),
|
: IM_COL32(179, 213, 253, 255),
|
||||||
line_thickness);
|
line_thickness);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +358,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
std::string path = OpenFileDialog(title);
|
std::string path = OpenFileDialog(title);
|
||||||
ProcessSelectedFile(path, props, file_label_);
|
ProcessSelectedFile(path, props, file_label_);
|
||||||
}
|
}
|
||||||
|
ShowControlBarTooltip(
|
||||||
|
localization::select_file[localization_language_index_]);
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
// net traffic stats button
|
// net traffic stats button
|
||||||
@@ -363,6 +384,12 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
: localization::show_net_traffic_stats
|
: localization::show_net_traffic_stats
|
||||||
[localization_language_index_];
|
[localization_language_index_];
|
||||||
}
|
}
|
||||||
|
const std::string net_traffic_stats_tooltip =
|
||||||
|
props->net_traffic_stats_button_pressed_
|
||||||
|
? localization::hide_net_traffic_stats[localization_language_index_]
|
||||||
|
: localization::show_net_traffic_stats
|
||||||
|
[localization_language_index_];
|
||||||
|
ShowControlBarTooltip(net_traffic_stats_tooltip);
|
||||||
|
|
||||||
if (button_color_style_pushed) {
|
if (button_color_style_pushed) {
|
||||||
ImGui::PopStyleColor();
|
ImGui::PopStyleColor();
|
||||||
@@ -389,6 +416,11 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
}
|
}
|
||||||
props->reset_control_bar_pos_ = true;
|
props->reset_control_bar_pos_ = true;
|
||||||
}
|
}
|
||||||
|
const std::string fullscreen_tooltip =
|
||||||
|
fullscreen_button_pressed_
|
||||||
|
? localization::exit_fullscreen[localization_language_index_]
|
||||||
|
: localization::fullscreen[localization_language_index_];
|
||||||
|
ShowControlBarTooltip(fullscreen_tooltip);
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
// close button
|
// close button
|
||||||
@@ -398,6 +430,8 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
ImVec2(button_width, button_height))) {
|
ImVec2(button_width, button_height))) {
|
||||||
CleanupPeer(props);
|
CleanupPeer(props);
|
||||||
}
|
}
|
||||||
|
ShowControlBarTooltip(
|
||||||
|
localization::disconnect[localization_language_index_]);
|
||||||
|
|
||||||
ImGui::SameLine();
|
ImGui::SameLine();
|
||||||
|
|
||||||
@@ -427,6 +461,10 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
: ICON_FA_ANGLE_RIGHT)
|
: ICON_FA_ANGLE_RIGHT)
|
||||||
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
|
: (props->is_control_bar_in_left_ ? ICON_FA_ANGLE_RIGHT
|
||||||
: ICON_FA_ANGLE_LEFT);
|
: ICON_FA_ANGLE_LEFT);
|
||||||
|
const std::string control_bar_tooltip =
|
||||||
|
props->control_bar_expand_
|
||||||
|
? localization::collapse_control_bar[localization_language_index_]
|
||||||
|
: localization::expand_control_bar[localization_language_index_];
|
||||||
if (ImGui::Button(control_bar.c_str(),
|
if (ImGui::Button(control_bar.c_str(),
|
||||||
ImVec2(button_height * 0.6f, button_height))) {
|
ImVec2(button_height * 0.6f, button_height))) {
|
||||||
props->control_bar_expand_ = !props->control_bar_expand_;
|
props->control_bar_expand_ = !props->control_bar_expand_;
|
||||||
@@ -438,6 +476,7 @@ int Render::ControlBar(std::shared_ptr<SubStreamWindowProperties>& props) {
|
|||||||
props->net_traffic_stats_button_pressed_ = false;
|
props->net_traffic_stats_button_pressed_ = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ShowControlBarTooltip(control_bar_tooltip);
|
||||||
|
|
||||||
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
|
if (props->net_traffic_stats_button_pressed_ && props->control_bar_expand_) {
|
||||||
NetTrafficStats(props);
|
NetTrafficStats(props);
|
||||||
|
|||||||
@@ -0,0 +1,279 @@
|
|||||||
|
#include "render.h"
|
||||||
|
|
||||||
|
#if _WIN32 && CROSSDESK_PORTABLE
|
||||||
|
|
||||||
|
#include <shellapi.h>
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "localization.h"
|
||||||
|
#include "rd_log.h"
|
||||||
|
#include "service_host.h"
|
||||||
|
|
||||||
|
namespace crossdesk {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::filesystem::path GetCurrentExecutablePath() {
|
||||||
|
std::vector<wchar_t> buffer(MAX_PATH);
|
||||||
|
while (true) {
|
||||||
|
DWORD length =
|
||||||
|
GetModuleFileNameW(nullptr, buffer.data(),
|
||||||
|
static_cast<DWORD>(buffer.size()));
|
||||||
|
if (length == 0) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
if (length < buffer.size()) {
|
||||||
|
return std::filesystem::path(buffer.data(), buffer.data() + length);
|
||||||
|
}
|
||||||
|
if (buffer.size() >= 32768) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
buffer.resize(buffer.size() * 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool InstallServiceWithElevation() {
|
||||||
|
const std::filesystem::path executable_path = GetCurrentExecutablePath();
|
||||||
|
if (executable_path.empty()) {
|
||||||
|
LOG_ERROR("Portable service install failed: current executable not found");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::filesystem::path service_path =
|
||||||
|
executable_path.parent_path() / L"crossdesk_service.exe";
|
||||||
|
const std::filesystem::path helper_path =
|
||||||
|
executable_path.parent_path() / L"crossdesk_session_helper.exe";
|
||||||
|
if (!std::filesystem::exists(service_path) ||
|
||||||
|
!std::filesystem::exists(helper_path)) {
|
||||||
|
LOG_ERROR(
|
||||||
|
"Portable service install failed: service binaries missing, service={}, "
|
||||||
|
"helper={}",
|
||||||
|
service_path.string(), helper_path.string());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring executable = executable_path.wstring();
|
||||||
|
std::wstring working_dir = executable_path.parent_path().wstring();
|
||||||
|
std::wstring parameters = L"--service-install";
|
||||||
|
|
||||||
|
SHELLEXECUTEINFOW execute_info{};
|
||||||
|
execute_info.cbSize = sizeof(execute_info);
|
||||||
|
execute_info.fMask = SEE_MASK_NOCLOSEPROCESS;
|
||||||
|
execute_info.hwnd = nullptr;
|
||||||
|
execute_info.lpVerb = L"runas";
|
||||||
|
execute_info.lpFile = executable.c_str();
|
||||||
|
execute_info.lpParameters = parameters.c_str();
|
||||||
|
execute_info.lpDirectory = working_dir.c_str();
|
||||||
|
execute_info.nShow = SW_HIDE;
|
||||||
|
|
||||||
|
if (!ShellExecuteExW(&execute_info)) {
|
||||||
|
LOG_ERROR("Portable service install failed: ShellExecuteExW error={}",
|
||||||
|
GetLastError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DWORD wait_result = WaitForSingleObject(execute_info.hProcess, INFINITE);
|
||||||
|
DWORD exit_code = 1;
|
||||||
|
if (wait_result == WAIT_OBJECT_0) {
|
||||||
|
GetExitCodeProcess(execute_info.hProcess, &exit_code);
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Portable service install wait failed, result={}", wait_result);
|
||||||
|
}
|
||||||
|
CloseHandle(execute_info.hProcess);
|
||||||
|
|
||||||
|
if (exit_code != 0) {
|
||||||
|
LOG_ERROR("Portable service install command failed, exit_code={}",
|
||||||
|
exit_code);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool started = StartCrossDeskService();
|
||||||
|
if (!started) {
|
||||||
|
LOG_WARN("Portable service installed but start failed");
|
||||||
|
}
|
||||||
|
return IsCrossDeskServiceInstalled() && started;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void Render::CheckPortableWindowsService() {
|
||||||
|
if (portable_service_prompt_checked_) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
portable_service_prompt_checked_ = true;
|
||||||
|
|
||||||
|
if (IsCrossDeskServiceInstalled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
portable_service_install_state_.store(PortableServiceInstallState::idle,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
show_portable_service_install_window_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Render::StartPortableWindowsServiceInstall() {
|
||||||
|
PortableServiceInstallState expected = PortableServiceInstallState::idle;
|
||||||
|
if (!portable_service_install_state_.compare_exchange_strong(
|
||||||
|
expected, PortableServiceInstallState::installing,
|
||||||
|
std::memory_order_acq_rel)) {
|
||||||
|
if (expected != PortableServiceInstallState::failed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
portable_service_install_state_.store(
|
||||||
|
PortableServiceInstallState::installing, std::memory_order_release);
|
||||||
|
}
|
||||||
|
|
||||||
|
JoinPortableWindowsServiceInstallThread();
|
||||||
|
portable_service_install_thread_ = std::thread([this]() {
|
||||||
|
const bool installed = InstallServiceWithElevation();
|
||||||
|
portable_service_install_state_.store(
|
||||||
|
installed ? PortableServiceInstallState::succeeded
|
||||||
|
: PortableServiceInstallState::failed,
|
||||||
|
std::memory_order_release);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void Render::JoinPortableWindowsServiceInstallThread() {
|
||||||
|
if (portable_service_install_thread_.joinable()) {
|
||||||
|
portable_service_install_thread_.join();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int Render::PortableServiceInstallWindow() {
|
||||||
|
if (!show_portable_service_install_window_) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ImGuiViewport* viewport = ImGui::GetMainViewport();
|
||||||
|
const float window_width = title_bar_button_width_ * 12.0f;
|
||||||
|
const float window_height = title_bar_button_width_ * 4.0f;
|
||||||
|
ImGui::SetNextWindowPos(
|
||||||
|
ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) /
|
||||||
|
2.0f,
|
||||||
|
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) /
|
||||||
|
2.0f),
|
||||||
|
ImGuiCond_Appearing);
|
||||||
|
ImGui::SetNextWindowSize(ImVec2(window_width, window_height),
|
||||||
|
ImGuiCond_Always);
|
||||||
|
|
||||||
|
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_);
|
||||||
|
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f);
|
||||||
|
|
||||||
|
ImGui::Begin(
|
||||||
|
localization::windows_service_setup_title[localization_language_index_]
|
||||||
|
.c_str(),
|
||||||
|
nullptr,
|
||||||
|
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
|
||||||
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings |
|
||||||
|
ImGuiWindowFlags_NoTitleBar);
|
||||||
|
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::SetWindowFontScale(0.55f);
|
||||||
|
ImGui::SetCursorPosX(window_width * 0.08f);
|
||||||
|
ImGui::Text(
|
||||||
|
"%s",
|
||||||
|
localization::windows_service_setup_title[localization_language_index_]
|
||||||
|
.c_str());
|
||||||
|
|
||||||
|
const PortableServiceInstallState state =
|
||||||
|
portable_service_install_state_.load(std::memory_order_acquire);
|
||||||
|
const char* status_text = nullptr;
|
||||||
|
if (state == PortableServiceInstallState::installing ||
|
||||||
|
state == PortableServiceInstallState::succeeded ||
|
||||||
|
state == PortableServiceInstallState::failed) {
|
||||||
|
status_text =
|
||||||
|
localization::installing_windows_service[localization_language_index_]
|
||||||
|
.c_str();
|
||||||
|
if (state == PortableServiceInstallState::succeeded) {
|
||||||
|
status_text =
|
||||||
|
localization::windows_service_install_success
|
||||||
|
[localization_language_index_]
|
||||||
|
.c_str();
|
||||||
|
} else if (state == PortableServiceInstallState::failed) {
|
||||||
|
status_text =
|
||||||
|
localization::windows_service_install_failed
|
||||||
|
[localization_language_index_]
|
||||||
|
.c_str();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(0.45f);
|
||||||
|
ImGui::SetCursorPosX(window_width * 0.04f);
|
||||||
|
ImGui::SetCursorPosY(window_height * 0.22f);
|
||||||
|
ImGui::BeginChild("PortableServiceInstallContent",
|
||||||
|
ImVec2(window_width * 0.92f, window_height * 0.5f),
|
||||||
|
ImGuiChildFlags_Borders, ImGuiWindowFlags_None);
|
||||||
|
ImGui::SetWindowFontScale(0.5f);
|
||||||
|
const float wrap_pos = ImGui::GetContentRegionAvail().x;
|
||||||
|
ImGui::PushTextWrapPos(wrap_pos);
|
||||||
|
ImGui::TextWrapped(
|
||||||
|
"%s",
|
||||||
|
localization::windows_service_setup_message[localization_language_index_]
|
||||||
|
.c_str());
|
||||||
|
if (status_text != nullptr) {
|
||||||
|
ImGui::Spacing();
|
||||||
|
ImGui::TextWrapped("%s", status_text);
|
||||||
|
}
|
||||||
|
ImGui::PopTextWrapPos();
|
||||||
|
ImGui::EndChild();
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(0.5f);
|
||||||
|
const float button_y = window_height * 0.76f;
|
||||||
|
const ImGuiStyle& style = ImGui::GetStyle();
|
||||||
|
const auto default_button_width = [&style](const std::string& label) {
|
||||||
|
return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f;
|
||||||
|
};
|
||||||
|
const std::string install_label =
|
||||||
|
localization::install_windows_service[localization_language_index_];
|
||||||
|
const std::string cancel_label =
|
||||||
|
localization::cancel[localization_language_index_];
|
||||||
|
const std::string ok_label = localization::ok[localization_language_index_];
|
||||||
|
const float buttons_width = state == PortableServiceInstallState::succeeded
|
||||||
|
? default_button_width(ok_label)
|
||||||
|
: default_button_width(install_label) +
|
||||||
|
style.ItemSpacing.x +
|
||||||
|
default_button_width(cancel_label);
|
||||||
|
ImGui::SetCursorPosX((window_width - buttons_width) * 0.5f);
|
||||||
|
ImGui::SetCursorPosY(button_y);
|
||||||
|
|
||||||
|
if (state == PortableServiceInstallState::succeeded) {
|
||||||
|
if (ImGui::Button(ok_label.c_str())) {
|
||||||
|
show_portable_service_install_window_ = false;
|
||||||
|
JoinPortableWindowsServiceInstallThread();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (state == PortableServiceInstallState::installing) {
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
}
|
||||||
|
if (ImGui::Button(install_label.c_str())) {
|
||||||
|
StartPortableWindowsServiceInstall();
|
||||||
|
}
|
||||||
|
if (state == PortableServiceInstallState::installing) {
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SameLine();
|
||||||
|
if (state == PortableServiceInstallState::installing) {
|
||||||
|
ImGui::BeginDisabled();
|
||||||
|
}
|
||||||
|
if (ImGui::Button(cancel_label.c_str())) {
|
||||||
|
show_portable_service_install_window_ = false;
|
||||||
|
}
|
||||||
|
if (state == PortableServiceInstallState::installing) {
|
||||||
|
ImGui::EndDisabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetWindowFontScale(1.0f);
|
||||||
|
ImGui::End();
|
||||||
|
ImGui::PopStyleVar(3);
|
||||||
|
ImGui::PopStyleColor();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace crossdesk
|
||||||
|
|
||||||
|
#endif
|
||||||
@@ -6,11 +6,27 @@
|
|||||||
#include <ApplicationServices/ApplicationServices.h>
|
#include <ApplicationServices/ApplicationServices.h>
|
||||||
#include <CoreGraphics/CoreGraphics.h>
|
#include <CoreGraphics/CoreGraphics.h>
|
||||||
#import <Foundation/Foundation.h>
|
#import <Foundation/Foundation.h>
|
||||||
#include <unistd.h>
|
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
constexpr uint32_t kPermissionRefreshIntervalVisibleMs = 500;
|
||||||
|
|
||||||
|
void OpenPrivacyPreferences(const char* pane) {
|
||||||
|
if (pane == nullptr || pane[0] == '\0') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string command =
|
||||||
|
"open \"x-apple.systempreferences:com.apple.preference.security?";
|
||||||
|
command += pane;
|
||||||
|
command += "\"";
|
||||||
|
system(command.c_str());
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
||||||
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
|
const float TRACK_HEIGHT = ImGui::GetFrameHeight();
|
||||||
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
|
const float TRACK_WIDTH = TRACK_HEIGHT * 1.8f;
|
||||||
@@ -35,16 +51,19 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
|||||||
bool hovered = ImGui::IsItemHovered();
|
bool hovered = ImGui::IsItemHovered();
|
||||||
bool clicked = ImGui::IsItemClicked() && enabled;
|
bool clicked = ImGui::IsItemClicked() && enabled;
|
||||||
|
|
||||||
ImVec4 track_color = active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
ImVec4 track_color =
|
||||||
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
active ? (hovered && enabled ? COLOR_ACTIVE_HOVER : COLOR_ACTIVE)
|
||||||
|
: (hovered && enabled ? COLOR_INACTIVE_HOVER : COLOR_INACTIVE);
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
track_color.w *= DISABLED_ALPHA;
|
track_color.w *= DISABLED_ALPHA;
|
||||||
}
|
}
|
||||||
|
|
||||||
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
|
ImVec2 track_min = ImVec2(track_pos.x, track_pos.y + 0.5f);
|
||||||
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH, track_pos.y + TRACK_HEIGHT - 0.5f);
|
ImVec2 track_max = ImVec2(track_pos.x + TRACK_WIDTH,
|
||||||
draw_list->AddRectFilled(track_min, track_max, ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
track_pos.y + TRACK_HEIGHT - 0.5f);
|
||||||
|
draw_list->AddRectFilled(track_min, track_max,
|
||||||
|
ImGui::GetColorU32(track_color), TRACK_RADIUS);
|
||||||
|
|
||||||
float knob_position = active ? 1.0f : 0.0f;
|
float knob_position = active ? 1.0f : 0.0f;
|
||||||
float knob_min_x = track_pos.x + KNOB_PADDING;
|
float knob_min_x = track_pos.x + KNOB_PADDING;
|
||||||
@@ -59,7 +78,8 @@ bool Render::DrawToggleSwitch(const char* id, bool active, bool enabled) {
|
|||||||
|
|
||||||
ImVec2 knob_min = ImVec2(knob_x, knob_y);
|
ImVec2 knob_min = ImVec2(knob_x, knob_y);
|
||||||
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
|
ImVec2 knob_max = ImVec2(knob_x + KNOB_WIDTH, knob_y + KNOB_HEIGHT);
|
||||||
draw_list->AddRectFilled(knob_min, knob_max, ImGui::GetColorU32(knob_color), KNOB_RADIUS);
|
draw_list->AddRectFilled(knob_min, knob_max,
|
||||||
|
ImGui::GetColorU32(knob_color), KNOB_RADIUS);
|
||||||
|
|
||||||
return clicked;
|
return clicked;
|
||||||
}
|
}
|
||||||
@@ -81,29 +101,82 @@ bool Render::CheckAccessibilityPermission() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Render::OpenAccessibilityPreferences() {
|
void Render::OpenAccessibilityPreferences() {
|
||||||
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
if (!mac_accessibility_permission_requested_) {
|
||||||
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
NSDictionary* options = @{(__bridge id)kAXTrustedCheckOptionPrompt : @YES};
|
||||||
|
AXIsProcessTrustedWithOptions((__bridge CFDictionaryRef)options);
|
||||||
system("open "
|
} else {
|
||||||
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
|
OpenPrivacyPreferences("Privacy_Accessibility");
|
||||||
"Accessibility\"");
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void Render::OpenScreenRecordingPreferences() {
|
void Render::OpenScreenRecordingPreferences() {
|
||||||
if (@available(macOS 10.15, *)) {
|
if (@available(macOS 10.15, *)) {
|
||||||
CGRequestScreenCaptureAccess();
|
if (!mac_screen_recording_permission_requested_) {
|
||||||
|
CGRequestScreenCaptureAccess();
|
||||||
|
} else {
|
||||||
|
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OpenPrivacyPreferences("Privacy_ScreenCapture");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void Render::RefreshMacPermissionStatus(bool force) {
|
||||||
|
const uint32_t now = static_cast<uint32_t>(SDL_GetTicks());
|
||||||
|
if (!force && mac_permission_status_initialized_ &&
|
||||||
|
now - mac_permission_last_check_tick_ <
|
||||||
|
kPermissionRefreshIntervalVisibleMs) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
system("open "
|
const bool old_screen_recording_granted =
|
||||||
"\"x-apple.systempreferences:com.apple.preference.security?Privacy_"
|
mac_screen_recording_permission_granted_;
|
||||||
"ScreenCapture\"");
|
const bool old_accessibility_granted = mac_accessibility_permission_granted_;
|
||||||
|
|
||||||
|
mac_screen_recording_permission_granted_ =
|
||||||
|
CheckScreenRecordingPermission();
|
||||||
|
mac_accessibility_permission_granted_ = CheckAccessibilityPermission();
|
||||||
|
mac_permission_last_check_tick_ = now;
|
||||||
|
mac_permission_status_initialized_ = true;
|
||||||
|
|
||||||
|
if (old_screen_recording_granted !=
|
||||||
|
mac_screen_recording_permission_granted_ ||
|
||||||
|
old_accessibility_granted != mac_accessibility_permission_granted_) {
|
||||||
|
LOG_INFO("macOS permission status: screen_recording={}, accessibility={}",
|
||||||
|
mac_screen_recording_permission_granted_,
|
||||||
|
mac_accessibility_permission_granted_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Render::EnsureMacScreenRecordingPermission() {
|
||||||
|
RefreshMacPermissionStatus(false);
|
||||||
|
if (mac_screen_recording_permission_granted_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
show_request_permission_window_ = true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Render::EnsureMacAccessibilityPermission() {
|
||||||
|
RefreshMacPermissionStatus(false);
|
||||||
|
if (mac_accessibility_permission_granted_) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
show_request_permission_window_ = true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Render::RequestPermissionWindow() {
|
int Render::RequestPermissionWindow() {
|
||||||
bool screen_recording_granted = CheckScreenRecordingPermission();
|
RefreshMacPermissionStatus(false);
|
||||||
bool accessibility_granted = CheckAccessibilityPermission();
|
|
||||||
|
|
||||||
show_request_permission_window_ = !screen_recording_granted || !accessibility_granted;
|
const bool screen_recording_granted =
|
||||||
|
mac_screen_recording_permission_granted_;
|
||||||
|
const bool accessibility_granted = mac_accessibility_permission_granted_;
|
||||||
|
|
||||||
|
show_request_permission_window_ =
|
||||||
|
!screen_recording_granted || !accessibility_granted;
|
||||||
|
|
||||||
if (!show_request_permission_window_) {
|
if (!show_request_permission_window_) {
|
||||||
return 0;
|
return 0;
|
||||||
@@ -162,8 +235,10 @@ int Render::RequestPermissionWindow() {
|
|||||||
if (accessibility_granted) {
|
if (accessibility_granted) {
|
||||||
DrawToggleSwitch("accessibility_toggle_on", true, false);
|
DrawToggleSwitch("accessibility_toggle_on", true, false);
|
||||||
} else {
|
} else {
|
||||||
if (DrawToggleSwitch("accessibility_toggle", accessibility_granted, !accessibility_granted)) {
|
if (DrawToggleSwitch("accessibility_toggle", false, true)) {
|
||||||
OpenAccessibilityPreferences();
|
OpenAccessibilityPreferences();
|
||||||
|
mac_accessibility_permission_requested_ = true;
|
||||||
|
RefreshMacPermissionStatus(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,12 +253,12 @@ int Render::RequestPermissionWindow() {
|
|||||||
ImGui::AlignTextToFramePadding();
|
ImGui::AlignTextToFramePadding();
|
||||||
ImGui::SetCursorPosX(checkbox_padding);
|
ImGui::SetCursorPosX(checkbox_padding);
|
||||||
if (screen_recording_granted) {
|
if (screen_recording_granted) {
|
||||||
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + 10.0f);
|
|
||||||
DrawToggleSwitch("screen_recording_toggle_on", true, false);
|
DrawToggleSwitch("screen_recording_toggle_on", true, false);
|
||||||
} else {
|
} else {
|
||||||
if (DrawToggleSwitch("screen_recording_toggle", screen_recording_granted,
|
if (DrawToggleSwitch("screen_recording_toggle", false, true)) {
|
||||||
!screen_recording_granted)) {
|
|
||||||
OpenScreenRecordingPreferences();
|
OpenScreenRecordingPreferences();
|
||||||
|
mac_screen_recording_permission_requested_ = true;
|
||||||
|
RefreshMacPermissionStatus(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,4 +277,4 @@ int Render::RequestPermissionWindow() {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -244,6 +244,31 @@ bool ScreenCapturerDxgi::CreateDuplicationForMonitor(int monitor_index) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ScreenCapturerDxgi::RecreateDuplicationForCurrentMonitor() {
|
||||||
|
ReleaseDuplication();
|
||||||
|
int current_monitor = monitor_index_.load();
|
||||||
|
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EnumerateDisplays();
|
||||||
|
if (display_info_list_.empty()) {
|
||||||
|
LOG_ERROR("DXGI: no displays found while recreating duplication");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (current_monitor < 0 ||
|
||||||
|
current_monitor >= static_cast<int>(display_info_list_.size())) {
|
||||||
|
current_monitor = 0;
|
||||||
|
monitor_index_ = 0;
|
||||||
|
}
|
||||||
|
if (CreateDuplicationForMonitor(current_monitor)) {
|
||||||
|
LOG_INFO("DXGI: recreated duplication for monitor {}",
|
||||||
|
monitor_index_.load());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void ScreenCapturerDxgi::ReleaseDuplication() {
|
void ScreenCapturerDxgi::ReleaseDuplication() {
|
||||||
staging_.Reset();
|
staging_.Reset();
|
||||||
if (duplication_) {
|
if (duplication_) {
|
||||||
@@ -254,6 +279,8 @@ void ScreenCapturerDxgi::ReleaseDuplication() {
|
|||||||
|
|
||||||
void ScreenCapturerDxgi::CaptureLoop() {
|
void ScreenCapturerDxgi::CaptureLoop() {
|
||||||
const int timeout_ms = 33;
|
const int timeout_ms = 33;
|
||||||
|
auto last_duplication_retry =
|
||||||
|
std::chrono::steady_clock::now() - std::chrono::milliseconds(1000);
|
||||||
while (running_) {
|
while (running_) {
|
||||||
if (paused_) {
|
if (paused_) {
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
@@ -261,6 +288,11 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!duplication_) {
|
if (!duplication_) {
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (now - last_duplication_retry >= std::chrono::milliseconds(500)) {
|
||||||
|
last_duplication_retry = now;
|
||||||
|
RecreateDuplicationForCurrentMonitor();
|
||||||
|
}
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -274,9 +306,7 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
|||||||
}
|
}
|
||||||
if (FAILED(hr)) {
|
if (FAILED(hr)) {
|
||||||
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
LOG_ERROR("DXGI: AcquireNextFrame failed, hr={}", (int)hr);
|
||||||
// attempt to recreate duplication
|
RecreateDuplicationForCurrentMonitor();
|
||||||
ReleaseDuplication();
|
|
||||||
CreateDuplicationForMonitor(monitor_index_);
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,4 +383,4 @@ void ScreenCapturerDxgi::CaptureLoop() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
|||||||
bool InitializeDxgi();
|
bool InitializeDxgi();
|
||||||
void EnumerateDisplays();
|
void EnumerateDisplays();
|
||||||
bool CreateDuplicationForMonitor(int monitor_index);
|
bool CreateDuplicationForMonitor(int monitor_index);
|
||||||
|
bool RecreateDuplicationForCurrentMonitor();
|
||||||
void CaptureLoop();
|
void CaptureLoop();
|
||||||
void ReleaseDuplication();
|
void ReleaseDuplication();
|
||||||
|
|
||||||
@@ -78,4 +79,4 @@ class ScreenCapturerDxgi : public ScreenCapturer {
|
|||||||
};
|
};
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -100,8 +100,7 @@ bool ScreenCapturerWgc::IsWgcSupported() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
||||||
int error = 0;
|
if (inited_ == true) return 0;
|
||||||
if (inited_ == true) return error;
|
|
||||||
|
|
||||||
// nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2];
|
// nv12_frame_ = new unsigned char[rect.right * rect.bottom * 3 / 2];
|
||||||
// nv12_frame_scaled_ = new unsigned char[1280 * 720 * 3 / 2];
|
// nv12_frame_scaled_ = new unsigned char[1280 * 720 * 3 / 2];
|
||||||
@@ -112,8 +111,18 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
|||||||
|
|
||||||
if (!IsWgcSupported()) {
|
if (!IsWgcSupported()) {
|
||||||
LOG_ERROR("WGC not supported");
|
LOG_ERROR("WGC not supported");
|
||||||
error = 2;
|
return 2;
|
||||||
return error;
|
}
|
||||||
|
|
||||||
|
return RebuildSessions(monitor_index_);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScreenCapturerWgc::RebuildSessions(int preferred_monitor_index) {
|
||||||
|
CleanUp();
|
||||||
|
|
||||||
|
if (!IsWgcSupported()) {
|
||||||
|
LOG_ERROR("WGC not supported");
|
||||||
|
return 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
monitor_ = GetPrimaryMonitor();
|
monitor_ = GetPrimaryMonitor();
|
||||||
@@ -125,6 +134,13 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (preferred_monitor_index < 0 ||
|
||||||
|
preferred_monitor_index >= static_cast<int>(display_info_list_.size())) {
|
||||||
|
preferred_monitor_index = 0;
|
||||||
|
}
|
||||||
|
monitor_index_ = preferred_monitor_index;
|
||||||
|
|
||||||
|
int error = 0;
|
||||||
for (int i = 0; i < display_info_list_.size(); i++) {
|
for (int i = 0; i < display_info_list_.size(); i++) {
|
||||||
const auto& display = display_info_list_[i];
|
const auto& display = display_info_list_[i];
|
||||||
LOG_INFO(
|
LOG_INFO(
|
||||||
@@ -138,20 +154,28 @@ int ScreenCapturerWgc::Init(const int fps, cb_desktop_data cb) {
|
|||||||
sessions_.back().session_->RegisterObserver(this);
|
sessions_.back().session_->RegisterObserver(this);
|
||||||
error = sessions_.back().session_->Initialize((HMONITOR)display.handle);
|
error = sessions_.back().session_->Initialize((HMONITOR)display.handle);
|
||||||
if (error != 0) {
|
if (error != 0) {
|
||||||
|
LOG_ERROR("WGC: initialize session {} failed, ret={}", i, error);
|
||||||
|
CleanUp();
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
sessions_[i].inited_ = true;
|
sessions_[i].inited_ = true;
|
||||||
inited_ = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Default on monitor {}:{}", monitor_index_,
|
LOG_INFO("Default on monitor {}:{}", monitor_index_,
|
||||||
display_info_list_[monitor_index_].name);
|
display_info_list_[monitor_index_].name);
|
||||||
|
|
||||||
initial_monitor_index_ = monitor_index_;
|
if (initial_monitor_index_ < 0 ||
|
||||||
|
initial_monitor_index_ >= static_cast<int>(display_info_list_.size())) {
|
||||||
|
initial_monitor_index_ = monitor_index_;
|
||||||
|
}
|
||||||
|
inited_ = true;
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ScreenCapturerWgc::Destroy() { return 0; }
|
int ScreenCapturerWgc::Destroy() {
|
||||||
|
CleanUp();
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
int ScreenCapturerWgc::Start(bool show_cursor) {
|
int ScreenCapturerWgc::Start(bool show_cursor) {
|
||||||
if (running_ == true) {
|
if (running_ == true) {
|
||||||
@@ -160,13 +184,37 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (inited_ == false) {
|
if (inited_ == false) {
|
||||||
LOG_ERROR("Screen capturer not inited");
|
const int ret = RebuildSessions(monitor_index_);
|
||||||
return 4;
|
if (ret != 0) {
|
||||||
|
LOG_ERROR("Screen capturer not inited");
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int ret = StartSessions(show_cursor);
|
||||||
|
if (ret == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARN("WGC: start failed, rebuilding sessions");
|
||||||
|
ret = RebuildSessions(monitor_index_);
|
||||||
|
if (ret != 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return StartSessions(show_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
int ScreenCapturerWgc::StartSessions(bool show_cursor) {
|
||||||
bool any_started = false;
|
bool any_started = false;
|
||||||
|
bool active_started = false;
|
||||||
int last_error = 0;
|
int last_error = 0;
|
||||||
for (int i = 0; i < sessions_.size(); i++) {
|
int active_monitor = monitor_index_;
|
||||||
|
if (active_monitor < 0 ||
|
||||||
|
active_monitor >= static_cast<int>(sessions_.size())) {
|
||||||
|
active_monitor = 0;
|
||||||
|
monitor_index_ = 0;
|
||||||
|
}
|
||||||
|
for (int i = 0; i < static_cast<int>(sessions_.size()); i++) {
|
||||||
if (sessions_[i].inited_ == false) {
|
if (sessions_[i].inited_ == false) {
|
||||||
LOG_ERROR("Session {} not inited", i);
|
LOG_ERROR("Session {} not inited", i);
|
||||||
continue;
|
continue;
|
||||||
@@ -182,16 +230,27 @@ int ScreenCapturerWgc::Start(bool show_cursor) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (i != 0) {
|
if (i != active_monitor) {
|
||||||
sessions_[i].session_->Pause();
|
sessions_[i].session_->Pause();
|
||||||
sessions_[i].paused_ = true;
|
sessions_[i].paused_ = true;
|
||||||
|
} else {
|
||||||
|
sessions_[i].session_->Resume();
|
||||||
|
sessions_[i].paused_ = false;
|
||||||
}
|
}
|
||||||
sessions_[i].running_ = true;
|
sessions_[i].running_ = true;
|
||||||
any_started = true;
|
any_started = true;
|
||||||
|
if (i == active_monitor) {
|
||||||
|
active_started = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
running_ = running_ || any_started;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
running_ = active_started;
|
||||||
|
if (!active_started) {
|
||||||
|
LOG_ERROR("WGC: active session did not start successfully");
|
||||||
|
Stop();
|
||||||
|
return last_error != 0 ? last_error : -1;
|
||||||
|
}
|
||||||
if (!any_started) {
|
if (!any_started) {
|
||||||
LOG_ERROR("WGC: no session started successfully");
|
LOG_ERROR("WGC: no session started successfully");
|
||||||
return last_error != 0 ? last_error : -1;
|
return last_error != 0 ? last_error : -1;
|
||||||
@@ -349,13 +408,16 @@ void ScreenCapturerWgc::OnFrame(const WgcSession::wgc_session_frame& frame,
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ScreenCapturerWgc::CleanUp() {
|
void ScreenCapturerWgc::CleanUp() {
|
||||||
if (inited_) {
|
running_ = false;
|
||||||
for (auto& session : sessions_) {
|
for (auto& session : sessions_) {
|
||||||
if (session.session_) {
|
if (session.session_) {
|
||||||
session.session_->Stop();
|
session.session_->Stop();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sessions_.clear();
|
|
||||||
}
|
}
|
||||||
|
sessions_.clear();
|
||||||
|
display_info_list_.clear();
|
||||||
|
gs_display_list.clear();
|
||||||
|
monitor_ = nullptr;
|
||||||
|
inited_ = false;
|
||||||
}
|
}
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -40,6 +40,8 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
|||||||
|
|
||||||
protected:
|
protected:
|
||||||
void CleanUp();
|
void CleanUp();
|
||||||
|
int RebuildSessions(int preferred_monitor_index);
|
||||||
|
int StartSessions(bool show_cursor);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
HMONITOR monitor_;
|
HMONITOR monitor_;
|
||||||
@@ -74,4 +76,4 @@ class ScreenCapturerWgc : public ScreenCapturer,
|
|||||||
std::mutex frame_mutex_;
|
std::mutex frame_mutex_;
|
||||||
};
|
};
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -29,11 +29,15 @@ namespace {
|
|||||||
using Json = nlohmann::json;
|
using Json = nlohmann::json;
|
||||||
|
|
||||||
constexpr DWORD kSecureDesktopStatusIntervalMs = 250;
|
constexpr DWORD kSecureDesktopStatusIntervalMs = 250;
|
||||||
constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 150;
|
constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500;
|
||||||
constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120;
|
constexpr DWORD kSecureDesktopHelperPipeTimeoutMs = 120;
|
||||||
constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500;
|
constexpr DWORD kSecureDesktopTransientErrorGraceMs = 1500;
|
||||||
constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000;
|
constexpr DWORD kSecureDesktopTransientErrorLogIntervalMs = 5000;
|
||||||
constexpr int kSecureDesktopCaptureMinIntervalMs = 100;
|
constexpr DWORD kPostSecureDesktopRestartRetryMs = 500;
|
||||||
|
constexpr DWORD kPostSecureDesktopRestartTimeoutMs = 10000;
|
||||||
|
constexpr int kSecureDesktopCaptureMinFps = 30;
|
||||||
|
constexpr int kSecureDesktopCaptureMaxIntervalMs =
|
||||||
|
1000 / kSecureDesktopCaptureMinFps;
|
||||||
|
|
||||||
struct SecureDesktopServiceStatus {
|
struct SecureDesktopServiceStatus {
|
||||||
bool service_available = false;
|
bool service_available = false;
|
||||||
@@ -129,10 +133,28 @@ class WgcPluginCapturer final : public ScreenCapturer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
|
std::string BuildSecureCaptureCommand(int left, int top, int width, int height,
|
||||||
bool show_cursor) {
|
bool show_cursor,
|
||||||
|
const std::string& stage) {
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top
|
stream << kCrossDeskSecureInputCaptureCommandPrefix << left << ":" << top
|
||||||
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0);
|
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0);
|
||||||
|
if (!stage.empty()) {
|
||||||
|
stream << ":" << stage;
|
||||||
|
}
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string BuildSecureCaptureStartCommand(int left, int top, int width,
|
||||||
|
int height, bool show_cursor,
|
||||||
|
int fps,
|
||||||
|
const std::string& stage) {
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << kCrossDeskSecureInputCaptureStartCommandPrefix << left << ":" << top
|
||||||
|
<< ":" << width << ":" << height << ":" << (show_cursor ? 1 : 0)
|
||||||
|
<< ":" << fps;
|
||||||
|
if (!stage.empty()) {
|
||||||
|
stream << ":" << stage;
|
||||||
|
}
|
||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +170,11 @@ bool IsTransientSecureDesktopFrameError(const std::string& error_message) {
|
|||||||
error_message.find("\"error\":\"bitblt_failed\"") != std::string::npos;
|
error_message.find("\"error\":\"bitblt_failed\"") != std::string::npos;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsTransientWindowsServiceStatusError(const std::string& error) {
|
||||||
|
return error == "pipe_unavailable" || error == "pipe_connect_failed" ||
|
||||||
|
error == "pipe_read_failed";
|
||||||
|
}
|
||||||
|
|
||||||
bool ReadPipeMessage(HANDLE pipe, std::vector<uint8_t>* response_out,
|
bool ReadPipeMessage(HANDLE pipe, std::vector<uint8_t>* response_out,
|
||||||
DWORD* error_code_out = nullptr) {
|
DWORD* error_code_out = nullptr) {
|
||||||
if (response_out == nullptr) {
|
if (response_out == nullptr) {
|
||||||
@@ -274,17 +301,15 @@ bool QuerySecureDesktopServiceStatus(SecureDesktopServiceStatus* status) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
bool QuerySecureDesktopHelperCommand(DWORD session_id,
|
||||||
int width, int height, bool show_cursor,
|
const std::string& command,
|
||||||
std::vector<uint8_t>* nv12_frame_out,
|
std::vector<uint8_t>* response_out,
|
||||||
int* captured_width_out,
|
std::string* error_out) {
|
||||||
int* captured_height_out,
|
if (response_out == nullptr) {
|
||||||
std::string* error_out) {
|
|
||||||
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
|
|
||||||
captured_height_out == nullptr) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
response_out->clear();
|
||||||
const std::wstring pipe_name =
|
const std::wstring pipe_name =
|
||||||
GetCrossDeskSecureInputHelperPipeName(session_id);
|
GetCrossDeskSecureInputHelperPipeName(session_id);
|
||||||
if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) {
|
if (!WaitNamedPipeW(pipe_name.c_str(), kSecureDesktopHelperPipeTimeoutMs)) {
|
||||||
@@ -306,8 +331,6 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
|||||||
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
||||||
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
||||||
|
|
||||||
const std::string command =
|
|
||||||
BuildSecureCaptureCommand(left, top, width, height, show_cursor);
|
|
||||||
DWORD bytes_written = 0;
|
DWORD bytes_written = 0;
|
||||||
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
|
if (!WriteFile(pipe, command.data(), static_cast<DWORD>(command.size()),
|
||||||
&bytes_written, nullptr)) {
|
&bytes_written, nullptr)) {
|
||||||
@@ -319,9 +342,8 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<uint8_t> response;
|
|
||||||
DWORD read_error = 0;
|
DWORD read_error = 0;
|
||||||
const bool read_ok = ReadPipeMessage(pipe, &response, &read_error);
|
const bool read_ok = ReadPipeMessage(pipe, response_out, &read_error);
|
||||||
CloseHandle(pipe);
|
CloseHandle(pipe);
|
||||||
if (!read_ok) {
|
if (!read_ok) {
|
||||||
if (error_out != nullptr) {
|
if (error_out != nullptr) {
|
||||||
@@ -330,6 +352,29 @@ bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool QuerySecureDesktopHelperFrame(DWORD session_id, int left, int top,
|
||||||
|
int width, int height, bool show_cursor,
|
||||||
|
const std::string& stage,
|
||||||
|
std::vector<uint8_t>* nv12_frame_out,
|
||||||
|
int* captured_width_out,
|
||||||
|
int* captured_height_out,
|
||||||
|
std::string* error_out) {
|
||||||
|
if (nv12_frame_out == nullptr || captured_width_out == nullptr ||
|
||||||
|
captured_height_out == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string command =
|
||||||
|
BuildSecureCaptureCommand(left, top, width, height, show_cursor, stage);
|
||||||
|
std::vector<uint8_t> response;
|
||||||
|
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||||
|
error_out)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return ParseSecureDesktopFrameResponse(response, nv12_frame_out,
|
return ParseSecureDesktopFrameResponse(response, nv12_frame_out,
|
||||||
captured_width_out,
|
captured_width_out,
|
||||||
captured_height_out, error_out);
|
captured_height_out, error_out);
|
||||||
@@ -349,21 +394,46 @@ int ScreenCapturerWin::Init(const int fps, cb_desktop_data cb) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const char* raw_display_name = display_name ? display_name : "";
|
||||||
std::string mapped_name;
|
std::string mapped_name;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||||
auto it = label_alias_.find(display_name);
|
auto it = label_alias_.find(raw_display_name);
|
||||||
if (it != label_alias_.end())
|
if (it != label_alias_.end())
|
||||||
mapped_name = it->second;
|
mapped_name = it->second;
|
||||||
else
|
else
|
||||||
mapped_name = display_name;
|
mapped_name = raw_display_name;
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(alias_mutex_);
|
std::lock_guard<std::mutex> lock(alias_mutex_);
|
||||||
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
|
if (canonical_labels_.find(mapped_name) == canonical_labels_.end()) {
|
||||||
|
if (post_secure_desktop_waiting_for_frame_.load(
|
||||||
|
std::memory_order_relaxed) &&
|
||||||
|
!post_secure_desktop_drop_logged_.exchange(
|
||||||
|
true, std::memory_order_relaxed)) {
|
||||||
|
LOG_WARN(
|
||||||
|
"Windows capturer dropping post-secure-desktop frame from "
|
||||||
|
"unknown display: display='{}', mapped='{}', size={}x{}, "
|
||||||
|
"bytes={}",
|
||||||
|
raw_display_name, mapped_name, w, h, size);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (post_secure_desktop_waiting_for_frame_.exchange(
|
||||||
|
false, std::memory_order_relaxed)) {
|
||||||
|
const ULONGLONG start_tick =
|
||||||
|
post_secure_desktop_started_tick_.exchange(
|
||||||
|
0, std::memory_order_relaxed);
|
||||||
|
const ULONGLONG elapsed_ms =
|
||||||
|
start_tick == 0 ? 0 : GetTickCount64() - start_tick;
|
||||||
|
post_secure_desktop_drop_logged_.store(false,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
LOG_INFO(
|
||||||
|
"Windows capturer first normal frame after secure desktop: "
|
||||||
|
"display='{}', mapped='{}', size={}x{}, bytes={}, elapsed_ms={}",
|
||||||
|
raw_display_name, mapped_name, w, h, size, elapsed_ms);
|
||||||
|
}
|
||||||
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
|
if (cb_orig_) cb_orig_(data, size, w, h, mapped_name.c_str());
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -481,6 +551,10 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
|||||||
|
|
||||||
running_.store(true, std::memory_order_relaxed);
|
running_.store(true, std::memory_order_relaxed);
|
||||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_waiting_for_frame_.store(false,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||||
if (!secure_capture_thread_.joinable()) {
|
if (!secure_capture_thread_.joinable()) {
|
||||||
secure_capture_thread_ =
|
secure_capture_thread_ =
|
||||||
std::thread([this]() { SecureDesktopCaptureLoop(); });
|
std::thread([this]() { SecureDesktopCaptureLoop(); });
|
||||||
@@ -491,11 +565,16 @@ int ScreenCapturerWin::Start(bool show_cursor) {
|
|||||||
int ScreenCapturerWin::Stop() {
|
int ScreenCapturerWin::Stop() {
|
||||||
running_.store(false, std::memory_order_relaxed);
|
running_.store(false, std::memory_order_relaxed);
|
||||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_waiting_for_frame_.store(false,
|
||||||
|
std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_drop_logged_.store(false, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_started_tick_.store(0, std::memory_order_relaxed);
|
||||||
int ret = 0;
|
int ret = 0;
|
||||||
if (impl_) {
|
if (impl_) {
|
||||||
ret = impl_->Stop();
|
ret = impl_->Stop();
|
||||||
}
|
}
|
||||||
StopSecureCaptureThread();
|
StopSecureCaptureThread();
|
||||||
|
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,6 +661,93 @@ void ScreenCapturerWin::StopSecureCaptureThread() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ScreenCapturerWin::RestartCaptureBackendAfterSecureDesktop() {
|
||||||
|
if (!impl_ || !running_.load(std::memory_order_relaxed)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||||
|
const int current_monitor = monitor_index_.load(std::memory_order_relaxed);
|
||||||
|
auto restore_monitor = [&]() {
|
||||||
|
RebuildAliasesFromImpl();
|
||||||
|
if (current_monitor > 0 && impl_->SwitchTo(current_monitor) != 0) {
|
||||||
|
monitor_index_.store(0, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
auto try_started_backend = [&](std::unique_ptr<ScreenCapturer> cand,
|
||||||
|
const char* name,
|
||||||
|
bool is_wgc_plugin) -> bool {
|
||||||
|
if (!cand) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int init_ret = cand->Init(fps_, cb_);
|
||||||
|
if (init_ret != 0) {
|
||||||
|
LOG_WARN("Windows capturer: {} init after secure desktop failed (ret={})",
|
||||||
|
name, init_ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const int start_ret = cand->Start(show_cursor);
|
||||||
|
if (start_ret != 0) {
|
||||||
|
LOG_WARN(
|
||||||
|
"Windows capturer: {} start after secure desktop failed (ret={})",
|
||||||
|
name, start_ret);
|
||||||
|
cand->Destroy();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (impl_) {
|
||||||
|
impl_->Destroy();
|
||||||
|
}
|
||||||
|
impl_ = std::move(cand);
|
||||||
|
impl_is_wgc_plugin_ = is_wgc_plugin;
|
||||||
|
restore_monitor();
|
||||||
|
LOG_INFO("Windows capturer: restarted {} after secure desktop", name);
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
LOG_INFO("Windows capturer: restarting capture backend after secure desktop");
|
||||||
|
impl_->Stop();
|
||||||
|
int ret = impl_->Start(show_cursor);
|
||||||
|
if (ret == 0) {
|
||||||
|
restore_monitor();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARN(
|
||||||
|
"Windows capturer: capture backend restart after secure desktop failed "
|
||||||
|
"(ret={}), rebuilding backend",
|
||||||
|
ret);
|
||||||
|
impl_->Destroy();
|
||||||
|
ret = impl_->Init(fps_, cb_);
|
||||||
|
if (ret == 0) {
|
||||||
|
ret = impl_->Start(show_cursor);
|
||||||
|
}
|
||||||
|
if (ret == 0) {
|
||||||
|
restore_monitor();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impl_is_wgc_plugin_ &&
|
||||||
|
try_started_backend(WgcPluginCapturer::Create(), "WGC plugin", true)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (try_started_backend(std::make_unique<ScreenCapturerDxgi>(), "DXGI",
|
||||||
|
false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (try_started_backend(std::make_unique<ScreenCapturerGdi>(), "GDI",
|
||||||
|
false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (impl_) {
|
||||||
|
LOG_WARN(
|
||||||
|
"Windows capturer: all backend restart attempts after secure desktop "
|
||||||
|
"failed (last_ret={})",
|
||||||
|
ret);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
|
bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
|
||||||
int* height,
|
int* height,
|
||||||
std::string* display_name) {
|
std::string* display_name) {
|
||||||
@@ -616,10 +782,239 @@ bool ScreenCapturerWin::GetCurrentCaptureRegion(int* left, int* top, int* width,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ScreenCapturerWin::CloseSecureDesktopSharedFrame() {
|
||||||
|
if (secure_frame_view_ != nullptr) {
|
||||||
|
UnmapViewOfFile(secure_frame_view_);
|
||||||
|
secure_frame_view_ = nullptr;
|
||||||
|
}
|
||||||
|
if (secure_frame_ready_event_ != nullptr) {
|
||||||
|
CloseHandle(secure_frame_ready_event_);
|
||||||
|
secure_frame_ready_event_ = nullptr;
|
||||||
|
}
|
||||||
|
if (secure_frame_mapping_ != nullptr) {
|
||||||
|
CloseHandle(secure_frame_mapping_);
|
||||||
|
secure_frame_mapping_ = nullptr;
|
||||||
|
}
|
||||||
|
secure_frame_view_size_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ScreenCapturerWin::StopSecureDesktopSharedCapture(DWORD session_id) {
|
||||||
|
DWORD target_session_id = session_id;
|
||||||
|
if (target_session_id == 0xFFFFFFFF) {
|
||||||
|
target_session_id = secure_shared_session_id_;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secure_shared_capture_started_ &&
|
||||||
|
target_session_id != 0xFFFFFFFF) {
|
||||||
|
std::vector<uint8_t> response;
|
||||||
|
std::string error_message;
|
||||||
|
QuerySecureDesktopHelperCommand(
|
||||||
|
target_session_id, kCrossDeskSecureInputCaptureStopCommand, &response,
|
||||||
|
&error_message);
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseSecureDesktopSharedFrame();
|
||||||
|
secure_shared_capture_started_ = false;
|
||||||
|
secure_shared_session_id_ = 0xFFFFFFFF;
|
||||||
|
secure_shared_left_ = 0;
|
||||||
|
secure_shared_top_ = 0;
|
||||||
|
secure_shared_width_ = 0;
|
||||||
|
secure_shared_height_ = 0;
|
||||||
|
secure_shared_fps_ = 0;
|
||||||
|
secure_shared_show_cursor_ = true;
|
||||||
|
secure_shared_stage_.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenCapturerWin::OpenSecureDesktopSharedFrame(DWORD session_id,
|
||||||
|
size_t min_size,
|
||||||
|
std::string* error_out) {
|
||||||
|
if (secure_frame_view_ != nullptr &&
|
||||||
|
secure_shared_session_id_ == session_id &&
|
||||||
|
secure_frame_view_size_ >= min_size) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
CloseSecureDesktopSharedFrame();
|
||||||
|
|
||||||
|
const std::wstring mapping_name =
|
||||||
|
GetCrossDeskSecureDesktopFrameMappingName(session_id);
|
||||||
|
HANDLE frame_mapping =
|
||||||
|
OpenFileMappingW(FILE_MAP_READ, FALSE, mapping_name.c_str());
|
||||||
|
if (frame_mapping == nullptr) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "open_frame_mapping_failed:" +
|
||||||
|
std::to_string(GetLastError());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* frame_view =
|
||||||
|
static_cast<uint8_t*>(MapViewOfFile(frame_mapping, FILE_MAP_READ, 0, 0, 0));
|
||||||
|
if (frame_view == nullptr) {
|
||||||
|
const DWORD error = GetLastError();
|
||||||
|
CloseHandle(frame_mapping);
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "map_frame_view_failed:" + std::to_string(error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::wstring event_name =
|
||||||
|
GetCrossDeskSecureDesktopFrameReadyEventName(session_id);
|
||||||
|
HANDLE frame_ready_event =
|
||||||
|
OpenEventW(SYNCHRONIZE, FALSE, event_name.c_str());
|
||||||
|
if (frame_ready_event == nullptr) {
|
||||||
|
const DWORD error = GetLastError();
|
||||||
|
UnmapViewOfFile(frame_view);
|
||||||
|
CloseHandle(frame_mapping);
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "open_frame_event_failed:" + std::to_string(error);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
secure_frame_mapping_ = frame_mapping;
|
||||||
|
secure_frame_ready_event_ = frame_ready_event;
|
||||||
|
secure_frame_view_ = frame_view;
|
||||||
|
secure_frame_view_size_ = min_size;
|
||||||
|
secure_shared_session_id_ = session_id;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenCapturerWin::ReadSecureDesktopSharedFrame(
|
||||||
|
DWORD wait_ms, std::vector<uint8_t>* nv12_frame_out, int* width_out,
|
||||||
|
int* height_out, std::string* error_out) {
|
||||||
|
if (nv12_frame_out == nullptr || width_out == nullptr ||
|
||||||
|
height_out == nullptr || secure_frame_view_ == nullptr ||
|
||||||
|
secure_frame_ready_event_ == nullptr) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DWORD wait_result = WaitForSingleObject(secure_frame_ready_event_,
|
||||||
|
wait_ms);
|
||||||
|
if (wait_result == WAIT_TIMEOUT) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "frame_wait_timeout";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (wait_result != WAIT_OBJECT_0) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "frame_wait_failed:" + std::to_string(GetLastError());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto* header =
|
||||||
|
reinterpret_cast<CrossDeskSecureDesktopSharedFrameHeader*>(
|
||||||
|
secure_frame_view_);
|
||||||
|
if (header->magic != kCrossDeskSecureDesktopFrameMagic ||
|
||||||
|
header->version != kCrossDeskSecureDesktopFrameVersion) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "invalid_shared_frame_header";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (header->writing != 0) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "shared_frame_write_in_progress";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const uint32_t sequence = header->sequence;
|
||||||
|
const uint32_t payload_size = header->payload_size;
|
||||||
|
const uint32_t buffer_size = header->buffer_size;
|
||||||
|
if (payload_size == 0 || payload_size > buffer_size ||
|
||||||
|
sizeof(*header) + static_cast<size_t>(payload_size) >
|
||||||
|
secure_frame_view_size_) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "invalid_shared_frame_size";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
nv12_frame_out->resize(payload_size);
|
||||||
|
std::memcpy(nv12_frame_out->data(), secure_frame_view_ + sizeof(*header),
|
||||||
|
payload_size);
|
||||||
|
MemoryBarrier();
|
||||||
|
if (header->writing != 0 || header->sequence != sequence) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "shared_frame_changed_during_read";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
*width_out = static_cast<int>(header->width);
|
||||||
|
*height_out = static_cast<int>(header->height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ScreenCapturerWin::StartSecureDesktopSharedCapture(
|
||||||
|
DWORD session_id, int left, int top, int width, int height,
|
||||||
|
const std::string& stage, bool show_cursor, int fps,
|
||||||
|
std::string* error_out) {
|
||||||
|
const size_t payload_size = static_cast<size_t>(width) * height * 3 / 2;
|
||||||
|
const size_t mapping_size =
|
||||||
|
sizeof(CrossDeskSecureDesktopSharedFrameHeader) + payload_size;
|
||||||
|
if (payload_size == 0) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = "invalid_capture_size";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (secure_shared_capture_started_ &&
|
||||||
|
secure_shared_session_id_ == session_id &&
|
||||||
|
secure_shared_left_ == left && secure_shared_top_ == top &&
|
||||||
|
secure_shared_width_ == width && secure_shared_height_ == height &&
|
||||||
|
secure_shared_stage_ == stage &&
|
||||||
|
secure_shared_show_cursor_ == show_cursor && secure_shared_fps_ == fps &&
|
||||||
|
OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||||
|
|
||||||
|
const std::string command =
|
||||||
|
BuildSecureCaptureStartCommand(left, top, width, height, show_cursor, fps,
|
||||||
|
stage);
|
||||||
|
std::vector<uint8_t> response;
|
||||||
|
if (!QuerySecureDesktopHelperCommand(session_id, command, &response,
|
||||||
|
error_out)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Json json = Json::parse(response.begin(), response.end(), nullptr, false);
|
||||||
|
if (json.is_discarded() || !json.value("ok", false)) {
|
||||||
|
if (error_out != nullptr) {
|
||||||
|
*error_out = ExtractPipeTextResponse(response);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
secure_shared_capture_started_ = true;
|
||||||
|
secure_shared_session_id_ = session_id;
|
||||||
|
secure_shared_left_ = left;
|
||||||
|
secure_shared_top_ = top;
|
||||||
|
secure_shared_width_ = width;
|
||||||
|
secure_shared_height_ = height;
|
||||||
|
secure_shared_show_cursor_ = show_cursor;
|
||||||
|
secure_shared_fps_ = fps;
|
||||||
|
secure_shared_stage_ = stage;
|
||||||
|
|
||||||
|
if (!OpenSecureDesktopSharedFrame(session_id, mapping_size, error_out)) {
|
||||||
|
StopSecureDesktopSharedCapture(session_id);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
||||||
const int frame_interval_ms =
|
const int frame_interval_ms =
|
||||||
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinIntervalMs, 1000 / fps_)
|
fps_ > 0 ? (std::min)(kSecureDesktopCaptureMaxIntervalMs, 1000 / fps_)
|
||||||
: kSecureDesktopCaptureMinIntervalMs;
|
: kSecureDesktopCaptureMaxIntervalMs;
|
||||||
ULONGLONG last_status_tick = 0;
|
ULONGLONG last_status_tick = 0;
|
||||||
ULONGLONG last_error_tick = 0;
|
ULONGLONG last_error_tick = 0;
|
||||||
bool last_capture_active = false;
|
bool last_capture_active = false;
|
||||||
@@ -627,6 +1022,9 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
std::string last_stage;
|
std::string last_stage;
|
||||||
std::string last_service_error;
|
std::string last_service_error;
|
||||||
ULONGLONG capture_stage_started_tick = 0;
|
ULONGLONG capture_stage_started_tick = 0;
|
||||||
|
bool post_secure_restart_pending = false;
|
||||||
|
ULONGLONG post_secure_restart_deadline_tick = 0;
|
||||||
|
ULONGLONG last_post_secure_restart_tick = 0;
|
||||||
SecureDesktopServiceStatus status;
|
SecureDesktopServiceStatus status;
|
||||||
std::vector<uint8_t> secure_frame;
|
std::vector<uint8_t> secure_frame;
|
||||||
|
|
||||||
@@ -653,6 +1051,11 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
"Windows capturer secure desktop service available, polling "
|
"Windows capturer secure desktop service available, polling "
|
||||||
"session_id={}",
|
"session_id={}",
|
||||||
status.active_session_id);
|
status.active_session_id);
|
||||||
|
} else if (IsTransientWindowsServiceStatusError(status.error)) {
|
||||||
|
LOG_INFO(
|
||||||
|
"Windows capturer secure desktop service temporarily unavailable: "
|
||||||
|
"error={}, code={}",
|
||||||
|
status.error, status.error_code);
|
||||||
} else {
|
} else {
|
||||||
LOG_WARN(
|
LOG_WARN(
|
||||||
"Windows capturer secure desktop service unavailable: "
|
"Windows capturer secure desktop service unavailable: "
|
||||||
@@ -673,6 +1076,10 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
std::memory_order_relaxed);
|
std::memory_order_relaxed);
|
||||||
if (status.capture_active != last_capture_active ||
|
if (status.capture_active != last_capture_active ||
|
||||||
status.interactive_stage != last_stage) {
|
status.interactive_stage != last_stage) {
|
||||||
|
const bool secure_capture_started =
|
||||||
|
!last_capture_active && status.capture_active;
|
||||||
|
const bool secure_capture_ended =
|
||||||
|
last_capture_active && !status.capture_active;
|
||||||
capture_stage_started_tick = now;
|
capture_stage_started_tick = now;
|
||||||
LOG_INFO(
|
LOG_INFO(
|
||||||
"Windows capturer secure desktop state: active={}, stage='{}', "
|
"Windows capturer secure desktop state: active={}, stage='{}', "
|
||||||
@@ -681,17 +1088,53 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
status.active_session_id);
|
status.active_session_id);
|
||||||
last_capture_active = status.capture_active;
|
last_capture_active = status.capture_active;
|
||||||
last_stage = status.interactive_stage;
|
last_stage = status.interactive_stage;
|
||||||
|
if (secure_capture_started) {
|
||||||
|
post_secure_restart_pending = false;
|
||||||
|
post_secure_desktop_waiting_for_frame_.store(
|
||||||
|
false, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_drop_logged_.store(
|
||||||
|
false, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_started_tick_.store(
|
||||||
|
0, std::memory_order_relaxed);
|
||||||
|
} else if (secure_capture_ended) {
|
||||||
|
post_secure_restart_pending = true;
|
||||||
|
post_secure_restart_deadline_tick =
|
||||||
|
now + kPostSecureDesktopRestartTimeoutMs;
|
||||||
|
last_post_secure_restart_tick = 0;
|
||||||
|
post_secure_desktop_waiting_for_frame_.store(
|
||||||
|
true, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_drop_logged_.store(
|
||||||
|
false, std::memory_order_relaxed);
|
||||||
|
post_secure_desktop_started_tick_.store(
|
||||||
|
now, std::memory_order_relaxed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
last_status_tick = now;
|
last_status_tick = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
if (!status.capture_active || status.active_session_id == 0xFFFFFFFF) {
|
||||||
|
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||||
|
if (post_secure_restart_pending) {
|
||||||
|
if (now >= post_secure_restart_deadline_tick) {
|
||||||
|
LOG_WARN(
|
||||||
|
"Windows capturer: capture backend restart after secure desktop "
|
||||||
|
"timed out");
|
||||||
|
post_secure_restart_pending = false;
|
||||||
|
} else if (last_post_secure_restart_tick == 0 ||
|
||||||
|
now - last_post_secure_restart_tick >=
|
||||||
|
kPostSecureDesktopRestartRetryMs) {
|
||||||
|
last_post_secure_restart_tick = now;
|
||||||
|
post_secure_restart_pending =
|
||||||
|
!RestartCaptureBackendAfterSecureDesktop();
|
||||||
|
}
|
||||||
|
}
|
||||||
std::this_thread::sleep_for(
|
std::this_thread::sleep_for(
|
||||||
std::chrono::milliseconds(status.service_available ? 50 : 200));
|
std::chrono::milliseconds(status.service_available ? 50 : 200));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!status.helper_running) {
|
if (!status.helper_running) {
|
||||||
|
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
std::this_thread::sleep_for(std::chrono::milliseconds(30));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -702,6 +1145,7 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
int height = 0;
|
int height = 0;
|
||||||
std::string display_name;
|
std::string display_name;
|
||||||
if (!GetCurrentCaptureRegion(&left, &top, &width, &height, &display_name)) {
|
if (!GetCurrentCaptureRegion(&left, &top, &width, &height, &display_name)) {
|
||||||
|
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -709,15 +1153,40 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
int captured_width = 0;
|
int captured_width = 0;
|
||||||
int captured_height = 0;
|
int captured_height = 0;
|
||||||
std::string error_message;
|
std::string error_message;
|
||||||
if (QuerySecureDesktopHelperFrame(
|
bool frame_delivered = false;
|
||||||
status.active_session_id, left, top, width, height,
|
const bool show_cursor = show_cursor_.load(std::memory_order_relaxed);
|
||||||
show_cursor_.load(std::memory_order_relaxed), &secure_frame,
|
const int shared_fps =
|
||||||
|
fps_ > 0 ? (std::max)(kSecureDesktopCaptureMinFps, fps_)
|
||||||
|
: kSecureDesktopCaptureMinFps;
|
||||||
|
|
||||||
|
if (StartSecureDesktopSharedCapture(status.active_session_id, left, top,
|
||||||
|
width, height,
|
||||||
|
status.interactive_stage, show_cursor,
|
||||||
|
shared_fps, &error_message) &&
|
||||||
|
ReadSecureDesktopSharedFrame(
|
||||||
|
static_cast<DWORD>(frame_interval_ms + 20), &secure_frame,
|
||||||
&captured_width, &captured_height, &error_message)) {
|
&captured_width, &captured_height, &error_message)) {
|
||||||
if (cb_orig_ && !secure_frame.empty()) {
|
if (cb_orig_ && !secure_frame.empty()) {
|
||||||
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||||
captured_width, captured_height, display_name.c_str());
|
captured_width, captured_height, display_name.c_str());
|
||||||
}
|
}
|
||||||
} else {
|
frame_delivered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frame_delivered &&
|
||||||
|
QuerySecureDesktopHelperFrame(status.active_session_id, left, top,
|
||||||
|
width, height, show_cursor,
|
||||||
|
status.interactive_stage,
|
||||||
|
&secure_frame, &captured_width,
|
||||||
|
&captured_height, &error_message)) {
|
||||||
|
if (cb_orig_ && !secure_frame.empty()) {
|
||||||
|
cb_orig_(secure_frame.data(), static_cast<int>(secure_frame.size()),
|
||||||
|
captured_width, captured_height, display_name.c_str());
|
||||||
|
}
|
||||||
|
frame_delivered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!frame_delivered) {
|
||||||
const bool transient_error =
|
const bool transient_error =
|
||||||
IsTransientSecureDesktopFrameError(error_message);
|
IsTransientSecureDesktopFrameError(error_message);
|
||||||
const bool in_grace_period = capture_stage_started_tick != 0 &&
|
const bool in_grace_period = capture_stage_started_tick != 0 &&
|
||||||
@@ -731,10 +1200,19 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (now - last_error_tick >= log_interval) {
|
if (now - last_error_tick >= log_interval) {
|
||||||
LOG_WARN(
|
if (transient_error) {
|
||||||
"Windows capturer secure desktop frame query failed, stage='{}', "
|
LOG_INFO(
|
||||||
"session_id={}, error={}",
|
"Windows capturer secure desktop transient frame query failed, "
|
||||||
status.interactive_stage, status.active_session_id, error_message);
|
"stage='{}', session_id={}, error={}",
|
||||||
|
status.interactive_stage, status.active_session_id,
|
||||||
|
error_message);
|
||||||
|
} else {
|
||||||
|
LOG_WARN(
|
||||||
|
"Windows capturer secure desktop frame query failed, stage='{}', "
|
||||||
|
"session_id={}, error={}",
|
||||||
|
status.interactive_stage, status.active_session_id,
|
||||||
|
error_message);
|
||||||
|
}
|
||||||
last_error_tick = now;
|
last_error_tick = now;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -742,7 +1220,8 @@ void ScreenCapturerWin::SecureDesktopCaptureLoop() {
|
|||||||
std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms));
|
std::this_thread::sleep_for(std::chrono::milliseconds(frame_interval_ms));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
StopSecureDesktopSharedCapture(secure_shared_session_id_);
|
||||||
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
secure_desktop_capture_active_.store(false, std::memory_order_relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
#include <cstdint>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
@@ -58,14 +59,44 @@ class ScreenCapturerWin : public ScreenCapturer {
|
|||||||
std::atomic<int> monitor_index_{0};
|
std::atomic<int> monitor_index_{0};
|
||||||
int initial_monitor_index_ = 0;
|
int initial_monitor_index_ = 0;
|
||||||
std::atomic<bool> secure_desktop_capture_active_{false};
|
std::atomic<bool> secure_desktop_capture_active_{false};
|
||||||
|
std::atomic<bool> post_secure_desktop_waiting_for_frame_{false};
|
||||||
|
std::atomic<bool> post_secure_desktop_drop_logged_{false};
|
||||||
|
std::atomic<ULONGLONG> post_secure_desktop_started_tick_{0};
|
||||||
std::thread secure_capture_thread_;
|
std::thread secure_capture_thread_;
|
||||||
|
HANDLE secure_frame_mapping_ = nullptr;
|
||||||
|
HANDLE secure_frame_ready_event_ = nullptr;
|
||||||
|
uint8_t* secure_frame_view_ = nullptr;
|
||||||
|
size_t secure_frame_view_size_ = 0;
|
||||||
|
DWORD secure_shared_session_id_ = 0xFFFFFFFF;
|
||||||
|
int secure_shared_left_ = 0;
|
||||||
|
int secure_shared_top_ = 0;
|
||||||
|
int secure_shared_width_ = 0;
|
||||||
|
int secure_shared_height_ = 0;
|
||||||
|
int secure_shared_fps_ = 0;
|
||||||
|
bool secure_shared_show_cursor_ = true;
|
||||||
|
std::string secure_shared_stage_;
|
||||||
|
bool secure_shared_capture_started_ = false;
|
||||||
|
|
||||||
void BuildCanonicalFromImpl();
|
void BuildCanonicalFromImpl();
|
||||||
void RebuildAliasesFromImpl();
|
void RebuildAliasesFromImpl();
|
||||||
void StopSecureCaptureThread();
|
void StopSecureCaptureThread();
|
||||||
|
bool RestartCaptureBackendAfterSecureDesktop();
|
||||||
void SecureDesktopCaptureLoop();
|
void SecureDesktopCaptureLoop();
|
||||||
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
|
bool GetCurrentCaptureRegion(int* left, int* top, int* width, int* height,
|
||||||
std::string* display_name);
|
std::string* display_name);
|
||||||
|
bool StartSecureDesktopSharedCapture(DWORD session_id, int left, int top,
|
||||||
|
int width, int height,
|
||||||
|
const std::string& stage,
|
||||||
|
bool show_cursor, int fps,
|
||||||
|
std::string* error_out);
|
||||||
|
void StopSecureDesktopSharedCapture(DWORD session_id);
|
||||||
|
bool OpenSecureDesktopSharedFrame(DWORD session_id, size_t min_size,
|
||||||
|
std::string* error_out);
|
||||||
|
bool ReadSecureDesktopSharedFrame(DWORD wait_ms,
|
||||||
|
std::vector<uint8_t>* nv12_frame_out,
|
||||||
|
int* width_out, int* height_out,
|
||||||
|
std::string* error_out);
|
||||||
|
void CloseSecureDesktopSharedFrame();
|
||||||
};
|
};
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -2,23 +2,10 @@
|
|||||||
|
|
||||||
#include <Windows.Graphics.Capture.Interop.h>
|
#include <Windows.Graphics.Capture.Interop.h>
|
||||||
|
|
||||||
#include <atomic>
|
#include <string>
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
#include "rd_log.h"
|
#include "rd_log.h"
|
||||||
|
|
||||||
#define CHECK_INIT \
|
|
||||||
if (!is_initialized_) { \
|
|
||||||
LOG_ERROR("AE_NEED_INIT"); \
|
|
||||||
return 4; \
|
|
||||||
}
|
|
||||||
|
|
||||||
#define CHECK_CLOSED \
|
|
||||||
if (cleaned_.load() == true) { \
|
|
||||||
throw winrt::hresult_error(RO_E_CLOSED); \
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace crossdesk {
|
namespace crossdesk {
|
||||||
|
|
||||||
extern "C" {
|
extern "C" {
|
||||||
@@ -40,7 +27,7 @@ int WgcSessionImpl::Initialize(HWND hwnd) {
|
|||||||
|
|
||||||
target_.hwnd = hwnd;
|
target_.hwnd = hwnd;
|
||||||
target_.is_window = true;
|
target_.is_window = true;
|
||||||
return Initialize();
|
return InitializeLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
|
int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
|
||||||
@@ -48,7 +35,7 @@ int WgcSessionImpl::Initialize(HMONITOR hmonitor) {
|
|||||||
|
|
||||||
target_.hmonitor = hmonitor;
|
target_.hmonitor = hmonitor;
|
||||||
target_.is_window = false;
|
target_.is_window = false;
|
||||||
return Initialize();
|
return InitializeLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
|
void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
|
||||||
@@ -59,68 +46,13 @@ void WgcSessionImpl::RegisterObserver(wgc_session_observer* observer) {
|
|||||||
int WgcSessionImpl::Start(bool show_cursor) {
|
int WgcSessionImpl::Start(bool show_cursor) {
|
||||||
std::lock_guard locker(lock_);
|
std::lock_guard locker(lock_);
|
||||||
|
|
||||||
if (is_running_) return 0;
|
return StartLocked(show_cursor);
|
||||||
|
|
||||||
int error = 1;
|
|
||||||
|
|
||||||
CHECK_INIT;
|
|
||||||
try {
|
|
||||||
last_show_cursor_ = show_cursor;
|
|
||||||
if (!capture_session_) {
|
|
||||||
auto current_size = capture_item_.Size();
|
|
||||||
capture_framepool_ =
|
|
||||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
|
||||||
CreateFreeThreaded(d3d11_direct_device_,
|
|
||||||
winrt::Windows::Graphics::DirectX::
|
|
||||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
|
||||||
2, current_size);
|
|
||||||
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
|
|
||||||
capture_frame_size_ = current_size;
|
|
||||||
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
|
|
||||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
|
|
||||||
capture_close_trigger_ = capture_item_.Closed(
|
|
||||||
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!capture_framepool_) throw std::exception();
|
|
||||||
|
|
||||||
is_running_ = true;
|
|
||||||
|
|
||||||
// we do not need to crate a thread to enter a message loop coz we use
|
|
||||||
// CreateFreeThreaded instead of Create to create a capture frame pool,
|
|
||||||
// we need to test the performance later
|
|
||||||
// loop_ = std::thread(std::bind(&WgcSessionImpl::message_func, this));
|
|
||||||
|
|
||||||
capture_session_.IsCursorCaptureEnabled(show_cursor);
|
|
||||||
capture_session_.StartCapture();
|
|
||||||
|
|
||||||
error = 0;
|
|
||||||
} catch (winrt::hresult_error) {
|
|
||||||
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
|
||||||
return 86;
|
|
||||||
} catch (...) {
|
|
||||||
return 86;
|
|
||||||
}
|
|
||||||
|
|
||||||
return error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int WgcSessionImpl::Stop() {
|
int WgcSessionImpl::Stop() {
|
||||||
std::lock_guard locker(lock_);
|
std::lock_guard locker(lock_);
|
||||||
|
|
||||||
CHECK_INIT;
|
CleanUpLocked();
|
||||||
|
|
||||||
is_running_ = false;
|
|
||||||
|
|
||||||
// if (loop_.joinable()) loop_.join();
|
|
||||||
|
|
||||||
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
|
|
||||||
|
|
||||||
if (capture_session_) {
|
|
||||||
capture_session_.Close();
|
|
||||||
capture_session_ = nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,7 +61,10 @@ int WgcSessionImpl::Pause() {
|
|||||||
|
|
||||||
is_paused_ = true;
|
is_paused_ = true;
|
||||||
|
|
||||||
CHECK_INIT;
|
if (!is_initialized_) {
|
||||||
|
LOG_ERROR("AE_NEED_INIT");
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +73,10 @@ int WgcSessionImpl::Resume() {
|
|||||||
|
|
||||||
is_paused_ = false;
|
is_paused_ = false;
|
||||||
|
|
||||||
CHECK_INIT;
|
if (!is_initialized_) {
|
||||||
|
LOG_ERROR("AE_NEED_INIT");
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,10 +113,10 @@ auto WgcSessionImpl::CreateCaptureItemForWindow(HWND hwnd) {
|
|||||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
||||||
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
||||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
||||||
interop_factory->CreateForWindow(
|
winrt::check_hresult(interop_factory->CreateForWindow(
|
||||||
hwnd,
|
hwnd,
|
||||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||||
reinterpret_cast<void**>(winrt::put_abi(item)));
|
reinterpret_cast<void**>(winrt::put_abi(item))));
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,10 +125,10 @@ auto WgcSessionImpl::CreateCaptureItemForMonitor(HMONITOR hmonitor) {
|
|||||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
|
||||||
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
|
||||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
winrt::Windows::Graphics::Capture::GraphicsCaptureItem item = {nullptr};
|
||||||
interop_factory->CreateForMonitor(
|
winrt::check_hresult(interop_factory->CreateForMonitor(
|
||||||
hmonitor,
|
hmonitor,
|
||||||
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
|
||||||
reinterpret_cast<void**>(winrt::put_abi(item)));
|
reinterpret_cast<void**>(winrt::put_abi(item))));
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,6 +156,104 @@ HRESULT WgcSessionImpl::CreateMappedTexture(
|
|||||||
d3d11_texture_mapped_.put());
|
d3d11_texture_mapped_.put());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int WgcSessionImpl::StartCaptureLocked(bool show_cursor) {
|
||||||
|
if (!is_initialized_) {
|
||||||
|
LOG_ERROR("AE_NEED_INIT");
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (!capture_item_) {
|
||||||
|
LOG_ERROR("WGC: capture item is null");
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!capture_session_) {
|
||||||
|
auto current_size = capture_item_.Size();
|
||||||
|
capture_framepool_ =
|
||||||
|
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||||
|
CreateFreeThreaded(d3d11_direct_device_,
|
||||||
|
winrt::Windows::Graphics::DirectX::
|
||||||
|
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||||
|
2, current_size);
|
||||||
|
capture_session_ = capture_framepool_.CreateCaptureSession(capture_item_);
|
||||||
|
capture_frame_size_ = current_size;
|
||||||
|
capture_framepool_trigger_ = capture_framepool_.FrameArrived(
|
||||||
|
winrt::auto_revoke, {this, &WgcSessionImpl::OnFrame});
|
||||||
|
capture_close_trigger_ = capture_item_.Closed(
|
||||||
|
winrt::auto_revoke, {this, &WgcSessionImpl::OnClosed});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!capture_framepool_ || !capture_session_) {
|
||||||
|
throw std::exception();
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_session_.IsCursorCaptureEnabled(show_cursor);
|
||||||
|
capture_session_.StartCapture();
|
||||||
|
is_running_ = true;
|
||||||
|
return 0;
|
||||||
|
} catch (const winrt::hresult_error&) {
|
||||||
|
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||||
|
return 86;
|
||||||
|
} catch (...) {
|
||||||
|
LOG_ERROR("AE_WGC_CREATE_CAPTURER_FAILED");
|
||||||
|
return 86;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int WgcSessionImpl::StartLocked(bool show_cursor) {
|
||||||
|
if (is_running_) return 0;
|
||||||
|
|
||||||
|
last_show_cursor_ = show_cursor;
|
||||||
|
if (!is_initialized_) {
|
||||||
|
const int init_ret = InitializeLocked();
|
||||||
|
if (init_ret != 0) {
|
||||||
|
return init_ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int ret = StartCaptureLocked(show_cursor);
|
||||||
|
if (ret == 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOG_WARN("WGC: start capture failed, rebuilding capture item");
|
||||||
|
CleanUpLocked();
|
||||||
|
ret = InitializeLocked();
|
||||||
|
if (ret != 0) {
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return StartCaptureLocked(show_cursor);
|
||||||
|
}
|
||||||
|
|
||||||
|
void WgcSessionImpl::StopLocked() {
|
||||||
|
is_running_ = false;
|
||||||
|
|
||||||
|
// if (loop_.joinable()) loop_.join();
|
||||||
|
|
||||||
|
if (capture_framepool_trigger_) capture_framepool_trigger_.revoke();
|
||||||
|
if (capture_close_trigger_) capture_close_trigger_.revoke();
|
||||||
|
|
||||||
|
if (capture_session_) {
|
||||||
|
try {
|
||||||
|
capture_session_.Close();
|
||||||
|
} catch (...) {
|
||||||
|
LOG_WARN("WGC: capture session close failed");
|
||||||
|
}
|
||||||
|
capture_session_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (capture_framepool_) {
|
||||||
|
try {
|
||||||
|
capture_framepool_.Close();
|
||||||
|
} catch (...) {
|
||||||
|
LOG_WARN("WGC: frame pool close failed");
|
||||||
|
}
|
||||||
|
capture_framepool_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
d3d11_texture_mapped_ = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void WgcSessionImpl::OnFrame(
|
void WgcSessionImpl::OnFrame(
|
||||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
|
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool const& sender,
|
||||||
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
|
[[maybe_unused]] winrt::Windows::Foundation::IInspectable const& args) {
|
||||||
@@ -225,7 +261,7 @@ void WgcSessionImpl::OnFrame(
|
|||||||
|
|
||||||
auto is_new_size = false;
|
auto is_new_size = false;
|
||||||
|
|
||||||
{
|
try {
|
||||||
auto frame = sender.TryGetNextFrame();
|
auto frame = sender.TryGetNextFrame();
|
||||||
auto frame_size = frame.ContentSize();
|
auto frame_size = frame.ContentSize();
|
||||||
|
|
||||||
@@ -239,60 +275,63 @@ void WgcSessionImpl::OnFrame(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// copy to mapped texture
|
// copy to mapped texture
|
||||||
{
|
if (is_paused_) {
|
||||||
if (is_paused_) {
|
return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto frame_captured =
|
|
||||||
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
|
|
||||||
|
|
||||||
if (!d3d11_texture_mapped_ || is_new_size) {
|
|
||||||
HRESULT tex_hr = CreateMappedTexture(frame_captured);
|
|
||||||
if (FAILED(tex_hr)) {
|
|
||||||
OutputDebugStringW(
|
|
||||||
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
|
|
||||||
.c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
|
|
||||||
frame_captured.get());
|
|
||||||
|
|
||||||
D3D11_MAPPED_SUBRESOURCE map_result;
|
|
||||||
HRESULT hr = d3d11_device_context_->Map(
|
|
||||||
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
|
|
||||||
0 /*coz we use CreateFreeThreaded, so we cant use flags
|
|
||||||
D3D11_MAP_FLAG_DO_NOT_WAIT*/
|
|
||||||
,
|
|
||||||
&map_result);
|
|
||||||
if (FAILED(hr)) {
|
|
||||||
OutputDebugStringW(
|
|
||||||
(L"map resource failed: " + std::to_wstring(hr)).c_str());
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// copy data from map_result.pData
|
|
||||||
if (map_result.pData && observer_) {
|
|
||||||
observer_->OnFrame(
|
|
||||||
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
|
||||||
static_cast<unsigned int>(frame_size.Height),
|
|
||||||
map_result.RowPitch,
|
|
||||||
const_cast<const unsigned char*>(
|
|
||||||
(unsigned char*)map_result.pData)},
|
|
||||||
id_);
|
|
||||||
}
|
|
||||||
|
|
||||||
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (is_new_size) {
|
auto frame_captured =
|
||||||
capture_framepool_.Recreate(d3d11_direct_device_,
|
GetDXGIInterfaceFromObject<ID3D11Texture2D>(frame.Surface());
|
||||||
winrt::Windows::Graphics::DirectX::
|
|
||||||
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
if (!d3d11_texture_mapped_ || is_new_size) {
|
||||||
2, capture_frame_size_);
|
HRESULT tex_hr = CreateMappedTexture(frame_captured);
|
||||||
|
if (FAILED(tex_hr)) {
|
||||||
|
OutputDebugStringW(
|
||||||
|
(L"CreateMappedTexture failed: " + std::to_wstring(tex_hr))
|
||||||
|
.c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
d3d11_device_context_->CopyResource(d3d11_texture_mapped_.get(),
|
||||||
|
frame_captured.get());
|
||||||
|
|
||||||
|
D3D11_MAPPED_SUBRESOURCE map_result;
|
||||||
|
HRESULT hr = d3d11_device_context_->Map(
|
||||||
|
d3d11_texture_mapped_.get(), 0, D3D11_MAP_READ,
|
||||||
|
0 /*coz we use CreateFreeThreaded, so we cant use flags
|
||||||
|
D3D11_MAP_FLAG_DO_NOT_WAIT*/
|
||||||
|
,
|
||||||
|
&map_result);
|
||||||
|
if (FAILED(hr)) {
|
||||||
|
OutputDebugStringW(
|
||||||
|
(L"map resource failed: " + std::to_wstring(hr)).c_str());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy data from map_result.pData
|
||||||
|
if (map_result.pData && observer_) {
|
||||||
|
observer_->OnFrame(
|
||||||
|
wgc_session_frame{static_cast<unsigned int>(frame_size.Width),
|
||||||
|
static_cast<unsigned int>(frame_size.Height),
|
||||||
|
map_result.RowPitch,
|
||||||
|
const_cast<const unsigned char*>(
|
||||||
|
(unsigned char*)map_result.pData)},
|
||||||
|
id_);
|
||||||
|
}
|
||||||
|
|
||||||
|
d3d11_device_context_->Unmap(d3d11_texture_mapped_.get(), 0);
|
||||||
|
|
||||||
|
if (is_new_size) {
|
||||||
|
capture_framepool_.Recreate(
|
||||||
|
d3d11_direct_device_,
|
||||||
|
winrt::Windows::Graphics::DirectX::
|
||||||
|
DirectXPixelFormat::B8G8R8A8UIntNormalized,
|
||||||
|
2, capture_frame_size_);
|
||||||
|
}
|
||||||
|
} catch (const winrt::hresult_error&) {
|
||||||
|
LOG_WARN("WGC: frame processing failed");
|
||||||
|
} catch (...) {
|
||||||
|
LOG_WARN("WGC: frame processing failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,11 +339,13 @@ void WgcSessionImpl::OnClosed(
|
|||||||
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||||
winrt::Windows::Foundation::IInspectable const&) {
|
winrt::Windows::Foundation::IInspectable const&) {
|
||||||
std::lock_guard locker(lock_);
|
std::lock_guard locker(lock_);
|
||||||
|
const bool was_running = is_running_;
|
||||||
|
const bool was_paused = is_paused_;
|
||||||
try {
|
try {
|
||||||
CleanUp();
|
CleanUpLocked();
|
||||||
is_initialized_ = false;
|
is_paused_ = was_paused;
|
||||||
if (Initialize() == 0) {
|
if (InitializeLocked() == 0) {
|
||||||
int ret = Start(last_show_cursor_);
|
int ret = was_running ? StartCaptureLocked(last_show_cursor_) : 0;
|
||||||
if (ret == 0) {
|
if (ret == 0) {
|
||||||
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
|
OutputDebugStringW(L"WgcSessionImpl::OnClosed: auto recovered");
|
||||||
} else {
|
} else {
|
||||||
@@ -319,9 +360,14 @@ void WgcSessionImpl::OnClosed(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int WgcSessionImpl::Initialize() {
|
int WgcSessionImpl::InitializeLocked() {
|
||||||
if (is_initialized_) return 0;
|
if (is_initialized_) return 0;
|
||||||
|
|
||||||
|
d3d11_texture_mapped_ = nullptr;
|
||||||
|
d3d11_device_context_ = nullptr;
|
||||||
|
d3d11_direct_device_ = nullptr;
|
||||||
|
capture_frame_size_ = {};
|
||||||
|
|
||||||
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
|
if (!(d3d11_direct_device_ = CreateD3D11Device())) {
|
||||||
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
|
LOG_ERROR("AE_D3D_CREATE_DEVICE_FAILED");
|
||||||
return 1;
|
return 1;
|
||||||
@@ -332,6 +378,10 @@ int WgcSessionImpl::Initialize() {
|
|||||||
capture_item_ = CreateCaptureItemForWindow(target_.hwnd);
|
capture_item_ = CreateCaptureItemForWindow(target_.hwnd);
|
||||||
else
|
else
|
||||||
capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor);
|
capture_item_ = CreateCaptureItemForMonitor(target_.hmonitor);
|
||||||
|
if (!capture_item_) {
|
||||||
|
LOG_ERROR("WGC: create capture item returned null");
|
||||||
|
return 86;
|
||||||
|
}
|
||||||
|
|
||||||
// Set up
|
// Set up
|
||||||
auto d3d11_device =
|
auto d3d11_device =
|
||||||
@@ -353,21 +403,18 @@ int WgcSessionImpl::Initialize() {
|
|||||||
void WgcSessionImpl::CleanUp() {
|
void WgcSessionImpl::CleanUp() {
|
||||||
std::lock_guard locker(lock_);
|
std::lock_guard locker(lock_);
|
||||||
|
|
||||||
auto expected = false;
|
CleanUpLocked();
|
||||||
if (cleaned_.compare_exchange_strong(expected, true)) {
|
}
|
||||||
capture_close_trigger_.revoke();
|
|
||||||
capture_framepool_trigger_.revoke();
|
|
||||||
|
|
||||||
if (capture_framepool_) capture_framepool_.Close();
|
void WgcSessionImpl::CleanUpLocked() {
|
||||||
|
StopLocked();
|
||||||
|
|
||||||
if (capture_session_) capture_session_.Close();
|
capture_item_ = nullptr;
|
||||||
|
d3d11_device_context_ = nullptr;
|
||||||
capture_framepool_ = nullptr;
|
d3d11_direct_device_ = nullptr;
|
||||||
capture_session_ = nullptr;
|
capture_frame_size_ = {};
|
||||||
capture_item_ = nullptr;
|
is_initialized_ = false;
|
||||||
|
is_paused_ = false;
|
||||||
is_initialized_ = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
|
LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM w_param,
|
||||||
|
|||||||
@@ -68,8 +68,12 @@ class WgcSessionImpl : public WgcSession {
|
|||||||
void OnClosed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
void OnClosed(winrt::Windows::Graphics::Capture::GraphicsCaptureItem const&,
|
||||||
winrt::Windows::Foundation::IInspectable const&);
|
winrt::Windows::Foundation::IInspectable const&);
|
||||||
|
|
||||||
int Initialize();
|
int InitializeLocked();
|
||||||
|
int StartLocked(bool show_cursor);
|
||||||
|
int StartCaptureLocked(bool show_cursor);
|
||||||
|
void StopLocked();
|
||||||
void CleanUp();
|
void CleanUp();
|
||||||
|
void CleanUpLocked();
|
||||||
|
|
||||||
// void message_func();
|
// void message_func();
|
||||||
|
|
||||||
@@ -94,7 +98,6 @@ class WgcSessionImpl : public WgcSession {
|
|||||||
winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
|
winrt::com_ptr<ID3D11DeviceContext> d3d11_device_context_{nullptr};
|
||||||
winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};
|
winrt::com_ptr<ID3D11Texture2D> d3d11_texture_mapped_{nullptr};
|
||||||
|
|
||||||
std::atomic<bool> cleaned_ = false;
|
|
||||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
|
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool
|
||||||
capture_framepool_{nullptr};
|
capture_framepool_{nullptr};
|
||||||
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ namespace crossdesk {
|
|||||||
|
|
||||||
inline bool IsSecureDesktopInteractionRequired(
|
inline bool IsSecureDesktopInteractionRequired(
|
||||||
const std::string& interactive_stage) {
|
const std::string& interactive_stage) {
|
||||||
return interactive_stage == "credential-ui" ||
|
return interactive_stage == "lock-screen" ||
|
||||||
|
interactive_stage == "credential-ui" ||
|
||||||
interactive_stage == "secure-desktop";
|
interactive_stage == "secure-desktop";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,4 +39,4 @@ inline bool ShouldNormalizeUnlockToUserDesktop(
|
|||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ constexpr char kSecureDesktopMouseIpcCommandPrefix[] = "secure-input-mouse:";
|
|||||||
constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe";
|
constexpr wchar_t kCrossDeskClientProcessName[] = L"crossdesk.exe";
|
||||||
constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000;
|
constexpr DWORD kCrossDeskClientMonitorIntervalMs = 1000;
|
||||||
constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000;
|
constexpr ULONGLONG kCrossDeskClientMonitorStartupGraceMs = 5000;
|
||||||
|
constexpr ULONGLONG kSasSecureDesktopGraceMs = 15000;
|
||||||
|
|
||||||
using SendSasFunction = VOID(WINAPI*)(BOOL);
|
using SendSasFunction = VOID(WINAPI*)(BOOL);
|
||||||
|
|
||||||
@@ -262,8 +263,8 @@ bool GrantCrossDeskServiceStartAccessToAuthenticatedUsers(SC_HANDLE service) {
|
|||||||
std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
||||||
const std::string& command,
|
const std::string& command,
|
||||||
DWORD timeout_ms) {
|
DWORD timeout_ms) {
|
||||||
constexpr int kPipeConnectRetryCount = 3;
|
|
||||||
constexpr DWORD kPipeConnectRetryDelayMs = 15;
|
constexpr DWORD kPipeConnectRetryDelayMs = 15;
|
||||||
|
const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms;
|
||||||
|
|
||||||
auto is_transient_pipe_error = [](DWORD error) {
|
auto is_transient_pipe_error = [](DWORD error) {
|
||||||
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY ||
|
return error == ERROR_FILE_NOT_FOUND || error == ERROR_PIPE_BUSY ||
|
||||||
@@ -271,12 +272,23 @@ std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
|||||||
};
|
};
|
||||||
|
|
||||||
HANDLE pipe = INVALID_HANDLE_VALUE;
|
HANDLE pipe = INVALID_HANDLE_VALUE;
|
||||||
for (int attempt = 0; attempt < kPipeConnectRetryCount; ++attempt) {
|
DWORD last_error = ERROR_SEM_TIMEOUT;
|
||||||
if (!WaitNamedPipeW(pipe_name.c_str(), timeout_ms)) {
|
while (GetTickCount64() <= deadline_tick) {
|
||||||
|
const ULONGLONG now = GetTickCount64();
|
||||||
|
const DWORD wait_timeout =
|
||||||
|
deadline_tick > now
|
||||||
|
? static_cast<DWORD>((std::min)(
|
||||||
|
deadline_tick - now, static_cast<ULONGLONG>(MAXDWORD)))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
if (!WaitNamedPipeW(pipe_name.c_str(), wait_timeout)) {
|
||||||
const DWORD error = GetLastError();
|
const DWORD error = GetLastError();
|
||||||
if (attempt + 1 < kPipeConnectRetryCount &&
|
last_error = error;
|
||||||
is_transient_pipe_error(error)) {
|
const ULONGLONG retry_tick = GetTickCount64();
|
||||||
Sleep(kPipeConnectRetryDelayMs);
|
if (is_transient_pipe_error(error) && retry_tick < deadline_tick) {
|
||||||
|
Sleep(static_cast<DWORD>((std::min)(
|
||||||
|
static_cast<ULONGLONG>(kPipeConnectRetryDelayMs),
|
||||||
|
deadline_tick - retry_tick)));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return BuildErrorJson("pipe_unavailable", error);
|
return BuildErrorJson("pipe_unavailable", error);
|
||||||
@@ -289,14 +301,21 @@ std::string QueryNamedPipeMessage(const std::wstring& pipe_name,
|
|||||||
}
|
}
|
||||||
|
|
||||||
const DWORD error = GetLastError();
|
const DWORD error = GetLastError();
|
||||||
if (attempt + 1 < kPipeConnectRetryCount &&
|
last_error = error;
|
||||||
is_transient_pipe_error(error)) {
|
const ULONGLONG retry_tick = GetTickCount64();
|
||||||
Sleep(kPipeConnectRetryDelayMs);
|
if (is_transient_pipe_error(error) && retry_tick < deadline_tick) {
|
||||||
|
Sleep(static_cast<DWORD>((std::min)(
|
||||||
|
static_cast<ULONGLONG>(kPipeConnectRetryDelayMs),
|
||||||
|
deadline_tick - retry_tick)));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return BuildErrorJson("pipe_connect_failed", error);
|
return BuildErrorJson("pipe_connect_failed", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (pipe == INVALID_HANDLE_VALUE) {
|
||||||
|
return BuildErrorJson("pipe_unavailable", last_error);
|
||||||
|
}
|
||||||
|
|
||||||
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
DWORD pipe_mode = PIPE_READMODE_MESSAGE;
|
||||||
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
SetNamedPipeHandleState(pipe, &pipe_mode, nullptr, nullptr);
|
||||||
|
|
||||||
@@ -337,20 +356,27 @@ std::string BuildSecureDesktopMouseIpcCommand(int x, int y, int wheel,
|
|||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string BuildSecureInputHelperKeyboardCommand(int key_code, bool is_down,
|
std::string BuildSecureInputHelperKeyboardCommand(
|
||||||
uint32_t scan_code,
|
int key_code, bool is_down, uint32_t scan_code, bool extended,
|
||||||
bool extended) {
|
const std::string& interactive_stage) {
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":"
|
stream << kCrossDeskSecureInputKeyboardCommandPrefix << key_code << ":"
|
||||||
<< (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0);
|
<< (is_down ? 1 : 0) << ":" << scan_code << ":" << (extended ? 1 : 0);
|
||||||
|
if (!interactive_stage.empty()) {
|
||||||
|
stream << ":" << interactive_stage;
|
||||||
|
}
|
||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string BuildSecureInputHelperMouseCommand(int x, int y, int wheel,
|
std::string BuildSecureInputHelperMouseCommand(
|
||||||
int flag) {
|
int x, int y, int wheel, int flag,
|
||||||
|
const std::string& interactive_stage) {
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":"
|
stream << kCrossDeskSecureInputMouseCommandPrefix << x << ":" << y << ":"
|
||||||
<< wheel << ":" << flag;
|
<< wheel << ":" << flag;
|
||||||
|
if (!interactive_stage.empty()) {
|
||||||
|
stream << ":" << interactive_stage;
|
||||||
|
}
|
||||||
return stream.str();
|
return stream.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,6 +591,23 @@ const char* DetermineInteractiveStage(bool lock_app_visible,
|
|||||||
return "user-desktop";
|
return "user-desktop";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool IsCredentialUiVisible(bool prelogin, bool session_locked,
|
||||||
|
bool logon_ui_running,
|
||||||
|
bool input_desktop_available,
|
||||||
|
bool secure_desktop_active) {
|
||||||
|
return (prelogin || session_locked || secure_desktop_active) &&
|
||||||
|
(logon_ui_running || !input_desktop_available);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::wstring SecureInputHelperDesktopForStage(
|
||||||
|
const std::string& interactive_stage) {
|
||||||
|
if (interactive_stage == "credential-ui" ||
|
||||||
|
interactive_stage == "secure-desktop") {
|
||||||
|
return L"winsta0\\Winlogon";
|
||||||
|
}
|
||||||
|
return L"winsta0\\default";
|
||||||
|
}
|
||||||
|
|
||||||
bool GetSessionUserName(DWORD session_id, std::wstring* username_out) {
|
bool GetSessionUserName(DWORD session_id, std::wstring* username_out) {
|
||||||
if (username_out == nullptr) {
|
if (username_out == nullptr) {
|
||||||
return false;
|
return false;
|
||||||
@@ -993,12 +1036,14 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
|||||||
session_helper_report_credential_ui_visible_ = false;
|
session_helper_report_credential_ui_visible_ = false;
|
||||||
session_helper_report_unlock_ui_visible_ = false;
|
session_helper_report_unlock_ui_visible_ = false;
|
||||||
secure_input_helper_running_ = false;
|
secure_input_helper_running_ = false;
|
||||||
|
sas_secure_desktop_seen_ = false;
|
||||||
last_sas_error_code_ = 0;
|
last_sas_error_code_ = 0;
|
||||||
last_sas_success_ = false;
|
last_sas_success_ = false;
|
||||||
session_helper_started_at_tick_ = 0;
|
session_helper_started_at_tick_ = 0;
|
||||||
session_helper_report_state_age_ms_ = 0;
|
session_helper_report_state_age_ms_ = 0;
|
||||||
session_helper_report_uptime_ms_ = 0;
|
session_helper_report_uptime_ms_ = 0;
|
||||||
secure_input_helper_started_at_tick_ = 0;
|
secure_input_helper_started_at_tick_ = 0;
|
||||||
|
sas_secure_desktop_until_tick_ = 0;
|
||||||
session_helper_process_handle_ = nullptr;
|
session_helper_process_handle_ = nullptr;
|
||||||
session_helper_stop_event_ = nullptr;
|
session_helper_stop_event_ = nullptr;
|
||||||
secure_input_helper_process_handle_ = nullptr;
|
secure_input_helper_process_handle_ = nullptr;
|
||||||
@@ -1010,6 +1055,7 @@ int CrossDeskServiceHost::InitializeRuntime() {
|
|||||||
session_helper_report_input_desktop_.clear();
|
session_helper_report_input_desktop_.clear();
|
||||||
session_helper_report_interactive_stage_.clear();
|
session_helper_report_interactive_stage_.clear();
|
||||||
secure_input_helper_last_error_.clear();
|
secure_input_helper_last_error_.clear();
|
||||||
|
secure_input_helper_interactive_stage_.clear();
|
||||||
last_session_event_type_ = 0;
|
last_session_event_type_ = 0;
|
||||||
last_session_event_session_id_ = active_session_id_;
|
last_session_event_session_id_ = active_session_id_;
|
||||||
RefreshSessionState();
|
RefreshSessionState();
|
||||||
@@ -1285,7 +1331,13 @@ bool CrossDeskServiceHost::IsHelperReportingLockScreenLocked() const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
|
bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
|
||||||
return prelogin_ || secure_desktop_active_ || logon_ui_visible_ ||
|
const bool service_host_credential_ui_visible =
|
||||||
|
!session_helper_status_ok_ &&
|
||||||
|
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||||
|
input_desktop_available_,
|
||||||
|
secure_desktop_active_);
|
||||||
|
return IsSasSecureDesktopGraceActiveLocked() || prelogin_ ||
|
||||||
|
secure_desktop_active_ || service_host_credential_ui_visible ||
|
||||||
session_helper_report_credential_ui_visible_ ||
|
session_helper_report_credential_ui_visible_ ||
|
||||||
session_helper_report_secure_desktop_active_ ||
|
session_helper_report_secure_desktop_active_ ||
|
||||||
session_helper_report_unlock_ui_visible_ ||
|
session_helper_report_unlock_ui_visible_ ||
|
||||||
@@ -1293,6 +1345,30 @@ bool CrossDeskServiceHost::HasSecureInputUiLocked() const {
|
|||||||
session_helper_report_interactive_stage_ == "secure-desktop";
|
session_helper_report_interactive_stage_ == "secure-desktop";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CrossDeskServiceHost::UpdateSasSecureDesktopGraceLocked(
|
||||||
|
const std::string& observed_stage) {
|
||||||
|
if (sas_secure_desktop_until_tick_ == 0) {
|
||||||
|
sas_secure_desktop_seen_ = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (observed_stage == "credential-ui" || observed_stage == "secure-desktop" ||
|
||||||
|
observed_stage == "lock-screen") {
|
||||||
|
sas_secure_desktop_seen_ = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sas_secure_desktop_seen_ && observed_stage == "user-desktop") {
|
||||||
|
sas_secure_desktop_until_tick_ = 0;
|
||||||
|
sas_secure_desktop_seen_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool CrossDeskServiceHost::IsSasSecureDesktopGraceActiveLocked() const {
|
||||||
|
return last_sas_success_ && sas_secure_desktop_until_tick_ != 0 &&
|
||||||
|
GetTickCount64() < sas_secure_desktop_until_tick_;
|
||||||
|
}
|
||||||
|
|
||||||
bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
|
bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
|
||||||
DWORD target_session_id) const {
|
DWORD target_session_id) const {
|
||||||
if (target_session_id == 0xFFFFFFFF) {
|
if (target_session_id == 0xFFFFFFFF) {
|
||||||
@@ -1303,6 +1379,28 @@ bool CrossDeskServiceHost::ShouldKeepSecureInputHelperLocked(
|
|||||||
IsHelperReportingLockScreenLocked());
|
IsHelperReportingLockScreenLocked());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string CrossDeskServiceHost::ResolveInteractiveStageLocked() const {
|
||||||
|
if (IsSasSecureDesktopGraceActiveLocked() &&
|
||||||
|
(session_helper_report_interactive_stage_.empty() ||
|
||||||
|
session_helper_report_interactive_stage_ == "user-desktop")) {
|
||||||
|
return "secure-desktop";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session_helper_report_interactive_stage_.empty()) {
|
||||||
|
return session_helper_report_interactive_stage_;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool service_host_credential_ui_visible =
|
||||||
|
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||||
|
input_desktop_available_,
|
||||||
|
secure_desktop_active_);
|
||||||
|
return DetermineInteractiveStage(
|
||||||
|
IsHelperReportingLockScreenLocked(),
|
||||||
|
session_helper_report_credential_ui_visible_ ||
|
||||||
|
service_host_credential_ui_visible,
|
||||||
|
session_helper_report_secure_desktop_active_ || secure_desktop_active_);
|
||||||
|
}
|
||||||
|
|
||||||
std::wstring CrossDeskServiceHost::GetSessionHelperPath() const {
|
std::wstring CrossDeskServiceHost::GetSessionHelperPath() const {
|
||||||
std::wstring current_executable = GetCurrentExecutablePathW();
|
std::wstring current_executable = GetCurrentExecutablePathW();
|
||||||
if (current_executable.empty()) {
|
if (current_executable.empty()) {
|
||||||
@@ -1392,6 +1490,7 @@ void CrossDeskServiceHost::ReapSecureInputHelper() {
|
|||||||
secure_input_helper_process_id_ = 0;
|
secure_input_helper_process_id_ = 0;
|
||||||
secure_input_helper_exit_code_ = exit_code;
|
secure_input_helper_exit_code_ = exit_code;
|
||||||
secure_input_helper_started_at_tick_ = 0;
|
secure_input_helper_started_at_tick_ = 0;
|
||||||
|
secure_input_helper_interactive_stage_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (process_handle != nullptr) {
|
if (process_handle != nullptr) {
|
||||||
@@ -1450,6 +1549,7 @@ void CrossDeskServiceHost::StopSecureInputHelper() {
|
|||||||
secure_input_helper_running_ = false;
|
secure_input_helper_running_ = false;
|
||||||
secure_input_helper_process_id_ = 0;
|
secure_input_helper_process_id_ = 0;
|
||||||
secure_input_helper_started_at_tick_ = 0;
|
secure_input_helper_started_at_tick_ = 0;
|
||||||
|
secure_input_helper_interactive_stage_.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stop_event_handle != nullptr) {
|
if (stop_event_handle != nullptr) {
|
||||||
@@ -1577,7 +1677,8 @@ bool CrossDeskServiceHost::LaunchSessionHelper(DWORD session_id) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
|
bool CrossDeskServiceHost::LaunchSecureInputHelper(
|
||||||
|
DWORD session_id, const std::string& interactive_stage) {
|
||||||
std::wstring helper_path = GetSecureInputHelperPath();
|
std::wstring helper_path = GetSecureInputHelperPath();
|
||||||
if (helper_path.empty() || !std::filesystem::exists(helper_path)) {
|
if (helper_path.empty() || !std::filesystem::exists(helper_path)) {
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
@@ -1611,7 +1712,10 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
|
|||||||
|
|
||||||
STARTUPINFOW startup_info{};
|
STARTUPINFOW startup_info{};
|
||||||
startup_info.cb = sizeof(startup_info);
|
startup_info.cb = sizeof(startup_info);
|
||||||
startup_info.lpDesktop = const_cast<LPWSTR>(L"winsta0\\Winlogon");
|
std::wstring secure_input_helper_desktop =
|
||||||
|
SecureInputHelperDesktopForStage(interactive_stage);
|
||||||
|
startup_info.lpDesktop =
|
||||||
|
const_cast<LPWSTR>(secure_input_helper_desktop.c_str());
|
||||||
PROCESS_INFORMATION process_info{};
|
PROCESS_INFORMATION process_info{};
|
||||||
BOOL created = FALSE;
|
BOOL created = FALSE;
|
||||||
|
|
||||||
@@ -1660,10 +1764,14 @@ bool CrossDeskServiceHost::LaunchSecureInputHelper(DWORD session_id) {
|
|||||||
secure_input_helper_last_error_.clear();
|
secure_input_helper_last_error_.clear();
|
||||||
secure_input_helper_running_ = true;
|
secure_input_helper_running_ = true;
|
||||||
secure_input_helper_started_at_tick_ = GetTickCount64();
|
secure_input_helper_started_at_tick_ = GetTickCount64();
|
||||||
|
secure_input_helper_interactive_stage_ = interactive_stage;
|
||||||
}
|
}
|
||||||
|
|
||||||
LOG_INFO("Secure input helper started: session_id={}, pid={}", session_id,
|
LOG_INFO(
|
||||||
process_info.dwProcessId);
|
"Secure input helper started: session_id={}, pid={}, stage='{}', "
|
||||||
|
"desktop='{}'",
|
||||||
|
session_id, process_info.dwProcessId, interactive_stage,
|
||||||
|
WideToUtf8(secure_input_helper_desktop));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1762,6 +1870,7 @@ void CrossDeskServiceHost::RefreshSessionHelperReportedState() {
|
|||||||
json.value("interactive_stage", std::string());
|
json.value("interactive_stage", std::string());
|
||||||
session_helper_report_state_age_ms_ = json.value("state_age_ms", 0ull);
|
session_helper_report_state_age_ms_ = json.value("state_age_ms", 0ull);
|
||||||
session_helper_report_uptime_ms_ = json.value("uptime_ms", 0ull);
|
session_helper_report_uptime_ms_ = json.value("uptime_ms", 0ull);
|
||||||
|
UpdateSasSecureDesktopGraceLocked(session_helper_report_interactive_stage_);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
void CrossDeskServiceHost::RecordSessionEvent(DWORD event_type,
|
||||||
@@ -1845,21 +1954,26 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
|||||||
bool keep_secure_input_helper = false;
|
bool keep_secure_input_helper = false;
|
||||||
bool launch_secure_input_helper = false;
|
bool launch_secure_input_helper = false;
|
||||||
DWORD secure_input_target_session_id = 0xFFFFFFFF;
|
DWORD secure_input_target_session_id = 0xFFFFFFFF;
|
||||||
|
std::string secure_input_interactive_stage;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
secure_input_target_session_id = active_session_id_;
|
secure_input_target_session_id = active_session_id_;
|
||||||
|
secure_input_interactive_stage = ResolveInteractiveStageLocked();
|
||||||
keep_secure_input_helper =
|
keep_secure_input_helper =
|
||||||
ShouldKeepSecureInputHelperLocked(secure_input_target_session_id);
|
ShouldKeepSecureInputHelperLocked(secure_input_target_session_id);
|
||||||
launch_secure_input_helper =
|
launch_secure_input_helper =
|
||||||
keep_secure_input_helper &&
|
keep_secure_input_helper &&
|
||||||
(!secure_input_helper_running_ ||
|
(!secure_input_helper_running_ ||
|
||||||
secure_input_helper_session_id_ != secure_input_target_session_id);
|
secure_input_helper_session_id_ != secure_input_target_session_id ||
|
||||||
|
secure_input_helper_interactive_stage_ !=
|
||||||
|
secure_input_interactive_stage);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keep_secure_input_helper) {
|
if (keep_secure_input_helper) {
|
||||||
if (launch_secure_input_helper) {
|
if (launch_secure_input_helper) {
|
||||||
StopSecureInputHelper();
|
StopSecureInputHelper();
|
||||||
LaunchSecureInputHelper(secure_input_target_session_id);
|
LaunchSecureInputHelper(secure_input_target_session_id,
|
||||||
|
secure_input_interactive_stage);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
StopSecureInputHelper();
|
StopSecureInputHelper();
|
||||||
@@ -1883,7 +1997,11 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
|||||||
EscapeJsonString(session_helper_report_input_desktop_);
|
EscapeJsonString(session_helper_report_input_desktop_);
|
||||||
std::string secure_input_helper_last_error =
|
std::string secure_input_helper_last_error =
|
||||||
EscapeJsonString(secure_input_helper_last_error_);
|
EscapeJsonString(secure_input_helper_last_error_);
|
||||||
|
std::string secure_input_helper_interactive_stage =
|
||||||
|
EscapeJsonString(secure_input_helper_interactive_stage_);
|
||||||
bool interactive_state_ready = session_helper_status_ok_;
|
bool interactive_state_ready = session_helper_status_ok_;
|
||||||
|
const bool sas_secure_desktop_grace_active =
|
||||||
|
IsSasSecureDesktopGraceActiveLocked();
|
||||||
const char* interactive_state_source =
|
const char* interactive_state_source =
|
||||||
interactive_state_ready ? "session-helper" : "service-host";
|
interactive_state_ready ? "session-helper" : "service-host";
|
||||||
const bool effective_session_locked = GetEffectiveSessionLockedLocked();
|
const bool effective_session_locked = GetEffectiveSessionLockedLocked();
|
||||||
@@ -1891,27 +2009,34 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
|||||||
interactive_state_ready
|
interactive_state_ready
|
||||||
? (effective_session_locked && IsHelperReportingLockScreenLocked())
|
? (effective_session_locked && IsHelperReportingLockScreenLocked())
|
||||||
: false;
|
: false;
|
||||||
|
const bool service_host_credential_ui_visible =
|
||||||
|
IsCredentialUiVisible(prelogin_, session_locked_, logon_ui_visible_,
|
||||||
|
input_desktop_available_,
|
||||||
|
secure_desktop_active_);
|
||||||
bool credential_ui_visible =
|
bool credential_ui_visible =
|
||||||
interactive_state_ready ? session_helper_report_credential_ui_visible_
|
interactive_state_ready ? session_helper_report_credential_ui_visible_
|
||||||
: logon_ui_visible_;
|
: service_host_credential_ui_visible;
|
||||||
bool unlock_ui_visible = interactive_state_ready
|
bool unlock_ui_visible = interactive_state_ready
|
||||||
? session_helper_report_unlock_ui_visible_
|
? session_helper_report_unlock_ui_visible_
|
||||||
: (logon_ui_visible_ || secure_desktop_active_);
|
: (credential_ui_visible ||
|
||||||
|
secure_desktop_active_);
|
||||||
|
unlock_ui_visible = unlock_ui_visible || sas_secure_desktop_grace_active;
|
||||||
bool interactive_secure_desktop_active =
|
bool interactive_secure_desktop_active =
|
||||||
interactive_state_ready ? session_helper_report_secure_desktop_active_
|
interactive_state_ready ? session_helper_report_secure_desktop_active_
|
||||||
: secure_desktop_active_;
|
: secure_desktop_active_;
|
||||||
|
interactive_secure_desktop_active =
|
||||||
|
interactive_secure_desktop_active || sas_secure_desktop_grace_active;
|
||||||
bool interactive_logon_ui_visible =
|
bool interactive_logon_ui_visible =
|
||||||
interactive_state_ready ? session_helper_report_logon_ui_visible_
|
credential_ui_visible;
|
||||||
: logon_ui_visible_;
|
|
||||||
bool interactive_session_locked = effective_session_locked ||
|
bool interactive_session_locked = effective_session_locked ||
|
||||||
interactive_lock_screen_visible ||
|
interactive_lock_screen_visible ||
|
||||||
unlock_ui_visible;
|
unlock_ui_visible ||
|
||||||
|
sas_secure_desktop_grace_active;
|
||||||
std::string interactive_input_desktop = EscapeJsonString(
|
std::string interactive_input_desktop = EscapeJsonString(
|
||||||
interactive_state_ready ? session_helper_report_input_desktop_
|
interactive_state_ready ? session_helper_report_input_desktop_
|
||||||
: input_desktop_name_);
|
: input_desktop_name_);
|
||||||
std::string interactive_stage = EscapeJsonString(DetermineInteractiveStage(
|
std::string raw_interactive_stage = ResolveInteractiveStageLocked();
|
||||||
interactive_lock_screen_visible, credential_ui_visible,
|
std::string interactive_stage = EscapeJsonString(raw_interactive_stage);
|
||||||
interactive_secure_desktop_active));
|
|
||||||
std::ostringstream stream;
|
std::ostringstream stream;
|
||||||
stream << "{\"ok\":true,\"service\":\"CrossDeskService\""
|
stream << "{\"ok\":true,\"service\":\"CrossDeskService\""
|
||||||
<< ",\"active_session_id\":" << active_session_id_
|
<< ",\"active_session_id\":" << active_session_id_
|
||||||
@@ -1932,6 +2057,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
|||||||
<< (interactive_logon_ui_visible ? "true" : "false")
|
<< (interactive_logon_ui_visible ? "true" : "false")
|
||||||
<< ",\"interactive_secure_desktop_active\":"
|
<< ",\"interactive_secure_desktop_active\":"
|
||||||
<< (interactive_secure_desktop_active ? "true" : "false")
|
<< (interactive_secure_desktop_active ? "true" : "false")
|
||||||
|
<< ",\"sas_secure_desktop_grace_active\":"
|
||||||
|
<< (sas_secure_desktop_grace_active ? "true" : "false")
|
||||||
<< ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false")
|
<< ",\"unlock_ui_visible\":" << (unlock_ui_visible ? "true" : "false")
|
||||||
<< ",\"credential_ui_visible\":"
|
<< ",\"credential_ui_visible\":"
|
||||||
<< (credential_ui_visible ? "true" : "false")
|
<< (credential_ui_visible ? "true" : "false")
|
||||||
@@ -2005,6 +2132,8 @@ std::string CrossDeskServiceHost::BuildStatusResponse() {
|
|||||||
<< secure_input_helper_last_error << "\""
|
<< secure_input_helper_last_error << "\""
|
||||||
<< ",\"secure_input_helper_last_error_code\":"
|
<< ",\"secure_input_helper_last_error_code\":"
|
||||||
<< secure_input_helper_last_error_code_
|
<< secure_input_helper_last_error_code_
|
||||||
|
<< ",\"secure_input_helper_stage\":\""
|
||||||
|
<< secure_input_helper_interactive_stage << "\""
|
||||||
<< ",\"secure_input_helper_uptime_ms\":"
|
<< ",\"secure_input_helper_uptime_ms\":"
|
||||||
<< (secure_input_helper_started_at_tick_ >= started_at_tick_
|
<< (secure_input_helper_started_at_tick_ >= started_at_tick_
|
||||||
? (GetTickCount64() - secure_input_helper_started_at_tick_)
|
? (GetTickCount64() - secure_input_helper_started_at_tick_)
|
||||||
@@ -2034,10 +2163,14 @@ std::string CrossDeskServiceHost::SendSecureAttentionSequence() {
|
|||||||
SasResult result = SendSasNow();
|
SasResult result = SendSasNow();
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
last_sas_tick_ = GetTickCount64();
|
const ULONGLONG now = GetTickCount64();
|
||||||
|
last_sas_tick_ = now;
|
||||||
last_sas_success_ = result.success;
|
last_sas_success_ = result.success;
|
||||||
last_sas_error_code_ = result.error_code;
|
last_sas_error_code_ = result.error_code;
|
||||||
last_sas_error_ = result.error;
|
last_sas_error_ = result.error;
|
||||||
|
sas_secure_desktop_until_tick_ =
|
||||||
|
result.success ? now + kSasSecureDesktopGraceMs : 0;
|
||||||
|
sas_secure_desktop_seen_ = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
@@ -2051,15 +2184,21 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
|||||||
RefreshSessionState();
|
RefreshSessionState();
|
||||||
ReapSecureInputHelper();
|
ReapSecureInputHelper();
|
||||||
EnsureSessionHelper();
|
EnsureSessionHelper();
|
||||||
|
RefreshSessionHelperReportedState();
|
||||||
|
|
||||||
DWORD target_session_id = 0xFFFFFFFF;
|
DWORD target_session_id = 0xFFFFFFFF;
|
||||||
bool helper_running = false;
|
bool helper_running = false;
|
||||||
bool can_inject = false;
|
bool can_inject = false;
|
||||||
|
std::string interactive_stage;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
target_session_id = active_session_id_;
|
target_session_id = active_session_id_;
|
||||||
|
interactive_stage = ResolveInteractiveStageLocked();
|
||||||
|
const bool helper_stage_matches =
|
||||||
|
secure_input_helper_interactive_stage_ == interactive_stage;
|
||||||
helper_running = secure_input_helper_running_ &&
|
helper_running = secure_input_helper_running_ &&
|
||||||
secure_input_helper_session_id_ == target_session_id;
|
secure_input_helper_session_id_ == target_session_id &&
|
||||||
|
helper_stage_matches;
|
||||||
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
|
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2072,7 +2211,7 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
|||||||
|
|
||||||
if (!helper_running) {
|
if (!helper_running) {
|
||||||
StopSecureInputHelper();
|
StopSecureInputHelper();
|
||||||
if (!LaunchSecureInputHelper(target_session_id)) {
|
if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
||||||
secure_input_helper_last_error_code_);
|
secure_input_helper_last_error_code_);
|
||||||
@@ -2082,7 +2221,7 @@ std::string CrossDeskServiceHost::SendSecureDesktopKeyboardInput(
|
|||||||
return QueryNamedPipeMessage(
|
return QueryNamedPipeMessage(
|
||||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
||||||
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
|
BuildSecureInputHelperKeyboardCommand(key_code, is_down, scan_code,
|
||||||
extended),
|
extended, interactive_stage),
|
||||||
1000);
|
1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2092,15 +2231,21 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
|
|||||||
RefreshSessionState();
|
RefreshSessionState();
|
||||||
ReapSecureInputHelper();
|
ReapSecureInputHelper();
|
||||||
EnsureSessionHelper();
|
EnsureSessionHelper();
|
||||||
|
RefreshSessionHelperReportedState();
|
||||||
|
|
||||||
DWORD target_session_id = 0xFFFFFFFF;
|
DWORD target_session_id = 0xFFFFFFFF;
|
||||||
bool helper_running = false;
|
bool helper_running = false;
|
||||||
bool can_inject = false;
|
bool can_inject = false;
|
||||||
|
std::string interactive_stage;
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
target_session_id = active_session_id_;
|
target_session_id = active_session_id_;
|
||||||
|
interactive_stage = ResolveInteractiveStageLocked();
|
||||||
|
const bool helper_stage_matches =
|
||||||
|
secure_input_helper_interactive_stage_ == interactive_stage;
|
||||||
helper_running = secure_input_helper_running_ &&
|
helper_running = secure_input_helper_running_ &&
|
||||||
secure_input_helper_session_id_ == target_session_id;
|
secure_input_helper_session_id_ == target_session_id &&
|
||||||
|
helper_stage_matches;
|
||||||
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
|
can_inject = GetEffectiveSessionLockedLocked() || HasSecureInputUiLocked();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2113,7 +2258,7 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
|
|||||||
|
|
||||||
if (!helper_running) {
|
if (!helper_running) {
|
||||||
StopSecureInputHelper();
|
StopSecureInputHelper();
|
||||||
if (!LaunchSecureInputHelper(target_session_id)) {
|
if (!LaunchSecureInputHelper(target_session_id, interactive_stage)) {
|
||||||
std::lock_guard<std::mutex> lock(state_mutex_);
|
std::lock_guard<std::mutex> lock(state_mutex_);
|
||||||
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
return BuildErrorJson(secure_input_helper_last_error_.c_str(),
|
||||||
secure_input_helper_last_error_code_);
|
secure_input_helper_last_error_code_);
|
||||||
@@ -2122,7 +2267,8 @@ std::string CrossDeskServiceHost::SendSecureDesktopMouseInput(int x, int y,
|
|||||||
|
|
||||||
return QueryNamedPipeMessage(
|
return QueryNamedPipeMessage(
|
||||||
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
GetCrossDeskSecureInputHelperPipeName(target_session_id),
|
||||||
BuildSecureInputHelperMouseCommand(x, y, wheel, flag), 1000);
|
BuildSecureInputHelperMouseCommand(x, y, wheel, flag, interactive_stage),
|
||||||
|
1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InstallCrossDeskService(const std::wstring& binary_path) {
|
bool InstallCrossDeskService(const std::wstring& binary_path) {
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ class CrossDeskServiceHost {
|
|||||||
bool LaunchSessionHelper(DWORD session_id);
|
bool LaunchSessionHelper(DWORD session_id);
|
||||||
void ReapSecureInputHelper();
|
void ReapSecureInputHelper();
|
||||||
void StopSecureInputHelper();
|
void StopSecureInputHelper();
|
||||||
bool LaunchSecureInputHelper(DWORD session_id);
|
bool LaunchSecureInputHelper(DWORD session_id,
|
||||||
|
const std::string& interactive_stage);
|
||||||
std::wstring GetSessionHelperPath() const;
|
std::wstring GetSessionHelperPath() const;
|
||||||
std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
|
std::wstring GetSessionHelperStopEventName(DWORD session_id) const;
|
||||||
std::wstring GetSecureInputHelperPath() const;
|
std::wstring GetSecureInputHelperPath() const;
|
||||||
@@ -55,7 +56,10 @@ class CrossDeskServiceHost {
|
|||||||
bool GetEffectiveSessionLockedLocked() const;
|
bool GetEffectiveSessionLockedLocked() const;
|
||||||
bool IsHelperReportingLockScreenLocked() const;
|
bool IsHelperReportingLockScreenLocked() const;
|
||||||
bool HasSecureInputUiLocked() const;
|
bool HasSecureInputUiLocked() const;
|
||||||
|
void UpdateSasSecureDesktopGraceLocked(const std::string& observed_stage);
|
||||||
|
bool IsSasSecureDesktopGraceActiveLocked() const;
|
||||||
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
|
bool ShouldKeepSecureInputHelperLocked(DWORD target_session_id) const;
|
||||||
|
std::string ResolveInteractiveStageLocked() const;
|
||||||
void RefreshSessionHelperReportedState();
|
void RefreshSessionHelperReportedState();
|
||||||
void RecordSessionEvent(DWORD event_type, DWORD session_id);
|
void RecordSessionEvent(DWORD event_type, DWORD session_id);
|
||||||
std::string HandleIpcCommand(const std::string& command);
|
std::string HandleIpcCommand(const std::string& command);
|
||||||
@@ -101,6 +105,7 @@ class CrossDeskServiceHost {
|
|||||||
ULONGLONG session_helper_report_state_age_ms_ = 0;
|
ULONGLONG session_helper_report_state_age_ms_ = 0;
|
||||||
ULONGLONG session_helper_report_uptime_ms_ = 0;
|
ULONGLONG session_helper_report_uptime_ms_ = 0;
|
||||||
ULONGLONG secure_input_helper_started_at_tick_ = 0;
|
ULONGLONG secure_input_helper_started_at_tick_ = 0;
|
||||||
|
ULONGLONG sas_secure_desktop_until_tick_ = 0;
|
||||||
bool session_locked_ = false;
|
bool session_locked_ = false;
|
||||||
bool logon_ui_visible_ = false;
|
bool logon_ui_visible_ = false;
|
||||||
bool prelogin_ = false;
|
bool prelogin_ = false;
|
||||||
@@ -117,6 +122,7 @@ class CrossDeskServiceHost {
|
|||||||
bool session_helper_report_unlock_ui_visible_ = false;
|
bool session_helper_report_unlock_ui_visible_ = false;
|
||||||
bool secure_input_helper_running_ = false;
|
bool secure_input_helper_running_ = false;
|
||||||
bool console_mode_ = false;
|
bool console_mode_ = false;
|
||||||
|
bool sas_secure_desktop_seen_ = false;
|
||||||
DWORD last_sas_error_code_ = 0;
|
DWORD last_sas_error_code_ = 0;
|
||||||
bool last_sas_success_ = false;
|
bool last_sas_success_ = false;
|
||||||
HANDLE session_helper_process_handle_ = nullptr;
|
HANDLE session_helper_process_handle_ = nullptr;
|
||||||
@@ -130,6 +136,7 @@ class CrossDeskServiceHost {
|
|||||||
std::string session_helper_report_input_desktop_;
|
std::string session_helper_report_input_desktop_;
|
||||||
std::string session_helper_report_interactive_stage_;
|
std::string session_helper_report_interactive_stage_;
|
||||||
std::string secure_input_helper_last_error_;
|
std::string secure_input_helper_last_error_;
|
||||||
|
std::string secure_input_helper_interactive_stage_;
|
||||||
|
|
||||||
static CrossDeskServiceHost* instance_;
|
static CrossDeskServiceHost* instance_;
|
||||||
};
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,15 @@ inline constexpr char kCrossDeskSecureInputKeyboardCommandPrefix[] =
|
|||||||
"keyboard:";
|
"keyboard:";
|
||||||
inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = "mouse:";
|
inline constexpr char kCrossDeskSecureInputMouseCommandPrefix[] = "mouse:";
|
||||||
inline constexpr char kCrossDeskSecureInputCaptureCommandPrefix[] = "capture:";
|
inline constexpr char kCrossDeskSecureInputCaptureCommandPrefix[] = "capture:";
|
||||||
|
inline constexpr char kCrossDeskSecureInputCaptureStartCommandPrefix[] =
|
||||||
|
"capture-start:";
|
||||||
|
inline constexpr char kCrossDeskSecureInputCaptureStopCommand[] =
|
||||||
|
"capture-stop";
|
||||||
inline constexpr DWORD kCrossDeskSecureInputPipeBufferBytes = 16 * 1024 * 1024;
|
inline constexpr DWORD kCrossDeskSecureInputPipeBufferBytes = 16 * 1024 * 1024;
|
||||||
|
inline constexpr wchar_t kCrossDeskSecureDesktopFrameMappingPrefix[] =
|
||||||
|
L"Global\\CrossDeskSecureDesktopFrame-";
|
||||||
|
inline constexpr wchar_t kCrossDeskSecureDesktopFrameReadyEventPrefix[] =
|
||||||
|
L"Global\\CrossDeskSecureDesktopFrameReady-";
|
||||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
inline constexpr uint32_t kCrossDeskSecureDesktopFrameMagic = 0x50444358;
|
||||||
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
|
inline constexpr uint32_t kCrossDeskSecureDesktopFrameVersion = 1;
|
||||||
|
|
||||||
@@ -37,6 +45,19 @@ struct CrossDeskSecureDesktopFrameHeader {
|
|||||||
uint32_t height;
|
uint32_t height;
|
||||||
uint32_t payload_size;
|
uint32_t payload_size;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct CrossDeskSecureDesktopSharedFrameHeader {
|
||||||
|
uint32_t magic;
|
||||||
|
uint32_t version;
|
||||||
|
volatile uint32_t writing;
|
||||||
|
uint32_t sequence;
|
||||||
|
int32_t left;
|
||||||
|
int32_t top;
|
||||||
|
uint32_t width;
|
||||||
|
uint32_t height;
|
||||||
|
uint32_t payload_size;
|
||||||
|
uint32_t buffer_size;
|
||||||
|
};
|
||||||
#pragma pack(pop)
|
#pragma pack(pop)
|
||||||
|
|
||||||
inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) {
|
inline std::wstring GetCrossDeskSessionHelperPipeName(DWORD session_id) {
|
||||||
@@ -49,6 +70,18 @@ inline std::wstring GetCrossDeskSecureInputHelperPipeName(DWORD session_id) {
|
|||||||
std::to_wstring(session_id);
|
std::to_wstring(session_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::wstring GetCrossDeskSecureDesktopFrameMappingName(
|
||||||
|
DWORD session_id) {
|
||||||
|
return std::wstring(kCrossDeskSecureDesktopFrameMappingPrefix) +
|
||||||
|
std::to_wstring(session_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline std::wstring GetCrossDeskSecureDesktopFrameReadyEventName(
|
||||||
|
DWORD session_id) {
|
||||||
|
return std::wstring(kCrossDeskSecureDesktopFrameReadyEventPrefix) +
|
||||||
|
std::to_wstring(session_id);
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace crossdesk
|
} // namespace crossdesk
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
+1
-1
Submodule submodules/minirtc updated: b8affa443e...bb0fae0617
@@ -39,6 +39,35 @@ bool ExpectContains(const char* name, const std::string& value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ExpectNotContains(const char* name, const std::string& value,
|
||||||
|
const std::string& unexpected) {
|
||||||
|
if (value.find(unexpected) == std::string::npos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExpectContainsAtLeast(const char* name, const std::string& value,
|
||||||
|
const std::string& expected, size_t min_count) {
|
||||||
|
size_t count = 0;
|
||||||
|
size_t pos = 0;
|
||||||
|
while ((pos = value.find(expected, pos)) != std::string::npos) {
|
||||||
|
++count;
|
||||||
|
pos += expected.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count >= min_count) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << name << " expected at least " << min_count
|
||||||
|
<< " occurrences of: " << expected << ", found " << count
|
||||||
|
<< "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
bool ExpectResetBeforeDisplayPopup(const std::string& value) {
|
bool ExpectResetBeforeDisplayPopup(const std::string& value) {
|
||||||
const std::string reset = "props->display_selectable_hovered_ = false;";
|
const std::string reset = "props->display_selectable_hovered_ = false;";
|
||||||
const std::string popup = "ImGui::BeginPopup(\"display\")";
|
const std::string popup = "ImGui::BeginPopup(\"display\")";
|
||||||
@@ -93,5 +122,62 @@ int main() {
|
|||||||
ok &= ExpectContains("control_bar.cpp", control_bar,
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
"props->shortcut_selectable_hovered_ =");
|
"props->shortcut_selectable_hovered_ =");
|
||||||
ok &= ExpectResetBeforeShortcutPopup(control_bar);
|
ok &= ExpectResetBeforeShortcutPopup(control_bar);
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"void ShowControlBarTooltip(const std::string& text)");
|
||||||
|
ok &= ExpectContainsAtLeast("control_bar.cpp", control_bar,
|
||||||
|
"ShowControlBarTooltip(", 10);
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::select_display"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::send_shortcut"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||||
|
"ShowControlBarTooltip("
|
||||||
|
"props->mouse_control_button_label_)");
|
||||||
|
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||||
|
"ShowControlBarTooltip("
|
||||||
|
"props->audio_capture_button_label_)");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::select_file"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||||
|
"ShowControlBarTooltip("
|
||||||
|
"props->net_traffic_stats_button_label_)");
|
||||||
|
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||||
|
"ShowControlBarTooltip("
|
||||||
|
"props->fullscreen_button_label_)");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::release_mouse"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::control_mouse"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::audio_capture"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::mute[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::hide_net_traffic_stats"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::show_net_traffic_stats"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::exit_fullscreen"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::fullscreen"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::disconnect"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::expand_control_bar"
|
||||||
|
"[localization_language_index_]");
|
||||||
|
ok &= ExpectContains("control_bar.cpp", control_bar,
|
||||||
|
"localization::collapse_control_bar"
|
||||||
|
"[localization_language_index_]");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "interactive_state.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::filesystem::path FindRepoRoot() {
|
||||||
|
std::filesystem::path current = std::filesystem::current_path();
|
||||||
|
while (!current.empty()) {
|
||||||
|
if (std::filesystem::exists(current / "xmake.lua") &&
|
||||||
|
std::filesystem::exists(
|
||||||
|
current / "src/service/windows/service_host.cpp")) {
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
current = current.parent_path();
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string ReadFile(const std::filesystem::path& path) {
|
||||||
|
std::ifstream file(path, std::ios::binary);
|
||||||
|
if (!file) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::ostringstream stream;
|
||||||
|
stream << file.rdbuf();
|
||||||
|
return stream.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExpectContains(const char* name, const std::string& value,
|
||||||
|
const std::string& expected) {
|
||||||
|
if (value.find(expected) != std::string::npos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << name << " missing expected text: " << expected << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExpectNotContains(const char* name, const std::string& value,
|
||||||
|
const std::string& unexpected) {
|
||||||
|
if (value.find(unexpected) == std::string::npos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ExpectTrue(const char* name, bool value) {
|
||||||
|
if (value) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << name << " expected true\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const std::filesystem::path repo_root = FindRepoRoot();
|
||||||
|
if (repo_root.empty()) {
|
||||||
|
std::cerr << "failed to locate repository root\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string control_bar =
|
||||||
|
ReadFile(repo_root / "src/gui/toolbars/control_bar.cpp");
|
||||||
|
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||||
|
const std::string render_h = ReadFile(repo_root / "src/gui/render.h");
|
||||||
|
const std::string service_host =
|
||||||
|
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
||||||
|
const std::string service_host_h =
|
||||||
|
ReadFile(repo_root / "src/service/windows/service_host.h");
|
||||||
|
const std::string session_helper =
|
||||||
|
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||||
|
|
||||||
|
bool ok = true;
|
||||||
|
ok &= ExpectTrue("secure desktop input routing",
|
||||||
|
crossdesk::IsSecureDesktopInteractionRequired(
|
||||||
|
"secure-desktop"));
|
||||||
|
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||||
|
"CanSendSecureAttentionSequence("
|
||||||
|
"props->remote_interactive_stage_)");
|
||||||
|
ok &= ExpectNotContains("control_bar.cpp", control_bar,
|
||||||
|
"ImGui::BeginDisabled();\n"
|
||||||
|
" }\n"
|
||||||
|
" if (ImGui::Selectable(sas_label.c_str()))");
|
||||||
|
ok &= ExpectNotContains("render.cpp", render, "sas_requires_lock_screen");
|
||||||
|
ok &= ExpectContains("render.h", render_h,
|
||||||
|
"optimistic_windows_secure_desktop_until_tick_");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"kWindowsServiceSasSecureDesktopGraceMs");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"status->sas_secure_desktop_grace_active");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"json.value(\"sas_secure_desktop_grace_active\", false)");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"status.sas_secure_desktop_grace_active");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"local_interactive_stage_ = \"secure-desktop\"");
|
||||||
|
ok &= ExpectContains("service_host.h", service_host_h,
|
||||||
|
"sas_secure_desktop_until_tick_");
|
||||||
|
ok &= ExpectContains("service_host.h", service_host_h,
|
||||||
|
"sas_secure_desktop_seen_");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"kSasSecureDesktopGraceMs");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"IsSasSecureDesktopGraceActiveLocked()");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"UpdateSasSecureDesktopGraceLocked("
|
||||||
|
"session_helper_report_interactive_stage_)");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"sas_secure_desktop_seen_ = true");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"sas_secure_desktop_until_tick_ = 0");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"sas_secure_desktop_until_tick_ =");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"now + kSasSecureDesktopGraceMs");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"\\\"sas_secure_desktop_grace_active\\\"");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"raw_interactive_stage = ResolveInteractiveStageLocked()");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"kSessionHelperStatePollMs = 1000");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"EVENT_SYSTEM_DESKTOPSWITCH");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"SetWinEventHook(");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"MsgWaitForMultipleObjects");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"WaitForSessionHelperStateChange(stop_event, "
|
||||||
|
"desktop_switch_event)");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"inaccessible_secure_input_desktop");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"desktop_info.error_code == ERROR_ACCESS_DENIED");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"secure_desktop_active = input_desktop_is_winlogon ||");
|
||||||
|
return ok ? 0 : 1;
|
||||||
|
}
|
||||||
@@ -39,6 +39,16 @@ bool ExpectContains(const char* name, const std::string& value,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool ExpectNotContains(const char* name, const std::string& value,
|
||||||
|
const std::string& unexpected) {
|
||||||
|
if (value.find(unexpected) == std::string::npos) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cerr << name << " contains unexpected text: " << unexpected << "\n";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int main() {
|
int main() {
|
||||||
@@ -50,13 +60,166 @@ int main() {
|
|||||||
|
|
||||||
const std::string service_host =
|
const std::string service_host =
|
||||||
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
ReadFile(repo_root / "src/service/windows/service_host.cpp");
|
||||||
|
const std::string service_host_h =
|
||||||
|
ReadFile(repo_root / "src/service/windows/service_host.h");
|
||||||
|
const std::string session_helper =
|
||||||
|
ReadFile(repo_root / "src/service/windows/session_helper_main.cpp");
|
||||||
|
const std::string targets =
|
||||||
|
ReadFile(repo_root / "xmake/targets.lua");
|
||||||
|
const std::string interactive_state =
|
||||||
|
ReadFile(repo_root / "src/service/windows/interactive_state.h");
|
||||||
|
const std::string render_callback =
|
||||||
|
ReadFile(repo_root / "src/gui/render_callback.cpp");
|
||||||
|
const std::string render = ReadFile(repo_root / "src/gui/render.cpp");
|
||||||
|
const std::string screen_capturer_h =
|
||||||
|
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.h");
|
||||||
|
const std::string screen_capturer_cpp =
|
||||||
|
ReadFile(repo_root / "src/screen_capturer/windows/screen_capturer_win.cpp");
|
||||||
|
|
||||||
bool ok = true;
|
bool ok = true;
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"ParseSecureDesktopMouseIpcCommand");
|
"ParseSecureDesktopMouseIpcCommand");
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"BuildSecureInputHelperMouseCommand");
|
"BuildSecureInputHelperMouseCommand");
|
||||||
|
ok &= ExpectContains("targets.lua", targets,
|
||||||
|
"target(\"crossdesk_session_helper\")");
|
||||||
|
ok &= ExpectContains("targets.lua", targets,
|
||||||
|
"add_files(\"scripts/windows/crossdesk.rc\")");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"EnablePerMonitorDpiAwareness");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"SetProcessDpiAwarenessContext(\n"
|
||||||
|
" DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2)");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"EnablePerMonitorDpiAwareness();\n\n"
|
||||||
|
" InitializeHelperLogger();");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"const ULONGLONG deadline_tick = GetTickCount64() + timeout_ms");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"while (GetTickCount64() <= deadline_tick)");
|
||||||
|
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||||
|
"constexpr int kPipeConnectRetryCount = 3");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"BuildSecureInputHelperKeyboardCommand(");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"const std::string& interactive_stage");
|
||||||
|
ok &= ExpectContains("service_host.h", service_host_h,
|
||||||
|
"bool LaunchSecureInputHelper(DWORD session_id,\n"
|
||||||
|
" const std::string& interactive_stage)");
|
||||||
|
ok &= ExpectContains("service_host.h", service_host_h,
|
||||||
|
"std::string secure_input_helper_interactive_stage_");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"SecureInputHelperDesktopForStage");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"return L\"winsta0\\\\Winlogon\"");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"return L\"winsta0\\\\default\"");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"secure_input_helper_interactive_stage_ == interactive_stage");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"secure_input_helper_interactive_stage_ = interactive_stage");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"secure_input_helper_interactive_stage_.clear()");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"LaunchSecureInputHelper(target_session_id, interactive_stage)");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"\\\"secure_input_helper_stage\\\":\\\"");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"session_helper_report_interactive_stage_");
|
||||||
ok &= ExpectContains("service_host.cpp", service_host,
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
"return SendSecureDesktopMouseInput");
|
"return SendSecureDesktopMouseInput");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"constexpr DWORD kWindowsServiceQueryTimeoutMs = 500");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"constexpr DWORD kSecureDesktopStatusPipeTimeoutMs = 500");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"IsTransientWindowsServiceStatusError(status.error)");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"IsTransientWindowsServiceStatusError(status.error)");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"Local Windows service temporarily unavailable");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"Windows capturer secure desktop service temporarily unavailable");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"Windows capturer secure desktop transient frame query failed");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"if (transient_error) {\n"
|
||||||
|
" LOG_INFO(");
|
||||||
|
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||||
|
"IsTransientSecureDesktopInputFailure");
|
||||||
|
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||||
|
"Secure desktop keyboard injection transient failure");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"MOUSEEVENTF_VIRTUALDESK");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"std::vector<INPUT> inputs");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"SendInput(static_cast<UINT>(inputs.size())");
|
||||||
|
ok &= ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||||
|
"SetCursorPos(request.x, request.y)");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"NormalizeAbsoluteMouseCoordinate");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"EnsureThreadInteractiveDesktop");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"OpenInputDesktop");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"DesktopNameForInteractiveStage");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"interactive_stage == \"credential-ui\"");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"return L\"Winlogon\"");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"interactive_stage == \"lock-screen\"");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"return L\"Default\"");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"EnsureThreadInteractiveDesktopForStage");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"switch_interactive_desktop_failed");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"Json BuildInputFailureJson");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"json[\"target_desktop\"]");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"json[\"current_desktop\"]");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"json[\"stage\"]");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"ParseSecureInputKeyboardCommand(command, &key_code, &is_down, &scan_code,\n"
|
||||||
|
" &extended, &interactive_stage)");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"InjectKeyboardInput(key_code, is_down, scan_code, extended,\n"
|
||||||
|
" interactive_stage)");
|
||||||
|
ok &= ExpectContains("session_helper_main.cpp", session_helper,
|
||||||
|
"InjectMouseInput(mouse_request)");
|
||||||
|
ok &= ExpectNotContains("session_helper_main.cpp", session_helper,
|
||||||
|
"EnsureThreadDesktop(L\"Winlogon\", &secure_desktop)");
|
||||||
|
ok &= ExpectContains("service_host.cpp", service_host,
|
||||||
|
"winsta0\\\\default");
|
||||||
|
ok &= ExpectNotContains("service_host.cpp", service_host,
|
||||||
|
"startup_info.lpDesktop = const_cast<LPWSTR>(L\"winsta0\\\\Winlogon\")");
|
||||||
|
ok &= ExpectContains("interactive_state.h", interactive_state,
|
||||||
|
"interactive_stage == \"lock-screen\"");
|
||||||
|
ok &= ExpectContains("render_callback.cpp", render_callback,
|
||||||
|
"RemoteAction remote_action{};");
|
||||||
|
ok &= ExpectContains("render.cpp", render,
|
||||||
|
"previous_secure_desktop_interaction");
|
||||||
|
ok &= ExpectNotContains(
|
||||||
|
"render_callback.cpp", render_callback,
|
||||||
|
"render->local_service_available_ &&\n"
|
||||||
|
" IsSecureDesktopInteractionRequired(render->local_interactive_stage_)");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.h", screen_capturer_h,
|
||||||
|
"std::string secure_shared_stage_;");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"const std::string& stage");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"secure_shared_stage_ == stage");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"secure_shared_stage_ = stage");
|
||||||
|
ok &= ExpectContains("screen_capturer_win.cpp", screen_capturer_cpp,
|
||||||
|
"secure_shared_stage_.clear()");
|
||||||
return ok ? 0 : 1;
|
return ok ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,12 @@ function setup_targets()
|
|||||||
set_default(false)
|
set_default(false)
|
||||||
add_files("tests/windows_mouse_controller_safety_test.cpp")
|
add_files("tests/windows_mouse_controller_safety_test.cpp")
|
||||||
|
|
||||||
|
target("windows_sas_guard_test")
|
||||||
|
set_kind("binary")
|
||||||
|
set_default(false)
|
||||||
|
add_includedirs("src/service/windows")
|
||||||
|
add_files("tests/windows_sas_guard_test.cpp")
|
||||||
|
|
||||||
target("display_popup_hover_state_test")
|
target("display_popup_hover_state_test")
|
||||||
set_kind("binary")
|
set_kind("binary")
|
||||||
set_default(false)
|
set_default(false)
|
||||||
@@ -217,6 +223,7 @@ function setup_targets()
|
|||||||
add_deps("rd_log", "path_manager")
|
add_deps("rd_log", "path_manager")
|
||||||
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
add_links("Advapi32", "User32", "Wtsapi32", "Gdi32")
|
||||||
add_files("src/service/windows/session_helper_main.cpp")
|
add_files("src/service/windows/session_helper_main.cpp")
|
||||||
|
add_files("scripts/windows/crossdesk.rc")
|
||||||
add_includedirs("src/service/windows", {public = true})
|
add_includedirs("src/service/windows", {public = true})
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user