[feat] activate existing window on duplicate launches, refs #119

This commit is contained in:
dijunkun
2026-10-18 14:17:26 +08:00
parent 34eb2b1fe3
commit 02c3952be0
12 changed files with 819 additions and 28 deletions
+22 -2
View File
@@ -26,6 +26,7 @@
#include "config_center.h"
#include "daemon.h"
#include "path_manager.h"
#include "platform/instance_activation.h"
#include "platform/single_instance.h"
#include "rd_log.h"
#include "render.h"
@@ -307,6 +308,25 @@ static crossdesk::platform::InstanceResult AcquireInstance(
return result;
}
static int ExitForExistingInstance(
crossdesk::platform::InstanceResult result, bool interactive_launch) {
if (interactive_launch &&
result == crossdesk::platform::InstanceResult::kAlreadyRunning) {
std::string error;
if (!crossdesk::platform::RequestInstanceActivation(error)) {
std::cerr << error << '\n';
#ifdef _WIN32
MessageBoxW(nullptr,
L"CrossDesk is already running, but its window could not be "
L"activated.\n\nIt may still be starting or be running in "
L"another Windows session. Please check the system tray.",
L"CrossDesk", MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND);
#endif
}
}
return result == crossdesk::platform::InstanceResult::kAlreadyRunning ? 0 : 1;
}
static int RunApplication(
int argc, char* argv[],
crossdesk::platform::SingleInstanceGuard& client_instance) {
@@ -458,11 +478,11 @@ int main(int argc, char* argv[]) {
if (!IsDaemonChild(argc, argv)) {
const auto result = AcquireInstance(launcher_instance, InstanceRole::kLauncher);
if (result != InstanceResult::kAcquired)
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
return ExitForExistingInstance(result, true);
}
const auto result = AcquireInstance(client_instance, InstanceRole::kClient);
if (result != InstanceResult::kAcquired)
return result == InstanceResult::kAlreadyRunning ? 0 : 1;
return ExitForExistingInstance(result, !IsDaemonChild(argc, argv));
#ifdef __linux__
const int exit_code = crossdesk::RunWithLinuxDisplay(
@@ -40,6 +40,9 @@
#include "features/gallery/preview_bandwidth.h"
#include "localization.h"
#include "platform.h"
#ifndef __OHOS__
#include "platform/instance_activation.h"
#endif
#include "platform/video_renderer.h"
#include "rendering/slint_video_presenter.h"
#include "rendering/video_geometry.h"
@@ -66,6 +69,7 @@
#include "platform/linux/gui/cuda_graphics.h"
#include "platform/linux/gui/tray/linux_tray.h"
#include "platform/linux/gui/wayland_activation.h"
#include "platform/linux/headless/headless_console.h"
// Xlib defines Status as a macro, which collides with UpdateChecker::Status.
#undef Status
@@ -248,8 +252,9 @@ bool X11WindowMatches(Display* display, ::Window x11_window,
kGeometryTolerance;
}
void X11ActivateGalleryWindow(Display* display) {
if (!display) return;
bool X11ActivateWindow(Display* display, const slint::Window& target,
bool gallery_window) {
if (!display) return false;
Atom type = None;
int format = 0;
unsigned long count = 0, remaining = 0;
@@ -259,13 +264,15 @@ void X11ActivateGalleryWindow(Display* display) {
0, 4096, False, XA_WINDOW, &type, &format, &count,
&remaining, &data) != Success || !data) {
if (data) XFree(data);
return;
return false;
}
bool activated = false;
if (type == XA_WINDOW && format == 32) {
const auto* windows = reinterpret_cast<const ::Window*>(data);
for (unsigned long i = 0; i < count; ++i) {
if (X11GetWindowProcessId(display, windows[i]) !=
static_cast<unsigned long>(getpid())) continue;
if (!gallery_window && !X11WindowMatches(display, windows[i], target)) continue;
unsigned char* name = nullptr;
unsigned long length = 0;
Atom name_type = None;
@@ -273,8 +280,11 @@ void X11ActivateGalleryWindow(Display* display) {
XGetWindowProperty(display, windows[i], XInternAtom(display, "_NET_WM_NAME", False),
0, 256, False, XInternAtom(display, "UTF8_STRING", False),
&name_type, &name_format, &length, &remaining, &name);
const bool matches = name && name_format == 8 &&
std::string_view(reinterpret_cast<char*>(name), length).starts_with("CrossDesk ");
const std::string_view title = name && name_format == 8
? std::string_view(reinterpret_cast<char*>(name), length)
: std::string_view{};
const bool matches = gallery_window ? title.starts_with("CrossDesk ")
: title == "CrossDesk";
if (name) XFree(name);
if (!matches) continue;
XEvent event{};
@@ -284,12 +294,15 @@ void X11ActivateGalleryWindow(Display* display) {
event.xclient.format = 32;
event.xclient.data.l[0] = 1;
event.xclient.data.l[1] = CurrentTime;
XSendEvent(display, root, False, SubstructureRedirectMask | SubstructureNotifyMask, &event);
activated = XSendEvent(display, root, False,
SubstructureRedirectMask | SubstructureNotifyMask,
&event) != 0;
XFlush(display);
break;
}
}
XFree(data);
return activated;
}
#endif
@@ -1019,6 +1032,9 @@ struct GuiApplication::SlintUi {
int localized_language = -1;
std::chrono::steady_clock::time_point last_clipboard_poll{};
slint::Timer video_timer;
#ifndef __OHOS__
std::unique_ptr<platform::InstanceActivationReceiver> activation_receiver;
#endif
#if _WIN32
std::unique_ptr<WinTray> tray;
#elif defined(__APPLE__)
@@ -1028,6 +1044,8 @@ struct GuiApplication::SlintUi {
#elif (defined(__linux__) && !defined(__OHOS__))
std::unique_ptr<LinuxTray> tray;
Display* x11_focus_display = nullptr;
int main_activation_attempts = 0;
std::string main_activation_token;
std::chrono::steady_clock::time_point last_x11_activation_poll{};
#endif
@@ -1044,6 +1062,14 @@ GuiApplication::GuiApplication() = default;
GuiApplication::~GuiApplication() = default;
int GuiApplication::Run() {
#ifndef __OHOS__
// Publish the GUI endpoint before configuration/media initialization, so a
// second launch can queue activation even while this window is starting.
auto activation_receiver =
std::make_unique<platform::InstanceActivationReceiver>();
std::string activation_error;
activation_receiver->Start(activation_error);
#endif
path_manager_ = std::make_unique<PathManager>("CrossDesk");
if (!path_manager_) {
return -1;
@@ -1051,6 +1077,9 @@ int GuiApplication::Run() {
exec_log_path_ = path_manager_->GetLogPath().string();
dll_log_path_ = exec_log_path_;
InitializeLogger();
#ifndef __OHOS__
if (!activation_error.empty()) LOG_WARN("{}", activation_error);
#endif
cache_path_ = path_manager_->GetCachePath().string();
announcements_.SetStorageDirectory(path_manager_->GetCachePath() /
"announcement_reads");
@@ -1091,6 +1120,9 @@ int GuiApplication::Run() {
InitializeModules();
update_checker_ = std::make_unique<UpdateChecker>(CROSSDESK_VERSION);
InitializeUi();
#ifndef __OHOS__
ui_->activation_receiver = std::move(activation_receiver);
#endif
// Configure the initial geometry before the native window becomes visible.
// Subsequent ticks only acknowledge that placement and its resolved DPI.
RestoreWindowGeometry(true);
@@ -1303,28 +1335,41 @@ void GuiApplication::InitializeUi() {
SyncMainWindow();
}
void GuiApplication::ShowMainWindow(const std::string& activation_token) {
#if !defined(__linux__) || defined(__OHOS__)
(void)activation_token;
#endif
if (!ui_) {
return;
}
ui_->main->show();
ui_->main->window().set_minimized(false);
#if _WIN32
const HWND window = ui_->main->window().win32_hwnd();
ConfigureWindowsWindowIcons(window);
ShowWindow(window, IsIconic(window) ? SW_RESTORE : SW_SHOW);
SetForegroundWindow(window);
#endif
#if defined(__APPLE__)
ConfigureMainWindowChrome(ui_->main->window().appkit_view());
MacActivateWindow(ui_->main->window().appkit_view());
// A window restored from the tray may only obtain its NSWindow after the
// callback returns, so let the regular UI tick configure it.
ui_->main_native_titlebar_attempts = 30;
#elif defined(__linux__) && !defined(__OHOS__)
// Mapping/unminimizing through Slint is asynchronous. Retry native lookup
// after the window manager has updated its client list and geometry.
ui_->main_activation_attempts = 30;
ui_->main_activation_token = activation_token;
#endif
}
void GuiApplication::InitializeSystemTray() {
#if defined(_WIN32) || defined(__APPLE__) || defined(__linux__)
if (!ui_) {
return;
}
auto show_window = [this] {
if (!ui_) {
return;
}
ui_->main->show();
#if _WIN32
ConfigureWindowsWindowIcons(ui_->main->window().win32_hwnd());
#endif
ui_->main->window().set_minimized(false);
#if defined(__APPLE__)
ConfigureMainWindowChrome(ui_->main->window().appkit_view());
MacActivateWindow(ui_->main->window().appkit_view());
// A window restored from the tray may only obtain its NSWindow after the
// callback returns, so let the regular UI tick configure it.
ui_->main_native_titlebar_attempts = 30;
#endif
};
auto show_window = [this] { ShowMainWindow(); };
auto hide_window = [this] {
if (ui_) {
ui_->main->hide();
@@ -2163,7 +2208,30 @@ void GuiApplication::Tick() {
}
}
clipboard_.ApplyPendingRemoteText();
#ifndef __OHOS__
std::string activation_token;
bool activation_requested =
ui_->activation_receiver && ui_->activation_receiver->TakeRequest(&activation_token);
#ifdef __APPLE__
if (ui_->tray && ui_->tray->TakeReopenRequest()) activation_requested = true;
#endif
if (activation_requested) ShowMainWindow(activation_token);
#endif
#if (defined(__linux__) && !defined(__OHOS__)) && !defined(__APPLE__)
if (ui_->main_activation_attempts > 0) {
--ui_->main_activation_attempts;
const auto& window = ui_->main->window();
if (window.wayland_surface()) {
if (platform::ActivateWaylandWindow(window.wayland_display(),
window.wayland_surface(), ui_->main_activation_token))
ui_->main_activation_attempts = 0;
} else {
if (!ui_->x11_focus_display) ui_->x11_focus_display = XOpenDisplay(nullptr);
if (X11ActivateWindow(ui_->x11_focus_display, window, false))
ui_->main_activation_attempts = 0;
}
if (ui_->main_activation_attempts == 0) ui_->main_activation_token.clear();
}
if (ui_->tray) {
ui_->tray->ProcessEvents();
}
@@ -4514,7 +4582,7 @@ void GuiApplication::ShowGalleryWindow() {
SetForegroundWindow(window.win32_hwnd());
#elif defined(__linux__) && !defined(__OHOS__)
if (!ui_->x11_focus_display) ui_->x11_focus_display = XOpenDisplay(nullptr);
X11ActivateGalleryWindow(ui_->x11_focus_display);
X11ActivateWindow(ui_->x11_focus_display, window, true);
#endif
ui_->gallery_synced = {};
SyncGallery();
@@ -49,6 +49,7 @@ private:
void InitializeModules();
void InitializeUi();
void InitializeSystemTray();
void ShowMainWindow(const std::string& activation_token = {});
bool MinimizeMainWindowToTray();
void BindMainCallbacks(bool gallery_window = false);
void BindStreamCallbacks();
@@ -0,0 +1,248 @@
#include "platform/instance_activation.h"
#include "platform/single_instance.h"
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <unistd.h>
#include <cerrno>
#include <chrono>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <string_view>
#include <thread>
#include <utility>
namespace crossdesk::platform {
namespace {
constexpr std::string_view kRequest = "CrossDesk.Activate.v1";
constexpr std::size_t kMaxActivationToken = 4096;
#if defined(__linux__)
std::string Environment(const char* name) {
const char* value = std::getenv(name);
return value ? value : "";
}
#endif
std::string ActivationDirectory() {
// Match the single-instance guard's user scope, independent of TMPDIR/HOME.
return "/tmp/crossdesk-activation-" + std::to_string(geteuid());
}
bool PrivateDirectory(bool create, std::string& error) {
const auto directory = ActivationDirectory();
if (create && mkdir(directory.c_str(), 0700) != 0 && errno != EEXIST) {
error = "Cannot create activation directory: " + std::string(strerror(errno));
return false;
}
struct stat info{};
if (lstat(directory.c_str(), &info) != 0) {
if (errno != ENOENT)
error = "Cannot inspect activation directory: " + std::string(strerror(errno));
return false;
}
if (!S_ISDIR(info.st_mode) || info.st_uid != geteuid() ||
(info.st_mode & 0777) != 0700) {
error = "Activation directory must be private and owned by this user";
return false;
}
return true;
}
std::string ActivationPath(const std::string& application, std::string& error) {
if (!ValidInstanceApplication(application)) {
error = "Invalid activation application name";
return {};
}
std::string session = "macos";
#if defined(__linux__)
if (!Environment("CROSSDESK_HEADLESS_ACTIVE").empty()) {
error = "Window activation is unavailable in a headless session";
return {};
}
const auto wayland = Environment("WAYLAND_DISPLAY");
auto display = Environment("DISPLAY");
if (!wayland.empty()) {
session = "wayland:" + Environment("XDG_RUNTIME_DIR") + "/" + wayland;
} else if (!display.empty()) {
// :0 and :0.0 address the same server; a screen suffix is not a session.
const auto colon = display.rfind(':');
const auto screen = display.find('.', colon == std::string::npos ? 0 : colon);
if (screen != std::string::npos) display.resize(screen);
if (display.rfind("unix:", 0) == 0) display.erase(0, 4);
session = "x11:" + display;
} else {
error = "No graphical session is available to activate the existing window";
return {};
}
#endif
// A stable bounded name fits both Darwin and Linux sockaddr_un limits.
std::uint64_t hash = 14695981039346656037ull;
for (unsigned char byte : application + '\0' + session) {
hash ^= byte;
hash *= 1099511628211ull;
}
return ActivationDirectory() + "/" + std::to_string(hash) + ".sock";
}
int CreateSocket(std::string& error) {
const int fd = socket(AF_UNIX, SOCK_DGRAM, 0);
if (fd < 0) {
error = "Cannot create activation socket: " + std::string(strerror(errno));
return -1;
}
if (fcntl(fd, F_SETFD, FD_CLOEXEC) < 0 ||
fcntl(fd, F_SETFL, O_NONBLOCK) < 0) {
error = "Cannot configure activation socket: " + std::string(strerror(errno));
close(fd);
return -1;
}
return fd;
}
sockaddr_un Address(const std::string& path) {
sockaddr_un address{};
address.sun_family = AF_UNIX;
// ActivationPath always produces a name shorter than sun_path on both OSes.
std::memcpy(address.sun_path, path.c_str(), path.size() + 1);
return address;
}
socklen_t AddressSize(const std::string& path) {
return static_cast<socklen_t>(offsetof(sockaddr_un, sun_path) + path.size() + 1);
}
} // namespace
InstanceActivationReceiver::InstanceActivationReceiver(std::string application)
: application_(std::move(application)) {}
InstanceActivationReceiver::~InstanceActivationReceiver() {
if (socket_ < 0) return;
struct stat info{};
if (lstat(socket_path_.c_str(), &info) == 0 &&
static_cast<std::uint64_t>(info.st_dev) == socket_device_ &&
static_cast<std::uint64_t>(info.st_ino) == socket_inode_) {
unlink(socket_path_.c_str());
}
close(socket_);
}
bool InstanceActivationReceiver::Start(std::string& error) {
error.clear();
if (socket_ >= 0) return true;
const auto path = ActivationPath(application_, error);
if (path.empty() || !PrivateDirectory(true, error)) return false;
// The caller holds the client lease. Any socket left at this path is from a
// crashed GUI, and may be removed without disrupting another live instance.
struct stat info{};
if (lstat(path.c_str(), &info) == 0) {
if (!S_ISSOCK(info.st_mode) || info.st_uid != geteuid()) {
error = "Activation endpoint is not a socket owned by this user";
return false;
}
if (unlink(path.c_str()) != 0) {
error = "Cannot remove stale activation socket: " + std::string(strerror(errno));
return false;
}
} else if (errno != ENOENT) {
error = "Cannot inspect activation socket: " + std::string(strerror(errno));
return false;
}
const int fd = CreateSocket(error);
if (fd < 0) return false;
const auto address = Address(path);
if (bind(fd, reinterpret_cast<const sockaddr*>(&address), AddressSize(path)) != 0) {
error = "Cannot bind activation socket: " + std::string(strerror(errno));
close(fd);
return false;
}
if (lstat(path.c_str(), &info) != 0) {
error = "Cannot inspect bound activation socket: " + std::string(strerror(errno));
unlink(path.c_str());
close(fd);
return false;
}
socket_ = fd;
socket_path_ = path;
socket_device_ = info.st_dev;
socket_inode_ = info.st_ino;
return true;
}
bool InstanceActivationReceiver::TakeRequest(std::string* activation_token) {
if (activation_token) activation_token->clear();
if (socket_ < 0) return false;
bool requested = false;
// Bound each UI tick's work even if many duplicate launches arrive together.
for (int i = 0; i < 32; ++i) {
char buffer[kRequest.size() + 1 + kMaxActivationToken + 1];
const auto size = recv(socket_, buffer, sizeof(buffer), 0);
if (size < 0) {
if (errno == EINTR) continue;
break;
}
const std::string_view message(buffer, size);
if (message == kRequest) {
requested = true;
} else if (message.size() > kRequest.size() &&
message.size() <= kRequest.size() + 1 + kMaxActivationToken &&
message.substr(0, kRequest.size()) == kRequest &&
message[kRequest.size()] == '\0') {
const auto token = message.substr(kRequest.size() + 1);
if (token.find('\0') == std::string_view::npos) {
requested = true;
if (activation_token) *activation_token = token;
}
}
}
return requested;
}
bool RequestInstanceActivation(std::string& error, const std::string& application) {
error.clear();
const auto path = ActivationPath(application, error);
if (path.empty()) return false;
const int fd = CreateSocket(error);
if (fd < 0) return false;
const auto address = Address(path);
std::string request(kRequest);
#if defined(__linux__)
const auto token = Environment("XDG_ACTIVATION_TOKEN");
if (!token.empty() && token.size() <= kMaxActivationToken) {
request += '\0';
request += token;
}
#endif
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
int last_error = ENOENT;
do {
if (PrivateDirectory(false, error)) {
if (sendto(fd, request.data(), request.size(), 0,
reinterpret_cast<const sockaddr*>(&address), AddressSize(path)) ==
static_cast<ssize_t>(request.size())) {
close(fd);
return true;
}
last_error = errno;
if (last_error != ENOENT && last_error != ECONNREFUSED &&
last_error != EAGAIN && last_error != EINTR) break;
} else if (!error.empty()) {
close(fd);
return false;
}
if (std::chrono::steady_clock::now() >= deadline) break;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
} while (true);
close(fd);
error = "Cannot notify the existing CrossDesk window in this graphical session: " +
std::string(strerror(last_error));
return false;
}
} // namespace crossdesk::platform
@@ -0,0 +1,59 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-17
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _INSTANCE_ACTIVATION_H_
#define _INSTANCE_ACTIVATION_H_
#ifdef _WIN32
#include <windows.h>
#else
#include <cstdint>
#endif
#include <string>
namespace crossdesk::platform {
// Created only by the interactive GUI, on its event-loop thread. Requests can
// arrive during startup and remain pending until the UI is ready to show
// itself.
class InstanceActivationReceiver {
public:
explicit InstanceActivationReceiver(std::string application = "CrossDesk");
~InstanceActivationReceiver();
InstanceActivationReceiver(const InstanceActivationReceiver&) = delete;
InstanceActivationReceiver& operator=(const InstanceActivationReceiver&) =
delete;
// The caller must hold this application's client single-instance lease.
bool Start(std::string& error);
bool TakeRequest(std::string* activation_token = nullptr);
private:
std::string application_;
#ifdef _WIN32
static LRESULT CALLBACK WindowProc(HWND window, UINT message, WPARAM wparam,
LPARAM lparam);
HWND window_ = nullptr;
UINT activation_message_ = 0;
bool requested_ = false;
#else
int socket_ = -1;
std::string socket_path_;
std::uint64_t socket_device_ = 0;
std::uint64_t socket_inode_ = 0;
#endif
};
// Targets the current user's GUI on the current desktop, including a daemon
// child. Waits briefly for startup/restart, without waiting on its UI thread.
bool RequestInstanceActivation(std::string& error,
const std::string& application = "CrossDesk");
} // namespace crossdesk::platform
#endif
@@ -0,0 +1,162 @@
#include "wayland_activation.h"
#include <dlfcn.h>
#include <cstdint>
#include <cstring>
namespace crossdesk::platform {
namespace {
// The public libwayland-client wire ABI. Load it at runtime so X11-only builds
// do not acquire a new Wayland build or runtime dependency.
struct Interface;
struct Message {
const char* name;
const char* signature;
const Interface** types;
};
struct Interface {
const char* name;
int version;
int method_count;
const Message* methods;
int event_count;
const Message* events;
};
struct WaylandApi {
void* library = dlopen("libwayland-client.so.0", RTLD_NOW | RTLD_LOCAL);
void* (*create_queue)(void*) = nullptr;
void (*destroy_queue)(void*) = nullptr;
void* (*create_wrapper)(void*) = nullptr;
void (*destroy_wrapper)(void*) = nullptr;
void (*set_queue)(void*, void*) = nullptr;
void (*destroy_proxy)(void*) = nullptr;
void* (*marshal)(void*, uint32_t, const Interface*, uint32_t, uint32_t, ...) = nullptr;
int (*add_listener)(void*, void (**)(void), void*) = nullptr;
int (*roundtrip)(void*, void*) = nullptr;
int (*flush)(void*) = nullptr;
const Interface* registry_interface = nullptr;
const Interface* surface_interface = nullptr;
bool available = false;
template <class T>
bool Load(T& target, const char* name) {
target = reinterpret_cast<T>(dlsym(library, name));
return target != nullptr;
}
WaylandApi() {
available = library &&
Load(create_queue, "wl_display_create_queue") &&
Load(destroy_queue, "wl_event_queue_destroy") &&
Load(create_wrapper, "wl_proxy_create_wrapper") &&
Load(destroy_wrapper, "wl_proxy_wrapper_destroy") &&
Load(set_queue, "wl_proxy_set_queue") &&
Load(destroy_proxy, "wl_proxy_destroy") &&
Load(marshal, "wl_proxy_marshal_flags") &&
Load(add_listener, "wl_proxy_add_listener") &&
Load(roundtrip, "wl_display_roundtrip_queue") &&
Load(flush, "wl_display_flush") &&
Load(registry_interface, "wl_registry_interface") &&
Load(surface_interface, "wl_surface_interface");
}
~WaylandApi() { if (library) dlclose(library); }
};
struct Activation {
WaylandApi& api;
void* queue = nullptr;
void* wrapper = nullptr;
void* registry = nullptr;
void* activation = nullptr;
void* token = nullptr;
std::string token_value;
const Interface* surface_types[1];
const Interface* activate_types[2];
const Message token_methods[5];
const Message token_events[1] = {{"done", "s", nullptr}};
const Interface token_interface;
const Interface* new_token_types[1];
const Message activation_methods[3];
const Interface activation_interface;
explicit Activation(WaylandApi& value)
: api(value), surface_types{api.surface_interface},
activate_types{nullptr, api.surface_interface},
token_methods{{"set_serial", "uo", nullptr}, {"set_app_id", "s", nullptr},
{"set_surface", "o", surface_types}, {"commit", "", nullptr},
{"destroy", "", nullptr}},
token_interface{"xdg_activation_token_v1", 1, 5, token_methods, 1, token_events},
new_token_types{&token_interface},
activation_methods{{"destroy", "", nullptr},
{"get_activation_token", "n", new_token_types},
{"activate", "so", activate_types}},
activation_interface{"xdg_activation_v1", 1, 3, activation_methods, 0, nullptr} {}
~Activation() {
// Destroy protocol objects as well as client-side proxies on every path.
constexpr uint32_t destroy = 1; // WL_MARSHAL_FLAG_DESTROY
if (token) api.marshal(token, 4, nullptr, 1, destroy);
if (activation) api.marshal(activation, 0, nullptr, 1, destroy);
if (registry) api.destroy_proxy(registry);
if (wrapper) api.destroy_wrapper(wrapper);
if (queue) api.destroy_queue(queue);
}
static void Global(void* data, void* registry, uint32_t name,
const char* interface, uint32_t version) {
auto& self = *static_cast<Activation*>(data);
if (self.activation || version < 1 || std::strcmp(interface, "xdg_activation_v1")) return;
self.activation = self.api.marshal(registry, 0, &self.activation_interface, 1, 0,
name, self.activation_interface.name, uint32_t{1}, nullptr);
}
static void GlobalRemove(void*, void*, uint32_t) {}
static void Done(void* data, void*, const char* value) {
static_cast<Activation*>(data)->token_value = value;
}
};
} // namespace
bool ActivateWaylandWindow(void* display, void* surface,
const std::string& activation_token) {
if (!display || !surface) return false;
static WaylandApi api;
if (!api.available) return false;
Activation request(api);
// Use a separate queue and a display wrapper: roundtrips must never dispatch
// Slint/winit's own events from inside its timer callback.
request.queue = api.create_queue(display);
request.wrapper = api.create_wrapper(display);
if (!request.queue || !request.wrapper) return false;
api.set_queue(request.wrapper, request.queue);
request.registry = api.marshal(request.wrapper, 1, api.registry_interface, 1, 0, nullptr);
if (!request.registry) return false;
void (*registry_listener[])(void) = {
reinterpret_cast<void (*)(void)>(&Activation::Global),
reinterpret_cast<void (*)(void)>(&Activation::GlobalRemove)};
if (api.add_listener(request.registry, registry_listener, &request) != 0 ||
api.roundtrip(display, request.queue) < 0 || !request.activation) return false;
request.token_value = activation_token;
if (request.token_value.empty()) {
// Terminal/tray launches may carry no startup token. Ask the compositor
// for one; it remains free to refuse focus without a recent input serial.
request.token = api.marshal(request.activation, 1, &request.token_interface, 1, 0, nullptr);
if (!request.token) return false;
void (*token_listener[])(void) = {reinterpret_cast<void (*)(void)>(&Activation::Done)};
if (api.add_listener(request.token, token_listener, &request) != 0) return false;
api.marshal(request.token, 1, nullptr, 1, 0, "crossdesk");
api.marshal(request.token, 2, nullptr, 1, 0, surface);
api.marshal(request.token, 3, nullptr, 1, 0);
if (api.roundtrip(display, request.queue) < 0 || request.token_value.empty()) return false;
}
api.marshal(request.activation, 2, nullptr, 1, 0, request.token_value.c_str(), surface);
api.flush(display);
return true;
}
} // namespace crossdesk::platform
@@ -0,0 +1,21 @@
/*
* @Author: DI JUNKUN
* @Date: 2026-10-17
* Copyright (c) 2026 by DI JUNKUN, All Rights Reserved.
*/
#ifndef _WAYLAND_ACTIVATION_H_
#define _WAYLAND_ACTIVATION_H_
#include <string>
namespace crossdesk::platform {
// Uses Slint's native handles and xdg-activation-v1 when the compositor offers
// it. A successful request still leaves the focus decision to the compositor.
bool ActivateWaylandWindow(void* display, void* surface,
const std::string& activation_token);
} // namespace crossdesk::platform
#endif
@@ -33,6 +33,7 @@ class MacTray {
void MinimizeToTray();
void RemoveTrayIcon();
bool TakeReopenRequest();
private:
std::unique_ptr<MacTrayImpl> impl_;
@@ -5,6 +5,7 @@
#include <SDL3/SDL.h>
#import <Cocoa/Cocoa.h>
#import <CoreServices/CoreServices.h>
#include "localization.h"
@@ -16,6 +17,8 @@
- (void)showMainWindow:(id)sender;
- (void)openSettings:(id)sender;
- (void)exitApplication:(id)sender;
- (void)reopenApplication:(NSAppleEventDescriptor *)event
withReply:(NSAppleEventDescriptor *)reply;
@end
namespace crossdesk {
@@ -28,6 +31,7 @@ struct MacTrayImpl {
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
EnsureStatusItem();
InstallReopenHandler();
}
explicit MacTrayImpl(std::function<void()> show_window_callback,
@@ -43,9 +47,13 @@ struct MacTrayImpl {
language_index(language_index_value),
target([[CrossDeskMacTrayTarget alloc] initWithOwner:this]) {
EnsureStatusItem();
InstallReopenHandler();
}
~MacTrayImpl() {
[[NSAppleEventManager sharedAppleEventManager]
removeEventHandlerForEventClass:kCoreEventClass
andEventID:kAEReopenApplication];
RemoveTrayIcon();
target = nil;
}
@@ -156,7 +164,28 @@ struct MacTrayImpl {
[NSApp activateIgnoringOtherApps:YES];
}
void RequestReopen() { reopen_requested = true; }
bool TakeReopenRequest() {
if (!reopen_handler_ready) {
// AppKit may replace handlers even after its did-finish-launching
// notification. The first GUI tick runs after finishLaunching returns.
InstallReopenHandler();
reopen_handler_ready = true;
}
return std::exchange(reopen_requested, false);
}
private:
void InstallReopenHandler() {
// Finder/Dock normally sends a reopen Apple event instead of launching a
// second process. Queue it for the UI tick, just like the IPC notification.
[[NSAppleEventManager sharedAppleEventManager]
setEventHandler:target
andSelector:@selector(reopenApplication:withReply:)
forEventClass:kCoreEventClass
andEventID:kAEReopenApplication];
}
void EnsureStatusItem() {
if (status_item) {
return;
@@ -265,6 +294,8 @@ struct MacTrayImpl {
int language_index = 0;
NSStatusItem *status_item = nil;
CrossDeskMacTrayTarget *target = nil;
bool reopen_requested = false;
bool reopen_handler_ready = false;
};
MacTray::MacTray(::SDL_Window *app_window, const std::string &tooltip,
@@ -285,8 +316,12 @@ MacTray::MacTray(std::function<void()> show_window,
MacTray::~MacTray() = default;
void MacActivateWindow(void *appkit_view) {
// IPC/Finder activation bypasses the tray's ShowWindow(), including when
// MinimizeToTray() previously removed the app from the Dock.
[NSApp setActivationPolicy:NSApplicationActivationPolicyRegular];
NSView *view = (__bridge NSView *)appkit_view;
NSWindow *window = [view window];
[window deminiaturize:nil];
[NSApp activateIgnoringOtherApps:YES];
[window makeKeyAndOrderFront:nil];
}
@@ -295,6 +330,8 @@ void MacTray::MinimizeToTray() { impl_->MinimizeToTray(); }
void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
bool MacTray::TakeReopenRequest() { return impl_->TakeReopenRequest(); }
} // namespace crossdesk
@implementation CrossDeskMacTrayTarget {
@@ -317,6 +354,13 @@ void MacTray::RemoveTrayIcon() { impl_->RemoveTrayIcon(); }
owner_->ShowMenu();
}
- (void)reopenApplication:(NSAppleEventDescriptor *)event
withReply:(NSAppleEventDescriptor *)reply {
(void)event;
(void)reply;
if (owner_) owner_->RequestReopen();
}
- (void)exitApplication:(id)sender {
(void)sender;
if (owner_) {
@@ -0,0 +1,161 @@
#include "platform/instance_activation.h"
#include "platform/single_instance.h"
#include <sddl.h>
#include <utility>
#include <vector>
namespace crossdesk::platform {
namespace {
constexpr wchar_t kWindowClass[] = L"CrossDesk.InstanceActivationWindow";
constexpr wchar_t kActivationMessage[] = L"CrossDesk.ActivateMainWindow.v1";
constexpr ULONGLONG kStartupWaitMs = 5000;
std::wstring ActivationWindowName(const std::string& application,
std::string& error) {
if (!ValidInstanceApplication(application)) {
error = "Invalid activation application name";
return {};
}
HANDLE token = nullptr;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
error =
"Cannot read activation user token: " + std::to_string(GetLastError());
return {};
}
DWORD size = 0;
GetTokenInformation(token, TokenUser, nullptr, 0, &size);
std::vector<unsigned char> buffer(size);
if (!GetTokenInformation(token, TokenUser, buffer.data(), size, &size)) {
const DWORD code = GetLastError();
CloseHandle(token);
error = "Cannot read activation user SID: " + std::to_string(code);
return {};
}
CloseHandle(token);
wchar_t* sid = nullptr;
if (!ConvertSidToStringSidW(
reinterpret_cast<TOKEN_USER*>(buffer.data())->User.Sid, &sid)) {
error =
"Cannot format activation user SID: " + std::to_string(GetLastError());
return {};
}
const std::wstring name =
std::wstring(application.begin(), application.end()) + L".Activation." + sid;
LocalFree(sid);
return name;
}
} // namespace
InstanceActivationReceiver::InstanceActivationReceiver(std::string application)
: application_(std::move(application)) {}
InstanceActivationReceiver::~InstanceActivationReceiver() {
if (window_) DestroyWindow(window_);
}
bool InstanceActivationReceiver::Start(std::string& error) {
error.clear();
if (window_) return true;
const auto name = ActivationWindowName(application_, error);
if (name.empty()) return false;
activation_message_ = RegisterWindowMessageW(kActivationMessage);
if (!activation_message_) {
error =
"Cannot register activation message: " + std::to_string(GetLastError());
return false;
}
WNDCLASSW window_class{};
window_class.lpfnWndProc = WindowProc;
window_class.hInstance = GetModuleHandleW(nullptr);
window_class.lpszClassName = kWindowClass;
if (!RegisterClassW(&window_class) &&
GetLastError() != ERROR_CLASS_ALREADY_EXISTS) {
error =
"Cannot register activation window: " + std::to_string(GetLastError());
return false;
}
window_ =
CreateWindowExW(0, kWindowClass, name.c_str(), 0, 0, 0, 0, 0,
HWND_MESSAGE, nullptr, window_class.hInstance, this);
if (!window_) {
error =
"Cannot create activation window: " + std::to_string(GetLastError());
return false;
}
// An unelevated portable launch may need to activate an elevated installed
// client. Allow only this payload-free notification through UIPI.
if (!ChangeWindowMessageFilterEx(window_, activation_message_, MSGFLT_ALLOW,
nullptr)) {
error =
"Cannot allow activation message: " + std::to_string(GetLastError());
DestroyWindow(window_);
window_ = nullptr;
return false;
}
return true;
}
bool InstanceActivationReceiver::TakeRequest(std::string* activation_token) {
if (activation_token) activation_token->clear();
return std::exchange(requested_, false);
}
LRESULT CALLBACK InstanceActivationReceiver::WindowProc(HWND window,
UINT message,
WPARAM wparam,
LPARAM lparam) {
if (message == WM_NCCREATE) {
const auto* create = reinterpret_cast<CREATESTRUCTW*>(lparam);
SetWindowLongPtrW(window, GWLP_USERDATA,
reinterpret_cast<LONG_PTR>(create->lpCreateParams));
}
auto* receiver = reinterpret_cast<InstanceActivationReceiver*>(
GetWindowLongPtrW(window, GWLP_USERDATA));
if (receiver && message == receiver->activation_message_) {
// Let the regular UI tick restore the window, rather than entering Slint
// from a native message callback (which may run inside a nested event
// loop).
receiver->requested_ = true;
return 0;
}
return DefWindowProcW(window, message, wparam, lparam);
}
bool RequestInstanceActivation(std::string& error, const std::string& application) {
error.clear();
const auto name = ActivationWindowName(application, error);
if (name.empty()) return false;
const UINT message = RegisterWindowMessageW(kActivationMessage);
if (!message) {
error =
"Cannot register activation message: " + std::to_string(GetLastError());
return false;
}
const ULONGLONG deadline = GetTickCount64() + kStartupWaitMs;
DWORD last_error = ERROR_NOT_FOUND;
do {
if (const HWND window =
FindWindowExW(HWND_MESSAGE, nullptr, kWindowClass, name.c_str())) {
DWORD process_id = 0;
if (GetWindowThreadProcessId(window, &process_id) && process_id != 0) {
// Transfer the new launch's foreground permission to the GUI process,
// not its supervisor, before posting the request. Windows may still
// deny foreground focus; the receiver will restore visibility anyway.
AllowSetForegroundWindow(process_id);
if (PostMessageW(window, message, 0, 0)) return true;
last_error = GetLastError();
}
}
if (GetTickCount64() >= deadline) break;
Sleep(50);
} while (true);
error = "Cannot notify the existing CrossDesk window on this desktop: " +
std::to_string(last_error);
return false;
}
} // namespace crossdesk::platform
@@ -48,7 +48,9 @@ InstanceResult SingleInstanceGuard::TryAcquire(InstanceRole role,
std::wstring(role_name.begin(), role_name.end());
LocalFree(sid);
// Object lifetime is the lease. No thread-owned mutex to transfer to a daemon.
HANDLE mutex = CreateMutexW(nullptr, FALSE, name.c_str());
// CreateMutexW requests full access to an existing mutex, which can prevent
// an unelevated launch from recognizing the same user's elevated instance.
HANDLE mutex = CreateMutexExW(nullptr, name.c_str(), 0, SYNCHRONIZE);
const DWORD code = GetLastError();
if (!mutex) {
error = "Cannot create single-instance mutex: " + std::to_string(code);
+5 -1
View File
@@ -154,9 +154,11 @@ function setup_targets()
add_includedirs("apps/desktop/src", {public = true})
if is_os("windows") then
add_files("apps/desktop/src/platform/windows/single_instance.cpp")
add_syslinks("Advapi32")
add_files("apps/desktop/src/platform/windows/gui/instance_activation.cpp")
add_syslinks("Advapi32", "User32")
else
add_files("apps/desktop/src/platform/common/posix_single_instance.cpp")
add_files("apps/desktop/src/platform/common/posix_instance_activation.cpp")
end
target("repository_structure_test")
@@ -528,6 +530,7 @@ function setup_targets()
add_defines("CROSSDESK_PORTABLE=1", {public = true})
end
add_deps("rd_log", "desktop_common", "assets", "config_center", "minirtc",
"single_instance",
"path_manager", "screen_capturer", "speaker_capturer",
"device_controller", "thumbnail", "version_checker", "tools",
"crossdesk_wire", "privacy", "crossdesk_ui")
@@ -576,6 +579,7 @@ function setup_targets()
add_links("GL")
add_deps("headless_console")
add_files("apps/desktop/src/platform/linux/gui/tray/linux_tray.cpp",
"apps/desktop/src/platform/linux/gui/wayland_activation.cpp",
"apps/desktop/src/platform/linux/gui/cuda_graphics.cpp",
"apps/desktop/src/platform/common/gui/opengl_video_renderer.cpp",
"apps/desktop/src/platform/common/gui/cuda_driver.cpp",