Compare commits

..

19 Commits

Author SHA1 Message Date
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
dijunkun
1457247a6a [feat] add build option USE_CUDA to enable or disable CUDA-based hardware codec acceleration and USE_CUDA=false by default 2025-11-19 23:15:56 +08:00
dijunkun
97ab9bfca5 [feat] add daemon support with automatic restart on crash 2025-11-19 22:09:51 +08:00
22 changed files with 1310 additions and 609 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
@@ -55,7 +55,7 @@ jobs:
XMAKE_GLOBALDIR: /data
run: |
ls -la $XMAKE_GLOBALDIR
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --root -y
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
@@ -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
@@ -120,7 +120,7 @@ jobs:
CUDA_PATH: /usr/local/cuda
XMAKE_GLOBALDIR: /data
run: |
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --root -y
xmake f --CROSSDESK_VERSION=${LEGAL_VERSION} --USE_CUDA=true --root -y
xmake b -vy --root crossdesk
- name: Decode and save certificate
@@ -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 }}-
@@ -189,7 +189,7 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${VERSION_NUM} -y
xmake f --CROSSDESK_VERSION=${VERSION_NUM} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
@@ -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-
@@ -298,7 +298,7 @@ jobs:
- name: Build CrossDesk
run: |
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} -y
xmake f --CROSSDESK_VERSION=${{ env.VERSION_NUM }} --USE_CUDA=true -y
xmake b -vy crossdesk
- name: Decode and save certificate
@@ -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}"

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

313
src/app/daemon.cpp Normal file
View File

@@ -0,0 +1,313 @@
#include "daemon.h"
#include <atomic>
#include <chrono>
#include <cstring>
#include <iostream>
#include <thread>
#include <vector>
#ifdef _WIN32
#include <process.h>
#include <tchar.h>
#include <windows.h>
#elif __APPLE__
#include <fcntl.h>
#include <limits.h>
#include <mach-o/dyld.h>
#include <signal.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <unistd.h>
#else
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <signal.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <cstring>
#endif
#ifndef _WIN32
Daemon* Daemon::instance_ = nullptr;
#endif
// get executable file path
static std::string GetExecutablePath() {
#ifdef _WIN32
char path[32768];
DWORD length = GetModuleFileNameA(nullptr, path, sizeof(path));
if (length > 0 && length < sizeof(path)) {
return std::string(path);
}
#elif __APPLE__
char path[PATH_MAX];
uint32_t size = sizeof(path);
if (_NSGetExecutablePath(path, &size) == 0) {
char resolved_path[PATH_MAX];
if (realpath(path, resolved_path) != nullptr) {
return std::string(resolved_path);
}
return std::string(path);
}
#else
char path[PATH_MAX];
ssize_t count = readlink("/proc/self/exe", path, sizeof(path) - 1);
if (count != -1) {
path[count] = '\0';
return std::string(path);
}
#endif
return "";
}
Daemon::Daemon(const std::string& name)
: name_(name)
#ifdef _WIN32
,
running_(false)
#else
,
running_(true)
#endif
{
}
void Daemon::stop() { running_ = false; }
bool Daemon::isRunning() const { return running_; }
bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32
running_ = true;
return runWithRestart(loop);
#elif __APPLE__
// macOS: Use child process monitoring (like Windows) to preserve GUI
running_ = true;
return runWithRestart(loop);
#else
// linux: Daemonize first, then run with restart monitoring
instance_ = this;
// 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
static int RunLoopCatchCpp(Daemon::MainLoopFunc& loop) {
try {
loop();
return 0; // normal exit
} catch (const std::exception& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
return 1; // c++ exception
} catch (...) {
std::cerr << "Unknown exception caught" << std::endl;
return 1; // other exception
}
}
static int RunLoopWithSEH(Daemon::MainLoopFunc& loop) {
__try {
return RunLoopCatchCpp(loop);
} __except (EXCEPTION_EXECUTE_HANDLER) {
// 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;
return 2; // System crash
}
}
#endif
// 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();
if (exe_path.empty()) {
std::cerr
<< "Failed to get executable path, falling back to direct execution"
<< std::endl;
while (isRunning()) {
try {
loop();
break;
} catch (...) {
restart_count++;
std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
}
}
return true;
}
while (isRunning()) {
#ifdef _WIN32
// windows: use CreateProcess to create child process
STARTUPINFOA si = {sizeof(si)};
PROCESS_INFORMATION pi = {0};
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');
BOOL success = CreateProcessA(
nullptr, // executable file path (specified in command line)
cmd_line_buf.data(), // command line arguments
nullptr, // process security attributes
nullptr, // thread security attributes
FALSE, // don't inherit handles
0, // creation flags
nullptr, // environment variables (inherit from parent)
nullptr, // current directory
&si, // startup info
&pi // process information
);
if (!success) {
std::cerr << "Failed to create child process, error: " << GetLastError()
<< std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++;
continue;
}
DWORD exit_code = 0;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
if (exit_code == 0) {
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) {
execl(exe_path.c_str(), exe_path.c_str(), "--child", nullptr);
_exit(1); // exec failed
} else if (pid > 0) {
int status = 0;
pid_t waited_pid = waitpid(pid, &status, 0);
if (waited_pid < 0) {
restart_count++;
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 {
std::cerr << "Failed to fork child process" << std::endl;
std::this_thread::sleep_for(
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++;
}
#endif
}
return true;
}

39
src/app/daemon.h Normal file
View File

@@ -0,0 +1,39 @@
/*
* @Author: DI JUNKUN
* @Date: 2025-11-19
* Copyright (c) 2025 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _DAEMON_H_
#define _DAEMON_H_
#include <functional>
#include <string>
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
class Daemon {
public:
using MainLoopFunc = std::function<void()>;
Daemon(const std::string& name);
bool start(MainLoopFunc loop);
void stop();
bool isRunning() const;
private:
std::string name_;
bool runWithRestart(MainLoopFunc loop);
#ifdef _WIN32
bool running_;
#else
static Daemon* instance_;
volatile bool running_;
#endif
};
#endif

View File

@@ -6,12 +6,61 @@
#endif
#endif
#include "rd_log.h"
#include <cstring>
#include <memory>
#include <string>
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "render.h"
int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) {
int main(int argc, char* argv[]) {
// check if running as child process
bool is_child = false;
for (int i = 1; i < argc; i++) {
if (strcmp(argv[i], "--child") == 0) {
is_child = true;
break;
}
}
if (is_child) {
// child process: run render directly
crossdesk::Render render;
render.Run();
return 0;
}
bool enable_daemon = false;
auto path_manager = std::make_unique<crossdesk::PathManager>("CrossDesk");
if (path_manager) {
std::string cert_path =
(path_manager->GetCertPath() / "crossdesk.cn_root.crt").string();
std::string cache_path = path_manager->GetCachePath().string();
crossdesk::ConfigCenter config_center(cache_path + "/config.ini",
cert_path);
enable_daemon = config_center.IsEnableDaemon();
}
if (enable_daemon) {
// start daemon with restart monitoring
Daemon daemon("CrossDesk");
// define main loop function: run render and stop daemon on normal exit
Daemon::MainLoopFunc main_loop = [&daemon]() {
crossdesk::Render render;
render.Run();
daemon.stop();
};
// start daemon and return result
bool success = daemon.start(main_loop);
return success ? 0 : 1;
}
// run without daemon: direct execution
crossdesk::Render render;
render.Run();
return 0;
}

View File

@@ -53,6 +53,7 @@ int ConfigCenter::Load() {
ini_.GetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
enable_autostart_ =
ini_.GetBoolValue(section_, "enable_autostart", enable_autostart_);
enable_daemon_ = ini_.GetBoolValue(section_, "enable_daemon", enable_daemon_);
enable_minimize_to_tray_ = ini_.GetBoolValue(
section_, "enable_minimize_to_tray", enable_minimize_to_tray_);
@@ -76,6 +77,7 @@ int ConfigCenter::Save() {
ini_.SetValue(section_, "cert_file_path", cert_file_path_.c_str());
ini_.SetBoolValue(section_, "enable_self_hosted", enable_self_hosted_);
ini_.SetBoolValue(section_, "enable_autostart", enable_autostart_);
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
ini_.SetBoolValue(section_, "enable_minimize_to_tray",
enable_minimize_to_tray_);
@@ -249,6 +251,18 @@ int ConfigCenter::SetAutostart(bool enable_autostart) {
return 0;
}
int ConfigCenter::SetDaemon(bool enable_daemon) {
enable_daemon_ = enable_daemon;
ini_.SetBoolValue(section_, "enable_daemon", enable_daemon_);
SI_Error rc = ini_.SaveFile(config_path_.c_str());
if (rc < 0) {
return -1;
}
return 0;
}
// getters
ConfigCenter::LANGUAGE ConfigCenter::GetLanguage() const { return language_; }
@@ -304,4 +318,6 @@ bool ConfigCenter::IsSelfHosted() const { return enable_self_hosted_; }
bool ConfigCenter::IsMinimizeToTray() const { return enable_minimize_to_tray_; }
bool ConfigCenter::IsEnableAutostart() const { return enable_autostart_; }
bool ConfigCenter::IsEnableDaemon() const { return enable_daemon_; }
} // namespace crossdesk

View File

@@ -41,6 +41,7 @@ class ConfigCenter {
int SetSelfHosted(bool enable_self_hosted);
int SetMinimizeToTray(bool enable_minimize_to_tray);
int SetAutostart(bool enable_autostart);
int SetDaemon(bool enable_daemon);
// read config
@@ -62,6 +63,7 @@ class ConfigCenter {
bool IsSelfHosted() const;
bool IsMinimizeToTray() const;
bool IsEnableAutostart() const;
bool IsEnableDaemon() const;
int Load();
int Save();
@@ -77,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";
@@ -89,6 +91,7 @@ class ConfigCenter {
bool enable_self_hosted_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_autostart_ = false;
bool enable_daemon_ = false;
};
} // namespace crossdesk
#endif

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

@@ -20,6 +20,15 @@
#define INPUT_WINDOW_PADDING_EN 96
#define SETTINGS_WINDOW_WIDTH_CN 202
#define SETTINGS_WINDOW_WIDTH_EN 248
#if USE_CUDA
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 405
#define SETTINGS_WINDOW_HEIGHT_EN 405
#else
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
#endif
#else
#if _WIN32
#define SETTINGS_WINDOW_HEIGHT_CN 375
#define SETTINGS_WINDOW_HEIGHT_EN 375
@@ -27,6 +36,7 @@
#define SETTINGS_WINDOW_HEIGHT_CN 345
#define SETTINGS_WINDOW_HEIGHT_EN 345
#endif
#endif
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_CN 228
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_WIDTH_EN 275
#define SELF_HOSTED_SERVER_CONFIG_WINDOW_HEIGHT_CN 195
@@ -49,6 +59,8 @@
#define ENABLE_SELF_HOSTED_SERVER_CHECKBOX_PADDING_EN 218
#define ENABLE_AUTOSTART_PADDING_CN 171
#define ENABLE_AUTOSTART_PADDING_EN 218
#define ENABLE_DAEMON_PADDING_CN 171
#define ENABLE_DAEMON_PADDING_EN 218
#define ENABLE_MINIZE_TO_TRAY_PADDING_CN 171
#define ENABLE_MINIZE_TO_TRAY_PADDING_EN 218
#define SELF_HOSTED_SERVER_HOST_INPUT_BOX_PADDING_CN 90

View File

@@ -171,6 +171,11 @@ static std::vector<std::string> confirm_delete_connection = {
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

@@ -261,6 +261,7 @@ int Render::LoadSettingsFromCacheFile() {
enable_srtp_ = config_center_->IsEnableSrtp();
enable_self_hosted_ = config_center_->IsSelfHosted();
enable_autostart_ = config_center_->IsEnableAutostart();
enable_daemon_ = config_center_->IsEnableDaemon();
enable_minimize_to_tray_ = config_center_->IsMinimizeToTray();
language_button_value_last_ = language_button_value_;
@@ -1339,6 +1340,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

@@ -448,7 +448,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,11 +460,13 @@ 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;
bool enable_autostart_last_ = false;
bool enable_daemon_ = false;
bool enable_daemon_last_ = false;
bool enable_minimize_to_tray_ = false;
bool enable_minimize_to_tray_last_ = false;
char signal_server_ip_self_[256] = "";

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

@@ -158,6 +158,7 @@ int Render::SettingWindow() {
video_encode_format_items, IM_ARRAYSIZE(video_encode_format_items));
}
#if USE_CUDA && !defined(__aarch64__) && !defined(__arm__)
ImGui::Separator();
{
@@ -176,6 +177,7 @@ int Render::SettingWindow() {
ImGui::Checkbox("##enable_hardware_video_codec",
&enable_hardware_video_codec_);
}
#endif
ImGui::Separator();
@@ -252,6 +254,34 @@ int Render::SettingWindow() {
ImGui::SetCursorPosY(settings_items_offset);
ImGui::Checkbox("##enable_autostart_", &enable_autostart_);
}
ImGui::Separator();
{
settings_items_offset += settings_items_padding;
ImGui::SetCursorPosY(settings_items_offset + 4);
ImGui::Text(
"%s",
localization::enable_daemon[localization_language_index_].c_str());
if (ConfigCenter::LANGUAGE::CHINESE == localization_language_) {
ImGui::SetCursorPosX(ENABLE_DAEMON_PADDING_CN);
} else {
ImGui::SetCursorPosX(ENABLE_DAEMON_PADDING_EN);
}
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();
@@ -373,6 +403,13 @@ int Render::SettingWindow() {
}
enable_autostart_last_ = enable_autostart_;
if (enable_daemon_) {
config_center_->SetDaemon(true);
} else {
config_center_->SetDaemon(false);
}
enable_daemon_last_ = enable_daemon_;
#if _WIN32
if (enable_minimize_to_tray_) {
config_center_->SetMinimizeToTray(true);

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

@@ -7,6 +7,12 @@ option("CROSSDESK_VERSION")
set_description("Set CROSSDESK_VERSION for build")
option_end()
option("USE_CUDA")
set_default(false)
set_showmenu(true)
set_description("Use CUDA for hardware codec acceleration")
option_end()
add_rules("mode.release", "mode.debug")
set_languages("c++17")
set_encodings("utf-8")
@@ -16,6 +22,8 @@ set_encodings("utf-8")
-- add_cxxflags("/W4", "/WX")
add_defines("UNICODE")
add_defines("USE_CUDA=" .. (is_config("USE_CUDA", true) and "1" or "0"))
if is_mode("debug") then
add_defines("CROSSDESK_DEBUG")
end
@@ -37,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")
@@ -179,4 +187,5 @@ target("gui")
target("crossdesk")
set_kind("binary")
add_deps("rd_log", "common", "gui")
add_files("src/app/main.cpp")
add_files("src/app/*.cpp")
add_includedirs("src/app", {public = true})