diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index aa78a4d..e359c67 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,6 +7,11 @@ on: tags: - "*" workflow_dispatch: + inputs: + patch: + description: "Hotfix patch number, for example 1 or 2. Use 0 for a normal build." + required: false + default: "0" permissions: contents: write @@ -37,21 +42,53 @@ jobs: steps: - name: Extract version number + shell: bash run: | - VERSION="${GITHUB_REF##*/}" - VERSION_NUM="${VERSION#v}" - echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV + VERSION_REF="${GITHUB_REF##*/}" + VERSION_BASE="${VERSION_REF#v}" + PATCH_NUMBER="${{ github.event.inputs.patch }}" + BUILD_DATE_OVERRIDE="" + + if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then + PATCH_NUMBER=0 + fi + + if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + PATCH_NUMBER="${BASH_REMATCH[3]}" + BUILD_DATE_OVERRIDE="${BASH_REMATCH[4]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}" + PATCH_NUMBER="${BASH_REMATCH[4]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + BUILD_DATE_OVERRIDE="${BASH_REMATCH[3]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + PATCH_NUMBER="${BASH_REMATCH[3]}" + fi + + echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_ENV + echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV + echo "BUILD_DATE_OVERRIDE=${BUILD_DATE_OVERRIDE}" >> $GITHUB_ENV - name: Set legal Debian version shell: bash run: | - SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) - BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) + BUILD_DATE="${BUILD_DATE_OVERRIDE}" + if [[ -z "${BUILD_DATE}" ]]; then + BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) + fi - if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then - LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" + if [[ ! "${VERSION_BASE}" =~ ^[0-9] ]]; then + VERSION_BASE="0.0.0-${VERSION_BASE}" + fi + + if [[ "${PATCH_NUMBER}" != "0" ]]; then + LEGAL_VERSION="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}" else - LEGAL_VERSION="v${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}" + LEGAL_VERSION="v${VERSION_BASE}-${BUILD_DATE}" fi echo "LEGAL_VERSION=${LEGAL_VERSION}" >> $GITHUB_ENV @@ -102,14 +139,43 @@ jobs: steps: - name: Extract version number id: version + shell: bash run: | - VERSION="${GITHUB_REF##*/}" - SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) + VERSION_REF="${GITHUB_REF##*/}" + VERSION_BASE="${VERSION_REF#v}" BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) - VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}" + PATCH_NUMBER="${{ github.event.inputs.patch }}" + + if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then + PATCH_NUMBER=0 + fi + + if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + PATCH_NUMBER="${BASH_REMATCH[3]}" + BUILD_DATE="${BASH_REMATCH[4]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + BUILD_DATE="${BASH_REMATCH[3]}" + PATCH_NUMBER="${BASH_REMATCH[4]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + BUILD_DATE="${BASH_REMATCH[3]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + PATCH_NUMBER="${BASH_REMATCH[3]}" + fi + + if [[ "${PATCH_NUMBER}" != "0" ]]; then + VERSION_NUM="v${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}" + else + VERSION_NUM="v${VERSION_BASE}-${BUILD_DATE}" + fi + echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV echo "VERSION_NUM=${VERSION_NUM}" echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_ENV + echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_ENV - name: Cache xmake dependencies uses: actions/cache@v5 @@ -163,10 +229,38 @@ jobs: $version = $ref -replace '^refs/(tags|heads)/', '' $version = $version -replace '^v', '' $version = $version -replace '/', '-' - $SHORT_SHA = $env:GITHUB_SHA.Substring(0,7) $BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd") - echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV + $PATCH_NUMBER = "${{ github.event.inputs.patch }}" + + if ($PATCH_NUMBER -notmatch '^[0-9]+$') { + $PATCH_NUMBER = "0" + } + + if ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$') { + $version = $Matches[1] + $PATCH_NUMBER = $Matches[3] + $BUILD_DATE = $Matches[4] + } elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') { + $version = $Matches[1] + $BUILD_DATE = $Matches[3] + $PATCH_NUMBER = $Matches[4] + } elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$') { + $version = $Matches[1] + $BUILD_DATE = $Matches[3] + } elseif ($version -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$' -and $PATCH_NUMBER -eq "0") { + $version = $Matches[1] + $PATCH_NUMBER = $Matches[3] + } + + if ($PATCH_NUMBER -ne "0") { + $VERSION_NUM = "v$version-$PATCH_NUMBER-$BUILD_DATE" + } else { + $VERSION_NUM = "v$version-$BUILD_DATE" + } + + echo "VERSION_NUM=$VERSION_NUM" >> $env:GITHUB_ENV echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV + echo "PATCH_NUMBER=$PATCH_NUMBER" >> $env:GITHUB_ENV - name: Cache xmake dependencies uses: actions/cache@v5 @@ -239,8 +333,7 @@ jobs: - name: Package shell: pwsh run: | - cd "${{ github.workspace }}\scripts\windows" - makensis /DVERSION=$env:VERSION_NUM nsis_script.nsi + & "${{ github.workspace }}\scripts\windows\pkg_x64.ps1" $env:VERSION_NUM - name: Build Portable CrossDesk run: | @@ -310,19 +403,47 @@ jobs: - name: Extract version number id: version + shell: bash run: | - VERSION="${GITHUB_REF##*/}" - SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7) + VERSION_REF="${GITHUB_REF##*/}" + VERSION_BASE="${VERSION_REF#v}" BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d) - BUILD_DATE_ISO=$(TZ=Asia/Shanghai date +%Y-%m-%d) - VERSION_NUM="${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}" + PATCH_NUMBER="${{ github.event.inputs.patch }}" + + if [[ ! "${PATCH_NUMBER}" =~ ^[0-9]+$ ]]; then + PATCH_NUMBER=0 + fi + + if [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + PATCH_NUMBER="${BASH_REMATCH[3]}" + BUILD_DATE="${BASH_REMATCH[4]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + BUILD_DATE="${BASH_REMATCH[3]}" + PATCH_NUMBER="${BASH_REMATCH[4]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + BUILD_DATE="${BASH_REMATCH[3]}" + elif [[ "${VERSION_BASE}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)$ && "${PATCH_NUMBER}" == "0" ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + PATCH_NUMBER="${BASH_REMATCH[3]}" + fi + + BUILD_DATE_ISO="${BUILD_DATE:0:4}-${BUILD_DATE:4:2}-${BUILD_DATE:6:2}" + if [[ "${PATCH_NUMBER}" != "0" ]]; then + VERSION_NUM="${VERSION_BASE}-${PATCH_NUMBER}-${BUILD_DATE}" + else + VERSION_NUM="${VERSION_BASE}-${BUILD_DATE}" + fi + VERSION_WITH_V="v${VERSION_NUM}" - VERSION_ONLY="${VERSION#v}" echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_OUTPUT echo "VERSION_WITH_V=${VERSION_WITH_V}" >> $GITHUB_OUTPUT - echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT + echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT echo "BUILD_DATE=${BUILD_DATE}" >> $GITHUB_OUTPUT echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT + echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT - name: Rename artifacts run: | @@ -380,8 +501,10 @@ jobs: run: | cat > version.json << EOF { - "version": "${{ steps.version.outputs.VERSION_ONLY }}", + "latest_version": "${{ steps.version.outputs.VERSION_NUM }}", + "version": "${{ steps.version.outputs.VERSION_NUM }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", + "patch": ${{ steps.version.outputs.PATCH_NUMBER }}, "releaseName": "", "releaseNotes": "", "tagName": "${{ steps.version.outputs.VERSION_WITH_V }}", diff --git a/.github/workflows/update-version-json.yml b/.github/workflows/update-version-json.yml index ae3db0f..d92bd95 100644 --- a/.github/workflows/update-version-json.yml +++ b/.github/workflows/update-version-json.yml @@ -20,21 +20,39 @@ jobs: - name: Extract version from tag id: version + shell: bash run: | TAG_NAME="${{ github.event.release.tag_name }}" - VERSION_ONLY="${TAG_NAME#v}" - echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT - echo "VERSION_ONLY=${VERSION_ONLY}" >> $GITHUB_OUTPUT + TAG_VERSION="${TAG_NAME#v}" + VERSION_FULL="${TAG_VERSION}" + VERSION_BASE="${TAG_VERSION}" + PATCH_NUMBER=0 - # Extract date from tag if available (format: v1.2.3-20251113-abc) - if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then - DATE_STR="${BASH_REMATCH[1]}" + # Extract date and patch from tags such as v1.2.3-1-20251113. + if [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]+)-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + PATCH_NUMBER="${BASH_REMATCH[3]}" + DATE_STR="${BASH_REMATCH[4]}" + BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" + elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + DATE_STR="${BASH_REMATCH[3]}" + PATCH_NUMBER="${BASH_REMATCH[4]}" + BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" + elif [[ "${TAG_VERSION}" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})$ ]]; then + VERSION_BASE="${BASH_REMATCH[1]}" + DATE_STR="${BASH_REMATCH[3]}" BUILD_DATE_ISO="${DATE_STR:0:4}-${DATE_STR:4:2}-${DATE_STR:6:2}" else # Use release published date BUILD_DATE_ISO=$(echo "${{ github.event.release.published_at }}" | cut -d'T' -f1) fi + + echo "TAG_NAME=${TAG_NAME}" >> $GITHUB_OUTPUT + echo "VERSION_FULL=${VERSION_FULL}" >> $GITHUB_OUTPUT + echo "VERSION_BASE=${VERSION_BASE}" >> $GITHUB_OUTPUT echo "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT + echo "PATCH_NUMBER=${PATCH_NUMBER}" >> $GITHUB_OUTPUT - name: Install jq run: sudo apt-get update && sudo apt-get install -y jq @@ -122,8 +140,10 @@ jobs: # Generate version.json using cat and heredoc cat > version.json << EOF { - "version": "${{ steps.version.outputs.VERSION_ONLY }}", + "latest_version": "${{ steps.version.outputs.VERSION_FULL }}", + "version": "${{ steps.version.outputs.VERSION_FULL }}", "releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}", + "patch": ${{ steps.version.outputs.PATCH_NUMBER }}, "releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }}, "releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }}, "tagName": "${{ steps.version.outputs.TAG_NAME }}", diff --git a/scripts/linux/pkg_amd64.sh b/scripts/linux/pkg_amd64.sh index eb68104..be9fab2 100644 --- a/scripts/linux/pkg_amd64.sh +++ b/scripts/linux/pkg_amd64.sh @@ -4,13 +4,31 @@ set -e PKG_NAME="crossdesk" APP_NAME="CrossDesk" -APP_VERSION="$1" ARCHITECTURE="amd64" MAINTAINER="Junkun Di " DESCRIPTION="A simple cross-platform remote desktop client." ALSA_RUNTIME_DEP="libasound2 | libasound2t64" PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" +normalize_app_version() { + local input="$1" + local prefix="" + local body="$input" + + if [[ "$body" == v* ]]; then + prefix="v" + body="${body#v}" + fi + + if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}" + else + echo "$input" + fi +} + +APP_VERSION="$(normalize_app_version "$1")" + # Remove 'v' prefix from version for Debian package (Debian version must start with digit) DEB_VERSION="${APP_VERSION#v}" diff --git a/scripts/linux/pkg_arm64.sh b/scripts/linux/pkg_arm64.sh index 7d304f9..ff97436 100644 --- a/scripts/linux/pkg_arm64.sh +++ b/scripts/linux/pkg_arm64.sh @@ -4,13 +4,31 @@ set -e PKG_NAME="crossdesk" APP_NAME="CrossDesk" -APP_VERSION="$1" ARCHITECTURE="arm64" MAINTAINER="Junkun Di " DESCRIPTION="A simple cross-platform remote desktop client." ALSA_RUNTIME_DEP="libasound2 | libasound2t64" PORTAL_RUNTIME_RECOMMENDS="xdg-desktop-portal, xdg-desktop-portal-gtk | xdg-desktop-portal-kde | xdg-desktop-portal-wlr" +normalize_app_version() { + local input="$1" + local prefix="" + local body="$input" + + if [[ "$body" == v* ]]; then + prefix="v" + body="${body#v}" + fi + + if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}" + else + echo "$input" + fi +} + +APP_VERSION="$(normalize_app_version "$1")" + # Remove 'v' prefix from version for Debian package (Debian version must start with digit) DEB_VERSION="${APP_VERSION#v}" diff --git a/scripts/macosx/pkg_arm64.sh b/scripts/macosx/pkg_arm64.sh index ff37114..edf3eeb 100755 --- a/scripts/macosx/pkg_arm64.sh +++ b/scripts/macosx/pkg_arm64.sh @@ -4,13 +4,31 @@ set -e APP_NAME="crossdesk" APP_NAME_UPPER="CrossDesk" EXECUTABLE_PATH="./build/macosx/arm64/release/crossdesk" -APP_VERSION="$1" PLATFORM="macos" ARCH="arm64" IDENTIFIER="cn.crossdesk.app" ICON_PATH="icons/macos/crossdesk.icns" MACOS_MIN_VERSION="10.12" +normalize_app_version() { + local input="$1" + local prefix="" + local body="$input" + + if [[ "$body" == v* ]]; then + prefix="v" + body="${body#v}" + fi + + if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}" + else + echo "$input" + fi +} + +APP_VERSION="$(normalize_app_version "$1")" + APP_BUNDLE="${APP_NAME_UPPER}.app" CONTENTS_DIR="${APP_BUNDLE}/Contents" MACOS_DIR="${CONTENTS_DIR}/MacOS" diff --git a/scripts/macosx/pkg_x64.sh b/scripts/macosx/pkg_x64.sh index 37b7a97..0d4babc 100755 --- a/scripts/macosx/pkg_x64.sh +++ b/scripts/macosx/pkg_x64.sh @@ -4,13 +4,31 @@ set -e APP_NAME="crossdesk" APP_NAME_UPPER="CrossDesk" EXECUTABLE_PATH="build/macosx/x86_64/release/crossdesk" -APP_VERSION="$1" PLATFORM="macos" ARCH="x64" IDENTIFIER="cn.crossdesk.app" ICON_PATH="icons/macos/crossdesk.icns" MACOS_MIN_VERSION="10.12" +normalize_app_version() { + local input="$1" + local prefix="" + local body="$input" + + if [[ "$body" == v* ]]; then + prefix="v" + body="${body#v}" + fi + + if [[ "$body" =~ ^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$ ]]; then + echo "${prefix}${BASH_REMATCH[1]}-${BASH_REMATCH[4]}-${BASH_REMATCH[3]}" + else + echo "$input" + fi +} + +APP_VERSION="$(normalize_app_version "$1")" + APP_BUNDLE="${APP_NAME_UPPER}.app" CONTENTS_DIR="${APP_BUNDLE}/Contents" MACOS_DIR="${CONTENTS_DIR}/MacOS" diff --git a/scripts/windows/crossdesk_portable.manifest b/scripts/windows/crossdesk_portable.manifest new file mode 100644 index 0000000..158968e --- /dev/null +++ b/scripts/windows/crossdesk_portable.manifest @@ -0,0 +1,32 @@ + + + + + + CrossDesk Portable Application + + + + + + + + + + + + true/pm + PerMonitorV2 + + + + + + + + + + diff --git a/scripts/windows/crossdesk_portable.rc b/scripts/windows/crossdesk_portable.rc new file mode 100644 index 0000000..a391fb5 --- /dev/null +++ b/scripts/windows/crossdesk_portable.rc @@ -0,0 +1,8 @@ +// Portable build resource. The app itself runs as the current user; service +// installation raises a separate UAC prompt only when the user chooses it. +IDI_ICON1 ICON "..\\..\\icons\\windows\\crossdesk.ico" + +#define CREATEPROCESS_MANIFEST_RESOURCE_ID 1 +#define RT_MANIFEST 24 + +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "crossdesk_portable.manifest" diff --git a/scripts/windows/pkg_x64.ps1 b/scripts/windows/pkg_x64.ps1 new file mode 100644 index 0000000..83fc30f --- /dev/null +++ b/scripts/windows/pkg_x64.ps1 @@ -0,0 +1,40 @@ +param( + [Parameter(Mandatory = $true)] + [string]$Version +) + +$ErrorActionPreference = "Stop" + +function Normalize-AppVersion { + param( + [Parameter(Mandatory = $true)] + [string]$InputVersion + ) + + $prefix = "" + $body = $InputVersion + + if ($body.StartsWith("v")) { + $prefix = "v" + $body = $body.Substring(1) + } + + if ($body -match '^([0-9]+(\.[0-9]+){1,3})-([0-9]{8})-([0-9]+)$') { + return "${prefix}$($Matches[1])-$($Matches[4])-$($Matches[3])" + } + + return $InputVersion +} + +$normalizedVersion = Normalize-AppVersion -InputVersion $Version +$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +Push-Location $scriptDir +try { + & makensis "/DVERSION=$normalizedVersion" "nsis_script.nsi" + if ($LASTEXITCODE -ne 0) { + exit $LASTEXITCODE + } +} finally { + Pop-Location +} diff --git a/src/config_center/config_center.cpp b/src/config_center/config_center.cpp index 3a6260e..3e7cbd5 100644 --- a/src/config_center/config_center.cpp +++ b/src/config_center/config_center.cpp @@ -79,6 +79,9 @@ int ConfigCenter::Load() { enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_); enable_minimize_to_tray_ = ini_.GetBoolValue( section_, "enable_minimize_to_tray", enable_minimize_to_tray_); + portable_service_prompt_suppressed_ = + ini_.GetBoolValue(section_, "portable_service_prompt_suppressed", + portable_service_prompt_suppressed_); const char* file_transfer_save_path_value = ini_.GetValue(section_, "file_transfer_save_path", nullptr); @@ -118,6 +121,8 @@ int ConfigCenter::Save() { ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_); ini_.SetBoolValue(section_, "enable_minimize_to_tray", enable_minimize_to_tray_); + ini_.SetBoolValue(section_, "portable_service_prompt_suppressed", + portable_service_prompt_suppressed_); ini_.SetValue(section_, "file_transfer_save_path", file_transfer_save_path_.c_str()); @@ -325,6 +330,18 @@ int ConfigCenter::SetDaemon(bool enable_daemon) { return 0; } +int ConfigCenter::SetPortableServicePromptSuppressed(bool suppressed) { + portable_service_prompt_suppressed_ = suppressed; + ini_.SetBoolValue(section_, "portable_service_prompt_suppressed", + portable_service_prompt_suppressed_); + SI_Error rc = ini_.SaveFile(config_path_.c_str()); + if (rc < 0) { + return -1; + } + + return 0; +} + // getters ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; } @@ -377,6 +394,10 @@ bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; } bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; } +bool ConfigCenter::IsPortableServicePromptSuppressed() const { + return portable_service_prompt_suppressed_; +} + int ConfigCenter::SetFileTransferSavePath(const std::string& path) { file_transfer_save_path_ = path; ini_.SetValue(section_, "file_transfer_save_path", diff --git a/src/config_center/config_center.h b/src/config_center/config_center.h index 2258f71..acb2a5c 100644 --- a/src/config_center/config_center.h +++ b/src/config_center/config_center.h @@ -39,6 +39,7 @@ class ConfigCenter { int SetMinimizeToTray(bool enable_minimize_to_tray); int SetAutostart(bool enable_autostart); int SetDaemon(bool enable_daemon); + int SetPortableServicePromptSuppressed(bool suppressed); int SetFileTransferSavePath(const std::string& path); // read config @@ -60,6 +61,7 @@ class ConfigCenter { bool IsMinimizeToTray() const; bool IsEnableAutostart() const; bool IsEnableDaemon() const; + bool IsPortableServicePromptSuppressed() const; std::string GetFileTransferSavePath() const; int Load(); @@ -87,6 +89,7 @@ class ConfigCenter { bool enable_minimize_to_tray_ = false; bool enable_autostart_ = false; bool enable_daemon_ = false; + bool portable_service_prompt_suppressed_ = false; std::string file_transfer_save_path_ = ""; }; } // namespace crossdesk diff --git a/src/device_controller/keyboard/mac/keyboard_capturer.cpp b/src/device_controller/keyboard/mac/keyboard_capturer.cpp index 7de0496..dc6ac0d 100644 --- a/src/device_controller/keyboard/mac/keyboard_capturer.cpp +++ b/src/device_controller/keyboard/mac/keyboard_capturer.cpp @@ -310,6 +310,10 @@ int KeyboardCapturer::SendKeyboardCommand(int key_code, bool is_down, if (IsFunctionKey(cg_key_code) && !is_down) { CGEventRef fn_release_event = CGEventCreateKeyboardEvent(NULL, fn_key_code_, false); + if (!fn_release_event) { + LOG_ERROR("CGEventCreateKeyboardEvent failed for fn release"); + return -1; + } CGEventPost(kCGHIDEventTap, fn_release_event); CFRelease(fn_release_event); } diff --git a/src/device_controller/mouse/mac/mouse_controller.cpp b/src/device_controller/mouse/mac/mouse_controller.cpp index 11b270a..ddbf635 100644 --- a/src/device_controller/mouse/mac/mouse_controller.cpp +++ b/src/device_controller/mouse/mac/mouse_controller.cpp @@ -1,6 +1,7 @@ #include "mouse_controller.h" #include +#include #include "rd_log.h" @@ -20,85 +21,101 @@ int MouseController::Destroy() { return 0; } int MouseController::SendMouseCommand(RemoteAction remote_action, int display_index) { + if (remote_action.type != ControlType::mouse) { + return 0; + } + + if (display_index < 0 || + display_index >= static_cast(display_info_list_.size())) { + LOG_WARN("Mouse command skipped, invalid display_index={}, displays={}", + display_index, display_info_list_.size()); + return -1; + } + + const DisplayInfo& display_info = display_info_list_[display_index]; + if (display_info.width <= 0 || display_info.height <= 0) { + LOG_WARN("Mouse command skipped, invalid display geometry: {}x{}", + display_info.width, display_info.height); + return -1; + } + + const float normalized_x = std::clamp(remote_action.m.x, 0.0f, 1.0f); + const float normalized_y = std::clamp(remote_action.m.y, 0.0f, 1.0f); int mouse_pos_x = - remote_action.m.x * display_info_list_[display_index].width + - display_info_list_[display_index].left; + normalized_x * display_info.width + display_info.left; int mouse_pos_y = - remote_action.m.y * display_info_list_[display_index].height + - display_info_list_[display_index].top; + normalized_y * display_info.height + display_info.top; - if (remote_action.type == ControlType::mouse) { - CGEventRef mouse_event = nullptr; - CGEventType mouse_type; - CGMouseButton mouse_button; - CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y); + CGEventRef mouse_event = nullptr; + CGEventType mouse_type; + CGMouseButton mouse_button; + CGPoint mouse_point = CGPointMake(mouse_pos_x, mouse_pos_y); - switch (remote_action.m.flag) { - case MouseFlag::left_down: - mouse_type = kCGEventLeftMouseDown; - left_dragging_ = true; - mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, - kCGMouseButtonLeft); - break; - case MouseFlag::left_up: - mouse_type = kCGEventLeftMouseUp; - left_dragging_ = false; - mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, - kCGMouseButtonLeft); - break; - case MouseFlag::right_down: - mouse_type = kCGEventRightMouseDown; - right_dragging_ = true; - mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, - kCGMouseButtonRight); - break; - case MouseFlag::right_up: - mouse_type = kCGEventRightMouseUp; - right_dragging_ = false; - mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, - kCGMouseButtonRight); - break; - case MouseFlag::middle_down: - mouse_type = kCGEventOtherMouseDown; - mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, - kCGMouseButtonCenter); - break; - case MouseFlag::middle_up: - mouse_type = kCGEventOtherMouseUp; - mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, - kCGMouseButtonCenter); - break; - case MouseFlag::wheel_vertical: - mouse_event = CGEventCreateScrollWheelEvent( - NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0); - break; - case MouseFlag::wheel_horizontal: - mouse_event = CGEventCreateScrollWheelEvent( - NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s); - break; - default: - if (left_dragging_) { - mouse_type = kCGEventLeftMouseDragged; - mouse_button = kCGMouseButtonLeft; - } else if (right_dragging_) { - mouse_type = kCGEventRightMouseDragged; - mouse_button = kCGMouseButtonRight; - } else { - mouse_type = kCGEventMouseMoved; - mouse_button = kCGMouseButtonLeft; - } + switch (remote_action.m.flag) { + case MouseFlag::left_down: + mouse_type = kCGEventLeftMouseDown; + left_dragging_ = true; + mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, + kCGMouseButtonLeft); + break; + case MouseFlag::left_up: + mouse_type = kCGEventLeftMouseUp; + left_dragging_ = false; + mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, + kCGMouseButtonLeft); + break; + case MouseFlag::right_down: + mouse_type = kCGEventRightMouseDown; + right_dragging_ = true; + mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, + kCGMouseButtonRight); + break; + case MouseFlag::right_up: + mouse_type = kCGEventRightMouseUp; + right_dragging_ = false; + mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, + kCGMouseButtonRight); + break; + case MouseFlag::middle_down: + mouse_type = kCGEventOtherMouseDown; + mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, + kCGMouseButtonCenter); + break; + case MouseFlag::middle_up: + mouse_type = kCGEventOtherMouseUp; + mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, + kCGMouseButtonCenter); + break; + case MouseFlag::wheel_vertical: + mouse_event = CGEventCreateScrollWheelEvent( + NULL, kCGScrollEventUnitLine, 2, remote_action.m.s, 0); + break; + case MouseFlag::wheel_horizontal: + mouse_event = CGEventCreateScrollWheelEvent( + NULL, kCGScrollEventUnitLine, 2, 0, remote_action.m.s); + break; + default: + if (left_dragging_) { + mouse_type = kCGEventLeftMouseDragged; + mouse_button = kCGMouseButtonLeft; + } else if (right_dragging_) { + mouse_type = kCGEventRightMouseDragged; + mouse_button = kCGMouseButtonRight; + } else { + mouse_type = kCGEventMouseMoved; + mouse_button = kCGMouseButtonLeft; + } - mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, - mouse_button); - break; - } + mouse_event = CGEventCreateMouseEvent(NULL, mouse_type, mouse_point, + mouse_button); + break; + } - if (mouse_event) { - CGEventPost(kCGHIDEventTap, mouse_event); - CFRelease(mouse_event); - } + if (mouse_event) { + CGEventPost(kCGHIDEventTap, mouse_event); + CFRelease(mouse_event); } return 0; } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/gui/assets/localization/localization_data.h b/src/gui/assets/localization/localization_data.h index 160f032..b78bc65 100644 --- a/src/gui/assets/localization/localization_data.h +++ b/src/gui/assets/localization/localization_data.h @@ -19,176 +19,203 @@ struct TranslationRow { }; // Single source of truth for all UI strings. -#define CROSSDESK_LOCALIZATION_ALL(X) \ - X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \ - X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \ - X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ - u8"Скопировано в буфер обмена") \ - X(password, u8"密码", "Password", u8"Пароль") \ - X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \ - X(remote_desktop, u8"远程桌面", "Remote Desktop", \ - u8"Удаленный рабочий стол") \ - X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \ - X(connect, u8"连接", "Connect", u8"Подключиться") \ - X(recent_connections, u8"近期连接", "Recent Connections", \ - u8"Недавние подключения") \ - X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \ - X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \ - X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \ - u8"Развернуть панель управления") \ - X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \ - u8"Свернуть панель управления") \ - X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \ - X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \ - u8"Показать статистику трафика") \ - X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \ - u8"Скрыть статистику трафика") \ - X(video, u8"视频", "Video", u8"Видео") \ - X(audio, u8"音频", "Audio", u8"Аудио") \ - X(data, u8"数据", "Data", u8"Данные") \ - X(total, u8"总计", "Total", u8"Итого") \ - X(in, u8"输入", "In", u8"Вход") \ - X(out, u8"输出", "Out", u8"Выход") \ - X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \ - X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \ - u8"Выйти из полноэкранного режима") \ - X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \ - X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \ - X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \ - X(mute, u8" 静音", " Mute", u8"Без звука") \ - X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \ - X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \ - X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \ - X(remote_password_box_visible, u8"远端密码框已出现", \ - "Remote password box visible", u8"Окно ввода пароля видно") \ - X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \ - "Remote lock screen visible, send SAS", \ - u8"Видна блокировка, отправьте SAS") \ - X(remote_secure_desktop_active, u8"远端已进入安全桌面", \ - "Remote secure desktop active", u8"Активен защищенный рабочий стол") \ - X(remote_service_unavailable, u8"远端Windows服务不可用", \ - "Remote Windows service unavailable", \ - u8"Служба Windows на удаленной стороне недоступна") \ - X(windows_service_setup_title, u8"安装 CrossDesk Service", \ - "Install CrossDesk Service", u8"Установить CrossDesk Service") \ - X(windows_service_setup_message, \ - u8"便携版需要安装本机Windows服务,以便在锁屏/登录界面/安全桌面下完整控制此电脑。检测到服务尚未安装,可点击安装并允许相关系统权限。", \ - "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.", \ - u8"Портативной версии нужна локальная служба Windows для полного управления на экране блокировки, входа и защищенном рабочем столе. Служба не установлена. Нажмите Установить и подтвердите системный запрос.") \ - X(install_windows_service, u8"安装", "Install", \ - u8"Установить службу") \ - X(installing_windows_service, u8"正在安装服务...", "Installing service...", \ - u8"Установка службы...") \ - X(windows_service_install_success, u8"服务已安装并启动", \ - "Service installed and started", u8"Служба установлена и запущена") \ - X(windows_service_install_failed, u8"服务安装失败,请确认便携目录内服务文件完整,并允许管理员权限。", \ - "Service installation failed. Check that the portable folder contains all service files and approve administrator permission.", \ - u8"Не удалось установить службу. Проверьте файлы службы в папке портативной версии и подтвердите права администратора.") \ - X(remote_unlock_requires_secure_desktop, \ - u8"当前仍需要安全桌面专用采集/输入", \ - "Secure desktop capture/input is still required", \ - u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \ - u8"стола") \ - X(settings, u8"设置", "Settings", u8"Настройки") \ - X(language, u8"语言:", "Language:", u8"Язык:") \ - X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \ - X(video_frame_rate, u8"画面采集帧率:", \ - "Video Capture Frame Rate:", u8"Частота захвата видео:") \ - X(video_quality_high, u8"高", "High", u8"Высокое") \ - X(video_quality_medium, u8"中", "Medium", u8"Среднее") \ - X(video_quality_low, u8"低", "Low", u8"Низкое") \ - X(video_encode_format, u8"视频编码格式:", \ - "Video Encode Format:", u8"Формат кодека видео:") \ - X(av1, u8"AV1", "AV1", "AV1") \ - X(h264, u8"H.264", "H.264", "H.264") \ - X(enable_hardware_video_codec, u8"启用硬件编解码器:", \ - "Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \ - X(enable_turn, u8"启用中继服务:", \ - "Enable TURN Service:", u8"Включить TURN-сервис:") \ - X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \ - X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \ - u8"Конфигурация self-hosted") \ - X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \ - u8"Настройки self-hosted") \ - X(self_hosted_server_address, u8"服务器地址:", \ - "Server Address:", u8"Адрес сервера:") \ - X(self_hosted_server_port, u8"信令服务端口:", \ - "Signal Service Port:", u8"Порт сигнального сервиса:") \ - X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \ - "Relay Service Port:", u8"Порт реле-сервиса:") \ - X(ok, u8"确认", "OK", u8"ОК") \ - X(cancel, u8"取消", "Cancel", u8"Отмена") \ - X(new_password, u8"请输入六位密码:", \ - "Please input a six-char password:", u8"Введите шестизначный пароль:") \ - X(input_password, u8"请输入密码:", \ - "Please input password:", u8"Введите пароль:") \ - X(validate_password, u8"验证密码中...", "Validate password ...", \ - u8"Проверка пароля...") \ - X(reinput_password, u8"请重新输入密码", "Please input password again", \ - u8"Повторно введите пароль") \ - X(remember_password, u8"记住密码", "Remember password", \ - u8"Запомнить пароль") \ - X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \ - X(signal_disconnected, u8"未连接服务器", "Disconnected", \ - u8"Нет подключения к серверу") \ - X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \ - X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \ - u8"P2P отключено") \ - X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \ - u8"Подключение P2P...") \ - X(receiving_screen, u8"画面接收中...", "Receiving screen...", \ - u8"Получение изображения...") \ - X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \ - X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \ - X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \ - X(about, u8"关于", "About", u8"О программе") \ - X(notification, u8"通知", "Notification", u8"Уведомление") \ - X(new_version_available, u8"新版本可用", "New Version Available", \ - u8"Доступна новая версия") \ - X(version, u8"版本", "Version", u8"Версия") \ - X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \ - X(access_website, u8"访问官网: ", \ - "Access Website: ", u8"Официальный сайт: ") \ - X(update, u8"更新", "Update", u8"Обновить") \ - X(confirm_delete_connection, u8"确认删除此连接", \ - "Confirm to delete this connection", u8"Удалить это подключение?") \ - X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \ - X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \ - X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \ - u8"Вступит в силу после перезапуска") \ - X(select_file, u8"选择文件发送", "Select File to Send", \ - u8"Выбрать файл для отправки") \ - X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \ - u8"Прогресс передачи файлов") \ - X(queued, u8"队列中", "Queued", u8"В очереди") \ - X(sending, u8"正在传输", "Sending", u8"Передача") \ - X(completed, u8"已完成", "Completed", u8"Завершено") \ - X(failed, u8"失败", "Failed", u8"Ошибка") \ - X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \ - X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \ - X(connection_status, 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"Для работы приложения требуются следующие разрешения:") \ +#define CROSSDESK_LOCALIZATION_ALL(X) \ + X(local_desktop, u8"本桌面", "Local Desktop", u8"Локальный рабочий стол") \ + X(local_id, u8"本机ID", "Local ID", u8"Локальный ID") \ + X(local_id_copied_to_clipboard, u8"已复制到剪贴板", "Copied to clipboard", \ + u8"Скопировано в буфер обмена") \ + X(password, u8"密码", "Password", u8"Пароль") \ + X(max_password_len, u8"最大6个字符", "Max 6 chars", u8"Макс. 6 символов") \ + X(remote_desktop, u8"远程桌面", "Remote Desktop", \ + u8"Удаленный рабочий стол") \ + X(remote_id, u8"对端ID", "Remote ID", u8"Удаленный ID") \ + X(connect, u8"连接", "Connect", u8"Подключиться") \ + X(recent_connections, u8"近期连接", "Recent Connections", \ + u8"Недавние подключения") \ + X(disconnect, u8"断开连接", "Disconnect", u8"Отключить") \ + X(select_display, u8"选择显示器", "Select Display", u8"Выбрать дисплей") \ + X(expand_control_bar, u8"展开控制栏", "Expand Control Bar", \ + u8"Развернуть панель управления") \ + X(collapse_control_bar, u8"收起控制栏", "Collapse Control Bar", \ + u8"Свернуть панель управления") \ + X(fullscreen, u8"全屏", " Fullscreen", u8"Полный экран") \ + X(show_net_traffic_stats, u8"显示网络状态", "Show Net Traffic Stats", \ + u8"Показать статистику трафика") \ + X(hide_net_traffic_stats, u8"隐藏网络状态", "Hide Net Traffic Stats", \ + u8"Скрыть статистику трафика") \ + X(video, u8"视频", "Video", u8"Видео") \ + X(audio, u8"音频", "Audio", u8"Аудио") \ + X(data, u8"数据", "Data", u8"Данные") \ + X(total, u8"总计", "Total", u8"Итого") \ + X(in, u8"输入", "In", u8"Вход") \ + X(out, u8"输出", "Out", u8"Выход") \ + X(loss_rate, u8"丢包率", "Loss Rate", u8"Потери пакетов") \ + X(exit_fullscreen, u8"退出全屏", "Exit fullscreen", \ + u8"Выйти из полноэкранного режима") \ + X(control_mouse, u8"控制鼠标", "Control Mouse", u8"Управление мышью") \ + X(release_mouse, u8"释放鼠标", "Release Mouse", u8"Освободить мышь") \ + X(audio_capture, u8"播放声音", "Audio Capture", u8"Воспроизведение звука") \ + X(mute, u8" 静音", " Mute", u8"Без звука") \ + X(send_shortcut, u8"发送组合键", "Send Shortcut", u8"Сочетания клавиш") \ + X(send_sas, u8"发送SAS", "Send SAS", u8"Отправить SAS") \ + X(lock_remote, u8"锁定远端", "Lock Remote", u8"Заблокировать") \ + X(remote_password_box_visible, u8"远端密码框已出现", \ + "Remote password box visible", u8"Окно ввода пароля видно") \ + X(remote_lock_screen_hint, u8"远端处于锁屏封面,可发送SAS", \ + "Remote lock screen visible, send SAS", \ + u8"Видна блокировка, отправьте SAS") \ + X(remote_secure_desktop_active, u8"远端已进入安全桌面", \ + "Remote secure desktop active", u8"Активен защищенный рабочий стол") \ + X(remote_service_unavailable, u8"远端Windows服务不可用", \ + "Remote Windows service unavailable", \ + u8"Служба Windows на удаленной стороне недоступна") \ + X(windows_service_setup_title, u8"安装 CrossDesk Service", \ + "Install CrossDesk Service", u8"Установить CrossDesk Service") \ + X(windows_service_setup_message, \ + u8"为支持该设备在锁屏状态下被远程控制,需要以管理员权限安装 CrossDesk " \ + u8"Service。\n未安装该服务不影响 CrossDesk " \ + u8"正常使用,仅无法在锁屏状态下控制本机。", \ + "To support remote control of this device while it is locked, CrossDesk " \ + "Service must be installed with administrator permission.\nWithout this " \ + "service, CrossDesk still works normally; only lock-screen control of " \ + "this computer is unavailable.", \ + u8"Чтобы поддерживать удаленное управление этим устройством на экране " \ + u8"блокировки, необходимо установить CrossDesk Service с правами " \ + u8"администратора.\nБез этой службы CrossDesk продолжит работать " \ + u8"нормально; будет недоступно только управление этим компьютером на " \ + u8"экране блокировки.") \ + X(install_windows_service, u8"安装", "Install", u8"Установить") \ + X(windows_service_settings_label, u8"锁屏控制服务:", \ + "Lock Screen Service:", u8"Служба блокировки экрана:") \ + X(windows_service_installed, u8"已安装", "Installed", u8"Установлена") \ + X(do_not_remind_again, u8"不再提醒", "Do not remind again", \ + u8"Больше не напоминать") \ + X(windows_service_prompt_suppressed_message, \ + u8"已不再提醒。后续如需启用锁屏状态下被远程控制,可在设置中点击“安装”。", \ + "You will not be reminded again. To enable remote control while locked " \ + "later, click Install in Settings.", \ + u8"Напоминание отключено. Чтобы позже включить удаленное управление на " \ + u8"экране блокировки, нажмите «Установить» в настройках.") \ + X(installing_windows_service, u8"正在安装服务...", "Installing service...", \ + u8"Установка службы...") \ + X(windows_service_install_success, u8"服务已安装并启动", \ + "Service installed and started", u8"Служба установлена и запущена") \ + X(windows_service_install_failed, \ + u8"服务安装失败。请确认 " \ + u8"CrossDesk.exe、crossdesk_service.exe、crossdesk_session_helper.exe " \ + u8"位于同一便携目录中,并在系统弹窗中允许管理员权限。", \ + "Service installation failed. Make sure CrossDesk.exe, " \ + "crossdesk_service.exe, and crossdesk_session_helper.exe are in the same " \ + "portable folder, then approve the administrator prompt.", \ + u8"Не удалось установить службу. Убедитесь, что CrossDesk.exe, " \ + u8"crossdesk_service.exe и crossdesk_session_helper.exe находятся в " \ + u8"одной папке портативной версии, затем подтвердите запрос прав " \ + u8"администратора.") \ + X(remote_unlock_requires_secure_desktop, \ + u8"当前仍需要安全桌面专用采集/输入", \ + "Secure desktop capture/input is still required", \ + u8"По-прежнему нужен отдельный захват/ввод для защищенного рабочего " \ + u8"стола") \ + X(settings, u8"设置", "Settings", u8"Настройки") \ + X(language, u8"语言:", "Language:", u8"Язык:") \ + X(video_quality, u8"视频质量:", "Video Quality:", u8"Качество видео:") \ + X(video_frame_rate, u8"画面采集帧率:", \ + "Video Capture Frame Rate:", u8"Частота захвата видео:") \ + X(video_quality_high, u8"高", "High", u8"Высокое") \ + X(video_quality_medium, u8"中", "Medium", u8"Среднее") \ + X(video_quality_low, u8"低", "Low", u8"Низкое") \ + X(video_encode_format, u8"视频编码格式:", \ + "Video Encode Format:", u8"Формат кодека видео:") \ + X(av1, u8"AV1", "AV1", "AV1") \ + X(h264, u8"H.264", "H.264", "H.264") \ + X(enable_hardware_video_codec, u8"启用硬件编解码器:", \ + "Enable Hardware Video Codec:", u8"Использовать аппаратный кодек:") \ + X(enable_turn, u8"启用中继服务:", \ + "Enable TURN Service:", u8"Включить TURN-сервис:") \ + X(enable_srtp, u8"启用SRTP:", "Enable SRTP:", u8"Включить SRTP:") \ + X(self_hosted_server_config, u8"自托管配置", "Self-Hosted Config", \ + u8"Конфигурация self-hosted") \ + X(self_hosted_server_settings, u8"自托管设置", "Self-Hosted Settings", \ + u8"Настройки self-hosted") \ + X(self_hosted_server_address, u8"服务器地址:", \ + "Server Address:", u8"Адрес сервера:") \ + X(self_hosted_server_port, u8"信令服务端口:", \ + "Signal Service Port:", u8"Порт сигнального сервиса:") \ + X(self_hosted_server_coturn_server_port, u8"中继服务端口:", \ + "Relay Service Port:", u8"Порт реле-сервиса:") \ + X(ok, u8"确认", "OK", u8"ОК") \ + X(cancel, u8"取消", "Cancel", u8"Отмена") \ + X(new_password, u8"请输入六位密码:", \ + "Please input a six-char password:", u8"Введите шестизначный пароль:") \ + X(input_password, u8"请输入密码:", \ + "Please input password:", u8"Введите пароль:") \ + X(validate_password, u8"验证密码中...", "Validate password ...", \ + u8"Проверка пароля...") \ + X(reinput_password, u8"请重新输入密码", "Please input password again", \ + u8"Повторно введите пароль") \ + X(remember_password, u8"记住密码", "Remember password", \ + u8"Запомнить пароль") \ + X(signal_connected, u8"已连接服务器", "Connected", u8"Подключено к серверу") \ + X(signal_disconnected, u8"未连接服务器", "Disconnected", \ + u8"Нет подключения к серверу") \ + X(p2p_connected, u8"对等连接已建立", "P2P Connected", u8"P2P подключено") \ + X(p2p_disconnected, u8"对等连接已断开", "P2P Disconnected", \ + u8"P2P отключено") \ + X(p2p_connecting, u8"正在建立对等连接...", "P2P Connecting ...", \ + u8"Подключение P2P...") \ + X(receiving_screen, u8"画面接收中...", "Receiving screen...", \ + u8"Получение изображения...") \ + X(p2p_failed, u8"对等连接失败", "P2P Failed", u8"Сбой P2P") \ + X(p2p_closed, u8"对等连接已关闭", "P2P closed", u8"P2P закрыто") \ + X(no_such_id, u8"无此ID", "No such ID", u8"ID не найден") \ + X(about, u8"关于", "About", u8"О программе") \ + X(notification, u8"通知", "Notification", u8"Уведомление") \ + X(new_version_available, u8"新版本可用", "New Version Available", \ + u8"Доступна новая версия") \ + X(version, u8"版本", "Version", u8"Версия") \ + X(release_date, u8"发布日期: ", "Release Date: ", u8"Дата релиза: ") \ + X(access_website, u8"访问官网: ", \ + "Access Website: ", u8"Официальный сайт: ") \ + X(update, u8"更新", "Update", u8"Обновить") \ + X(confirm_delete_connection, u8"确认删除此连接", \ + "Confirm to delete this connection", u8"Удалить это подключение?") \ + X(enable_autostart, u8"开机自启:", "Auto Start:", u8"Автозапуск:") \ + X(enable_daemon, u8"启用守护进程:", "Enable Daemon:", u8"Включить демон:") \ + X(takes_effect_after_restart, u8"重启后生效", "Takes effect after restart", \ + u8"Вступит в силу после перезапуска") \ + X(select_file, u8"选择文件发送", "Select File to Send", \ + u8"Выбрать файл для отправки") \ + X(file_transfer_progress, u8"文件传输进度", "File Transfer Progress", \ + u8"Прогресс передачи файлов") \ + X(queued, u8"队列中", "Queued", u8"В очереди") \ + X(sending, u8"正在传输", "Sending", u8"Передача") \ + X(completed, u8"已完成", "Completed", u8"Завершено") \ + X(failed, u8"失败", "Failed", u8"Ошибка") \ + X(controller, u8"控制端:", "Controller:", u8"Контроллер:") \ + X(file_transfer, u8"文件传输:", "File Transfer:", u8"Передача файлов:") \ + X(connection_status, 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"Выход") inline constexpr TranslationRow kTranslationRows[] = { diff --git a/src/gui/render.cpp b/src/gui/render.cpp index 01d2c95..81a172e 100644 --- a/src/gui/render.cpp +++ b/src/gui/render.cpp @@ -609,6 +609,11 @@ int Render::LoadSettingsFromCacheFile() { enable_autostart_ = config_center_->IsEnableAutostart(); enable_daemon_ = config_center_->IsEnableDaemon(); enable_minimize_to_tray_ = config_center_->IsMinimizeToTray(); +#if _WIN32 && CROSSDESK_PORTABLE + portable_service_prompt_suppressed_ = + config_center_->IsPortableServicePromptSuppressed(); + portable_service_do_not_remind_ = portable_service_prompt_suppressed_; +#endif // File transfer save path { @@ -764,11 +769,16 @@ int Render::StartSpeakerCapturer() { } if (speaker_capturer_) { - speaker_capturer_->Start(); + const int ret = speaker_capturer_->Start(); + if (ret != 0) { + LOG_ERROR("Start speaker capturer failed: {}", ret); + return ret; + } start_speaker_capturer_ = true; + return 0; } - return 0; + return -1; } int Render::StopSpeakerCapturer() { @@ -1149,8 +1159,9 @@ void Render::UpdateInteractions() { } if (start_speaker_capturer_ && !speaker_capturer_is_started_) { - StartSpeakerCapturer(); - speaker_capturer_is_started_ = true; + if (0 == StartSpeakerCapturer()) { + speaker_capturer_is_started_ = true; + } } else if (!start_speaker_capturer_ && speaker_capturer_is_started_) { StopSpeakerCapturer(); speaker_capturer_is_started_ = false; @@ -1708,26 +1719,6 @@ int Render::DrawServerWindow() { } int Render::Run() { - latest_version_info_ = CheckUpdate(); - if (!latest_version_info_.empty() && - latest_version_info_.contains("version") && - latest_version_info_["version"].is_string()) { - latest_version_ = 'v' + latest_version_info_["version"].get(); - if (latest_version_info_.contains("releaseNotes") && - latest_version_info_["releaseNotes"].is_string()) { - release_notes_ = latest_version_info_["releaseNotes"].get(); - } else { - release_notes_ = ""; - } - update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_); - if (update_available_) { - show_update_notification_window_ = true; - } - } else { - latest_version_ = ""; - update_available_ = false; - } - path_manager_ = std::make_unique("CrossDesk"); if (path_manager_) { exec_log_path_ = path_manager_->GetLogPath().string(); @@ -1756,6 +1747,41 @@ int Render::Run() { InitializeLogger(); LOG_INFO("CrossDesk version: {}", CROSSDESK_VERSION); + latest_version_info_ = CheckUpdate(); + if (!latest_version_info_.empty()) { + std::string version; + if (latest_version_info_.contains("latest_version") && + latest_version_info_["latest_version"].is_string()) { + version = latest_version_info_["latest_version"].get(); + } else if (latest_version_info_.contains("version") && + latest_version_info_["version"].is_string()) { + version = latest_version_info_["version"].get(); + } + + if (!version.empty()) { + latest_version_ = 'v' + version; + } else { + latest_version_ = ""; + } + if (latest_version_info_.contains("releaseNotes") && + latest_version_info_["releaseNotes"].is_string()) { + release_notes_ = latest_version_info_["releaseNotes"].get(); + } else { + release_notes_ = ""; + } + update_available_ = + !version.empty() && IsNewerVersion(CROSSDESK_VERSION, latest_version_); + LOG_INFO("Update check: current={}, latest={}, available={}", + CROSSDESK_VERSION, latest_version_, update_available_); + if (update_available_) { + show_update_notification_window_ = true; + } + } else { + latest_version_ = ""; + update_available_ = false; + LOG_WARN("Update check skipped: version.json is empty or missing latest_version"); + } + InitializeSettings(); InitializeSDL(); InitializeModules(); diff --git a/src/gui/render.h b/src/gui/render.h index 180f004..674c58c 100644 --- a/src/gui/render.h +++ b/src/gui/render.h @@ -553,8 +553,8 @@ class Render { std::string remote_client_id_ = ""; std::unordered_set pressed_keyboard_keys_; std::mutex pressed_keyboard_keys_mutex_; - SDL_Event last_mouse_event; - SDL_AudioStream* output_stream_; + SDL_Event last_mouse_event{}; + SDL_AudioStream* output_stream_ = nullptr; uint32_t STREAM_REFRESH_EVENT = 0; #if _WIN32 std::atomic pending_windows_service_sas_{false}; @@ -567,6 +567,9 @@ class Render { #if CROSSDESK_PORTABLE bool portable_service_prompt_checked_ = false; bool show_portable_service_install_window_ = false; + bool show_portable_service_prompt_suppressed_window_ = false; + bool portable_service_do_not_remind_ = false; + bool portable_service_prompt_suppressed_ = false; std::atomic portable_service_install_state_{ PortableServiceInstallState::idle}; std::thread portable_service_install_thread_; @@ -684,8 +687,8 @@ class Render { // Map file_id to FileTransferState for global file transfer (props == null) std::unordered_map file_id_to_transfer_state_; std::shared_mutex file_id_to_transfer_state_mutex_; - SDL_AudioDeviceID input_dev_; - SDL_AudioDeviceID output_dev_; + SDL_AudioDeviceID input_dev_ = 0; + SDL_AudioDeviceID output_dev_ = 0; ScreenCapturerFactory* screen_capturer_factory_ = nullptr; ScreenCapturer* screen_capturer_ = nullptr; SpeakerCapturerFactory* speaker_capturer_factory_ = nullptr; @@ -694,7 +697,7 @@ class Render { MouseController* mouse_controller_ = nullptr; KeyboardCapturer* keyboard_capturer_ = nullptr; std::vector display_info_list_; - uint64_t last_frame_time_; + uint64_t last_frame_time_ = 0; std::string last_video_frame_stream_id_; bool show_new_version_icon_ = false; bool show_new_version_icon_in_menu_ = true; diff --git a/src/gui/render_callback.cpp b/src/gui/render_callback.cpp index 8d6bcb5..202e533 100644 --- a/src/gui/render_callback.cpp +++ b/src/gui/render_callback.cpp @@ -1196,8 +1196,13 @@ void Render::OnReceiveDataBufferCb(const char* data, size_t size, remote_action.k.extended); } else if (remote_action.type == ControlType::display_id && render->screen_capturer_) { - render->selected_display_ = remote_action.d; - render->screen_capturer_->SwitchTo(remote_action.d); + const int ret = render->screen_capturer_->SwitchTo(remote_action.d); + if (ret == 0) { + render->selected_display_ = remote_action.d; + } else { + LOG_WARN("Display switch skipped, invalid display_id={}", + remote_action.d); + } } } } diff --git a/src/gui/windows/main_settings_window.cpp b/src/gui/windows/main_settings_window.cpp index 691badc..bfbd331 100644 --- a/src/gui/windows/main_settings_window.cpp +++ b/src/gui/windows/main_settings_window.cpp @@ -4,10 +4,19 @@ #include "render.h" #include "tinyfiledialogs.h" +#if _WIN32 && CROSSDESK_PORTABLE +#include "service_host.h" +#endif + namespace crossdesk { int Render::SettingWindow() { ImGuiIO& io = ImGui::GetIO(); + float portable_y_padding = 0.0f; +#if _WIN32 && CROSSDESK_PORTABLE + portable_y_padding = 0.05f; +#endif + if (show_settings_window_) { if (settings_window_pos_reset_) { const ImGuiViewport* viewport = ImGui::GetMainViewport(); @@ -18,12 +27,14 @@ int Render::SettingWindow() { ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.05f)); ImGui::SetNextWindowSize( - ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.9f)); + ImVec2(io.DisplaySize.x * 0.315f, + io.DisplaySize.y * (0.9f + portable_y_padding))); #else ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.343f, io.DisplaySize.y * 0.08f)); ImGui::SetNextWindowSize( - ImVec2(io.DisplaySize.x * 0.315f, io.DisplaySize.y * 0.85f)); + ImVec2(io.DisplaySize.x * 0.315f, + io.DisplaySize.y * (0.85f + portable_y_padding))); #endif } else { #if (((defined(_WIN32) || defined(__linux__)) && !defined(__aarch64__) && \ @@ -32,12 +43,14 @@ int Render::SettingWindow() { ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.05f)); ImGui::SetNextWindowSize( - ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.9f)); + ImVec2(io.DisplaySize.x * 0.42f, + io.DisplaySize.y * (0.9f + portable_y_padding))); #else ImGui::SetNextWindowPos( ImVec2(io.DisplaySize.x * 0.297f, io.DisplaySize.y * 0.08f)); ImGui::SetNextWindowSize( - ImVec2(io.DisplaySize.x * 0.407f, io.DisplaySize.y * 0.85f)); + ImVec2(io.DisplaySize.x * 0.42f, + io.DisplaySize.y * (0.85f + portable_y_padding))); #endif } @@ -73,23 +86,21 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); + ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f); } ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); - if (ImGui::BeginCombo( - "##language", - localization::GetSupportedLanguages() - [localization::detail::ClampLanguageIndex( - language_button_value_)] - .display_name - .c_str())) { + if (ImGui::BeginCombo("##language", + localization::GetSupportedLanguages() + [localization::detail::ClampLanguageIndex( + language_button_value_)] + .display_name.c_str())) { ImGui::SetWindowFontScale(0.5f); for (int i = 0; i < static_cast(supported_languages.size()); ++i) { bool selected = (i == language_button_value_); - if (ImGui::Selectable( - supported_languages[i].display_name.c_str(), selected)) + if (ImGui::Selectable(supported_languages[i].display_name.c_str(), + selected)) language_button_value_ = i; if (selected) { ImGui::SetItemDefaultFocus(); @@ -125,7 +136,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); + ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f); } ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); @@ -158,7 +169,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); + ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f); } ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); @@ -194,7 +205,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 3.0f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); + ImGui::SetCursorPosX(title_bar_button_width_ * 4.7f); } ImGui::SetNextItemWidth(title_bar_button_width_ * 1.8f); @@ -228,7 +239,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); + ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f); } ImGui::Checkbox("##enable_hardware_video_codec", @@ -249,7 +260,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); + ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f); } ImGui::Checkbox("##enable_turn", &enable_turn_); @@ -268,7 +279,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); + ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f); } ImGui::Checkbox("##enable_srtp", &enable_srtp_); @@ -289,7 +300,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); + ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f); } ImGui::Checkbox("##enable_self_hosted", &enable_self_hosted_); @@ -308,7 +319,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); + ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f); } ImGui::Checkbox("##enable_autostart_", &enable_autostart_); @@ -327,7 +338,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); + ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f); } ImGui::Checkbox("##enable_daemon_", &enable_daemon_); @@ -359,7 +370,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 4.275f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 5.755f); + ImGui::SetCursorPosX(title_bar_button_width_ * 5.955f); } ImGui::Checkbox("##enable_minimize_to_tray_", @@ -384,7 +395,7 @@ int Render::SettingWindow() { if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 2.82f); } else { - ImGui::SetCursorPosX(title_bar_button_width_ * 4.3f); + ImGui::SetCursorPosX(title_bar_button_width_ * 4.5f); } std::string display_path = @@ -429,6 +440,80 @@ int Render::SettingWindow() { ImGui::EndDisabled(); } +#if _WIN32 && CROSSDESK_PORTABLE + ImGui::Separator(); + + { + settings_items_offset += settings_items_padding; + ImGui::SetCursorPosY(settings_items_offset); + ImGui::AlignTextToFramePadding(); + ImGui::Text("%s", localization::windows_service_settings_label + [localization_language_index_] + .c_str()); + ImGui::SameLine(); + if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { + ImGui::SetCursorPosX(title_bar_button_width_ * 4.0f); + } else if (ConfigCenter::LANGUAGE::ENGLISH == localization_language_) { + ImGui::SetCursorPosX(title_bar_button_width_ * 5.42f); + } else { + ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f); + } + + const PortableServiceInstallState state = + portable_service_install_state_.load(std::memory_order_acquire); + const bool service_installed = + IsCrossDeskServiceInstalled() || + state == PortableServiceInstallState::succeeded; + if (service_installed) { + if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { + ImGui::SetCursorPosX(title_bar_button_width_ * 3.9f); + } else if (ConfigCenter::LANGUAGE::ENGLISH == + localization_language_) { + ImGui::SetCursorPosX(title_bar_button_width_ * 5.32f); + } else { + ImGui::SetCursorPosX(title_bar_button_width_ * 4.6f); + } + ImGui::Text("%s", localization::windows_service_installed + [localization_language_index_] + .c_str()); + } else { + if (state == PortableServiceInstallState::installing) { + ImGui::BeginDisabled(); + } + if (ImGui::Button(localization::install_windows_service + [localization_language_index_] + .c_str())) { + StartPortableWindowsServiceInstall(); + } + + if (state == PortableServiceInstallState::installing) { + ImGui::EndDisabled(); + ImGui::SameLine(); + ImGui::Text("%s", localization::installing_windows_service + [localization_language_index_] + .c_str()); + } else if (state == PortableServiceInstallState::failed) { + ImGui::SameLine(); + ImGui::Text( + "%s", + localization::failed[localization_language_index_].c_str()); + if (ImGui::IsItemHovered()) { + ImGui::BeginTooltip(); + ImGui::SetWindowFontScale(0.5f); + ImGui::PushTextWrapPos(title_bar_button_width_ * 10.0f); + ImGui::TextWrapped("%s", + localization::windows_service_install_failed + [localization_language_index_] + .c_str()); + ImGui::PopTextWrapPos(); + ImGui::SetWindowFontScale(1.0f); + ImGui::EndTooltip(); + } + } + } + } +#endif + if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) { ImGui::SetCursorPosX(title_bar_button_width_ * 1.59f); } else { @@ -436,7 +521,7 @@ int Render::SettingWindow() { } settings_items_offset += - settings_items_padding + title_bar_button_width_ * 0.3f; + settings_items_padding + title_bar_button_width_ * 0.15f; ImGui::SetCursorPosY(settings_items_offset); ImGui::PopStyleVar(); @@ -463,9 +548,8 @@ int Render::SettingWindow() { LOG_INFO("Set localization language: {}", localization::GetSupportedLanguages() [localization::detail::ClampLanguageIndex( - localization_language_index_)] - .code - .c_str()); + localization_language_index_)] + .code.c_str()); // Video quality if (video_quality_button_value_ == 0) { diff --git a/src/gui/windows/portable_service_install_window.cpp b/src/gui/windows/portable_service_install_window.cpp index bfc59d6..84669e7 100644 --- a/src/gui/windows/portable_service_install_window.cpp +++ b/src/gui/windows/portable_service_install_window.cpp @@ -4,6 +4,7 @@ #include +#include #include #include "localization.h" @@ -16,9 +17,8 @@ namespace { std::filesystem::path GetCurrentExecutablePath() { std::vector buffer(MAX_PATH); while (true) { - DWORD length = - GetModuleFileNameW(nullptr, buffer.data(), - static_cast(buffer.size())); + DWORD length = GetModuleFileNameW(nullptr, buffer.data(), + static_cast(buffer.size())); if (length == 0) { return {}; } @@ -46,7 +46,8 @@ bool InstallServiceWithElevation() { if (!std::filesystem::exists(service_path) || !std::filesystem::exists(helper_path)) { LOG_ERROR( - "Portable service install failed: service binaries missing, service={}, " + "Portable service install failed: service binaries missing, " + "service={}, " "helper={}", service_path.string(), helper_path.string()); return false; @@ -106,12 +107,17 @@ void Render::CheckPortableWindowsService() { return; } + if (portable_service_prompt_suppressed_) { + return; + } + portable_service_install_state_.store(PortableServiceInstallState::idle, std::memory_order_relaxed); show_portable_service_install_window_ = true; } void Render::StartPortableWindowsServiceInstall() { + portable_service_do_not_remind_ = false; PortableServiceInstallState expected = PortableServiceInstallState::idle; if (!portable_service_install_state_.compare_exchange_strong( expected, PortableServiceInstallState::installing, @@ -140,18 +146,82 @@ void Render::JoinPortableWindowsServiceInstallThread() { } int Render::PortableServiceInstallWindow() { - if (!show_portable_service_install_window_) { + if (!show_portable_service_install_window_ && + !show_portable_service_prompt_suppressed_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; + const float window_width = + (std::min)(viewport->WorkSize.x * 0.6f, title_bar_button_width_ * 18.0f); + const float window_height = + (std::min)(viewport->WorkSize.y * 0.5f, title_bar_button_width_ * 8.0f); + + if (show_portable_service_prompt_suppressed_window_) { + const float notice_width = window_width; + const float notice_height = (std::min)(viewport->WorkSize.y * 0.35f, + title_bar_button_width_ * 4.6f); + ImGui::SetNextWindowPos( + ImVec2( + viewport->WorkPos.x + (viewport->WorkSize.x - notice_width) / 2.0f, + viewport->WorkPos.y + + (viewport->WorkSize.y - notice_height) / 2.0f), + ImGuiCond_Appearing); + ImGui::SetNextWindowSize(ImVec2(notice_width, notice_height), + ImGuiCond_Always); + + ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, window_rounding_); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, window_rounding_ * 0.5f); + + ImGui::Begin( + localization::notification[localization_language_index_].c_str(), + nullptr, + ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse | + ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings | + ImGuiWindowFlags_NoTitleBar); + + ImGui::Spacing(); + ImGui::SetWindowFontScale(0.55f); + ImGui::SetCursorPosX(notice_width * 0.08f); + ImGui::Text( + "%s", localization::notification[localization_language_index_].c_str()); + + ImGui::SetWindowFontScale(0.5f); + ImGui::SetCursorPosX(notice_width * 0.06f); + ImGui::SetCursorPosY(notice_height * 0.28f); + ImGui::PushTextWrapPos(notice_width * 0.88f); + ImGui::TextWrapped("%s", + localization::windows_service_prompt_suppressed_message + [localization_language_index_] + .c_str()); + ImGui::PopTextWrapPos(); + + const std::string ok_label = localization::ok[localization_language_index_]; + const ImGuiStyle& style = ImGui::GetStyle(); + const float ok_width = + ImGui::CalcTextSize(ok_label.c_str()).x + style.FramePadding.x * 2.0f; + ImGui::SetCursorPosX((notice_width - ok_width) * 0.5f); + ImGui::SetCursorPosY(notice_height * 0.75f); + if (ImGui::Button(ok_label.c_str())) { + show_portable_service_prompt_suppressed_window_ = false; + } + + ImGui::SetWindowFontScale(1.0f); + ImGui::End(); + ImGui::PopStyleVar(3); + ImGui::PopStyleColor(); + } + + if (!show_portable_service_install_window_) { + return 0; + } + ImGui::SetNextWindowPos( - ImVec2((viewport->WorkSize.x - viewport->WorkPos.x - window_width) / - 2.0f, - (viewport->WorkSize.y - viewport->WorkPos.y - window_height) / - 2.0f), + ImVec2( + viewport->WorkPos.x + (viewport->WorkSize.x - window_width) / 2.0f, + viewport->WorkPos.y + (viewport->WorkSize.y - window_height) / 2.0f), ImGuiCond_Appearing); ImGui::SetNextWindowSize(ImVec2(window_width, window_height), ImGuiCond_Always); @@ -187,15 +257,13 @@ int Render::PortableServiceInstallWindow() { 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(); + 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(); + status_text = localization::windows_service_install_failed + [localization_language_index_] + .c_str(); } } @@ -203,7 +271,7 @@ int Render::PortableServiceInstallWindow() { ImGui::SetCursorPosX(window_width * 0.04f); ImGui::SetCursorPosY(window_height * 0.22f); ImGui::BeginChild("PortableServiceInstallContent", - ImVec2(window_width * 0.92f, window_height * 0.5f), + ImVec2(window_width * 0.92f, window_height * 0.45f), ImGuiChildFlags_Borders, ImGuiWindowFlags_None); ImGui::SetWindowFontScale(0.5f); const float wrap_pos = ImGui::GetContentRegionAvail().x; @@ -220,7 +288,15 @@ int Render::PortableServiceInstallWindow() { ImGui::EndChild(); ImGui::SetWindowFontScale(0.5f); - const float button_y = window_height * 0.76f; + ImGui::SetCursorPosX(window_width * 0.08f); + ImGui::SetCursorPosY(window_height * 0.71f); + ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 1.0f); + ImGui::Checkbox( + localization::do_not_remind_again[localization_language_index_].c_str(), + &portable_service_do_not_remind_); + ImGui::PopStyleVar(); + + const float button_y = window_height * 0.84f; const ImGuiStyle& style = ImGui::GetStyle(); const auto default_button_width = [&style](const std::string& label) { return ImGui::CalcTextSize(label.c_str()).x + style.FramePadding.x * 2.0f; @@ -259,6 +335,11 @@ int Render::PortableServiceInstallWindow() { ImGui::BeginDisabled(); } if (ImGui::Button(cancel_label.c_str())) { + if (portable_service_do_not_remind_) { + portable_service_prompt_suppressed_ = true; + config_center_->SetPortableServicePromptSuppressed(true); + show_portable_service_prompt_suppressed_window_ = true; + } show_portable_service_install_window_ = false; } if (state == PortableServiceInstallState::installing) { diff --git a/src/screen_capturer/macosx/screen_capturer_sck.cpp b/src/screen_capturer/macosx/screen_capturer_sck.cpp index a0d7441..7775d5b 100644 --- a/src/screen_capturer/macosx/screen_capturer_sck.cpp +++ b/src/screen_capturer/macosx/screen_capturer_sck.cpp @@ -16,7 +16,11 @@ int ScreenCapturerSck::Init(const int fps, cb_desktop_data cb) { } screen_capturer_sck_impl_ = CreateScreenCapturerSck(); - screen_capturer_sck_impl_->Init(fps, on_data_); + const int ret = screen_capturer_sck_impl_->Init(fps, on_data_); + if (ret != 0) { + screen_capturer_sck_impl_.reset(); + return ret; + } return 0; } @@ -29,8 +33,11 @@ int ScreenCapturerSck::Destroy() { } int ScreenCapturerSck::Start(bool show_cursor) { - screen_capturer_sck_impl_->Start(show_cursor); - return 0; + if (!screen_capturer_sck_impl_) { + return -1; + } + + return screen_capturer_sck_impl_->Start(show_cursor); } int ScreenCapturerSck::Stop() { @@ -80,4 +87,4 @@ std::vector ScreenCapturerSck::GetDisplayInfoList() { void ScreenCapturerSck::OnFrame() {} void ScreenCapturerSck::CleanUp() {} -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/screen_capturer/macosx/screen_capturer_sck_impl.mm b/src/screen_capturer/macosx/screen_capturer_sck_impl.mm index a7b0923..2758be0 100644 --- a/src/screen_capturer/macosx/screen_capturer_sck_impl.mm +++ b/src/screen_capturer/macosx/screen_capturer_sck_impl.mm @@ -16,7 +16,12 @@ #include #include #include +#include #include +#include +#include +#include +#include #include #include #include "display_info.h" @@ -28,6 +33,15 @@ class ScreenCapturerSckImpl; static const int kFullDesktopScreenId = -1; +static std::string NSErrorToString(NSError *error) { + if (!error) { + return ""; + } + + const char *description = [error.localizedDescription UTF8String]; + return description ? description : ""; +} + // The ScreenCaptureKit API was available in macOS 12.3, but full-screen capture // was reported to be broken before macOS 13 - see http://crbug.com/40234870. // Also, the `SCContentFilter` fields `contentRect` and `pointPixelScale` were @@ -78,6 +92,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer { std::map display_id_map_reverse_; std::map display_id_name_map_; unsigned char *nv12_frame_ = nullptr; + size_t nv12_frame_size_ = 0; int width_ = 0; int height_ = 0; int fps_ = 60; @@ -100,7 +115,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer { // Helper object to receive Objective-C callbacks from ScreenCaptureKit and call into this C++ // object. The helper may outlive this C++ instance, if a completion-handler is passed to // ScreenCaptureKit APIs and the C++ object is deleted before the handler executes. - SckHelper *__strong helper_; + SckHelper *__strong helper_ = nil; // Callback for returning captured frames, or errors, to the caller. Only used on the caller's // thread. cb_desktop_data _on_data = nullptr; @@ -110,7 +125,7 @@ class API_AVAILABLE(macos(14.0)) ScreenCapturerSckImpl : public ScreenCapturer { // Guards some variables that may be accessed on different threads. std::mutex lock_; // Provides captured desktop frames. - SCStream *__strong stream_; + SCStream *__strong stream_ = nil; // Currently selected display, or 0 if the full desktop is selected. This capturer does not // support full-desktop capture, and will fall back to the first display. CGDirectDisplayID current_display_ = 0; @@ -182,6 +197,19 @@ ScreenCapturerSckImpl::ScreenCapturerSckImpl() { } ScreenCapturerSckImpl::~ScreenCapturerSckImpl() { + SckHelper *helper_to_release = nil; + { + std::lock_guard lock(lock_); + if (stream_) { + [stream_ stopCaptureWithCompletionHandler:nil]; + stream_ = nil; + } + _on_data = nullptr; + helper_to_release = helper_; + helper_ = nil; + } + [helper_to_release releaseCapturer]; + display_info_list_.clear(); display_id_map_.clear(); display_id_map_reverse_.clear(); @@ -190,15 +218,22 @@ ScreenCapturerSckImpl::~ScreenCapturerSckImpl() { if (nv12_frame_) { delete[] nv12_frame_; nv12_frame_ = nullptr; + nv12_frame_size_ = 0; } - - [stream_ stopCaptureWithCompletionHandler:nil]; - [helper_ releaseCapturer]; } int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) { + if (!cb) { + LOG_ERROR("Screen capturer callback is null"); + return -1; + } + _on_data = cb; - fps_ = fps; + fps_ = fps > 0 ? fps : 60; + display_info_list_.clear(); + display_id_map_.clear(); + display_id_map_reverse_.clear(); + display_id_name_map_.clear(); if (@available(macOS 10.15, *)) { bool has_permission = CGPreflightScreenCaptureAccess(); @@ -216,8 +251,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) { getShareableContentWithCompletionHandler:^(SCShareableContent *result, NSError *error) { if (error) { capture_error = error; - LOG_ERROR("Failed to get shareable content: {}", - std::string([error.localizedDescription UTF8String])); + LOG_ERROR("Failed to get shareable content: {}", NSErrorToString(error)); } else { content = result; } @@ -227,7 +261,7 @@ int ScreenCapturerSckImpl::Init(const int fps, cb_desktop_data cb) { if (capture_error || !content || content.displays.count == 0) { LOG_ERROR("Failed to get display info, error: {}", - std::string([capture_error.localizedDescription UTF8String])); + NSErrorToString(capture_error)); return -1; } @@ -284,51 +318,58 @@ int ScreenCapturerSckImpl::Start(bool show_cursor) { } int ScreenCapturerSckImpl::SwitchTo(int monitor_index) { - if (stream_) { - [stream_ stopCaptureWithCompletionHandler:^(NSError *error) { - std::lock_guard lock(lock_); - stream_ = nil; - current_display_ = display_id_map_[monitor_index]; - StartOrReconfigureCapturer(); - }]; - } else { - current_display_ = display_id_map_[monitor_index]; - StartOrReconfigureCapturer(); + auto display_it = display_id_map_.find(monitor_index); + if (display_it == display_id_map_.end()) { + LOG_WARN("SwitchTo skipped, invalid monitor_index={}, displays={}", + monitor_index, display_id_map_.size()); + return -1; } + + const CGDirectDisplayID target_display = display_it->second; + { + std::lock_guard lock(lock_); + current_display_ = target_display; + } + StartOrReconfigureCapturer(); return 0; } int ScreenCapturerSckImpl::ResetToInitialMonitor() { int target = initial_monitor_index_; if (display_info_list_.empty()) return -1; - CGDirectDisplayID target_display = display_id_map_[target]; - if (current_display_ == target_display) return 0; - if (stream_) { - [stream_ stopCaptureWithCompletionHandler:^(NSError *error) { - std::lock_guard lock(lock_); - stream_ = nil; - current_display_ = target_display; - StartOrReconfigureCapturer(); - }]; - } else { - current_display_ = target_display; - StartOrReconfigureCapturer(); + auto display_it = display_id_map_.find(target); + if (display_it == display_id_map_.end()) { + LOG_WARN("ResetToInitialMonitor skipped, invalid monitor_index={}", target); + return -1; } + + CGDirectDisplayID target_display = display_it->second; + if (current_display_ == target_display) return 0; + { + std::lock_guard lock(lock_); + current_display_ = target_display; + } + StartOrReconfigureCapturer(); return 0; } int ScreenCapturerSckImpl::Destroy() { - std::lock_guard lock(lock_); - if (stream_) { - LOG_INFO("Destroying stream"); - [stream_ stopCaptureWithCompletionHandler:nil]; - stream_ = nil; + SckHelper *helper_to_release = nil; + { + std::lock_guard lock(lock_); + if (stream_) { + LOG_INFO("Destroying stream"); + [stream_ stopCaptureWithCompletionHandler:nil]; + stream_ = nil; + } + current_display_ = 0; + permanent_error_ = false; + _on_data = nullptr; + helper_to_release = helper_; + helper_ = nil; } - current_display_ = 0; - permanent_error_ = false; - _on_data = nullptr; - [helper_ releaseCapturer]; - helper_ = nil; + + [helper_to_release releaseCapturer]; return 0; } @@ -416,7 +457,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten // TODO: crbug.com/327458809 - Choose an appropriate sampleHandlerQueue for // best performance. - NSError *add_stream_output_error; + NSError *add_stream_output_error = nil; dispatch_queue_t queue = dispatch_queue_create("ScreenCaptureKit.Queue", DISPATCH_QUEUE_SERIAL); bool add_stream_output_result = [stream_ addStreamOutput:helper_ type:SCStreamOutputTypeScreen @@ -425,7 +466,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten if (!add_stream_output_result) { stream_ = nil; - LOG_ERROR("addStreamOutput failed"); + LOG_ERROR("addStreamOutput failed: {}", NSErrorToString(add_stream_output_error)); permanent_error_ = true; return; } @@ -436,7 +477,7 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten // calls stopCaptureWithCompletionHandler on the stream, which cancels // this handler. permanent_error_ = true; - LOG_ERROR("startCaptureWithCompletionHandler failed"); + LOG_ERROR("startCaptureWithCompletionHandler failed: {}", NSErrorToString(error)); } else { LOG_INFO("Capture started"); } @@ -448,8 +489,18 @@ void ScreenCapturerSckImpl::OnShareableContentCreated(SCShareableContent *conten void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer, CFDictionaryRef attachment) { + (void)attachment; + if (!pixelBuffer) { + return; + } + size_t width = CVPixelBufferGetWidth(pixelBuffer); size_t height = CVPixelBufferGetHeight(pixelBuffer); + if (width == 0 || height == 0 || CVPixelBufferGetPlaneCount(pixelBuffer) < 2) { + LOG_ERROR("Invalid CVPixelBuffer: width={}, height={}, planes={}", width, height, + CVPixelBufferGetPlaneCount(pixelBuffer)); + return; + } CVReturn status = CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); if (status != kCVReturnSuccess) { @@ -458,18 +509,37 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer, } size_t required_size = width * height * 3 / 2; - if (!nv12_frame_ || (width_ * height_ * 3 / 2 < required_size)) { + if (required_size > static_cast((std::numeric_limits::max)())) { + LOG_ERROR("Captured frame is too large: {} bytes", required_size); + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + return; + } + + std::lock_guard lock(lock_); + if (!_on_data) { + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + return; + } + + if (!nv12_frame_ || nv12_frame_size_ < required_size) { delete[] nv12_frame_; nv12_frame_ = new unsigned char[required_size]; - width_ = width; - height_ = height; + nv12_frame_size_ = required_size; } + width_ = static_cast(width); + height_ = static_cast(height); void *base_y = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0); size_t stride_y = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0); void *base_uv = CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 1); size_t stride_uv = CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 1); + if (!base_y || !base_uv || stride_y < width || stride_uv < width) { + LOG_ERROR("Invalid CVPixelBuffer planes: base_y={}, base_uv={}, stride_y={}, stride_uv={}", + base_y != nullptr, base_uv != nullptr, stride_y, stride_uv); + CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); + return; + } unsigned char *dst_y = nv12_frame_; for (size_t row = 0; row < height; ++row) { @@ -481,7 +551,8 @@ void ScreenCapturerSckImpl::OnNewCVPixelBuffer(CVPixelBufferRef pixelBuffer, memcpy(dst_uv + row * width, static_cast(base_uv) + row * stride_uv, width); } - _on_data(nv12_frame_, width * height * 3 / 2, width, height, + _on_data(nv12_frame_, static_cast(required_size), static_cast(width), + static_cast(height), display_id_name_map_[current_display_].c_str()); CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly); @@ -503,10 +574,14 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() { } SckHelper *local_helper = helper_; + if (!local_helper) { + LOG_ERROR("Cannot reconfigure capturer: helper is null"); + return; + } + auto handler = ^(SCShareableContent *content, NSError *error) { if (error) { - LOG_ERROR("getShareableContent failed: {}", - std::string([error.localizedDescription UTF8String])); + LOG_ERROR("getShareableContent failed: {}", NSErrorToString(error)); [local_helper onShareableContentCreated:nil]; return; } @@ -576,4 +651,4 @@ void ScreenCapturerSckImpl::StartOrReconfigureCapturer() { std::unique_ptr ScreenCapturerSck::CreateScreenCapturerSck() { return std::make_unique(); -} \ No newline at end of file +} diff --git a/src/speaker_capturer/macosx/speaker_capturer_macosx.mm b/src/speaker_capturer/macosx/speaker_capturer_macosx.mm index c40db75..5cea7d8 100644 --- a/src/speaker_capturer/macosx/speaker_capturer_macosx.mm +++ b/src/speaker_capturer/macosx/speaker_capturer_macosx.mm @@ -9,6 +9,17 @@ namespace crossdesk { class SpeakerCapturerMacosx; } +namespace { +std::string NSErrorToString(NSError* error) { + if (!error) { + return ""; + } + + const char* description = [error.localizedDescription UTF8String]; + return description ? description : ""; +} +} // namespace + @interface SpeakerCaptureDelegate : NSObject @property(nonatomic, assign) crossdesk::SpeakerCapturerMacosx* owner; - (instancetype)initWithOwner:(crossdesk::SpeakerCapturerMacosx*)owner; @@ -28,15 +39,36 @@ class SpeakerCapturerMacosx; ofType:(SCStreamOutputType)type { if (type != SCStreamOutputTypeAudio) return; + crossdesk::SpeakerCapturerMacosx* owner = _owner; + if (!owner || !owner->cb_) { + return; + } + CMBlockBufferRef blockBuffer = CMSampleBufferGetDataBuffer(sampleBuffer); + if (!blockBuffer) { + return; + } + size_t length = CMBlockBufferGetDataLength(blockBuffer); char* dataPtr = NULL; - CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr); + OSStatus dataStatus = + CMBlockBufferGetDataPointer(blockBuffer, 0, NULL, NULL, &dataPtr); + if (dataStatus != noErr || dataPtr == nullptr || length == 0) { + return; + } + CMAudioFormatDescriptionRef formatDesc = CMSampleBufferGetFormatDescription(sampleBuffer); + if (!formatDesc) { + return; + } + const AudioStreamBasicDescription* asbd = CMAudioFormatDescriptionGetStreamBasicDescription(formatDesc); + if (!asbd || asbd->mChannelsPerFrame == 0) { + return; + } - if (_owner->cb_ && dataPtr && length > 0 && asbd) { + if (owner->cb_) { std::vector out_pcm16; if (asbd->mFormatFlags & kAudioFormatFlagIsFloat) { int channels = asbd->mChannelsPerFrame; @@ -86,7 +118,10 @@ class SpeakerCapturerMacosx; size_t total_bytes = out_pcm16.size() * sizeof(short); unsigned char* p = (unsigned char*)out_pcm16.data(); for (size_t offset = 0; offset + frame_bytes <= total_bytes; offset += frame_bytes) { - _owner->cb_(p + offset, frame_bytes, "audio"); + if (!owner->cb_) { + return; + } + owner->cb_(p + offset, frame_bytes, "audio"); } } } @@ -155,7 +190,7 @@ int SpeakerCapturerMacosx::Init(speaker_data_cb cb) { if (error || !impl_->content) { LOG_ERROR("Failed to get shareable content: {}", - std::string([error.localizedDescription UTF8String])); + NSErrorToString(error)); return -1; } @@ -209,7 +244,7 @@ int SpeakerCapturerMacosx::Start() { error:&addOutputError]; if (!ok || addOutputError) { LOG_ERROR("addStreamOutput error: {}", - std::string([addOutputError.localizedDescription UTF8String])); + NSErrorToString(addOutputError)); impl_->stream = nil; impl_->delegate = nil; return -1; @@ -220,7 +255,7 @@ int SpeakerCapturerMacosx::Start() { [impl_->stream startCaptureWithCompletionHandler:^(NSError* _Nullable error) { if (error) { LOG_ERROR("startCaptureWithCompletionHandler error: {}", - std::string([error.localizedDescription UTF8String])); + NSErrorToString(error)); ret = -1; } dispatch_semaphore_signal(semaStart); @@ -238,13 +273,14 @@ int SpeakerCapturerMacosx::Stop() { [impl_->stream stopCaptureWithCompletionHandler:^(NSError* error) { if (error) { LOG_ERROR("stopCaptureWithCompletionHandler error: {}", - std::string([error.localizedDescription UTF8String])); + NSErrorToString(error)); } dispatch_semaphore_signal(sema); }]; dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); impl_->stream = nil; + impl_->delegate.owner = nullptr; impl_->delegate = nil; return 0; @@ -269,4 +305,4 @@ int SpeakerCapturerMacosx::Destroy() { int SpeakerCapturerMacosx::Pause() { return 0; } int SpeakerCapturerMacosx::Resume() { return Start(); } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/version_checker/version_checker.cpp b/src/version_checker/version_checker.cpp index a27b3cc..d8edf38 100644 --- a/src/version_checker/version_checker.cpp +++ b/src/version_checker/version_checker.cpp @@ -8,7 +8,15 @@ #include +#include "rd_log.h" + +#include +#include +#include +#include +#include #include +#include #include #include #include @@ -16,12 +24,296 @@ namespace crossdesk { static std::string latest_release_date_ = ""; +static bool latest_patch_available_ = false; +static int latest_patch_ = 0; + +std::vector SplitVersion(const std::string& ver); + +namespace { + +constexpr size_t kMaxInlinePatchDigits = 4; + +struct ParsedVersion { + std::vector numbers; + std::string date; + bool has_patch = false; + int patch = 0; +}; + +bool IsDigit(char c) { + return std::isdigit(static_cast(c)) != 0; +} + +bool IsAlphaNumeric(char c) { + return std::isalnum(static_cast(c)) != 0; +} + +bool IsAllDigits(const std::string& value) { + if (value.empty()) { + return false; + } + + for (char c : value) { + if (!IsDigit(c)) { + return false; + } + } + return true; +} + +bool TryParseNonNegativeInt(const std::string& value, int* result) { + if (!IsAllDigits(value)) { + return false; + } + + try { + const long long parsed = std::stoll(value); + if (parsed > std::numeric_limits::max()) { + return false; + } + *result = static_cast(parsed); + return true; + } catch (...) { + return false; + } +} + +bool TryParseInlinePatch(const std::string& value, int* result) { + if (value.size() > kMaxInlinePatchDigits) { + return false; + } + + return TryParseNonNegativeInt(value, result); +} + +size_t FindNumericStart(const std::string& version) { + size_t start = 0; + while (start < version.size() && !IsDigit(version[start])) { + start++; + } + return start; +} + +size_t FindNumericEnd(const std::string& version, size_t start) { + size_t end = start; + while (end < version.size() && + (IsDigit(version[end]) || version[end] == '.')) { + end++; + } + return end; +} + +bool HasDigitBoundary(const std::string& value, size_t pos, size_t len) { + const bool before_ok = pos == 0 || !IsDigit(value[pos - 1]); + const size_t end = pos + len; + const bool after_ok = end >= value.size() || !IsDigit(value[end]); + return before_ok && after_ok; +} + +bool IsCompactDateAt(const std::string& value, size_t pos) { + if (pos + 8 > value.size() || !HasDigitBoundary(value, pos, 8)) { + return false; + } + + for (size_t i = 0; i < 8; ++i) { + if (!IsDigit(value[pos + i])) { + return false; + } + } + return true; +} + +std::string CompactDateToIso(const std::string& compact_date) { + return compact_date.substr(0, 4) + "-" + compact_date.substr(4, 2) + "-" + + compact_date.substr(6, 2); +} + +bool ExtractDateFromText(const std::string& value, + std::string* date, + size_t* date_end) { + for (size_t i = 0; i < value.size(); ++i) { + if (IsCompactDateAt(value, i)) { + *date = CompactDateToIso(value.substr(i, 8)); + *date_end = i + 8; + return true; + } + } + + return false; +} + +ParsedVersion ParseVersion(const std::string& version) { + const size_t numeric_start = FindNumericStart(version); + const size_t numeric_end = FindNumericEnd(version, numeric_start); + + ParsedVersion parsed; + parsed.numbers = SplitVersion(version.substr(numeric_start, + numeric_end - numeric_start)); + + const std::string suffix = version.substr(numeric_end); + size_t pos = 0; + while (pos < suffix.size()) { + while (pos < suffix.size() && !IsAlphaNumeric(suffix[pos])) { + pos++; + } + + const size_t token_start = pos; + while (pos < suffix.size() && IsAlphaNumeric(suffix[pos])) { + pos++; + } + + if (token_start == pos) { + continue; + } + + const std::string token = suffix.substr(token_start, pos - token_start); + if (parsed.date.empty() && IsCompactDateAt(token, 0)) { + parsed.date = CompactDateToIso(token); + continue; + } + + int patch = 0; + if (!parsed.has_patch && TryParseInlinePatch(token, &patch)) { + parsed.has_patch = true; + parsed.patch = patch; + } + } + + return parsed; +} + +int CompareNumericVersion(const std::vector& current, + const std::vector& latest) { + std::vector current_parts = current; + std::vector latest_parts = latest; + const size_t len = std::max(current_parts.size(), latest_parts.size()); + current_parts.resize(len, 0); + latest_parts.resize(len, 0); + + for (size_t i = 0; i < len; ++i) { + if (latest_parts[i] > current_parts[i]) { + return 1; + } + if (latest_parts[i] < current_parts[i]) { + return -1; + } + } + + return 0; +} + +void ResetLatestMetadata() { + latest_release_date_ = ""; + latest_patch_available_ = false; + latest_patch_ = 0; +} + +bool ReadPatchField(const nlohmann::json& json, int* patch) { + if (!json.contains("patch")) { + return false; + } + + const auto& patch_value = json["patch"]; + if (patch_value.is_number_integer()) { + const long long parsed = patch_value.get(); + if (parsed < 0 || parsed > std::numeric_limits::max()) { + return false; + } + *patch = static_cast(parsed); + return true; + } + + if (patch_value.is_string()) { + return TryParseNonNegativeInt(patch_value.get(), patch); + } + + return false; +} + +void LogHttpError(const httplib::Result& result) { + LOG_WARN("Failed to fetch version.json: error={}, message={}", + static_cast(result.error()), httplib::to_string(result.error())); +#ifdef CPPHTTPLIB_OPENSSL_SUPPORT + LOG_WARN("version.json SSL error={}, OpenSSL error={}", result.ssl_error(), + result.ssl_openssl_error()); +#endif +} + +#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__) +bool PathExists(const std::string& path) { + if (path.empty()) { + return false; + } + + std::error_code ec; + return std::filesystem::exists(path, ec); +} + +std::string GetEnvPathIfExists(const char* key) { + const char* value = std::getenv(key); + if (!value) { + return ""; + } + + const std::string path = value; + return PathExists(path) ? path : ""; +} + +std::string FindFirstExistingPath( + const std::vector& candidates) { + for (const auto& candidate : candidates) { + if (PathExists(candidate)) { + return candidate; + } + } + return ""; +} + +void ConfigureLinuxCaCerts(httplib::Client* cli) { + const std::string ca_file = [&]() { + const std::string env_path = GetEnvPathIfExists("SSL_CERT_FILE"); + if (!env_path.empty()) { + return env_path; + } + + return FindFirstExistingPath({ + "/etc/ssl/certs/ca-certificates.crt", + "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/cert.pem", + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + }); + }(); + + const std::string ca_dir = [&]() { + const std::string env_path = GetEnvPathIfExists("SSL_CERT_DIR"); + if (!env_path.empty()) { + return env_path; + } + + return FindFirstExistingPath({ + "/etc/ssl/certs", + "/etc/pki/tls/certs", + "/etc/openssl/certs", + }); + }(); + + if (ca_file.empty() && ca_dir.empty()) { + LOG_WARN("No Linux CA bundle found for version.json request; relying on OpenSSL defaults"); + return; + } + + cli->set_ca_cert_path(ca_file, ca_dir); + LOG_INFO("Configured version.json TLS CA bundle: file={}, dir={}", + ca_file.empty() ? "" : ca_file, + ca_dir.empty() ? "" : ca_dir); +} +#endif + +} // namespace std::string ExtractNumericPart(const std::string& ver) { - size_t start = 0; - while (start < ver.size() && !std::isdigit(ver[start])) start++; - size_t end = start; - while (end < ver.size() && (std::isdigit(ver[end]) || ver[end] == '.')) end++; + const size_t start = FindNumericStart(ver); + const size_t end = FindNumericEnd(ver, start); return ver.substr(start, end - start); } @@ -42,25 +334,13 @@ std::vector SplitVersion(const std::string& ver) { // extract date from version string (format: v1.2.3-20251113-abc // or 1.2.3-20251113-abc) std::string ExtractDateFromVersion(const std::string& version) { - size_t dash1 = version.find('-'); - if (dash1 != std::string::npos) { - size_t dash2 = version.find('-', dash1 + 1); - if (dash2 != std::string::npos) { - std::string date_part = version.substr(dash1 + 1, dash2 - dash1 - 1); - - bool is_date = true; - for (char c : date_part) { - if (!std::isdigit(c)) { - is_date = false; - break; - } - } - if (is_date) { - // convert YYYYMMDD to YYYY-MM-DD - return date_part.substr(0, 4) + "-" + date_part.substr(4, 2) + "-" + - date_part.substr(6, 2); - } - } + const size_t numeric_start = FindNumericStart(version); + const size_t numeric_end = FindNumericEnd(version, numeric_start); + const std::string suffix = version.substr(numeric_end); + std::string date; + size_t date_end = 0; + if (ExtractDateFromText(suffix, &date, &date_end)) { + return date; } return ""; } @@ -73,55 +353,41 @@ bool IsNewerDate(const std::string& date1, const std::string& date2) { } bool IsNewerVersion(const std::string& current, const std::string& latest) { - auto v1 = SplitVersion(ExtractNumericPart(current)); - auto v2 = SplitVersion(ExtractNumericPart(latest)); - - size_t len = std::max(v1.size(), v2.size()); - v1.resize(len, 0); - v2.resize(len, 0); - - for (size_t i = 0; i < len; ++i) { - if (v2[i] > v1[i]) return true; - if (v2[i] < v1[i]) return false; - } - - // if versions are equal, compare by release date - if (!latest_release_date_.empty()) { - // try to extract date from current version string - std::string current_date = ExtractDateFromVersion(current); - if (!current_date.empty()) { - return IsNewerDate(current_date, latest_release_date_); - } else { - return true; - } - } - - return false; + return IsNewerVersionWithMetadata( + current, latest, latest_release_date_, + latest_patch_available_ ? latest_patch_ : -1); } -bool IsNewerVersionWithDate(const std::string& current_version, - const std::string& current_date, - const std::string& latest_version, - const std::string& latest_date) { - // compare versions - auto v1 = SplitVersion(ExtractNumericPart(current_version)); - auto v2 = SplitVersion(ExtractNumericPart(latest_version)); +bool IsNewerVersionWithMetadata(const std::string& current, + const std::string& latest, + const std::string& latest_date, + int latest_patch) { + (void)latest_date; - size_t len = std::max(v1.size(), v2.size()); - v1.resize(len, 0); - v2.resize(len, 0); + const ParsedVersion current_version = ParseVersion(current); + const ParsedVersion latest_version = ParseVersion(latest); - for (size_t i = 0; i < len; ++i) { - if (v2[i] > v1[i]) return true; - if (v2[i] < v1[i]) return false; + const int numeric_compare = + CompareNumericVersion(current_version.numbers, latest_version.numbers); + if (numeric_compare > 0) { + return true; + } + if (numeric_compare < 0) { + return false; } - // if versions are equal, compare by release date - if (!current_date.empty() && !latest_date.empty()) { - return IsNewerDate(current_date, latest_date); + const bool metadata_has_patch = latest_patch >= 0; + const bool latest_has_patch = metadata_has_patch || latest_version.has_patch; + if (latest_has_patch || current_version.has_patch) { + const int resolved_latest_patch = + metadata_has_patch ? latest_patch + : (latest_version.has_patch ? latest_version.patch + : 0); + const int resolved_current_patch = + current_version.has_patch ? current_version.patch : 0; + return resolved_latest_patch > resolved_current_patch; } - // if dates are not available, versions are equal return false; } @@ -130,8 +396,14 @@ nlohmann::json CheckUpdate() { cli.set_connection_timeout(5); cli.set_read_timeout(5); + cli.set_follow_location(true); - if (auto res = cli.Get("/version.json")) { +#if defined(CPPHTTPLIB_OPENSSL_SUPPORT) && defined(__linux__) + ConfigureLinuxCaCerts(&cli); +#endif + + auto res = cli.Get("/version.json"); + if (res) { if (res->status == 200) { try { auto j = nlohmann::json::parse(res->body); @@ -140,19 +412,28 @@ nlohmann::json CheckUpdate() { } else { latest_release_date_ = ""; } + latest_patch_ = 0; + latest_patch_available_ = ReadPatchField(j, &latest_patch_); + LOG_INFO("Fetched version.json: latest_version={}, releaseDate={}, patch={}", + j.value("latest_version", j.value("version", "")), + j.value("releaseDate", ""), + latest_patch_available_ ? latest_patch_ : -1); return j; - } catch (std::exception&) { - latest_release_date_ = ""; + } catch (const std::exception& e) { + LOG_WARN("Failed to parse version.json: {}", e.what()); + ResetLatestMetadata(); return nlohmann::json{}; } } else { - latest_release_date_ = ""; + LOG_WARN("Failed to fetch version.json: HTTP status={}", res->status); + ResetLatestMetadata(); return nlohmann::json{}; } } else { - latest_release_date_ = ""; + LogHttpError(res); + ResetLatestMetadata(); return nlohmann::json{}; } } -} // namespace crossdesk \ No newline at end of file +} // namespace crossdesk diff --git a/src/version_checker/version_checker.h b/src/version_checker/version_checker.h index 75e09c1..d6382ed 100644 --- a/src/version_checker/version_checker.h +++ b/src/version_checker/version_checker.h @@ -16,6 +16,12 @@ nlohmann::json CheckUpdate(); bool IsNewerVersion(const std::string& current, const std::string& latest); +// Pass latest_patch < 0 when patch metadata is unavailable. +bool IsNewerVersionWithMetadata(const std::string& current, + const std::string& latest, + const std::string& latest_date, + int latest_patch); + } // namespace crossdesk -#endif \ No newline at end of file +#endif diff --git a/tests/version_checker_test.cpp b/tests/version_checker_test.cpp new file mode 100644 index 0000000..1ff14c0 --- /dev/null +++ b/tests/version_checker_test.cpp @@ -0,0 +1,54 @@ +#include "version_checker.h" + +#include +#include + +namespace { + +bool ExpectEqual(const std::string& name, bool actual, bool expected) { + if (actual == expected) { + return true; + } + + std::cerr << name << " mismatch\n" + << " expected: " << expected << "\n" + << " actual: " << actual << "\n"; + return false; +} + +} // namespace + +int main() { + bool ok = true; + + ok &= ExpectEqual("new patch-before-date is newer", + crossdesk::IsNewerVersionWithMetadata( + "v1.3.5-20260529", "v1.3.5-1-20260529", "", -1), + true); + ok &= ExpectEqual("larger patch wins regardless of date", + crossdesk::IsNewerVersionWithMetadata( + "v1.3.5-2-20260530", "v1.3.5-3-20260529", "", -1), + true); + ok &= ExpectEqual("smaller patch loses regardless of date", + crossdesk::IsNewerVersionWithMetadata( + "v1.3.5-3-20260529", "v1.3.5-2-20260530", "", -1), + false); + ok &= ExpectEqual("old date-before-patch remains supported", + crossdesk::IsNewerVersionWithMetadata( + "v1.3.5-20260529-1", "v1.3.5-20260529-2", "", -1), + true); + ok &= ExpectEqual("metadata patch overrides date", + crossdesk::IsNewerVersionWithMetadata( + "v1.3.5-9-20260530", "v1.3.5", "2026-05-31", 10), + true); + ok &= ExpectEqual("date alone does not update same version", + crossdesk::IsNewerVersionWithMetadata( + "v1.3.5-20260529", "v1.3.5-20260530", "", -1), + false); + ok &= ExpectEqual("numeric version still wins", + crossdesk::IsNewerVersionWithMetadata( + "v1.3.5-9-20260529", "v1.3.6-1-20260529", "", -1), + true); + + return ok ? 0 : 1; +} diff --git a/tests/windows_manifest_resource_test.cpp b/tests/windows_manifest_resource_test.cpp index f63e68b..3caeea8 100644 --- a/tests/windows_manifest_resource_test.cpp +++ b/tests/windows_manifest_resource_test.cpp @@ -82,16 +82,27 @@ int main() { } const std::string rc = ReadFile(repo_root / "scripts/windows/crossdesk.rc"); + const std::string portable_rc = + ReadFile(repo_root / "scripts/windows/crossdesk_portable.rc"); const std::string manifest = ReadFile(repo_root / "scripts/windows/crossdesk.manifest"); const std::string debug_manifest = ReadFile(repo_root / "scripts/windows/crossdesk_debug.manifest"); + const std::string portable_manifest = + ReadFile(repo_root / "scripts/windows/crossdesk_portable.manifest"); + const std::string targets = ReadFile(repo_root / "xmake/targets.lua"); bool ok = true; ok &= ExpectContains("crossdesk.rc", rc, "crossdesk.manifest"); ok &= ExpectContains("crossdesk.rc", rc, "crossdesk_debug.manifest"); ok &= ExpectContains("crossdesk.rc", rc, "CROSSDESK_DEBUG"); ok &= ExpectContains("crossdesk.rc", rc, "RT_MANIFEST"); + ok &= ExpectContains("crossdesk_portable.rc", portable_rc, + "crossdesk_portable.manifest"); + ok &= ExpectContains("crossdesk_portable.rc", portable_rc, "RT_MANIFEST"); + ok &= ExpectContains("xmake/targets.lua", targets, + "scripts/windows/crossdesk_portable.rc"); + ok &= ExpectContains("xmake/targets.lua", targets, "CROSSDESK_PORTABLE"); ok &= ExpectContains("crossdesk.manifest", manifest, "level=\"requireAdministrator\""); ok &= ExpectContains("crossdesk.manifest", manifest, @@ -108,10 +119,22 @@ int main() { "http://schemas.microsoft.com/SMI/2016/WindowsSettings"); ok &= ExpectNotContains("crossdesk_debug.manifest", debug_manifest, "processorArchitecture=\"*\""); + ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest, + "level=\"asInvoker\""); + ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest, + "level=\"requireAdministrator\""); + ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest, + "http://schemas.microsoft.com/SMI/2005/WindowsSettings"); + ok &= ExpectContains("crossdesk_portable.manifest", portable_manifest, + "http://schemas.microsoft.com/SMI/2016/WindowsSettings"); + ok &= ExpectNotContains("crossdesk_portable.manifest", portable_manifest, + "processorArchitecture=\"*\""); #ifdef _WIN32 ok &= ExpectActivationContext(repo_root / "scripts/windows/crossdesk.manifest"); ok &= ExpectActivationContext( repo_root / "scripts/windows/crossdesk_debug.manifest"); + ok &= ExpectActivationContext( + repo_root / "scripts/windows/crossdesk_portable.manifest"); #endif return ok ? 0 : 1; } diff --git a/xmake/targets.lua b/xmake/targets.lua index 443c79a..6b8d0ed 100644 --- a/xmake/targets.lua +++ b/xmake/targets.lua @@ -3,6 +3,11 @@ function setup_targets() includes("submodules", "thirdparty") + local crossdesk_windows_resource = "scripts/windows/crossdesk.rc" + if is_config("CROSSDESK_PORTABLE", true) then + crossdesk_windows_resource = "scripts/windows/crossdesk_portable.rc" + end + target("rd_log") set_kind("object") add_packages("spdlog") @@ -65,6 +70,19 @@ function setup_targets() set_default(false) add_files("tests/display_popup_hover_state_test.cpp") + target("version_checker_test") + set_kind("binary") + set_default(false) + add_packages("cpp-httplib") + add_deps("rd_log") + add_includedirs("src/version_checker") + add_files("tests/version_checker_test.cpp", + "src/version_checker/version_checker.cpp") + if is_os("macosx") then + add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN") + add_frameworks("Security", "CoreFoundation") + end + target("screen_capturer") set_kind("object") add_deps("rd_log", "common") @@ -164,6 +182,10 @@ function setup_targets() add_deps("rd_log") add_files("src/version_checker/*.cpp") add_includedirs("src/version_checker", {public = true}) + if is_os("macosx") then + add_defines("CPPHTTPLIB_USE_CERTS_FROM_MACOSX_KEYCHAIN") + add_frameworks("Security", "CoreFoundation") + end target("tools") set_kind("object") @@ -223,7 +245,7 @@ function setup_targets() add_deps("rd_log", "path_manager") add_links("Advapi32", "User32", "Wtsapi32", "Gdi32") add_files("src/service/windows/session_helper_main.cpp") - add_files("scripts/windows/crossdesk.rc") + add_files(crossdesk_windows_resource) add_includedirs("src/service/windows", {public = true}) end @@ -237,6 +259,6 @@ function setup_targets() add_includedirs("src/service/windows", {public = true}) add_links("Advapi32", "Wtsapi32", "Ole32", "Userenv") add_deps("wgc_plugin", "crossdesk_service", "crossdesk_session_helper") - add_files("scripts/windows/crossdesk.rc") + add_files(crossdesk_windows_resource) end end