Compare commits

..

20 Commits

Author SHA1 Message Date
dijunkun
7c940d6b15 [ci] fix tag error in update-version-json.yml 2025-11-25 03:30:28 +08:00
dijunkun
86501b05dd [feat] show notification window if there is a new version avaliable 2025-11-25 01:20:16 +08:00
dijunkun
01ebed9b37 [ci] upload release notes to version.json 2025-11-25 01:19:12 +08:00
dijunkun
2188adb1f1 [chore] disable CROSSDESK_DEBUG in keyboard and mouse control 2025-11-24 16:47:33 +08:00
dijunkun
51409e16c8 [chore] update README 2025-11-23 01:54:54 +08:00
dijunkun
6ca3b58ae2 [fix] update MiniRTC to resolve occasional crash during connection shutdown, refs #29 2025-11-23 01:47:48 +08:00
dijunkun
692e176e34 [ci] use github.sha instead of hashFiles for xmake dependency cache 2025-11-23 00:35:26 +08:00
dijunkun
4fb7acaa61 [feat] set enable TURN by default 2025-11-23 00:02:04 +08:00
dijunkun
c0d6429a54 [fix] resolve missing mouse cursor display when web client connects to Linux devices, refs #30 2025-11-22 23:57:49 +08:00
dijunkun
07c7c7f179 [chore] update README 2025-11-21 02:40:51 +08:00
dijunkun
c5ceeb0d80 [ci] use China timezone for build date in version number 2025-11-21 02:33:39 +08:00
dijunkun
5ce0a891df [fix] resolve failures in connection destruction 2025-11-21 01:50:08 +08:00
dijunkun
f94ef49210 [fix] release keyboard hook after remote control disconnects, refs #23 2025-11-21 00:56:01 +08:00
dijunkun
5d0a4d1385 [fix] fix mouse wheel and touchpad swipe commands, refs #9, #23 2025-11-21 00:55:19 +08:00
dijunkun
dd482cee60 [fix] use static linking of libffi in glib to avoid version conflicts, fixes #16 2025-11-20 17:20:15 +08:00
dijunkun
e3c2edfb1c [fix] fix daemon not working on Linux 2025-11-20 15:35:37 +08:00
dijunkun
f3901d09ea [feat] add tooltip for the daemon option in the settings windows 2025-11-20 14:47:54 +08:00
Junkun Di
2b12749477 [chore] update README_EN.md 2025-11-20 13:05:14 +08:00
Junkun Di
759488f675 [chore] update README.md 2025-11-20 13:04:32 +08:00
dijunkun
4bb4240a9e [chore] update README 2025-11-20 11:15:34 +08:00
25 changed files with 2568 additions and 1964 deletions

View File

@@ -35,7 +35,7 @@ jobs:
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(date +%Y%m%d)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
@@ -101,7 +101,7 @@ jobs:
id: set_deb_version
run: |
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(date +%Y%m%d)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
if [[ ! "${VERSION_NUM}" =~ ^[0-9] ]]; then
LEGAL_VERSION="v0.0.0-${VERSION_NUM}-${BUILD_DATE}-${SHORT_SHA}"
else
@@ -164,7 +164,7 @@ jobs:
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(date +%Y%m%d)
BUILD_DATE=$(TZ=Asia/Shanghai date +%Y%m%d)
VERSION_NUM="v${VERSION#v}-${BUILD_DATE}-${SHORT_SHA}"
echo "VERSION_NUM=${VERSION_NUM}" >> $GITHUB_ENV
echo "VERSION_NUM=${VERSION_NUM}"
@@ -174,7 +174,7 @@ jobs:
uses: actions/cache@v4
with:
path: ~/.xmake/packages
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ hashFiles('**/xmake.lua') }}
key: ${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-${{ github.sha }}
restore-keys: |
${{ runner.os }}-xmake-deps-${{ matrix.cache-key }}-
@@ -229,7 +229,7 @@ jobs:
$version = $version -replace '^v', ''
$version = $version -replace '/', '-'
$SHORT_SHA = $env:GITHUB_SHA.Substring(0,7)
$BUILD_DATE = Get-Date -Format "yyyyMMdd"
$BUILD_DATE = ([System.TimeZoneInfo]::ConvertTimeBySystemTimeZoneId((Get-Date), "China Standard Time")).ToString("yyyyMMdd")
echo "VERSION_NUM=v$version-$BUILD_DATE-$SHORT_SHA" >> $env:GITHUB_ENV
echo "BUILD_DATE=$BUILD_DATE" >> $env:GITHUB_ENV
@@ -237,7 +237,7 @@ jobs:
uses: actions/cache@v4
with:
path: D:\xmake_global\.xmake\packages
key: ${{ runner.os }}-xmake-deps-intel-${{ hashFiles('**/xmake.lua') }}
key: ${{ runner.os }}-xmake-deps-intel-${{ github.sha }}
restore-keys: |
${{ runner.os }}-xmake-deps-intel-
@@ -340,8 +340,8 @@ jobs:
run: |
VERSION="${GITHUB_REF##*/}"
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
BUILD_DATE=$(date +%Y%m%d)
BUILD_DATE_ISO=$(date +%Y-%m-%d)
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}"
VERSION_WITH_V="v${VERSION_NUM}"
VERSION_ONLY="${VERSION#v}"
@@ -408,6 +408,9 @@ jobs:
{
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"releaseName": "",
"releaseNotes": "",
"tagName": "${{ steps.version.outputs.VERSION_WITH_V }}",
"downloads": {
"windows-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe",

View File

@@ -0,0 +1,140 @@
name: Update version.json from Release
on:
release:
types: [published, edited]
permissions:
contents: write
jobs:
update-version-json:
name: Update version.json with release information
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Extract version from tag
id: version
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
# Extract date from tag if available (format: v1.2.3-20251113-abc)
if [[ "${TAG_NAME}" =~ -([0-9]{8})- ]]; then
DATE_STR="${BASH_REMATCH[1]}"
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 "BUILD_DATE_ISO=${BUILD_DATE_ISO}" >> $GITHUB_OUTPUT
- name: Install jq
run: sudo apt-get update && sudo apt-get install -y jq
- name: Get release information
id: release_info
run: |
# Use jq to properly escape JSON
RELEASE_BODY="${{ github.event.release.body }}"
RELEASE_NAME="${{ github.event.release.name }}"
# Handle empty values
if [ -z "$RELEASE_BODY" ]; then
RELEASE_BODY=""
fi
if [ -z "$RELEASE_NAME" ]; then
RELEASE_NAME=""
fi
# Save to temporary files for proper handling
echo -n "$RELEASE_BODY" > /tmp/release_body.txt
echo -n "$RELEASE_NAME" > /tmp/release_name.txt
# Use jq to escape JSON strings
RELEASE_BODY_JSON=$(jq -Rs . < /tmp/release_body.txt)
RELEASE_NAME_JSON=$(jq -Rs . < /tmp/release_name.txt)
echo "RELEASE_BODY=${RELEASE_BODY_JSON}" >> $GITHUB_OUTPUT
echo "RELEASE_NAME=${RELEASE_NAME_JSON}" >> $GITHUB_OUTPUT
- name: Download current version.json from server
id: download_version
continue-on-error: true
run: |
# Try to download current version.json from server
curl -f -s "https://version.crossdesk.cn/version.json" -o version.json || echo "Failed to download, will create new one"
- name: Generate or update version.json
run: |
# If version.json exists, try to preserve downloads section
if [ -f version.json ] && jq -e '.downloads' version.json > /dev/null 2>&1; then
EXISTING_DOWNLOADS=$(jq -c '.downloads' version.json)
if [ "$EXISTING_DOWNLOADS" != "null" ] && [ "$EXISTING_DOWNLOADS" != "{}" ]; then
DOWNLOADS_JSON="$EXISTING_DOWNLOADS"
else
DOWNLOADS_JSON=""
fi
else
DOWNLOADS_JSON=""
fi
# If downloads is empty, use default structure
if [ -z "$DOWNLOADS_JSON" ]; then
DOWNLOADS_JSON=$(cat << DOWNLOADS_EOF
{
"windows-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe",
"filename": "crossdesk-win-x64-${{ steps.version.outputs.TAG_NAME }}.exe"
},
"macos-x64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-x64-${{ steps.version.outputs.TAG_NAME }}.pkg"
},
"macos-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg",
"filename": "crossdesk-macos-arm64-${{ steps.version.outputs.TAG_NAME }}.pkg"
},
"linux-amd64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb",
"filename": "crossdesk-linux-amd64-${{ steps.version.outputs.TAG_NAME }}.deb"
},
"linux-arm64": {
"url": "https://downloads.crossdesk.cn/crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb",
"filename": "crossdesk-linux-arm64-${{ steps.version.outputs.TAG_NAME }}.deb"
}
}
DOWNLOADS_EOF
)
fi
# Generate version.json using cat and heredoc
cat > version.json << EOF
{
"version": "${{ steps.version.outputs.VERSION_ONLY }}",
"releaseDate": "${{ steps.version.outputs.BUILD_DATE_ISO }}",
"releaseName": ${{ steps.release_info.outputs.RELEASE_NAME }},
"releaseNotes": ${{ steps.release_info.outputs.RELEASE_BODY }},
"tagName": "${{ steps.version.outputs.TAG_NAME }}",
"downloads": ${DOWNLOADS_JSON}
}
EOF
cat version.json
- name: Upload version.json to server
uses: burnett01/rsync-deployments@5.2
with:
switches: -avzr --delete
path: version.json
remote_path: /var/www/html/version/
remote_host: ${{ secrets.SERVER_HOST }}
remote_user: ${{ secrets.SERVER_USER }}
remote_key: ${{ secrets.SERVER_KEY }}

View File

@@ -11,8 +11,14 @@
[ [English](README_EN.md) / 中文 ]
PC 客户端
![sup_example](https://github.com/user-attachments/assets/eeb64fbe-1f07-4626-be1c-b77396beb905)
Web 客户端
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
## 简介
CrossDesk 是一个轻量级的跨平台远程桌面软件,支持 Web 端控制远程设备。
@@ -55,7 +61,7 @@ CrossDesk 是 [MiniRTC](https://github.com/kunkundi/minirtc.git) 实时音视频
Linux环境下需安装以下包
```
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxfixes-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
```
编译
@@ -70,7 +76,14 @@ git submodule update
xmake b -vy crossdesk
```
编译选项
```
--USE_CUDA=true/false: 启用 CUDA 硬件编解码,默认不启用
--CROSSDESK_VERSION=xxx: 指定 CrossDesk 的版本
# 示例
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
运行
```
xmake r crossdesk
@@ -78,13 +91,14 @@ xmake r crossdesk
### 无 CUDA 环境下的开发支持
对于**未安装 CUDA 环境的 Linux 开发者**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
对于**未安装 CUDA 环境的 Linux 开发者,如果希望编译后的成果物拥有硬件编解码能力**,这里提供了预配置的 [Ubuntu 22.04 Docker 镜像](https://hub.docker.com/r/crossdesk/ubuntu22.04)。该镜像内置必要的构建依赖,可在容器中开箱即用,无需额外配置即可直接编译项目。
进入容器,下载工程后执行:
```
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
@@ -106,6 +120,7 @@ set CUDA_PATH=path_to_cuda_installdir
```
重新执行:
```
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
@@ -166,7 +181,7 @@ sudo docker run -d \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:latest
crossdesk/crossdesk-server:v1.1.1
```
上述命令中,用户需注意的参数如下:

View File

@@ -11,8 +11,14 @@
[ [中文](README.md) / English ]
PC Client
![sup_example](https://github.com/user-attachments/assets/3f17d8f3-7c4a-4b63-bae4-903363628687)
Web Client
<p align="center">
<img width="850" height="550" alt="6bddcbed47ffd4b9988a4037c7f4f524" src="https://github.com/user-attachments/assets/e44f73f9-24ac-46a3-a189-b7f8b6669881" />
</p>
# Intro
CrossDesk is a lightweight cross-platform remote desktop software.
@@ -58,7 +64,7 @@ Requirements:
Following packages need to be installed on Linux:
```
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
sudo apt-get install -y software-properties-common git curl unzip build-essential libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxcb-randr0-dev libxcb-xtest0-dev libxcb-xinerama0-dev libxcb-shape0-dev libxcb-xkb-dev libxcb-xfixes0-dev libxfixes-dev libxv-dev libxtst-dev libasound2-dev libsndio-dev libxcb-shm0-dev libasound2-dev libpulse-dev
```
Build:
@@ -73,7 +79,14 @@ git submodule update
xmake b -vy crossdesk
```
Build options:
```
--USE_CUDA=true/false: enable CUDA acceleration codec, default: false
--CROSSDESK_VERSION=xxx: set the version number
# example:
xmake f --CROSSDESK_VERSION=1.0.0 --USE_CUDA=true
```
Run:
```
xmake r crossdesk
@@ -81,7 +94,7 @@ xmake r crossdesk
#### Development Without CUDA Environment
For **Linux developers who do not have a CUDA environment** installed, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
For **Linux developers who do not have a CUDA environment installed and want to enable hardware codec feature**, a preconfigured [Ubuntu 22.04 Docker image](https://hub.docker.com/r/crossdesk/ubuntu22.04) is provided.
This image comes with all required build dependencies and allows you to build the project directly inside the container without any additional setup.
After entering the container, download the project and run:
@@ -89,6 +102,7 @@ After entering the container, download the project and run:
export CUDA_PATH=/usr/local/cuda
export XMAKE_GLOBALDIR=/data
xmake f --USE_CUDA=true
xmake b --root -vy crossdesk
```
@@ -111,6 +125,7 @@ set CUDA_PATH=path_to_cuda_installdir:
```
Then re-run:
```
xmake f --USE_CUDA=true
xmake b -vy crossdesk
```
@@ -174,7 +189,7 @@ sudo docker run -d \
-v /path/to/your/certs:/crossdesk-server/certs \
-v /path/to/your/db:/crossdesk-server/db \
-v /path/to/your/logs:/crossdesk-server/logs \
crossdesk/crossdesk-server:v1.1.0
crossdesk/crossdesk-server:v1.1.1
```
The parameters you need to pay attention to are as follows:

BIN
image.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -21,6 +21,7 @@
#include <sys/wait.h>
#include <unistd.h>
#else
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
@@ -77,21 +78,9 @@ Daemon::Daemon(const std::string& name)
{
}
void Daemon::stop() {
#ifdef _WIN32
running_ = false;
#else
running_ = false;
#endif
}
void Daemon::stop() { running_ = false; }
bool Daemon::isRunning() const {
#ifdef _WIN32
return running_;
#else
return running_;
#endif
}
bool Daemon::isRunning() const { return running_; }
bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32
@@ -102,15 +91,77 @@ bool Daemon::start(MainLoopFunc loop) {
running_ = true;
return runWithRestart(loop);
#else
// Linux: Use traditional daemonization
// linux: Daemonize first, then run with restart monitoring
instance_ = this;
runUnix(loop);
return true;
// check if running from terminal before fork
bool from_terminal =
(isatty(STDIN_FILENO) != 0) || (isatty(STDOUT_FILENO) != 0);
// first fork: detach from terminal
pid_t pid = fork();
if (pid < 0) {
std::cerr << "Failed to fork daemon process" << std::endl;
return false;
}
if (pid > 0) _exit(0);
if (setsid() < 0) {
std::cerr << "Failed to create new session" << std::endl;
return false;
}
pid = fork();
if (pid < 0) {
std::cerr << "Failed to fork daemon process (second fork)" << std::endl;
return false;
}
if (pid > 0) _exit(0);
umask(0);
chdir("/");
// redirect file descriptors: keep stdout/stderr if from terminal, else
// redirect to /dev/null
int fd = open("/dev/null", O_RDWR);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
if (!from_terminal) {
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
}
if (fd > 2) close(fd);
}
// set up signal handlers
signal(SIGTERM, [](int) {
if (instance_) instance_->stop();
});
signal(SIGINT, [](int) {
if (instance_) instance_->stop();
});
// ignore SIGPIPE
signal(SIGPIPE, SIG_IGN);
// set up SIGCHLD handler to reap zombie processes
struct sigaction sa_chld;
sa_chld.sa_handler = [](int) {
// reap zombie processes
while (waitpid(-1, nullptr, WNOHANG) > 0) {
// continue until no more zombie children
}
};
sigemptyset(&sa_chld.sa_mask);
sa_chld.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa_chld, nullptr);
running_ = true;
return runWithRestart(loop);
#endif
}
#ifdef _WIN32
// windows: execute loop and catch C++ exceptions
static int RunLoopCatchCpp(Daemon::MainLoopFunc& loop) {
try {
loop();
@@ -124,12 +175,11 @@ static int RunLoopCatchCpp(Daemon::MainLoopFunc& loop) {
}
}
// windows: Use SEH wrapper function to catch system-level crashes
static int RunLoopWithSEH(Daemon::MainLoopFunc& loop) {
__try {
return RunLoopCatchCpp(loop);
} __except (EXCEPTION_EXECUTE_HANDLER) {
// Catch system-level crashes (access violation, divide by zero, etc.)
// catch system-level crashes (access violation, divide by zero, etc.)
DWORD code = GetExceptionCode();
std::cerr << "System crash detected (SEH exception code: 0x" << std::hex
<< code << std::dec << ")" << std::endl;
@@ -138,9 +188,7 @@ static int RunLoopWithSEH(Daemon::MainLoopFunc& loop) {
}
#endif
// run function with restart logic (infinite restart)
// use child process monitoring: parent process creates child process to run
// main program, monitors child process status, restarts on crash
// run with restart logic: parent monitors child process and restarts on crash
bool Daemon::runWithRestart(MainLoopFunc loop) {
int restart_count = 0;
std::string exe_path = GetExecutablePath();
@@ -148,7 +196,6 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
std::cerr
<< "Failed to get executable path, falling back to direct execution"
<< std::endl;
// fallback to direct execution
while (isRunning()) {
try {
loop();
@@ -170,7 +217,6 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
STARTUPINFOA si = {sizeof(si)};
PROCESS_INFORMATION pi = {0};
// build command line arguments (add --child flag)
std::string cmd_line = "\"" + exe_path + "\" --child";
std::vector<char> cmd_line_buf(cmd_line.begin(), cmd_line.end());
cmd_line_buf.push_back('\0');
@@ -197,7 +243,6 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
continue;
}
// wait for child process to exit
DWORD exit_code = 0;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code);
@@ -205,56 +250,57 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
CloseHandle(pi.hThread);
if (exit_code == 0) {
// normal exit
break;
} else {
// abnormal exit, restart
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
break; // normal exit
}
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
#else
// linux: use fork + exec to create child process
pid_t pid = fork();
if (pid == 0) {
// child process: execute main program, pass --child argument
execl(exe_path.c_str(), exe_path.c_str(), "--child", nullptr);
// if exec fails, exit
_exit(1);
_exit(1); // exec failed
} else if (pid > 0) {
// parent process: wait for child process to exit
int status = 0;
waitpid(pid, &status, 0);
pid_t waited_pid = waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
if (exit_code == 0) {
// normal exit
break;
} else {
// abnormal exit, restart
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
}
} else if (WIFSIGNALED(status)) {
// child process terminated by signal (crash)
int sig = WTERMSIG(status);
if (waited_pid < 0) {
restart_count++;
std::cerr << "Child process crashed with signal " << sig
std::cerr << "waitpid failed, errno: " << errno
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
continue;
}
if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status);
if (exit_code == 0) {
break; // normal exit
}
restart_count++;
std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else if (WIFSIGNALED(status)) {
restart_count++;
std::cerr << "Child process crashed with signal " << WTERMSIG(status)
<< ", restarting... (attempt " << restart_count << ")"
<< std::endl;
} else {
restart_count++;
std::cerr << "Child process exited with unknown status, restarting... "
"(attempt "
<< restart_count << ")" << std::endl;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
} else {
// fork failed
std::cerr << "Failed to fork child process" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
@@ -265,132 +311,3 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
return true;
}
#ifndef _WIN32
void Daemon::runUnix(MainLoopFunc loop) {
struct sigaction sa;
sa.sa_handler = [](int) {};
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
sigaction(SIGPIPE, &sa, nullptr);
sigaction(SIGCHLD, &sa, nullptr);
// daemon mode: fork twice, redirect output
pid_t pid = fork();
if (pid > 0) _exit(0);
setsid();
pid = fork();
if (pid > 0) _exit(0);
umask(0);
chdir("/");
int fd = open("/dev/null", O_RDWR);
if (fd >= 0) {
dup2(fd, STDIN_FILENO);
dup2(fd, STDOUT_FILENO);
dup2(fd, STDERR_FILENO);
if (fd > 2) close(fd);
}
// catch signals for exit
signal(SIGTERM, [](int) { instance_->stop(); });
signal(SIGINT, [](int) { instance_->stop(); });
// catch crash signals, use atomic flags to mark crash
static std::atomic<bool> crash_detected{false};
static std::atomic<bool> should_restart{false};
// use safer signal handling: only set flags, don't call longjmp
struct sigaction sa_crash;
sa_crash.sa_handler = [](int sig) {
const char* sig_name = "Unknown";
switch (sig) {
case SIGSEGV:
sig_name = "SIGSEGV";
break;
case SIGABRT:
sig_name = "SIGABRT";
break;
case SIGFPE:
sig_name = "SIGFPE";
break;
case SIGILL:
sig_name = "SIGILL";
break;
}
std::cerr << "Crash signal detected: " << sig_name
<< ", will restart after process exits" << std::endl;
crash_detected = true;
should_restart = true;
// don't call longjmp, let program exit normally, restart by monitoring
// thread
};
sigemptyset(&sa_crash.sa_mask);
sa_crash.sa_flags = SA_RESETHAND; // handle only once, avoid recursion
sigaction(SIGSEGV, &sa_crash, nullptr);
sigaction(SIGABRT, &sa_crash, nullptr);
sigaction(SIGFPE, &sa_crash, nullptr);
sigaction(SIGILL, &sa_crash, nullptr);
running_ = true;
// run with restart logic (infinite restart)
// run main loop in separate thread, main thread monitors thread status
int restart_count = 0;
while (running_) {
crash_detected = false;
should_restart = false;
std::atomic<bool> loop_completed{false};
std::exception_ptr loop_exception = nullptr;
// run main loop in separate thread
std::thread loop_thread([&loop, &loop_completed, &loop_exception]() {
try {
loop();
loop_completed = true;
} catch (const std::exception& e) {
loop_exception = std::current_exception();
loop_completed = true;
} catch (...) {
loop_exception = std::current_exception();
loop_completed = true;
}
});
// wait for thread to complete
loop_thread.join();
// check exit reason
if (loop_exception) {
restart_count++;
try {
std::rethrow_exception(loop_exception);
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
} catch (...) {
std::cerr << "Unknown exception caught" << std::endl;
}
std::cerr << "Restarting... (attempt " << restart_count << ")"
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
continue;
}
// check if crash signal detected
if (crash_detected || should_restart) {
restart_count++;
std::cerr << "Crash detected, restarting... (attempt " << restart_count
<< ")" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
continue;
}
// normal exit
if (loop_completed) {
break;
}
}
}
#endif

View File

@@ -10,7 +10,6 @@
#include <functional>
#include <string>
// default restart delay (milliseconds)
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
class Daemon {
@@ -19,13 +18,10 @@ class Daemon {
Daemon(const std::string& name);
// start daemon (restart after 1 second by default)
bool start(MainLoopFunc loop);
// request exit
void stop();
// check if running
bool isRunning() const;
private:
@@ -36,7 +32,6 @@ class Daemon {
bool running_;
#else
static Daemon* instance_;
void runUnix(MainLoopFunc loop);
volatile bool running_;
#endif
};

View File

@@ -79,7 +79,7 @@ class ConfigCenter {
VIDEO_FRAME_RATE video_frame_rate_ = VIDEO_FRAME_RATE::FPS_60;
VIDEO_ENCODE_FORMAT video_encode_format_ = VIDEO_ENCODE_FORMAT::H264;
bool hardware_video_codec_ = false;
bool enable_turn_ = false;
bool enable_turn_ = true;
bool enable_srtp_ = false;
std::string signal_server_host_ = "api.crossdesk.cn";
std::string signal_server_host_default_ = "api.crossdesk.cn";

View File

@@ -51,7 +51,16 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
}
int KeyboardCapturer::Unhook() {
g_on_key_action = nullptr;
g_user_ptr = nullptr;
running_ = false;
if (display_) {
XSelectInput(display_, DefaultRootWindow(display_), 0);
XFlush(display_);
}
return 0;
}

View File

@@ -10,6 +10,10 @@ static void* g_user_ptr = nullptr;
CGEventRef eventCallback(CGEventTapProxy proxy, CGEventType type,
CGEventRef event, void* userInfo) {
if (!g_on_key_action) {
return event;
}
KeyboardCapturer* keyboard_capturer = (KeyboardCapturer*)userInfo;
if (!keyboard_capturer) {
LOG_ERROR("keyboard_capturer is nullptr");
@@ -120,12 +124,23 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
}
int KeyboardCapturer::Unhook() {
g_on_key_action = nullptr;
g_user_ptr = nullptr;
if (event_tap_) {
CGEventTapEnable(event_tap_, false);
}
if (run_loop_source_) {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), run_loop_source_,
kCFRunLoopCommonModes);
CFRelease(run_loop_source_);
run_loop_source_ = nullptr;
}
if (event_tap_) {
CFRelease(event_tap_);
event_tap_ = nullptr;
}
return 0;

View File

@@ -39,7 +39,12 @@ int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
}
int KeyboardCapturer::Unhook() {
UnhookWindowsHookEx(keyboard_hook_);
if (keyboard_hook_) {
g_on_key_action = nullptr;
g_user_ptr = nullptr;
UnhookWindowsHookEx(keyboard_hook_);
keyboard_hook_ = nullptr;
}
return 0;
}

File diff suppressed because it is too large Load Diff

View File

@@ -73,5 +73,7 @@
#define SETTINGS_OK_BUTTON_PADDING_EN 83
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_CN 78
#define SELF_HOSTED_SERVER_CONFIG_OK_BUTTON_PADDING_EN 91
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN 162
#define UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN 146
#endif

View File

@@ -158,12 +158,18 @@ static std::vector<std::string> no_such_id = {
static std::vector<std::string> about = {
reinterpret_cast<const char*>(u8"关于"), "About"};
static std::vector<std::string> notification = {
reinterpret_cast<const char*>(u8"通知"), "Notification"};
static std::vector<std::string> new_version_available = {
reinterpret_cast<const char*>(u8"新版本可用"), "New Version Available"};
static std::vector<std::string> version = {
reinterpret_cast<const char*>(u8"版本"), "Version"};
static std::vector<std::string> release_date = {
reinterpret_cast<const char*>(u8"发布日期: "), "Release Date: "};
static std::vector<std::string> access_website = {
reinterpret_cast<const char*>(u8"访问官网: "), "Access Website: "};
static std::vector<std::string> update = {
reinterpret_cast<const char*>(u8"更新"), "Update"};
static std::vector<std::string> confirm_delete_connection = {
reinterpret_cast<const char*>(u8"确认删除此连接"),
@@ -173,6 +179,9 @@ static std::vector<std::string> enable_autostart = {
reinterpret_cast<const char*>(u8"开机自启:"), "Auto Start:"};
static std::vector<std::string> enable_daemon = {
reinterpret_cast<const char*>(u8"启用守护进程:"), "Enable Daemon:"};
static std::vector<std::string> takes_effect_after_restart = {
reinterpret_cast<const char*>(u8"重启后生效"),
"Takes effect after restart"};
#if _WIN32
static std::vector<std::string> minimize_to_tray = {
reinterpret_cast<const char*>(u8"退出时最小化到系统托盘:"),

View File

@@ -736,6 +736,51 @@ int Render::SetupFontAndStyle() {
static const ImWchar icon_ranges[] = {ICON_MIN_FA, ICON_MAX_FA, 0};
io.Fonts->AddFontFromMemoryTTF(fa_solid_900_ttf, fa_solid_900_ttf_len, 30.0f,
&config, icon_ranges);
// Load system Chinese font as fallback
config.MergeMode = false;
config.FontDataOwnedByAtlas = false;
system_chinese_font_ = nullptr;
#if defined(_WIN32)
// Windows: Try Microsoft YaHei (微软雅黑) first, then SimSun (宋体)
const char* font_paths[] = {"C:/Windows/Fonts/msyh.ttc",
"C:/Windows/Fonts/msyhbd.ttc",
"C:/Windows/Fonts/simsun.ttc", nullptr};
#elif defined(__APPLE__)
// macOS: Try PingFang SC first, then STHeiti
const char* font_paths[] = {"/System/Library/Fonts/PingFang.ttc",
"/System/Library/Fonts/STHeiti Light.ttc",
"/System/Library/Fonts/STHeiti Medium.ttc",
nullptr};
#else
// Linux: Try common Chinese fonts
const char* font_paths[] = {
"/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
"/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
"/usr/share/fonts/truetype/arphic/uming.ttc",
"/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", nullptr};
#endif
for (int i = 0; font_paths[i] != nullptr; i++) {
std::ifstream font_file(font_paths[i], std::ios::binary);
if (font_file.good()) {
font_file.close();
system_chinese_font_ = io.Fonts->AddFontFromFileTTF(
font_paths[i], 32.0f, &config, io.Fonts->GetGlyphRangesChineseFull());
if (system_chinese_font_ != nullptr) {
LOG_INFO("Loaded system Chinese font: {}", font_paths[i]);
break;
}
}
}
// If no system font found, use default font
if (system_chinese_font_ == nullptr) {
system_chinese_font_ = io.Fonts->AddFontDefault(&config);
LOG_WARN("System Chinese font not found, using default font");
}
io.Fonts->Build();
ImGui::StyleColorsLight();
@@ -848,6 +893,8 @@ int Render::DrawMainWindow() {
MainWindow();
UpdateNotificationWindow();
ImGui::End();
// Rendering
@@ -913,8 +960,25 @@ int Render::DrawStreamWindow() {
}
int Render::Run() {
latest_version_ = CheckUpdate();
update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_);
latest_version_info_ = CheckUpdate();
if (!latest_version_info_.empty() &&
latest_version_info_.contains("version") &&
latest_version_info_["version"].is_string()) {
latest_version_ = latest_version_info_["version"];
if (latest_version_info_.contains("releaseNotes") &&
latest_version_info_["releaseNotes"].is_string()) {
release_notes_ = latest_version_info_["releaseNotes"];
} else {
release_notes_ = "";
}
update_available_ = IsNewerVersion(CROSSDESK_VERSION, latest_version_);
if (update_available_) {
show_update_notification_window_ = true;
}
} else {
latest_version_ = "";
update_available_ = false;
}
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (path_manager_) {
@@ -1340,6 +1404,7 @@ void Render::ProcessSdlEvent(const SDL_Event& event) {
is_client_mode_ = false;
reload_recent_connections_ = true;
fullscreen_button_pressed_ = false;
start_keyboard_capturer_ = false;
just_created_ = false;
recent_connection_image_save_time_ = SDL_GetTicks();
} else {

View File

@@ -13,6 +13,7 @@
#include <chrono>
#include <fstream>
#include <mutex>
#include <nlohmann/json.hpp>
#include <optional>
#include <string>
#include <unordered_map>
@@ -151,6 +152,7 @@ class Render {
int CreateStreamRenderWindow();
int TitleBar(bool main_window);
int MainWindow();
int UpdateNotificationWindow();
int StreamWindow();
int LocalWindow();
int RemoteWindow();
@@ -165,6 +167,8 @@ class Render {
bool ConnectionStatusWindow(
std::shared_ptr<SubStreamWindowProperties>& props);
int ShowRecentConnections();
void Hyperlink(const std::string& label, const std::string& url,
const float window_width);
private:
int ConnectTo(const std::string& remote_id, const char* password,
@@ -300,6 +304,7 @@ class Render {
SDL_Window* main_window_ = nullptr;
SDL_Renderer* main_renderer_ = nullptr;
ImGuiContext* main_ctx_ = nullptr;
ImFont* system_chinese_font_ = nullptr; // System Chinese font for fallback
bool exit_ = false;
const int sdl_refresh_ms_ = 16; // ~60 FPS
#if _WIN32
@@ -307,8 +312,10 @@ class Render {
#endif
// main window properties
std::string latest_version_ = "";
nlohmann::json latest_version_info_ = nlohmann::json{};
bool update_available_ = false;
std::string latest_version_ = "";
std::string release_notes_ = "";
bool start_mouse_controller_ = false;
bool mouse_controller_is_started_ = false;
bool start_screen_capturer_ = false;
@@ -349,6 +356,8 @@ class Render {
float notification_window_height_ = 80;
float about_window_width_ = 300;
float about_window_height_ = 170;
float update_notification_window_width_ = 400;
float update_notification_window_height_ = 320;
int screen_width_ = 1280;
int screen_height_ = 720;
int selected_display_ = 0;
@@ -404,6 +413,7 @@ class Render {
bool show_about_window_ = false;
bool show_connection_status_window_ = false;
bool show_reset_password_window_ = false;
bool show_update_notification_window_ = false;
bool fullscreen_button_pressed_ = false;
bool focus_on_input_widget_ = true;
bool is_client_mode_ = false;
@@ -448,7 +458,7 @@ class Render {
int video_frame_rate_button_value_ = 1;
int video_encode_format_button_value_ = 0;
bool enable_hardware_video_codec_ = false;
bool enable_turn_ = false;
bool enable_turn_ = true;
bool enable_srtp_ = false;
char signal_server_ip_[256] = "api.crossdesk.cn";
char signal_server_port_[6] = "9099";
@@ -460,7 +470,7 @@ class Render {
int video_frame_rate_button_value_last_ = 0;
int video_encode_format_button_value_last_ = 0;
bool enable_hardware_video_codec_last_ = false;
bool enable_turn_last_ = false;
bool enable_turn_last_ = true;
bool enable_srtp_last_ = false;
bool enable_self_hosted_last_ = false;
bool enable_autostart_ = false;

View File

@@ -1,3 +1,5 @@
#include <cmath>
#include "device_controller.h"
#include "localization.h"
#include "platform.h"
@@ -101,28 +103,39 @@ int Render::ProcessMouseEvent(const SDL_Event& event) {
last_mouse_event.button.y >= props->stream_render_rect_.y &&
last_mouse_event.button.y <= props->stream_render_rect_.y +
props->stream_render_rect_.h) {
int scroll_x = event.wheel.x;
int scroll_y = event.wheel.y;
float scroll_x = event.wheel.x;
float scroll_y = event.wheel.y;
if (event.wheel.direction == SDL_MOUSEWHEEL_FLIPPED) {
scroll_x = -scroll_x;
scroll_y = -scroll_y;
}
remote_action.type = ControlType::mouse;
if (scroll_x == 0) {
auto roundUp = [](float value) -> int {
if (value > 0) {
return static_cast<int>(std::ceil(value));
} else if (value < 0) {
return static_cast<int>(std::floor(value));
}
return 0;
};
if (std::abs(scroll_y) >= std::abs(scroll_x)) {
remote_action.m.flag = MouseFlag::wheel_vertical;
remote_action.m.s = scroll_y;
} else if (scroll_y == 0) {
remote_action.m.s = roundUp(scroll_y);
} else {
remote_action.m.flag = MouseFlag::wheel_horizontal;
remote_action.m.s = scroll_x;
remote_action.m.s = roundUp(scroll_x);
}
render_width = props->stream_render_rect_.w;
render_height = props->stream_render_rect_.h;
remote_action.m.x =
(float)(event.button.x - props->stream_render_rect_.x) / render_width;
(float)(last_mouse_event.button.x - props->stream_render_rect_.x) /
render_width;
remote_action.m.y =
(float)(event.button.y - props->stream_render_rect_.y) /
(float)(last_mouse_event.button.y - props->stream_render_rect_.y) /
render_height;
std::string msg = remote_action.to_json();
@@ -225,8 +238,6 @@ void Render::OnReceiveVideoBufferCb(const XVideoFrame* video_frame,
props->video_height_ = video_frame->height;
props->video_size_ = video_frame->size;
LOG_ERROR("receive: {}x{}", props->video_width_, props->video_height_);
if (need_to_update_render_rect) {
render->UpdateRenderRect();
}
@@ -458,12 +469,12 @@ void Render::OnConnectionStatusCb(ConnectionStatus status, const char* user_id,
render->need_to_send_host_info_ = true;
render->start_screen_capturer_ = true;
render->start_speaker_capturer_ = true;
#ifdef CROSSDESK_DEBUG
render->start_mouse_controller_ = false;
render->start_keyboard_capturer_ = false;
#else
// #ifdef CROSSDESK_DEBUG
// render->start_mouse_controller_ = false;
// render->start_keyboard_capturer_ = false;
// #else
render->start_mouse_controller_ = true;
#endif
// #endif
if (std::all_of(render->connection_status_.begin(),
render->connection_status_.end(), [](const auto& kv) {
return kv.first.find("web") != std::string::npos;

View File

@@ -8,8 +8,8 @@
namespace crossdesk {
void Hyperlink(const std::string& label, const std::string& url,
const float window_width) {
void Render::Hyperlink(const std::string& label, const std::string& url,
const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::SetCursorPosX(window_width * 0.1f);
ImGui::Text("%s", label.c_str());

View File

@@ -272,6 +272,15 @@ int Render::SettingWindow() {
}
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_daemon_", &enable_daemon_);
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
ImGui::SetWindowFontScale(0.5f);
ImGui::Text("%s", localization::takes_effect_after_restart
[localization_language_index_]
.c_str());
ImGui::SetWindowFontScale(1.0f);
ImGui::EndTooltip();
}
}
#if _WIN32
ImGui::Separator();

View File

@@ -0,0 +1,205 @@
#include <algorithm>
#include <cstdlib>
#include <string>
#include "layout.h"
#include "localization.h"
#include "rd_log.h"
#include "render.h"
namespace crossdesk {
std::string CleanMarkdown(const std::string& markdown) {
std::string result = markdown;
// remove # title mark
size_t pos = 0;
while (pos < result.length()) {
if (result[pos] == '\n' || pos == 0) {
size_t line_start = (result[pos] == '\n') ? pos + 1 : pos;
if (line_start < result.length() && result[line_start] == '#') {
size_t hash_end = line_start;
while (hash_end < result.length() &&
(result[hash_end] == '#' || result[hash_end] == ' ')) {
hash_end++;
}
result.erase(line_start, hash_end - line_start);
pos = line_start;
continue;
}
}
pos++;
}
// remove ** bold mark
pos = 0;
while ((pos = result.find("**", pos)) != std::string::npos) {
result.erase(pos, 2);
}
// remove all spaces
result.erase(std::remove(result.begin(), result.end(), ' '), result.end());
// replace . with 、
pos = 0;
while ((pos = result.find('.', pos)) != std::string::npos) {
result.replace(pos, 1, "");
pos += 1; // Move to next position after the replacement
}
return result;
}
int Render::UpdateNotificationWindow() {
if (show_update_notification_window_ && update_available_) {
const ImGuiViewport* viewport = ImGui::GetMainViewport();
float window_width = update_notification_window_width_;
float window_height = update_notification_window_height_;
ImGui::SetNextWindowPos(
ImVec2(
(viewport->WorkSize.x - viewport->WorkPos.x - window_width) / 2,
(viewport->WorkSize.y - viewport->WorkPos.y - window_height) / 2),
ImGuiCond_FirstUseEver);
ImGui::SetNextWindowSize(ImVec2(window_width, window_height));
ImGui::PushStyleColor(ImGuiCol_WindowBg, ImVec4(1.0f, 1.0f, 1.0f, 1.0f));
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 1.0f);
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 5.0f);
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 3.0f);
ImGui::SetWindowFontScale(0.5f);
ImGui::Begin(
localization::notification[localization_language_index_].c_str(),
nullptr,
ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoSavedSettings);
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
// use system Chinese font
if (system_chinese_font_ != nullptr) {
ImGui::PushFont(system_chinese_font_);
}
// title: new version available
ImGui::SetCursorPosX(window_width * 0.1f);
ImGui::SetWindowFontScale(0.7f);
std::string title =
localization::new_version_available[localization_language_index_] +
": v" + latest_version_;
ImGui::Text("%s", title.c_str());
ImGui::SetWindowFontScale(0.5f);
ImGui::Spacing();
// website link
std::string download_text =
localization::access_website[localization_language_index_] +
"https://crossdesk.cn";
Hyperlink(download_text, "https://crossdesk.cn", window_width);
ImGui::Spacing();
float reserved_height = 140.0f;
float scrollable_height = window_height - reserved_height;
// scrollable content area
ImGui::SetCursorPosX(window_width * 0.05f);
ImGui::BeginChild("ScrollableContent",
ImVec2(window_width * 0.9f, scrollable_height),
ImGuiChildFlags_Border, ImGuiWindowFlags_None);
// set text wrap position to current available width (accounts for
// scrollbar)
float wrap_pos = ImGui::GetContentRegionAvail().x;
ImGui::PushTextWrapPos(wrap_pos);
// release name
if (latest_version_info_.contains("releaseName") &&
latest_version_info_["releaseName"].is_string() &&
!latest_version_info_["releaseName"].empty()) {
ImGui::SetCursorPosX(window_width * 0.05f);
std::string release_name =
latest_version_info_["releaseName"].get<std::string>();
ImGui::TextWrapped("%s", release_name.c_str());
ImGui::Spacing();
}
// release notes
if (!release_notes_.empty()) {
ImGui::SetCursorPosX(window_width * 0.05f);
std::string cleaned_notes = CleanMarkdown(release_notes_);
ImGui::TextWrapped("%s", cleaned_notes.c_str());
ImGui::Spacing();
}
// release date
if (latest_version_info_.contains("releaseDate") &&
latest_version_info_["releaseDate"].is_string() &&
!latest_version_info_["releaseDate"].empty()) {
ImGui::SetCursorPosX(window_width * 0.05f);
std::string date_label =
localization::release_date[localization_language_index_];
std::string release_date = latest_version_info_["releaseDate"];
std::string date_text = date_label + release_date;
ImGui::Text("%s", date_text.c_str());
ImGui::Spacing();
}
// pop text wrap position
ImGui::PopTextWrapPos();
ImGui::EndChild();
ImGui::Spacing();
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(UPDATE_NOTIFICATION_OK_BUTTON_PADDING_CN);
} else {
ImGui::SetCursorPosX(UPDATE_NOTIFICATION_OK_BUTTON_PADDING_EN);
}
// update button
if (ImGui::Button(
localization::update[localization_language_index_].c_str())) {
// open download page
std::string url = "https://crossdesk.cn";
#if defined(_WIN32)
std::string cmd = "start " + url;
#elif defined(__APPLE__)
std::string cmd = "open " + url;
#else
std::string cmd = "xdg-open " + url;
#endif
system(cmd.c_str());
show_update_notification_window_ = false;
}
ImGui::SameLine();
if (ImGui::Button(
localization::cancel[localization_language_index_].c_str())) {
show_update_notification_window_ = false;
}
ImGui::SetWindowFontScale(1.0f);
ImGui::SetWindowFontScale(0.5f);
// pop system font
if (system_chinese_font_ != nullptr) {
ImGui::PopFont();
}
ImGui::End();
ImGui::SetWindowFontScale(1.0f);
ImGui::PopStyleVar(3);
ImGui::PopStyleColor();
}
return 0;
}
} // namespace crossdesk

View File

@@ -1,5 +1,7 @@
#include "screen_capturer_x11.h"
#include <X11/extensions/Xfixes.h>
#include <chrono>
#include <thread>
@@ -100,6 +102,7 @@ int ScreenCapturerX11::Destroy() {
int ScreenCapturerX11::Start(bool show_cursor) {
if (running_) return 0;
show_cursor_ = show_cursor;
running_ = true;
paused_ = false;
thread_ = std::thread([this]() {
@@ -156,6 +159,20 @@ void ScreenCapturerX11::OnFrame() {
AllPlanes, ZPixmap);
if (!image) return;
// if enable show cursor, draw cursor
if (show_cursor_) {
Window root_return, child_return;
int root_x, root_y, win_x, win_y;
unsigned int mask;
if (XQueryPointer(display_, root_, &root_return, &child_return, &root_x,
&root_y, &win_x, &win_y, &mask)) {
if (root_x >= left_ && root_x < left_ + width_ && root_y >= top_ &&
root_y < top_ + height_) {
DrawCursor(image, root_x - left_, root_y - top_);
}
}
}
bool needs_copy = image->bytes_per_line != width_ * 4;
std::vector<uint8_t> argb_buf;
uint8_t* src_argb = nullptr;
@@ -186,4 +203,82 @@ void ScreenCapturerX11::OnFrame() {
XDestroyImage(image);
}
void ScreenCapturerX11::DrawCursor(XImage* image, int x, int y) {
if (!display_ || !image) {
return;
}
// check XFixes extension
int event_base, error_base;
if (!XFixesQueryExtension(display_, &event_base, &error_base)) {
return;
}
XFixesCursorImage* cursor_image = XFixesGetCursorImage(display_);
if (!cursor_image) {
return;
}
int cursor_width = cursor_image->width;
int cursor_height = cursor_image->height;
int draw_x = x - cursor_image->xhot;
int draw_y = y - cursor_image->yhot;
// draw cursor on image
for (int cy = 0; cy < cursor_height; ++cy) {
for (int cx = 0; cx < cursor_width; ++cx) {
int img_x = draw_x + cx;
int img_y = draw_y + cy;
if (img_x < 0 || img_x >= image->width || img_y < 0 ||
img_y >= image->height) {
continue;
}
unsigned long cursor_pixel = cursor_image->pixels[cy * cursor_width + cx];
unsigned char a = (cursor_pixel >> 24) & 0xFF;
// if alpha is 0, skip
if (a == 0) {
continue;
}
unsigned long img_pixel = XGetPixel(image, img_x, img_y);
unsigned char img_r = (img_pixel >> 16) & 0xFF;
unsigned char img_g = (img_pixel >> 8) & 0xFF;
unsigned char img_b = img_pixel & 0xFF;
unsigned char cursor_r = (cursor_pixel >> 16) & 0xFF;
unsigned char cursor_g = (cursor_pixel >> 8) & 0xFF;
unsigned char cursor_b = cursor_pixel & 0xFF;
// alpha mix
unsigned char final_r, final_g, final_b;
if (a == 255) {
// if alpha is 255, use cursor color
final_r = cursor_r;
final_g = cursor_g;
final_b = cursor_b;
} else {
float alpha = a / 255.0f;
float inv_alpha = 1.0f - alpha;
final_r =
static_cast<unsigned char>(cursor_r * alpha + img_r * inv_alpha);
final_g =
static_cast<unsigned char>(cursor_g * alpha + img_g * inv_alpha);
final_b =
static_cast<unsigned char>(cursor_b * alpha + img_b * inv_alpha);
}
// set pixel
unsigned long new_pixel = (final_r << 16) | (final_g << 8) | final_b;
XPutPixel(image, img_x, img_y, new_pixel);
}
}
XFree(cursor_image);
}
} // namespace crossdesk

View File

@@ -4,19 +4,15 @@
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _VERSION_CHECKER_H_
#define _VERSION_CHECKER_H_
#include "version_checker.h"
#include <httplib.h>
#include <iostream>
#include <nlohmann/json.hpp>
#include <sstream>
#include <string>
#include <vector>
using json = nlohmann::json;
namespace crossdesk {
static std::string latest_release_date_ = "";
@@ -129,7 +125,7 @@ bool IsNewerVersionWithDate(const std::string& current_version,
return false;
}
std::string CheckUpdate() {
nlohmann::json CheckUpdate() {
httplib::Client cli("https://version.crossdesk.cn");
cli.set_connection_timeout(5);
@@ -138,28 +134,25 @@ std::string CheckUpdate() {
if (auto res = cli.Get("/version.json")) {
if (res->status == 200) {
try {
auto j = json::parse(res->body);
std::string latest = j["version"];
auto j = nlohmann::json::parse(res->body);
if (j.contains("releaseDate") && j["releaseDate"].is_string()) {
latest_release_date_ = j["releaseDate"];
} else {
latest_release_date_ = "";
}
return latest;
return j;
} catch (std::exception&) {
latest_release_date_ = "";
return "";
return nlohmann::json{};
}
} else {
latest_release_date_ = "";
return "";
return nlohmann::json{};
}
} else {
latest_release_date_ = "";
return "";
return nlohmann::json{};
}
}
std::string GetLatestReleaseDate() { return latest_release_date_; }
} // namespace crossdesk
#endif
} // namespace crossdesk

View File

@@ -7,11 +7,12 @@
#ifndef _VERSION_CHECKER_H_
#define _VERSION_CHECKER_H_
#include <nlohmann/json.hpp>
#include <string>
namespace crossdesk {
std::string CheckUpdate();
nlohmann::json CheckUpdate();
bool IsNewerVersion(const std::string& current, const std::string& latest);

View File

@@ -45,7 +45,7 @@ elseif is_os("linux") then
add_links("pulse-simple", "pulse")
add_requires("libyuv")
add_syslinks("pthread", "dl")
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr")
add_links("SDL3", "asound", "X11", "Xtst", "Xrandr", "Xfixes")
add_cxflags("-Wno-unused-variable")
elseif is_os("macosx") then
add_links("SDL3")