#include #include #include #include #include #include #include #include #include #include #include #define MAX_GAMES 32 #define ENVIRON_BUF_SIZE (64 * 1024) #define CMDLINE_BUF_SIZE 4096 #define RECV_BUF_SIZE 8192 static pid_t games[MAX_GAMES]; /* 0 = empty slot */ static int game_count = 0; static char environ_buf[ENVIRON_BUF_SIZE]; static char cmdline_buf[CMDLINE_BUF_SIZE]; static char recv_buf[RECV_BUF_SIZE]; static int find_slot_by_pid(pid_t pid) { for (int i = 0; i < MAX_GAMES; i++) { if (games[i] == pid) return i; } return -1; } static int find_free_slot(void) { for (int i = 0; i < MAX_GAMES; i++) { if (!games[i]) return i; } return -1; } static void add_game(pid_t pid) { int slot = find_free_slot(); if (slot < 0) return; games[slot] = pid; if (game_count++ == 0) { printf("Game launched\n"); fflush(stdout); } } static void remove_game(pid_t pid) { int slot = find_slot_by_pid(pid); if (slot < 0) return; games[slot] = 0; if (--game_count == 0) { printf("Game exited\n"); fflush(stdout); } } static ssize_t read_file(const char *path, char *buf, size_t size) { int fd = open(path, O_RDONLY); if (fd < 0) return -1; ssize_t n = read(fd, buf, size - 1); close(fd); if (n > 0) buf[n] = '\0'; return n; } /* Return pointer to value inside null-separated environ block, or NULL. */ static const char *env_get(const char *env, size_t size, const char *key) { const size_t klen = strlen(key); size_t index = 0; while (index < size) { const char *env_start = env + index; const char *env_end = memchr(env_start, '\0', size - index); if(!env_end) break; const size_t env_len = env_end - env_start; if(env_len >= klen + 1 && memcmp(env_start, key, klen) == 0 && env_start[klen] == '=') return env_start + klen + 1; index += env_len + 1; } return NULL; } static size_t get_argv_len(const char *cmdline, size_t size) { const char *argv0_end = memchr(cmdline, '\0', size); if(argv0_end) return argv0_end - cmdline; else return 0; } static const char* memchr_reverse(const char *p, int c, size_t size) { for(size_t i = 0; i < size; ++i) { if(p[size - 1 - i] == c) return p + i; } return NULL; } static const char* get_arg_by_index(const char *cmdline, size_t size, size_t index) { size_t current_index = 0; size_t cmdline_index = 0; while (index < size) { const char *arg_start = cmdline + cmdline_index; const char *arg_end = memchr(arg_start, '\0', size - cmdline_index); if(!arg_end) break; const size_t arg_len = arg_end - arg_start; if(current_index == index) return arg_start; cmdline_index += arg_len + 1; current_index += 1; } return NULL; } static bool memeql(const char *haystack, size_t haystack_size, const char *needle) { const size_t needle_size = strlen(needle); return haystack_size == needle_size && memcmp(haystack, needle, needle_size) == 0; } static bool starts_with(const char *haystack, size_t haystack_size, const char *needle) { const size_t needle_size = strlen(needle); return haystack_size >= needle_size && memcmp(haystack, needle, needle_size) == 0; } static int is_wine_binary(const char *argv0, size_t size) { const char *base = memchr_reverse(argv0, '/', size); base = base ? base + 1 : argv0; const size_t base_len = argv0 + size - base; return memeql(base, base_len, "wine") || memeql(base, base_len, "wine64") || memeql(base, base_len, "wine-preloader") || memeql(base, base_len, "wine64-preloader"); } static int has_game_arch_suffix(const char *argv0, size_t size) { static const char *suffixes[] = { ".x86_64", ".x64", ".x86" }; for (int i = 0; i < 3; i++) { const size_t slen = strlen(suffixes[i]); if (size >= slen && memcmp(argv0 + size - slen, suffixes[i], slen) == 0) return 1; } return 0; } static void check_process(pid_t pid) { if (find_slot_by_pid(pid) >= 0) return; char path[64]; ssize_t env_n, cmd_n; bool is_steam_fossilize = false; bool is_stream_script = false; /* Proton launched outside steam has this while it doesn't have SteamAppId nor is the process called wine */ bool has_wine_prefix_env = false; snprintf(path, sizeof(path), "/proc/%d/environ", pid); env_n = read_file(path, environ_buf, sizeof(environ_buf)); if (env_n > 0) { is_steam_fossilize = env_get(environ_buf, env_n, "STEAM_FOSSILIZE_DUMP_PATH_READ_ONLY"); is_stream_script = env_get(environ_buf, env_n, "STEAMSCRIPT_VERSION"); has_wine_prefix_env = env_get(environ_buf, env_n, "WINEPREFIX"); const char *appid = env_get(environ_buf, env_n, "SteamAppId"); if (!appid) appid = env_get(environ_buf, env_n, "SteamGameId"); if (appid && appid[0] >= '1' && appid[0] <= '9') { add_game(pid); return; } } snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); cmd_n = read_file(path, cmdline_buf, sizeof(cmdline_buf)); if(cmd_n <= 0) return; const char *argv0 = cmdline_buf; const size_t argv0_len = get_argv_len(cmdline_buf, cmd_n); const char *argv1 = get_arg_by_index(cmdline_buf, cmd_n, 1); const size_t argv1_len = argv1 ? get_argv_len(argv1, cmdline_buf + cmd_n - argv1) : 0; if (cmd_n > 0 && (is_wine_binary(argv0, argv0_len) || has_game_arch_suffix(argv0, argv0_len) || has_wine_prefix_env) && !is_steam_fossilize && !is_stream_script && !memeql(argv1, argv1_len, "--version") && !starts_with(argv0, argv0_len, "C:\\windows\\system32")) { // fprintf(stderr, "env: "); // write(STDOUT_FILENO, environ_buf, env_n); // fprintf(stderr, "\ncmdline: "); // write(STDOUT_FILENO, cmdline_buf, cmd_n); // fprintf(stderr, "\n"); add_game(pid); } } static void handle_proc_event(const struct proc_event *ev) { switch (ev->what) { case PROC_EVENT_EXEC: check_process(ev->event_data.exec.process_tgid); break; case PROC_EVENT_EXIT: /* Only act when the whole process (not just a thread) exits */ if (ev->event_data.exit.process_pid == ev->event_data.exit.process_tgid) remove_game(ev->event_data.exit.process_tgid); break; default: break; } } static void process_netlink_msg(const char *buf, size_t len) { const struct nlmsghdr *nl = (const struct nlmsghdr *)buf; for (; NLMSG_OK(nl, (unsigned int)len); nl = NLMSG_NEXT(nl, len)) { if (nl->nlmsg_type == NLMSG_NOOP || nl->nlmsg_type == NLMSG_ERROR || nl->nlmsg_type == NLMSG_OVERRUN) continue; if (nl->nlmsg_len < NLMSG_HDRLEN + sizeof(struct cn_msg)) continue; const struct cn_msg *cn = (const struct cn_msg *)NLMSG_DATA(nl); if (cn->id.idx != CN_IDX_PROC || cn->id.val != CN_VAL_PROC) continue; if (cn->len < sizeof(struct proc_event)) continue; handle_proc_event((const struct proc_event *)cn->data); } } static int send_mcast_op(int sock, enum proc_cn_mcast_op op) { /* cn_msg ends with data[0], so we can't place fields after it in a struct. * Use a flat buffer and fill via pointers instead. */ char buf[NLMSG_SPACE(sizeof(struct cn_msg) + sizeof(enum proc_cn_mcast_op))]; memset(buf, 0, sizeof(buf)); struct nlmsghdr *nl = (struct nlmsghdr *)buf; nl->nlmsg_len = sizeof(buf); nl->nlmsg_type = NLMSG_DONE; nl->nlmsg_pid = (unsigned int)getpid(); struct cn_msg *cn = (struct cn_msg *)NLMSG_DATA(nl); cn->id.idx = CN_IDX_PROC; cn->id.val = CN_VAL_PROC; cn->len = sizeof(enum proc_cn_mcast_op); memcpy(cn->data, &op, sizeof(op)); return send(sock, buf, sizeof(buf), 0) < 0 ? -1 : 0; } static int setup_netlink(void) { int sock = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR); if (sock < 0) { perror("socket(AF_NETLINK, SOCK_DGRAM, NETLINK_CONNECTOR)"); return -1; } struct sockaddr_nl sa; memset(&sa, 0, sizeof(sa)); sa.nl_family = AF_NETLINK; sa.nl_groups = CN_IDX_PROC; sa.nl_pid = (unsigned int)getpid(); if (bind(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { perror("bind"); close(sock); return -1; } if (send_mcast_op(sock, PROC_CN_MCAST_LISTEN) < 0) { perror("send PROC_CN_MCAST_LISTEN"); close(sock); return -1; } return sock; } static void scan_existing_processes(void) { DIR *d = opendir("/proc"); if (!d) return; struct dirent *ent; while ((ent = readdir(d)) != NULL) { const char *s = ent->d_name; if (*s < '1' || *s > '9') continue; pid_t pid = 0; for (; *s; s++) { if (*s < '0' || *s > '9') { pid = 0; break; } pid = pid * 10 + (*s - '0'); } if (pid > 0) check_process(pid); } closedir(d); } int main(void) { memset(games, 0, sizeof(games)); int sock = setup_netlink(); if (sock < 0) return 1; scan_existing_processes(); for (;;) { ssize_t n = recv(sock, recv_buf, sizeof(recv_buf), 0); if (n < 0) { if (errno == EINTR) continue; if (errno == ENOBUFS) { scan_existing_processes(); continue; } perror("recv"); break; } process_netlink_msg(recv_buf, (size_t)n); } send_mcast_op(sock, PROC_CN_MCAST_IGNORE); close(sock); return 0; }