Compare commits

...

6 Commits

9 changed files with 214 additions and 86 deletions

View File

@@ -34,9 +34,16 @@
#endif #endif
#ifndef _WIN32 #ifndef _WIN32
Daemon* Daemon::instance_ = nullptr; volatile std::sig_atomic_t Daemon::stop_requested_ = 0;
#endif #endif
namespace {
constexpr int kRestartDelayMs = 1000;
#ifndef _WIN32
constexpr int kWaitPollIntervalMs = 200;
#endif
} // namespace
// get executable file path // get executable file path
static std::string GetExecutablePath() { static std::string GetExecutablePath() {
#ifdef _WIN32 #ifdef _WIN32
@@ -66,33 +73,35 @@ static std::string GetExecutablePath() {
return ""; return "";
} }
Daemon::Daemon(const std::string& name) Daemon::Daemon(const std::string& name) : name_(name), running_(false) {}
: name_(name)
#ifdef _WIN32 void Daemon::stop() {
, running_.store(false);
running_(false) #ifndef _WIN32
#else stop_requested_ = 1;
,
running_(true)
#endif #endif
{
} }
void Daemon::stop() { running_ = false; } bool Daemon::isRunning() const {
#ifndef _WIN32
bool Daemon::isRunning() const { return running_; } return running_.load() && (stop_requested_ == 0);
#else
return running_.load();
#endif
}
bool Daemon::start(MainLoopFunc loop) { bool Daemon::start(MainLoopFunc loop) {
#ifdef _WIN32 #ifdef _WIN32
running_ = true; running_.store(true);
return runWithRestart(loop); return runWithRestart(loop);
#elif __APPLE__ #elif __APPLE__
// macOS: Use child process monitoring (like Windows) to preserve GUI // macOS: Use child process monitoring (like Windows) to preserve GUI
running_ = true; stop_requested_ = 0;
running_.store(true);
return runWithRestart(loop); return runWithRestart(loop);
#else #else
// linux: Daemonize first, then run with restart monitoring // linux: Daemonize first, then run with restart monitoring
instance_ = this; stop_requested_ = 0;
// check if running from terminal before fork // check if running from terminal before fork
bool from_terminal = bool from_terminal =
@@ -134,29 +143,13 @@ bool Daemon::start(MainLoopFunc loop) {
} }
// set up signal handlers // set up signal handlers
signal(SIGTERM, [](int) { signal(SIGTERM, [](int) { stop_requested_ = 1; });
if (instance_) instance_->stop(); signal(SIGINT, [](int) { stop_requested_ = 1; });
});
signal(SIGINT, [](int) {
if (instance_) instance_->stop();
});
// ignore SIGPIPE // ignore SIGPIPE
signal(SIGPIPE, SIG_IGN); signal(SIGPIPE, SIG_IGN);
// set up SIGCHLD handler to reap zombie processes running_.store(true);
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); return runWithRestart(loop);
#endif #endif
} }
@@ -204,8 +197,7 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
restart_count++; restart_count++;
std::cerr << "Exception caught, restarting... (attempt " std::cerr << "Exception caught, restarting... (attempt "
<< restart_count << ")" << std::endl; << restart_count << ")" << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
} }
} }
return true; return true;
@@ -237,27 +229,41 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
if (!success) { if (!success) {
std::cerr << "Failed to create child process, error: " << GetLastError() std::cerr << "Failed to create child process, error: " << GetLastError()
<< std::endl; << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++; restart_count++;
continue; continue;
} }
while (isRunning()) {
DWORD wait_result = WaitForSingleObject(pi.hProcess, 200);
if (wait_result == WAIT_OBJECT_0) {
break;
}
if (wait_result == WAIT_FAILED) {
std::cerr << "Failed waiting child process, error: " << GetLastError()
<< std::endl;
break;
}
}
if (!isRunning()) {
TerminateProcess(pi.hProcess, 1);
WaitForSingleObject(pi.hProcess, 3000);
}
DWORD exit_code = 0; DWORD exit_code = 0;
WaitForSingleObject(pi.hProcess, INFINITE);
GetExitCodeProcess(pi.hProcess, &exit_code); GetExitCodeProcess(pi.hProcess, &exit_code);
CloseHandle(pi.hProcess); CloseHandle(pi.hProcess);
CloseHandle(pi.hThread); CloseHandle(pi.hThread);
if (exit_code == 0) { if (!isRunning() || exit_code == 0) {
break; // normal exit break; // normal exit
} }
restart_count++; restart_count++;
std::cerr << "Child process exited with code " << exit_code std::cerr << "Child process exited with code " << exit_code
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
<< std::endl; << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
#else #else
// linux: use fork + exec to create child process // linux: use fork + exec to create child process
pid_t pid = fork(); pid_t pid = fork();
@@ -266,21 +272,39 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
_exit(1); // exec failed _exit(1); // exec failed
} else if (pid > 0) { } else if (pid > 0) {
int status = 0; int status = 0;
pid_t waited_pid = waitpid(pid, &status, 0); pid_t waited_pid = -1;
while (isRunning()) {
waited_pid = waitpid(pid, &status, WNOHANG);
if (waited_pid == pid) {
break;
}
if (waited_pid < 0 && errno != EINTR) {
break;
}
std::this_thread::sleep_for(
std::chrono::milliseconds(kWaitPollIntervalMs));
}
if (!isRunning() && waited_pid != pid) {
kill(pid, SIGTERM);
waited_pid = waitpid(pid, &status, 0);
}
if (waited_pid < 0) { if (waited_pid < 0) {
if (!isRunning()) {
break;
}
restart_count++; restart_count++;
std::cerr << "waitpid failed, errno: " << errno std::cerr << "waitpid failed, errno: " << errno
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
<< std::endl; << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
continue; continue;
} }
if (WIFEXITED(status)) { if (WIFEXITED(status)) {
int exit_code = WEXITSTATUS(status); int exit_code = WEXITSTATUS(status);
if (exit_code == 0) { if (!isRunning() || exit_code == 0) {
break; // normal exit break; // normal exit
} }
restart_count++; restart_count++;
@@ -288,6 +312,9 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
<< std::endl; << std::endl;
} else if (WIFSIGNALED(status)) { } else if (WIFSIGNALED(status)) {
if (!isRunning()) {
break;
}
restart_count++; restart_count++;
std::cerr << "Child process crashed with signal " << WTERMSIG(status) std::cerr << "Child process crashed with signal " << WTERMSIG(status)
<< ", restarting... (attempt " << restart_count << ")" << ", restarting... (attempt " << restart_count << ")"
@@ -298,12 +325,10 @@ bool Daemon::runWithRestart(MainLoopFunc loop) {
"(attempt " "(attempt "
<< restart_count << ")" << std::endl; << restart_count << ")" << std::endl;
} }
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
} else { } else {
std::cerr << "Failed to fork child process" << std::endl; std::cerr << "Failed to fork child process" << std::endl;
std::this_thread::sleep_for( std::this_thread::sleep_for(std::chrono::milliseconds(kRestartDelayMs));
std::chrono::milliseconds(DAEMON_DEFAULT_RESTART_DELAY_MS));
restart_count++; restart_count++;
} }
#endif #endif

View File

@@ -7,11 +7,11 @@
#ifndef _DAEMON_H_ #ifndef _DAEMON_H_
#define _DAEMON_H_ #define _DAEMON_H_
#include <atomic>
#include <csignal>
#include <functional> #include <functional>
#include <string> #include <string>
#define DAEMON_DEFAULT_RESTART_DELAY_MS 1000
class Daemon { class Daemon {
public: public:
using MainLoopFunc = std::function<void()>; using MainLoopFunc = std::function<void()>;
@@ -28,12 +28,10 @@ class Daemon {
std::string name_; std::string name_;
bool runWithRestart(MainLoopFunc loop); bool runWithRestart(MainLoopFunc loop);
#ifdef _WIN32 #ifndef _WIN32
bool running_; static volatile std::sig_atomic_t stop_requested_;
#else
static Daemon* instance_;
volatile bool running_;
#endif #endif
std::atomic<bool> running_;
}; };
#endif #endif

View File

@@ -1,5 +1,8 @@
#include "keyboard_capturer.h" #include "keyboard_capturer.h"
#include <errno.h>
#include <poll.h>
#include "keyboard_converter.h" #include "keyboard_converter.h"
#include "rd_log.h" #include "rd_log.h"
@@ -10,7 +13,7 @@ static void* g_user_ptr = nullptr;
static int KeyboardEventHandler(Display* display, XEvent* event) { static int KeyboardEventHandler(Display* display, XEvent* event) {
if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) { if (event->xkey.type == KeyPress || event->xkey.type == KeyRelease) {
KeySym keySym = XKeycodeToKeysym(display, event->xkey.keycode, 0); KeySym keySym = XLookupKeysym(&event->xkey, 0);
int key_code = XKeysymToKeycode(display, keySym); int key_code = XKeysymToKeycode(display, keySym);
bool is_key_down = (event->xkey.type == KeyPress); bool is_key_down = (event->xkey.type == KeyPress);
@@ -21,7 +24,9 @@ static int KeyboardEventHandler(Display* display, XEvent* event) {
return 0; return 0;
} }
KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) { KeyboardCapturer::KeyboardCapturer()
: display_(nullptr), root_(0), running_(false) {
XInitThreads();
display_ = XOpenDisplay(nullptr); display_ = XOpenDisplay(nullptr);
if (!display_) { if (!display_) {
LOG_ERROR("Failed to open X display."); LOG_ERROR("Failed to open X display.");
@@ -29,35 +34,87 @@ KeyboardCapturer::KeyboardCapturer() : display_(nullptr), running_(true) {
} }
KeyboardCapturer::~KeyboardCapturer() { KeyboardCapturer::~KeyboardCapturer() {
Unhook();
if (display_) { if (display_) {
XCloseDisplay(display_); XCloseDisplay(display_);
display_ = nullptr;
} }
} }
int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) { int KeyboardCapturer::Hook(OnKeyAction on_key_action, void* user_ptr) {
if (!display_) {
LOG_ERROR("Display not initialized.");
return -1;
}
g_on_key_action = on_key_action; g_on_key_action = on_key_action;
g_user_ptr = user_ptr; g_user_ptr = user_ptr;
XSelectInput(display_, DefaultRootWindow(display_), if (running_) {
KeyPressMask | KeyReleaseMask); return 0;
while (running_) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
} }
root_ = DefaultRootWindow(display_);
XSelectInput(display_, root_, KeyPressMask | KeyReleaseMask);
XFlush(display_);
running_ = true;
const int x11_fd = ConnectionNumber(display_);
event_thread_ = std::thread([this, x11_fd]() {
while (running_) {
while (running_ && XPending(display_) > 0) {
XEvent event;
XNextEvent(display_, &event);
KeyboardEventHandler(display_, &event);
}
if (!running_) {
break;
}
struct pollfd pfd = {x11_fd, POLLIN, 0};
int poll_ret = poll(&pfd, 1, 50);
if (poll_ret < 0) {
if (errno == EINTR) {
continue;
}
LOG_ERROR("poll for X11 events failed.");
running_ = false;
break;
}
if (poll_ret == 0) {
continue;
}
if ((pfd.revents & (POLLERR | POLLHUP | POLLNVAL)) != 0) {
LOG_ERROR("poll got invalid X11 event fd state.");
running_ = false;
break;
}
if ((pfd.revents & POLLIN) == 0) {
continue;
}
}
});
return 0; return 0;
} }
int KeyboardCapturer::Unhook() { int KeyboardCapturer::Unhook() {
running_ = false;
if (event_thread_.joinable()) {
event_thread_.join();
}
g_on_key_action = nullptr; g_on_key_action = nullptr;
g_user_ptr = nullptr; g_user_ptr = nullptr;
running_ = false; if (display_ && root_ != 0) {
XSelectInput(display_, root_, 0);
if (display_) {
XSelectInput(display_, DefaultRootWindow(display_), 0);
XFlush(display_); XFlush(display_);
} }

View File

@@ -11,6 +11,9 @@
#include <X11/extensions/XTest.h> #include <X11/extensions/XTest.h>
#include <X11/keysym.h> #include <X11/keysym.h>
#include <atomic>
#include <thread>
#include "device_controller.h" #include "device_controller.h"
namespace crossdesk { namespace crossdesk {
@@ -28,7 +31,8 @@ class KeyboardCapturer : public DeviceController {
private: private:
Display* display_; Display* display_;
Window root_; Window root_;
bool running_; std::atomic<bool> running_;
std::thread event_thread_;
}; };
} // namespace crossdesk } // namespace crossdesk
#endif #endif

View File

@@ -7,6 +7,7 @@
#include <X11/Xlib.h> #include <X11/Xlib.h>
#endif #endif
#include <cstdlib>
#include <cstring> #include <cstring>
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
@@ -874,11 +875,38 @@ int Render::AudioDeviceInit() {
desired_out.format = SDL_AUDIO_S16; desired_out.format = SDL_AUDIO_S16;
desired_out.channels = 1; desired_out.channels = 1;
output_stream_ = SDL_OpenAudioDeviceStream(SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, auto open_stream = [&]() -> bool {
&desired_out, nullptr, nullptr); output_stream_ = SDL_OpenAudioDeviceStream(
if (!output_stream_) { SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK, &desired_out, nullptr, nullptr);
return output_stream_ != nullptr;
};
if (!open_stream()) {
#if defined(__linux__) && !defined(__APPLE__)
LOG_WARN(
"Failed to open output stream with driver [{}]: {}",
getenv("SDL_AUDIODRIVER") ? getenv("SDL_AUDIODRIVER") : "(default)",
SDL_GetError());
setenv("SDL_AUDIODRIVER", "dummy", 1);
SDL_QuitSubSystem(SDL_INIT_AUDIO);
if (!SDL_InitSubSystem(SDL_INIT_AUDIO)) {
LOG_ERROR("Failed to reinitialize SDL audio with dummy driver: {}",
SDL_GetError());
return -1;
}
if (!open_stream()) {
LOG_ERROR("Failed to open output stream with dummy driver: {}",
SDL_GetError());
return -1;
}
LOG_WARN("Audio output disabled, using SDL dummy audio driver");
#else
LOG_ERROR("Failed to open output stream: {}", SDL_GetError()); LOG_ERROR("Failed to open output stream: {}", SDL_GetError());
return -1; return -1;
#endif
} }
SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_)); SDL_ResumeAudioDevice(SDL_GetAudioStreamDevice(output_stream_));
@@ -1533,6 +1561,13 @@ void Render::InitializeSettings() {
} }
void Render::InitializeSDL() { void Render::InitializeSDL() {
#if defined(__linux__) && !defined(__APPLE__)
if (!getenv("SDL_AUDIODRIVER")) {
// Prefer PulseAudio first on Linux to avoid hard ALSA plugin dependency.
setenv("SDL_AUDIODRIVER", "pulseaudio", 0);
}
#endif
if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) { if (!SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO)) {
LOG_ERROR("Error: {}", SDL_GetError()); LOG_ERROR("Error: {}", SDL_GetError());
return; return;

View File

@@ -49,8 +49,6 @@ bool Render::OpenUrl(const std::string& url) {
void Render::Hyperlink(const std::string& label, const std::string& url, void Render::Hyperlink(const std::string& label, const std::string& url,
const float window_width) { const float window_width) {
ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255)); ImGui::PushStyleColor(ImGuiCol_Text, IM_COL32(0, 0, 255, 255));
ImGui::SetCursorPosX((window_width - ImGui::CalcTextSize(label.c_str()).x) /
2.0f);
ImGui::TextUnformatted(label.c_str()); ImGui::TextUnformatted(label.c_str());
ImGui::PopStyleColor(); ImGui::PopStyleColor();
@@ -106,7 +104,7 @@ int Render::AboutWindow() {
ImGui::SetCursorPosX(about_window_width * 0.1f); ImGui::SetCursorPosX(about_window_width * 0.1f);
ImGui::Text("%s", text.c_str()); ImGui::Text("%s", text.c_str());
if (update_available_) { if (0) {
std::string new_version_available = std::string new_version_available =
localization::new_version_available[localization_language_index_] + localization::new_version_available[localization_language_index_] +
": "; ": ";
@@ -114,10 +112,15 @@ int Render::AboutWindow() {
ImGui::Text("%s", new_version_available.c_str()); ImGui::Text("%s", new_version_available.c_str());
std::string access_website = std::string access_website =
localization::access_website[localization_language_index_]; localization::access_website[localization_language_index_];
ImGui::SetCursorPosX((about_window_width -
ImGui::CalcTextSize(latest_version_.c_str()).x) /
2.0f);
Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width); Hyperlink(latest_version_, "https://crossdesk.cn", about_window_width);
}
ImGui::Text(""); ImGui::Spacing();
} else {
ImGui::Text("%s", "");
}
std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved."; std::string copyright_text = "© 2025 by JUNKUN DI. All rights reserved.";
std::string license_text = "Licensed under GNU LGPL v3."; std::string license_text = "Licensed under GNU LGPL v3.";

View File

@@ -103,6 +103,7 @@ int Render::UpdateNotificationWindow() {
localization::access_website[localization_language_index_] + localization::access_website[localization_language_index_] +
"https://crossdesk.cn"; "https://crossdesk.cn";
ImGui::SetWindowFontScale(0.5f); ImGui::SetWindowFontScale(0.5f);
ImGui::SetCursorPosX(update_notification_window_width * 0.1f);
Hyperlink(download_text, "https://crossdesk.cn", Hyperlink(download_text, "https://crossdesk.cn",
update_notification_window_width); update_notification_window_width);
ImGui::SetWindowFontScale(1.0f); ImGui::SetWindowFontScale(1.0f);
@@ -204,4 +205,3 @@ int Render::UpdateNotificationWindow() {
} }
} // namespace crossdesk } // namespace crossdesk

View File

@@ -452,11 +452,17 @@ static void MonitorThreadFunc() {
LOG_INFO("Clipboard event monitoring started (Linux XFixes)"); LOG_INFO("Clipboard event monitoring started (Linux XFixes)");
XEvent event; XEvent event;
constexpr int kEventPollIntervalMs = 20;
while (g_monitoring.load()) { while (g_monitoring.load()) {
XNextEvent(g_x11_display, &event); // Avoid blocking on XNextEvent so StopMonitoring() can stop quickly.
if (event.type == event_base + XFixesSelectionNotify) { while (g_monitoring.load() && XPending(g_x11_display) > 0) {
HandleClipboardChange(); XNextEvent(g_x11_display, &event);
if (event.type == event_base + XFixesSelectionNotify) {
HandleClipboardChange();
}
} }
std::this_thread::sleep_for(
std::chrono::milliseconds(kEventPollIntervalMs));
} }
XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0); XFixesSelectSelectionInput(g_x11_display, event_window, g_clipboard_atom, 0);