@@ -269,6 +269,35 @@ void streaming_will_stop() {
269269 // Nothing to do
270270 }
271271
272+ static pid_t g_restart_child_pid = 0 ; // /< PID of the restarted child process for signal forwarding.
273+
274+ /* *
275+ * @brief Forward a signal to the restarted child process.
276+ *
277+ * This handler is installed in the parent (supervisor) process after forking
278+ * the new Sunshine instance. It ensures that signals like SIGINT (Ctrl+C)
279+ * are delivered to the child.
280+ *
281+ * @param sig The signal number to forward.
282+ */
283+ static void forward_signal_to_child (int sig) {
284+ if (g_restart_child_pid > 0 ) {
285+ kill (g_restart_child_pid, sig);
286+ }
287+ }
288+
289+ /* *
290+ * @brief Request a Sunshine process restart on exit.
291+ *
292+ * This is registered as an atexit handler by restart(). It forks a child
293+ * process with a fresh PID so that macOS WindowServer treats it as a new
294+ * application (required for the system tray icon to reinitialize).
295+ *
296+ * The parent process stays alive as a transparent supervisor: it forwards
297+ * signals (SIGINT, SIGTERM, SIGHUP) to the child and blocks in waitpid().
298+ * This keeps the shell tracking the original PID as its foreground job,
299+ * preserving Ctrl+C and terminal log output.
300+ */
272301 void restart_on_exit () {
273302 char executable[2048 ];
274303 uint32_t size = sizeof (executable);
@@ -277,17 +306,54 @@ void restart_on_exit() {
277306 return ;
278307 }
279308
280- // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves
309+ // ASIO doesn't use O_CLOEXEC, so we have to close all fds ourselves.
281310 int openmax = (int ) sysconf (_SC_OPEN_MAX);
282311 for (int fd = STDERR_FILENO + 1 ; fd < openmax; fd++) {
283312 close (fd);
284313 }
285314
286- // Re-exec ourselves with the same arguments
287- if (execv (executable, lifetime::get_argv ()) < 0 ) {
288- BOOST_LOG (fatal) << " execv() failed: " sv << errno;
315+ // Fork a child process to get a fresh PID.
316+ // A new PID is required on macOS because WindowServer associates GUI state
317+ // (tray icons, activation policy) with the PID. After execv with the same PID,
318+ // WindowServer retains stale state and silently refuses to show new tray icons.
319+ pid_t child = fork ();
320+ if (child < 0 ) {
321+ BOOST_LOG (fatal) << " fork() failed: " sv << errno;
289322 return ;
290323 }
324+
325+ if (child == 0 ) {
326+ // Child: create a new process group so that the parent's signal
327+ // forwarding targets only this child, not the parent itself.
328+ setpgid (0 , 0 );
329+
330+ // Replace this child with the new Sunshine instance
331+ execv (executable, lifetime::get_argv ());
332+
333+ // If execv fails, exit the child immediately without running atexit handlers
334+ _exit (1 );
335+ }
336+
337+ // Parent: become a transparent supervisor.
338+ // The parent stays alive so the shell continues to track it as the
339+ // foreground job, keeping Ctrl+C and terminal output working.
340+ g_restart_child_pid = child;
341+
342+ struct sigaction sa;
343+ memset (&sa, 0 , sizeof (sa));
344+ sa.sa_handler = forward_signal_to_child;
345+ sigemptyset (&sa.sa_mask );
346+ sa.sa_flags = 0 ;
347+ sigaction (SIGINT , &sa, nullptr );
348+ sigaction (SIGTERM , &sa, nullptr );
349+ sigaction (SIGHUP , &sa, nullptr );
350+
351+ int status;
352+ waitpid (child, &status, 0 );
353+
354+ // Exit immediately without running additional atexit handlers or
355+ // static destructors. The child has already taken over.
356+ _exit (WIFEXITED (status) ? WEXITSTATUS (status) : 1 );
291357 }
292358
293359 void restart () {
0 commit comments