1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340/**
* @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) || defined(__FreeBSD__)
#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;
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) {
BOOST_LOG(error) << "System tray is not initialized"sv;
return 1;
}
// Block until an event is processed or tray_quit() is called
return tray_loop(1);
}
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;
return;
}
// Main tray event loop
while (process_tray_events() == 0);
BOOST_LOG(info) << "System tray thread ended"sv;
}
int init_tray_threaded() {
try {
auto tray_thread = std::thread(tray_thread_worker);
// The tray thread doesn't require strong lifetime management.
// It will exit asynchronously when tray_exit() is called.
tray_thread.detach();
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;
}
}
} // namespace system_tray
#endif