mirror of
https://github.com/kunkundi/crossdesk.git
synced 2026-10-19 02:13:03 +08:00
[feat] activate existing window on duplicate launches, refs #119
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user