sunshine-virtual-display/sunshine-display-daemon.c
2026-10-24 21:30:46 -03:00

481 lines
15 KiB
C

/*
* Sunshine Virtual Display Daemon para KDE Plasma 6 (Wayland)
* Escrito em C para consumo mínimo de recursos (RAM < 2MB, 0% CPU).
*
* Monitora continuamente o estado dos monitores:
* - Se o monitor físico for desligado: cria tela virtual 1920x1080 para Sunshine/Moonlight.
* - Se o monitor físico for ligado: ativa o monitor físico como principal no KWin ANTES
* de encerrar a tela virtual, garantindo estabilidade e impedindo que a sessão feche.
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <stdarg.h>
#include <unistd.h>
#include <time.h>
#include <dirent.h>
#include <signal.h>
#include <errno.h>
#include <poll.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#define VMON_NAME "sunshine-vmon"
#define VMON_OUTPUT "Virtual-sunshine-vmon"
#define RES_W "1920"
#define RES_H "1080"
#define FPS "120"
static volatile sig_atomic_t g_keep_running = 1;
static pid_t g_vmon_pid = -1;
static char g_last_physical_output[64] = "DP-1";
static char g_runtime_dir[256] = "";
static char g_log_path[512] = "";
static void log_msg(const char *fmt, ...) {
time_t now = time(NULL);
struct tm tm_buf;
localtime_r(&now, &tm_buf);
char time_str[64];
strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M:%S", &tm_buf);
FILE *fp = NULL;
if (g_log_path[0] != '\0') {
fp = fopen(g_log_path, "a");
}
va_list args;
va_start(args, fmt);
if (fp) {
fprintf(fp, "[%s] ", time_str);
vfprintf(fp, fmt, args);
fprintf(fp, "\n");
fflush(fp);
fclose(fp);
} else {
printf("[%s] ", time_str);
vprintf(fmt, args);
printf("\n");
fflush(stdout);
}
va_end(args);
}
static void sigterm_handler(int sig) {
(void)sig;
g_keep_running = 0;
}
/* Verifica e recolhe processo filho da tela virtual de forma síncrona (sem race condition de SIGCHLD) */
static void reap_vmon_if_exited(void) {
if (g_vmon_pid > 1) {
int status;
pid_t w = waitpid(g_vmon_pid, &status, WNOHANG);
if (w == g_vmon_pid || (w == -1 && errno == ECHILD)) {
g_vmon_pid = -1;
}
}
}
/* Detecta dinamicamente o socket wayland ativo (ex: wayland-0, wayland-1) */
static void detect_wayland_display(char *out_display, size_t max_len) {
const char *env_w = getenv("WAYLAND_DISPLAY");
if (env_w && env_w[0] != '\0') {
char test_path[512];
snprintf(test_path, sizeof(test_path), "%s/%s", g_runtime_dir, env_w);
if (access(test_path, F_OK) == 0) {
snprintf(out_display, max_len, "%s", env_w);
return;
}
}
DIR *dir = opendir(g_runtime_dir);
if (!dir) {
snprintf(out_display, max_len, "wayland-0");
return;
}
char best[256] = "";
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
if (strncmp(entry->d_name, "wayland-", 8) == 0) {
size_t nlen = strlen(entry->d_name);
if (nlen > 5 && strcmp(entry->d_name + nlen - 5, ".lock") == 0) {
continue;
}
if (strcmp(entry->d_name, best) > 0) {
snprintf(best, sizeof(best), "%s", entry->d_name);
}
}
}
closedir(dir);
if (best[0] != '\0') {
snprintf(out_display, max_len, "%s", best);
} else {
snprintf(out_display, max_len, "wayland-0");
}
}
static void refresh_environment(void) {
char w_display[64];
detect_wayland_display(w_display, sizeof(w_display));
setenv("WAYLAND_DISPLAY", w_display, 1);
setenv("QT_QPA_PLATFORM", "wayland", 1);
char bus_path[512];
snprintf(bus_path, sizeof(bus_path), "unix:path=%s/bus", g_runtime_dir);
setenv("DBUS_SESSION_BUS_ADDRESS", bus_path, 1);
}
/* Aguarda até o compositor Wayland estar pronto e com socket disponível */
static bool wait_for_wayland(int timeout_seconds) {
for (int i = 0; i < timeout_seconds * 10; i++) {
char w_display[64];
detect_wayland_display(w_display, sizeof(w_display));
char test_path[512];
snprintf(test_path, sizeof(test_path), "%s/%s", g_runtime_dir, w_display);
if (access(test_path, F_OK) == 0) {
refresh_environment();
return true;
}
usleep(100000); /* 100ms */
}
return false;
}
/* Executa um comando silenciosamente */
static void run_cmd_quiet(const char *cmd) {
int ret = system(cmd);
(void)ret;
}
/*
* Detecta monitor físico diretamente via kernel sysfs (/sys/class/drm/card*-* /status).
* Extremamente rápido (<10µs), sem dependência de Wayland/Qt/D-Bus, à prova de falhas.
*/
static bool has_drm_physical_monitor(char *primary_output, size_t max_len) {
DIR *dir = opendir("/sys/class/drm");
if (!dir) return false;
struct dirent *entry;
bool found = false;
while ((entry = readdir(dir)) != NULL) {
if (strncmp(entry->d_name, "card", 4) != 0) continue;
if (strchr(entry->d_name, '-') == NULL) continue;
if (strstr(entry->d_name, "Writeback") != NULL) continue;
char status_path[512];
snprintf(status_path, sizeof(status_path), "/sys/class/drm/%s/status", entry->d_name);
FILE *f = fopen(status_path, "r");
if (!f) continue;
char status[32] = "";
if (fgets(status, sizeof(status), f)) {
size_t slen = strlen(status);
while (slen > 0 && (status[slen - 1] == '\n' || status[slen - 1] == '\r')) {
status[--slen] = '\0';
}
if (strcmp(status, "connected") == 0) {
found = true;
if (primary_output && max_len > 0) {
const char *dash = strchr(entry->d_name, '-');
if (dash) {
snprintf(primary_output, max_len, "%.63s", dash + 1);
} else {
snprintf(primary_output, max_len, "%.63s", entry->d_name);
}
}
fclose(f);
break;
}
}
fclose(f);
}
closedir(dir);
return found;
}
/* Wrapper para checagem de monitor físico */
static bool has_physical_monitor(void) {
char detected[64] = "";
if (has_drm_physical_monitor(detected, sizeof(detected))) {
if (detected[0] != '\0') {
snprintf(g_last_physical_output, sizeof(g_last_physical_output), "%s", detected);
}
return true;
}
return false;
}
static void start_vmon(void) {
reap_vmon_if_exited();
if (g_vmon_pid > 1 && kill(g_vmon_pid, 0) == 0) {
return;
}
log_msg("Monitor físico DESLIGADO detectado! Criando tela virtual %sx%s...", RES_W, RES_H);
run_cmd_quiet("pkill -f 'krfb-virtualmonitor.*" VMON_NAME "' >/dev/null 2>&1");
if (!wait_for_wayland(5)) {
log_msg("Aviso: Servidor Wayland não respondeu a tempo para criar tela virtual.");
return;
}
pid_t pid = fork();
if (pid == 0) {
/* Processo filho */
int devnull = open("/dev/null", O_RDWR);
if (devnull >= 0) {
dup2(devnull, STDIN_FILENO);
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
close(devnull);
}
char *args[] = {
"krfb-virtualmonitor",
"--desktopfile", "org.kde.krfb.virtualmonitor",
"--resolution", RES_W "x" RES_H,
"--scale", "1",
"--name", VMON_NAME,
"--password", "sunshine_dummy_pwd",
"--port", "5912",
NULL
};
execvp("krfb-virtualmonitor", args);
_exit(1);
} else if (pid > 1) {
g_vmon_pid = pid;
usleep(800000); /* 0.8s para o KWin registrar a nova tela virtual */
refresh_environment();
char doctor_cmd[512];
snprintf(doctor_cmd, sizeof(doctor_cmd),
"kscreen-doctor output.%s.addCustomMode.%s.%s.120000.reduced output.%s.enable output.%s.priority.1 output.%s.mode.%sx%s@%s >/dev/null 2>&1",
VMON_OUTPUT, RES_W, RES_H, VMON_OUTPUT, VMON_OUTPUT, VMON_OUTPUT, RES_W, RES_H, FPS);
run_cmd_quiet(doctor_cmd);
log_msg("Tela virtual %s ativa em %sx%s@%sHz como tela principal (PID %d).", VMON_OUTPUT, RES_W, RES_H, FPS, (int)g_vmon_pid);
/* Reinicia o Sunshine para vincular à nova tela virtual */
run_cmd_quiet("systemctl --user restart sunshine >/dev/null 2>&1");
log_msg("Sunshine pronto para conexões na tela virtual.");
}
}
static void terminate_vmon_process(void) {
pid_t target_pid = g_vmon_pid;
g_vmon_pid = -1;
/*
* CRÍTICO: Jamais chamar kill() se target_pid <= 1.
* No POSIX, kill(-1, SIGKILL) mata TODOS os processos do usuário (derrubando a sessão)!
*/
if (target_pid > 1) {
kill(target_pid, SIGTERM);
int status;
bool exited = false;
for (int i = 0; i < 15; i++) {
pid_t w = waitpid(target_pid, &status, WNOHANG);
if (w == target_pid || (w == -1 && errno == ECHILD)) {
exited = true;
break;
}
usleep(100000);
}
if (!exited) {
kill(target_pid, SIGKILL);
waitpid(target_pid, &status, 0);
}
}
run_cmd_quiet("pkill -f 'krfb-virtualmonitor.*" VMON_NAME "' >/dev/null 2>&1");
}
static void stop_vmon(void) {
log_msg("Monitor físico (%s) LIGADO detectado! Aguardando estabilização do sinal DisplayPort...", g_last_physical_output);
/*
* Aguarda 2.5 segundos para que a placa AMD e o cabo DisplayPort completem o
* DPCD link training e a negociação DDC/EDID sem colisões de KMS no driver.
*/
usleep(2500000);
refresh_environment();
/*
* PASSO 1: Ativa o monitor físico e define-o como prioridade 1 ENQUANTO a tela virtual
* AINDA ESTÁ VIVA. Dessa forma o KWin nunca fica com 0 telas ativas.
*/
char doctor_cmd[512];
snprintf(doctor_cmd, sizeof(doctor_cmd),
"kscreen-doctor output.%.60s.enable output.%.60s.priority.1 >/dev/null 2>&1",
g_last_physical_output, g_last_physical_output);
bool activated = false;
for (int attempt = 1; attempt <= 4; attempt++) {
int ret = system(doctor_cmd);
if (WIFEXITED(ret) && WEXITSTATUS(ret) == 0) {
activated = true;
log_msg("Monitor físico %s ativado com sucesso como tela principal (tentativa %d).", g_last_physical_output, attempt);
break;
}
log_msg("Aguardando KWin aceitar ativação do monitor físico %s (tentativa %d)...", g_last_physical_output, attempt);
sleep(1);
}
if (!activated) {
log_msg("Aviso: Falha ao ativar monitor físico %s após tentativas. Mantendo tela virtual ativa por segurança.", g_last_physical_output);
return;
}
/* Aguarda 1.0s para o KWin renderizar as janelas e barras no monitor físico */
usleep(1000000);
/*
* PASSO 2: Encerra o processo da tela virtual com segurança total (PID > 1).
*/
terminate_vmon_process();
usleep(300000);
log_msg("Tela virtual encerrada. Imagem transferida para o monitor físico %s com sucesso.", g_last_physical_output);
}
static void cleanup(void) {
terminate_vmon_process();
log_msg("Daemon encerrado.");
}
int main(int argc, char *argv[]) {
uid_t uid = getuid();
const char *rt = getenv("XDG_RUNTIME_DIR");
if (rt && rt[0] != '\0') {
snprintf(g_runtime_dir, sizeof(g_runtime_dir), "%s", rt);
} else {
snprintf(g_runtime_dir, sizeof(g_runtime_dir), "/run/user/%u", (unsigned)uid);
setenv("XDG_RUNTIME_DIR", g_runtime_dir, 1);
}
const char *home = getenv("HOME");
if (home) {
snprintf(g_log_path, sizeof(g_log_path), "%s/.config/sunshine/vmon.log", home);
}
/* Subcomandos rápidos */
if (argc > 1) {
if (strcmp(argv[1], "--status") == 0) {
bool phys = has_physical_monitor();
printf("Monitor físico ativo: %s\n", phys ? "SIM" : "NÃO");
printf("Última saída física detectada: %s\n", g_last_physical_output);
return 0;
}
if (strcmp(argv[1], "--version") == 0) {
printf("sunshine-display-daemon (C edition) v2.3\n");
return 0;
}
if (strcmp(argv[1], "--help") == 0) {
printf("Uso: %s [--status | --version | --help | --test-iterations N]\n", argv[0]);
return 0;
}
}
int max_test_iterations = 0;
if (argc > 2 && strcmp(argv[1], "--test-iterations") == 0) {
max_test_iterations = atoi(argv[2]);
}
/* Configura sinais (sem SIGCHLD assíncrono para evitar race condition no g_vmon_pid) */
struct sigaction sa_term;
memset(&sa_term, 0, sizeof(sa_term));
sa_term.sa_handler = sigterm_handler;
sigaction(SIGTERM, &sa_term, NULL);
sigaction(SIGINT, &sa_term, NULL);
log_msg("Serviço sunshine-virtual-display (C edition) iniciado e ativo.");
/* Aguarda o Wayland estar pronto antes de prosseguir */
wait_for_wayland(5);
bool was_physical_active = has_physical_monitor();
if (!was_physical_active) {
log_msg("Iniciando com monitor físico desligado. Criando tela virtual imediatamente...");
start_vmon();
} else {
log_msg("Iniciando com monitor físico ativo (%s).", g_last_physical_output);
}
/* Socket Netlink para eventos de hotplug do kernel uevent */
int nl_fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_KOBJECT_UEVENT);
if (nl_fd >= 0) {
struct sockaddr_nl sa_nl;
memset(&sa_nl, 0, sizeof(sa_nl));
sa_nl.nl_family = AF_NETLINK;
sa_nl.nl_groups = 1; /* Notificações do kernel */
if (bind(nl_fd, (struct sockaddr *)&sa_nl, sizeof(sa_nl)) < 0) {
close(nl_fd);
nl_fd = -1;
}
}
struct pollfd pfd;
pfd.fd = nl_fd;
pfd.events = POLLIN;
int iteration = 0;
char nl_buf[2048];
while (g_keep_running) {
if (max_test_iterations > 0 && ++iteration > max_test_iterations) {
break;
}
reap_vmon_if_exited();
/* Espera evento do kernel ou timeout de 1 segundo */
int ret = 0;
if (nl_fd >= 0) {
ret = poll(&pfd, 1, 1000);
if (ret > 0 && (pfd.revents & POLLIN)) {
/* Consome buffer do netlink */
ssize_t n = recv(nl_fd, nl_buf, sizeof(nl_buf) - 1, MSG_DONTWAIT);
if (n > 0) {
nl_buf[n] = '\0';
/* Se for uevent irrelevante (não drm/card), ignora */
if (strstr(nl_buf, "drm") == NULL && strstr(nl_buf, "card") == NULL) {
continue;
}
}
}
} else {
sleep(1);
}
if (!g_keep_running) {
break;
}
bool physical_active = has_physical_monitor();
if (physical_active != was_physical_active) {
if (!physical_active) {
start_vmon();
} else {
stop_vmon();
}
was_physical_active = physical_active;
}
}
if (nl_fd >= 0) {
close(nl_fd);
}
cleanup();
return 0;
}