Files
Sunshine/src/system_tray.cpp

406 lines
12 KiB
C++

/**
* @file src/system_tray.cpp
* @brief Definitions for the system tray icon and notification system.
*/
// macros
#if defined SUNSHINE_TRAY && SUNSHINE_TRAY >= 1
#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <accctrl.h>
#include <aclapi.h>
#define TRAY_ICON WEB_DIR "images/sunshine.ico"
#define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing.ico"
#define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing.ico"
#define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked.ico"
#elif defined(__linux__) || defined(linux) || defined(__linux)
#define TRAY_ICON SUNSHINE_TRAY_PREFIX "-tray"
#define TRAY_ICON_PLAYING SUNSHINE_TRAY_PREFIX "-playing"
#define TRAY_ICON_PAUSING SUNSHINE_TRAY_PREFIX "-pausing"
#define TRAY_ICON_LOCKED SUNSHINE_TRAY_PREFIX "-locked"
#elif defined(__APPLE__) || defined(__MACH__)
#define TRAY_ICON WEB_DIR "images/logo-sunshine-16.png"
#define TRAY_ICON_PLAYING WEB_DIR "images/sunshine-playing-16.png"
#define TRAY_ICON_PAUSING WEB_DIR "images/sunshine-pausing-16.png"
#define TRAY_ICON_LOCKED WEB_DIR "images/sunshine-locked-16.png"
#include <dispatch/dispatch.h>
#endif
// standard includes
#include <atomic>
#include <chrono>
#include <csignal>
#include <format>
#include <string>
#include <thread>
// lib includes
#include <boost/filesystem.hpp>
#include <boost/process/v1/environment.hpp>
#include <tray/src/tray.h>
// local includes
#include "confighttp.h"
#include "display_device.h"
#include "logging.h"
#include "platform/common.h"
#include "process.h"
#include "src/entry_handler.h"
using namespace std::literals;
// system_tray namespace
namespace system_tray {
static std::atomic tray_initialized = false;
// Threading variables for all platforms
static std::thread tray_thread;
static std::atomic tray_thread_running = false;
static std::atomic tray_thread_should_exit = false;
void tray_open_ui_cb([[maybe_unused]] struct tray_menu *item) {
BOOST_LOG(info) << "Opening UI from system tray"sv;
launch_ui();
}
void tray_donate_github_cb([[maybe_unused]] struct tray_menu *item) {
platf::open_url("https://github.com/sponsors/LizardByte");
}
void tray_donate_patreon_cb([[maybe_unused]] struct tray_menu *item) {
platf::open_url("https://www.patreon.com/LizardByte");
}
void tray_donate_paypal_cb([[maybe_unused]] struct tray_menu *item) {
platf::open_url("https://www.paypal.com/paypalme/ReenigneArcher");
}
void tray_reset_display_device_config_cb([[maybe_unused]] struct tray_menu *item) {
BOOST_LOG(info) << "Resetting display device config from system tray"sv;
std::ignore = display_device::reset_persistence();
}
void tray_restart_cb([[maybe_unused]] struct tray_menu *item) {
BOOST_LOG(info) << "Restarting from system tray"sv;
platf::restart();
}
void tray_quit_cb([[maybe_unused]] struct tray_menu *item) {
BOOST_LOG(info) << "Quitting from system tray"sv;
#ifdef _WIN32
// If we're running in a service, return a special status to
// tell it to terminate too, otherwise it will just respawn us.
if (GetConsoleWindow() == nullptr) {
lifetime::exit_sunshine(ERROR_SHUTDOWN_IN_PROGRESS, true);
return;
}
#endif
lifetime::exit_sunshine(0, true);
}
// Tray menu
static struct tray tray = {
.icon = TRAY_ICON,
.tooltip = PROJECT_NAME,
.menu =
(struct tray_menu[]) {
// todo - use boost/locale to translate menu strings
{.text = "Open Sunshine", .cb = tray_open_ui_cb},
{.text = "-"},
{.text = "Donate",
.submenu =
(struct tray_menu[]) {
{.text = "GitHub Sponsors", .cb = tray_donate_github_cb},
{.text = "Patreon", .cb = tray_donate_patreon_cb},
{.text = "PayPal", .cb = tray_donate_paypal_cb},
{.text = nullptr}
}},
{.text = "-"},
// Currently display device settings are only supported on Windows
#ifdef _WIN32
{.text = "Reset Display Device Config", .cb = tray_reset_display_device_config_cb},
#endif
{.text = "Restart", .cb = tray_restart_cb},
{.text = "Quit", .cb = tray_quit_cb},
{.text = nullptr}
},
.iconPathCount = 4,
.allIconPaths = {TRAY_ICON, TRAY_ICON_LOCKED, TRAY_ICON_PLAYING, TRAY_ICON_PAUSING},
};
int init_tray() {
#ifdef _WIN32
// If we're running as SYSTEM, Explorer.exe will not have permission to open our thread handle
// to monitor for thread termination. If Explorer fails to open our thread, our tray icon
// will persist forever if we terminate unexpectedly. To avoid this, we will modify our thread
// DACL to add an ACE that allows SYNCHRONIZE access to Everyone.
{
PACL old_dacl;
PSECURITY_DESCRIPTOR sd;
auto error = GetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, &old_dacl, nullptr, &sd);
if (error != ERROR_SUCCESS) {
BOOST_LOG(warning) << "GetSecurityInfo() failed: "sv << error;
return 1;
}
auto free_sd = util::fail_guard([sd]() {
LocalFree(sd);
});
SID_IDENTIFIER_AUTHORITY sid_authority = SECURITY_WORLD_SID_AUTHORITY;
PSID world_sid;
if (!AllocateAndInitializeSid(&sid_authority, 1, SECURITY_WORLD_RID, 0, 0, 0, 0, 0, 0, 0, &world_sid)) {
error = GetLastError();
BOOST_LOG(warning) << "AllocateAndInitializeSid() failed: "sv << error;
return 1;
}
auto free_sid = util::fail_guard([world_sid]() {
FreeSid(world_sid);
});
EXPLICIT_ACCESS ea {};
ea.grfAccessPermissions = SYNCHRONIZE;
ea.grfAccessMode = GRANT_ACCESS;
ea.grfInheritance = NO_INHERITANCE;
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.ptstrName = (LPSTR) world_sid;
PACL new_dacl;
error = SetEntriesInAcl(1, &ea, old_dacl, &new_dacl);
if (error != ERROR_SUCCESS) {
BOOST_LOG(warning) << "SetEntriesInAcl() failed: "sv << error;
return 1;
}
auto free_new_dacl = util::fail_guard([new_dacl]() {
LocalFree(new_dacl);
});
error = SetSecurityInfo(GetCurrentThread(), SE_KERNEL_OBJECT, DACL_SECURITY_INFORMATION, nullptr, nullptr, new_dacl, nullptr);
if (error != ERROR_SUCCESS) {
BOOST_LOG(warning) << "SetSecurityInfo() failed: "sv << error;
return 1;
}
}
// Wait for the shell to be initialized before registering the tray icon.
// This ensures the tray icon works reliably after a logoff/logon cycle.
while (GetShellWindow() == nullptr) {
Sleep(1000);
}
#endif
if (tray_init(&tray) < 0) {
BOOST_LOG(warning) << "Failed to create system tray"sv;
return 1;
}
BOOST_LOG(info) << "System tray created"sv;
tray_initialized = true;
return 0;
}
int process_tray_events() {
if (!tray_initialized) {
return 1;
}
// Process one iteration of the tray loop with non-blocking mode (0)
if (const int result = tray_loop(0); result != 0) {
BOOST_LOG(warning) << "System tray loop failed"sv;
return result;
}
return 0;
}
int end_tray() {
if (tray_initialized) {
tray_initialized = false;
tray_exit();
}
return 0;
}
void update_tray_playing(std::string app_name) {
if (!tray_initialized) {
return;
}
tray.notification_title = nullptr;
tray.notification_text = nullptr;
tray.notification_cb = nullptr;
tray.notification_icon = nullptr;
tray.icon = TRAY_ICON_PLAYING;
tray_update(&tray);
tray.icon = TRAY_ICON_PLAYING;
tray.notification_title = "Stream Started";
static std::string msg = std::format("Streaming started for {}", app_name);
tray.notification_text = msg.c_str();
tray.tooltip = msg.c_str();
tray.notification_icon = TRAY_ICON_PLAYING;
tray_update(&tray);
}
void update_tray_pausing(std::string app_name) {
if (!tray_initialized) {
return;
}
tray.notification_title = nullptr;
tray.notification_text = nullptr;
tray.notification_cb = nullptr;
tray.notification_icon = nullptr;
tray.icon = TRAY_ICON_PAUSING;
tray_update(&tray);
static std::string msg = std::format("Streaming paused for {}", app_name);
tray.icon = TRAY_ICON_PAUSING;
tray.notification_title = "Stream Paused";
tray.notification_text = msg.c_str();
tray.tooltip = msg.c_str();
tray.notification_icon = TRAY_ICON_PAUSING;
tray_update(&tray);
}
void update_tray_stopped(std::string app_name) {
if (!tray_initialized) {
return;
}
tray.notification_title = nullptr;
tray.notification_text = nullptr;
tray.notification_cb = nullptr;
tray.notification_icon = nullptr;
tray.icon = TRAY_ICON;
tray_update(&tray);
static std::string msg = std::format("Application {} successfully stopped", app_name);
tray.icon = TRAY_ICON;
tray.notification_icon = TRAY_ICON;
tray.notification_title = "Application Stopped";
tray.notification_text = msg.c_str();
tray.tooltip = PROJECT_NAME;
tray_update(&tray);
}
void update_tray_require_pin() {
if (!tray_initialized) {
return;
}
tray.notification_title = nullptr;
tray.notification_text = nullptr;
tray.notification_cb = nullptr;
tray.notification_icon = nullptr;
tray.icon = TRAY_ICON;
tray_update(&tray);
tray.icon = TRAY_ICON;
tray.notification_title = "Incoming Pairing Request";
tray.notification_text = "Click here to complete the pairing process";
tray.notification_icon = TRAY_ICON_LOCKED;
tray.tooltip = PROJECT_NAME;
tray.notification_cb = []() {
launch_ui("/pin");
};
tray_update(&tray);
}
// Threading functions available on all platforms
static void tray_thread_worker() {
BOOST_LOG(info) << "System tray thread started"sv;
// Initialize the tray in this thread
if (init_tray() != 0) {
BOOST_LOG(error) << "Failed to initialize tray in thread"sv;
tray_thread_running = false;
return;
}
tray_thread_running = true;
// Main tray event loop
while (!tray_thread_should_exit) {
if (process_tray_events() != 0) {
BOOST_LOG(warning) << "Tray event processing failed in thread"sv;
break;
}
// Sleep to avoid busy waiting
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
// Clean up the tray
end_tray();
tray_thread_running = false;
BOOST_LOG(info) << "System tray thread ended"sv;
}
int init_tray_threaded() {
if (tray_thread_running) {
BOOST_LOG(warning) << "Tray thread is already running"sv;
return 1;
}
tray_thread_should_exit = false;
try {
tray_thread = std::thread(tray_thread_worker);
// Wait for the thread to start and initialize
const auto start_time = std::chrono::steady_clock::now();
while (!tray_thread_running && !tray_thread_should_exit) {
std::this_thread::sleep_for(std::chrono::milliseconds(10));
// Timeout after 10 seconds
if (std::chrono::steady_clock::now() - start_time > std::chrono::seconds(10)) {
BOOST_LOG(error) << "Tray thread initialization timeout"sv;
tray_thread_should_exit = true;
if (tray_thread.joinable()) {
tray_thread.join();
}
return 1;
}
}
if (!tray_thread_running) {
BOOST_LOG(error) << "Tray thread failed to start"sv;
if (tray_thread.joinable()) {
tray_thread.join();
}
return 1;
}
BOOST_LOG(info) << "System tray thread initialized successfully"sv;
return 0;
} catch (const std::exception &e) {
BOOST_LOG(error) << "Failed to create tray thread: " << e.what();
return 1;
}
}
int end_tray_threaded() {
if (!tray_thread_running) {
return 0;
}
BOOST_LOG(info) << "Stopping system tray thread"sv;
tray_thread_should_exit = true;
if (tray_thread.joinable()) {
tray_thread.join();
}
BOOST_LOG(info) << "System tray thread stopped"sv;
return 0;
}
} // namespace system_tray
#endif