From 44b761dbe2b3f2f597b3d2ff03e6ffdc517c1ed0 Mon Sep 17 00:00:00 2001 From: cheerfulScumbag <164391367+cheerfulScumbag@users.noreply.github.com> Date: Sun, 22 Mar 2026 22:26:25 +0000 Subject: [PATCH 1/4] Add opt-in session save and restore support --- assets/config.conf | 11 + meson.build | 1 + src/config/parse_config.h | 89 +++++++ src/mango.c | 258 ++++++++++++++++++++ src/session/session.c | 489 ++++++++++++++++++++++++++++++++++++++ src/session/session.h | 39 +++ 6 files changed, 887 insertions(+) create mode 100644 src/session/session.c create mode 100644 src/session/session.h diff --git a/assets/config.conf b/assets/config.conf index eb326d16..6101cbc2 100644 --- a/assets/config.conf +++ b/assets/config.conf @@ -78,6 +78,17 @@ no_border_when_single=0 axis_bind_apply_timeout=100 focus_on_activate=1 idleinhibit_ignore_visible=0 +# Restore and save session state across compositor restarts. +# Disabled by default to preserve Mango's current behavior. +# Minimized restore is currently unsupported. +# Restore expects target outputs to already exist before clients map. +session_restore=0 +# Relaunch mapping used when Mango cannot infer a launch command from a client. +# Format: session_launch=app_id|command +# or: session_launch=app_id|title|command +# session_launch=foot|foot +# session_launch=foot|gamma|foot -a foot -T gamma -e sh -lc "sleep 600" +# session_launch=org.kde.dolphin|dolphin . sloppyfocus=1 warpcursor=1 focus_cross_monitor=0 diff --git a/meson.build b/meson.build index 64394cd9..4fcd44dd 100644 --- a/meson.build +++ b/meson.build @@ -97,6 +97,7 @@ endif executable('mango', 'src/mango.c', 'src/common/util.c', + 'src/session/session.c', 'src/ext-protocol/wlr_ext_workspace_v1.c', wayland_sources, dependencies : [ diff --git a/src/config/parse_config.h b/src/config/parse_config.h index e02b5017..c35adf38 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -52,6 +52,12 @@ typedef struct { char *value; } ConfigEnv; +typedef struct { + char app_id[256]; + char title[512]; + char command[1024]; +} ConfigSessionLaunchRule; + typedef struct { const char *id; const char *title; @@ -251,6 +257,7 @@ typedef struct { uint32_t axis_bind_apply_timeout; uint32_t focus_on_activate; + int32_t session_restore; int32_t idleinhibit_ignore_visible; int32_t sloppyfocus; int32_t warpcursor; @@ -352,6 +359,8 @@ typedef struct { char **exec_once; int32_t exec_once_count; + ConfigSessionLaunchRule *session_launch_rules; + int32_t session_launch_rules_count; char *cursor_theme; uint32_t cursor_size; @@ -1426,6 +1435,71 @@ bool parse_option(Config *config, char *key, char *value) { config->allow_shortcuts_inhibit = atoi(value); } else if (strcmp(key, "allow_lock_transparent") == 0) { config->allow_lock_transparent = atoi(value); + } else if (strcmp(key, "session_restore") == 0) { + config->session_restore = atoi(value); + } else if (strcmp(key, "session_launch") == 0) { + ConfigSessionLaunchRule rule = {0}; + ConfigSessionLaunchRule *new_rules = NULL; + char *first_sep = strchr(value, '|'); + char *second_sep = NULL; + size_t app_len, title_len = 0, cmd_len; + + if (!first_sep) { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m Invalid session_launch " + "format. Expected app_id|command or " + "app_id|title|command\033[0m\n"); + return false; + } + + second_sep = strchr(first_sep + 1, '|'); + app_len = (size_t)(first_sep - value); + if (second_sep) { + title_len = (size_t)(second_sep - (first_sep + 1)); + cmd_len = strlen(second_sep + 1); + } else { + cmd_len = strlen(first_sep + 1); + } + + if (app_len == 0 || cmd_len == 0 || app_len >= sizeof(rule.app_id) || + cmd_len >= sizeof(rule.command) || + title_len >= sizeof(rule.title)) { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m Invalid session_launch " + "entry length\033[0m\n"); + return false; + } + + memcpy(rule.app_id, value, app_len); + rule.app_id[app_len] = '\0'; + if (second_sep) { + memcpy(rule.title, first_sep + 1, title_len); + rule.title[title_len] = '\0'; + memcpy(rule.command, second_sep + 1, cmd_len + 1); + } else { + memcpy(rule.command, first_sep + 1, cmd_len + 1); + } + trim_whitespace(rule.app_id); + trim_whitespace(rule.title); + trim_whitespace(rule.command); + if (rule.app_id[0] == '\0' || rule.command[0] == '\0') { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m session_launch requires " + "both app_id and command\033[0m\n"); + return false; + } + + new_rules = realloc(config->session_launch_rules, + sizeof(*config->session_launch_rules) * + (config->session_launch_rules_count + 1)); + if (!new_rules) { + fprintf(stderr, + "\033[1m\033[31m[ERROR]:\033[33m Failed to allocate " + "session_launch rules\033[0m\n"); + return false; + } + config->session_launch_rules = new_rules; + config->session_launch_rules[config->session_launch_rules_count++] = rule; } else if (strcmp(key, "no_border_when_single") == 0) { config->no_border_when_single = atoi(value); } else if (strcmp(key, "no_radius_when_single") == 0) { @@ -2826,6 +2900,14 @@ void free_circle_layout(Config *config) { config->circle_layout_count = 0; // 重置计数 } +void free_session_launch_rules(Config *config) { + if (config->session_launch_rules) { + free(config->session_launch_rules); + config->session_launch_rules = NULL; + } + config->session_launch_rules_count = 0; +} + void free_baked_points(void) { if (baked_points_move) { free(baked_points_move); @@ -3096,6 +3178,7 @@ void free_config(void) { // 释放 circle_layout free_circle_layout(&config); + free_session_launch_rules(&config); // 释放动画资源 free_baked_points(); @@ -3171,6 +3254,7 @@ void override_config(void) { config.axis_bind_apply_timeout = CLAMP_INT(config.axis_bind_apply_timeout, 0, 1000); config.focus_on_activate = CLAMP_INT(config.focus_on_activate, 0, 1); + config.session_restore = CLAMP_INT(config.session_restore, 0, 1); config.idleinhibit_ignore_visible = CLAMP_INT(config.idleinhibit_ignore_visible, 0, 1); config.sloppyfocus = CLAMP_INT(config.sloppyfocus, 0, 1); @@ -3275,6 +3359,9 @@ void set_value_default() { config.axis_bind_apply_timeout = 100; config.focus_on_activate = 1; + config.session_restore = 0; + config.session_launch_rules = NULL; + config.session_launch_rules_count = 0; config.new_is_master = 1; config.default_mfact = 0.55f; config.default_nmaster = 1; @@ -3511,6 +3598,8 @@ bool parse_config(void) { config.exec_count = 0; config.exec_once = NULL; config.exec_once_count = 0; + config.session_launch_rules = NULL; + config.session_launch_rules_count = 0; config.scroller_proportion_preset = NULL; config.scroller_proportion_preset_count = 0; config.circle_layout = NULL; diff --git a/src/mango.c b/src/mango.c index 8fdff709..53915ca7 100644 --- a/src/mango.c +++ b/src/mango.c @@ -407,6 +407,7 @@ struct Client { float focused_opacity; float unfocused_opacity; char oldmonname[128]; + char session_launch_command[1024]; int32_t noblur; double master_mfact_per, master_inner_per, stack_inner_per; double old_master_mfact_per, old_master_inner_per, old_stack_inner_per; @@ -945,6 +946,7 @@ struct Pertag { }; #include "config/parse_config.h" +#include "session/session.h" static struct wl_signal mango_print_status; @@ -1015,6 +1017,257 @@ static struct wl_event_source *sync_keymap; #include "layout/horizontal.h" #include "layout/vertical.h" +static void session_write_json_string(FILE *out, const char *str) { + const unsigned char *p = (const unsigned char *)(str ? str : ""); + + fputc('"', out); + for (; *p != '\0'; ++p) { + switch (*p) { + case '\\': + fputs("\\\\", out); + break; + case '"': + fputs("\\\"", out); + break; + case '\n': + fputs("\\n", out); + break; + case '\r': + fputs("\\r", out); + break; + case '\t': + fputs("\\t", out); + break; + default: + if (*p < 0x20) + fprintf(out, "\\u%04x", *p); + else + fputc(*p, out); + } + } + fputc('"', out); +} + +const char *mango_session_client_appid(Client *c) { return client_get_appid(c); } + +const char *mango_session_client_title(Client *c) { return client_get_title(c); } + +const char *mango_session_client_monitor(Client *c) { + return (c && c->mon && c->mon->wlr_output) ? c->mon->wlr_output->name : ""; +} + +const char *mango_session_lookup_launch_command(const char *app_id, + const char *title) { + if (!app_id || app_id[0] == '\0') + return ""; + + for (int32_t i = 0; i < config.session_launch_rules_count; ++i) { + ConfigSessionLaunchRule *rule = &config.session_launch_rules[i]; + if (strcmp(rule->app_id, app_id) != 0) + continue; + if (rule->title[0] == '\0') + continue; + if (title && strcmp(rule->title, title) == 0) + return rule->command; + } + + for (int32_t i = 0; i < config.session_launch_rules_count; ++i) { + ConfigSessionLaunchRule *rule = &config.session_launch_rules[i]; + if (strcmp(rule->app_id, app_id) == 0 && rule->title[0] == '\0') + return rule->command; + } + + return ""; +} + +void mango_session_remember_client_launch_command(Client *c, const char *command) { + if (!c) + return; + + memset(c->session_launch_command, 0, sizeof(c->session_launch_command)); + if (!command || command[0] == '\0') + return; + + strncpy(c->session_launch_command, command, + sizeof(c->session_launch_command) - 1); + c->session_launch_command[sizeof(c->session_launch_command) - 1] = '\0'; +} + +void mango_session_spawn_command(const char *command) { + if (!command || command[0] == '\0') + return; + + if (fork() == 0) { + signal(SIGSEGV, SIG_IGN); + signal(SIGABRT, SIG_IGN); + signal(SIGILL, SIG_IGN); + + dup2(STDERR_FILENO, STDOUT_FILENO); + setsid(); + + execlp("sh", "sh", "-c", command, (char *)NULL); + execlp("bash", "bash", "-c", command, (char *)NULL); + _exit(EXIT_FAILURE); + } +} + +static bool session_client_should_save(Client *c) { + const char *appid; + + if (!c || c->iskilling || !c->mon || c->tags == 0) + return false; + if (client_is_unmanaged(c) || client_is_x11_popup(c)) + return false; + if (client_get_parent(c) != NULL) + return false; + if (c->is_in_scratchpad || c->is_scratchpad_show || c->isnamedscratchpad) + return false; + if (c->swallowing || c->swallowedby) + return false; + + appid = client_get_appid(c); + return appid && appid[0] != '\0' && strcmp(appid, "broken") != 0; +} + +int32_t mango_session_is_config_enabled(void) { return config.session_restore; } + +static const char *session_client_launch_command(Client *c) { + const char *appid; + const char *title; + const char *mapped_command; + + if (c && c->session_launch_command[0] != '\0') + return c->session_launch_command; + + appid = client_get_appid(c); + + if (!appid || appid[0] == '\0' || strcmp(appid, "broken") == 0) + return ""; + + title = client_get_title(c); + mapped_command = mango_session_lookup_launch_command(appid, title); + if (mapped_command && mapped_command[0] != '\0') + return mapped_command; + + /* First relaunch pass: use app_id as the best-effort launch command. + * Explicit config mappings will override this for wrappers and PWAs. */ + return appid; +} + +int32_t mango_session_write_snapshot(FILE *out) { + Client *c; + int32_t count = 0; + + fputs("[\n", out); + wl_list_for_each(c, &clients, link) { + const char *monitor_name; + const char *appid; + const char *launch_command; + const char *title; + const char *separator; + + if (!session_client_should_save(c)) + continue; + + monitor_name = + (c->mon && c->mon->wlr_output) ? c->mon->wlr_output->name : ""; + appid = client_get_appid(c); + launch_command = session_client_launch_command(c); + title = client_get_title(c); + separator = count == 0 ? "" : ",\n"; + + fputs(separator, out); + fputs(" {\n", out); + fputs(" \"app_id\": ", out); + session_write_json_string(out, appid); + fputs(",\n \"title\": ", out); + session_write_json_string(out, title); + fprintf(out, + ",\n \"pid\": %d,\n \"monitor\": ", + client_get_pid(c)); + session_write_json_string(out, monitor_name); + fputs(",\n \"launch_command\": ", out); + session_write_json_string(out, launch_command); + fprintf(out, + ",\n \"tags\": %u,\n \"is_floating\": %d,\n " + "\"is_fullscreen\": %d,\n \"is_minimized\": %d,\n " + "\"geom\": {\"x\": %d, \"y\": %d, \"width\": %d, \"height\": %d}," + "\n \"float_geom\": {\"x\": %d, \"y\": %d, \"width\": %d, " + "\"height\": %d}\n }", + c->tags, c->isfloating, c->isfullscreen, c->isminimized, c->geom.x, + c->geom.y, c->geom.width, c->geom.height, c->float_geom.x, + c->float_geom.y, c->float_geom.width, c->float_geom.height); + count++; + } + fputs("\n]\n", out); + + return count; +} + +void mango_session_apply_restore_entry(Client *c, + const SessionRestoreEntry *entry) { + Monitor *target = NULL, *m = NULL; + uint32_t tags; + + if (!c || !entry || !c->mon) + return; + + tags = entry->tags; + if (tags == 0) + tags = c->tags ? c->tags : c->mon->tagset[c->mon->seltags]; + + if (entry->monitor[0] != '\0') { + wl_list_for_each(m, &mons, link) { + if (!m->wlr_output->enabled) + continue; + if (strcmp(m->wlr_output->name, entry->monitor) == 0) { + target = m; + break; + } + } + } + + if (target && target != c->mon) { + setmon(c, target, tags, false); + } else { + c->tags = tags; + } + + arrange(c->mon, false, false); + + if (entry->is_floating) { + c->float_geom = (struct wlr_box){ + .x = entry->float_geom.x, + .y = entry->float_geom.y, + .width = entry->float_geom.width, + .height = entry->float_geom.height, + }; + if (c->float_geom.width <= 0 || c->float_geom.height <= 0) { + c->float_geom = (struct wlr_box){ + .x = entry->geom.x, + .y = entry->geom.y, + .width = entry->geom.width, + .height = entry->geom.height, + }; + } + c->geom = c->float_geom; + setfloating(c, 1); + resize(c, c->geom, 0); + } else if (c->isfloating) { + setfloating(c, 0); + } + + if (entry->is_fullscreen && !c->isfullscreen) { + setfullscreen(c, 1); + } else if (!entry->is_fullscreen && c->isfullscreen) { + setfullscreen(c, 0); + } + + /* Skip minimized restore for now to avoid introducing new focus changes. */ + client_update_oldmonname_record(c, c->mon); + printstatus(); +} + void client_change_mon(Client *c, Monitor *m) { setmon(c, m, c->tags, true); if (c->isfloating) { @@ -2286,6 +2539,8 @@ void cleanuplisteners(void) { } void cleanup(void) { + session_save_now(true); + session_shutdown(); cleanuplisteners(); #ifdef XWAYLAND wlr_xwayland_destroy(xwayland); @@ -4230,6 +4485,7 @@ mapnotify(struct wl_listener *listener, void *data) { // make sure the animation is open type c->is_pending_open_animation = true; resize(c, c->geom, 0); + session_handle_client_mapped(c); printstatus(); } @@ -4999,6 +5255,7 @@ run(char *startup_cmd) { run_exec(); run_exec_once(); + session_maybe_restore_startup(); /* Run the Wayland event loop. This does not return until you exit the * compositor. Starting the backend rigged up all of the necessary event @@ -5515,6 +5772,7 @@ void setup(void) { setenv("_JAVA_AWT_WM_NONREPARENTING", "1", 1); parse_config(); + session_init(); if (cli_debug_log) { config.log_level = WLR_DEBUG; } diff --git a/src/session/session.c b/src/session/session.c new file mode 100644 index 00000000..596349f2 --- /dev/null +++ b/src/session/session.c @@ -0,0 +1,489 @@ +#include "session.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../common/util.h" + +extern int32_t mango_session_is_config_enabled(void); +extern int32_t mango_session_write_snapshot(FILE *out); +extern const char *mango_session_client_appid(Client *c); +extern const char *mango_session_client_title(Client *c); +extern const char *mango_session_client_monitor(Client *c); +extern const char *mango_session_lookup_launch_command(const char *app_id, + const char *title); +extern void mango_session_remember_client_launch_command(Client *c, + const char *command); +extern void mango_session_spawn_command(const char *command); +extern void mango_session_apply_restore_entry(Client *c, + const SessionRestoreEntry *entry); + +typedef struct { + SessionRestoreEntry entry; + bool used; +} PendingSessionEntry; + +static PendingSessionEntry *pending_entries; +static size_t pending_count; +static bool restore_started; + +static bool mkdir_p(const char *dir) { + char tmp[PATH_MAX]; + size_t len; + + if (!dir || dir[0] == '\0') + return false; + + len = strlen(dir); + if (len >= sizeof(tmp)) + return false; + + memcpy(tmp, dir, len + 1); + + for (char *p = tmp + 1; *p != '\0'; ++p) { + if (*p != '/') + continue; + *p = '\0'; + if (mkdir(tmp, 0755) < 0 && errno != EEXIST) + return false; + *p = '/'; + } + + return mkdir(tmp, 0755) == 0 || errno == EEXIST; +} + +static char *session_data_dir(void) { + const char *xdg_data = getenv("XDG_DATA_HOME"); + if (xdg_data && xdg_data[0] != '\0') + return string_printf("%s/mango", xdg_data); + + const char *home = getenv("HOME"); + if (!home || home[0] == '\0') + return NULL; + + return string_printf("%s/.local/share/mango", home); +} + +static char *session_file_path(void) { + char *dir = session_data_dir(); + char *path; + if (!dir) + return NULL; + path = string_printf("%s/session.json", dir); + free(dir); + return path; +} + +static void free_pending_entries(void) { + free(pending_entries); + pending_entries = NULL; + pending_count = 0; +} + +static const char *skip_ws(const char *p) { + while (p && (*p == ' ' || *p == '\n' || *p == '\r' || *p == '\t')) + ++p; + return p; +} + +static const char *find_matching_brace(const char *start) { + int depth = 0; + bool in_string = false; + bool escaped = false; + + for (const char *p = start; p && *p != '\0'; ++p) { + if (in_string) { + if (escaped) { + escaped = false; + } else if (*p == '\\') { + escaped = true; + } else if (*p == '"') { + in_string = false; + } + continue; + } + + if (*p == '"') { + in_string = true; + } else if (*p == '{') { + depth++; + } else if (*p == '}') { + depth--; + if (depth == 0) + return p; + } + } + + return NULL; +} + +static bool parse_json_string_value(const char *value, char *dest, size_t dest_size) { + size_t i = 0; + const char *p = skip_ws(value); + + if (!p || *p != '"' || dest_size == 0) + return false; + + ++p; + while (*p != '\0' && *p != '"' && i + 1 < dest_size) { + if (*p == '\\') { + ++p; + if (*p == '\0') + break; + switch (*p) { + case 'n': + dest[i++] = '\n'; + break; + case 'r': + dest[i++] = '\r'; + break; + case 't': + dest[i++] = '\t'; + break; + case '\\': + case '"': + case '/': + dest[i++] = *p; + break; + default: + dest[i++] = *p; + break; + } + ++p; + continue; + } + dest[i++] = *p++; + } + + dest[i] = '\0'; + return true; +} + +static bool extract_json_string(const char *obj, const char *key, char *dest, + size_t dest_size) { + const char *p = strstr(obj, key); + if (!p) + return false; + p = strchr(p, ':'); + if (!p) + return false; + return parse_json_string_value(p + 1, dest, dest_size); +} + +static bool extract_json_int(const char *obj, const char *key, int32_t *out) { + char *end = NULL; + const char *p = strstr(obj, key); + long value; + + if (!p) + return false; + p = strchr(p, ':'); + if (!p) + return false; + p = skip_ws(p + 1); + if (!p) + return false; + + value = strtol(p, &end, 10); + if (end == p) + return false; + + *out = (int32_t)value; + return true; +} + +static bool extract_json_object_range(const char *obj, const char *key, + const char **start, const char **end) { + const char *p = strstr(obj, key); + if (!p) + return false; + p = strchr(p, ':'); + if (!p) + return false; + p = skip_ws(p + 1); + if (!p || *p != '{') + return false; + *start = p; + *end = find_matching_brace(p); + return *end != NULL; +} + +static bool parse_geom_object(const char *obj, const char *key, SessionRect *geom) { + const char *start = NULL, *end = NULL; + char *sub = NULL; + bool ok; + size_t len; + + if (!extract_json_object_range(obj, key, &start, &end)) + return false; + + len = (size_t)(end - start + 1); + sub = ecalloc(len + 1, 1); + memcpy(sub, start, len); + + ok = extract_json_int(sub, "\"x\"", &geom->x) && + extract_json_int(sub, "\"y\"", &geom->y) && + extract_json_int(sub, "\"width\"", &geom->width) && + extract_json_int(sub, "\"height\"", &geom->height); + + free(sub); + return ok; +} + +static bool parse_session_entry(const char *obj, SessionRestoreEntry *entry) { + int32_t tags = 0; + + memset(entry, 0, sizeof(*entry)); + + if (!extract_json_string(obj, "\"app_id\"", entry->app_id, + sizeof(entry->app_id))) + return false; + + extract_json_string(obj, "\"title\"", entry->title, sizeof(entry->title)); + extract_json_string(obj, "\"monitor\"", entry->monitor, + sizeof(entry->monitor)); + extract_json_string(obj, "\"launch_command\"", entry->launch_command, + sizeof(entry->launch_command)); + extract_json_int(obj, "\"pid\"", &entry->pid); + extract_json_int(obj, "\"tags\"", &tags); + entry->tags = (uint32_t)tags; + extract_json_int(obj, "\"is_floating\"", &entry->is_floating); + extract_json_int(obj, "\"is_fullscreen\"", &entry->is_fullscreen); + extract_json_int(obj, "\"is_minimized\"", &entry->is_minimized); + parse_geom_object(obj, "\"geom\"", &entry->geom); + parse_geom_object(obj, "\"float_geom\"", &entry->float_geom); + + return true; +} + +static bool load_pending_entries(void) { + char *path = session_file_path(); + char *contents = NULL; + FILE *in = NULL; + const char *cursor; + bool loaded = false; + + free_pending_entries(); + if (!path) + return false; + + in = fopen(path, "r"); + if (!in) + goto cleanup; + + if (fseek(in, 0, SEEK_END) != 0) + goto cleanup; + long size = ftell(in); + if (size < 0 || fseek(in, 0, SEEK_SET) != 0) + goto cleanup; + + contents = ecalloc((size_t)size + 1, 1); + if (fread(contents, 1, (size_t)size, in) != (size_t)size) + goto cleanup; + contents[size] = '\0'; + + cursor = contents; + while ((cursor = strchr(cursor, '{')) != NULL) { + const char *end = find_matching_brace(cursor); + SessionRestoreEntry entry; + char *obj; + size_t len; + + if (!end) + break; + + len = (size_t)(end - cursor + 1); + obj = ecalloc(len + 1, 1); + memcpy(obj, cursor, len); + + if (parse_session_entry(obj, &entry)) { + PendingSessionEntry *new_entries = realloc( + pending_entries, sizeof(*pending_entries) * (pending_count + 1)); + if (!new_entries) { + free(obj); + goto cleanup; + } + pending_entries = new_entries; + pending_entries[pending_count].entry = entry; + pending_entries[pending_count].used = false; + pending_count++; + loaded = true; + } + + free(obj); + cursor = end + 1; + } + +cleanup: + if (in) + fclose(in); + free(contents); + free(path); + return loaded; +} + +static PendingSessionEntry *find_pending_match(const char *appid, const char *title, + const char *monitor) { + PendingSessionEntry *fallback = NULL; + PendingSessionEntry *monitor_match = NULL; + + for (size_t i = 0; i < pending_count; ++i) { + PendingSessionEntry *candidate = &pending_entries[i]; + if (candidate->used) + continue; + if (strcmp(candidate->entry.app_id, appid) != 0) + continue; + + if (title && title[0] != '\0' && + strcmp(candidate->entry.title, title) == 0) { + if (monitor && monitor[0] != '\0' && + strcmp(candidate->entry.monitor, monitor) == 0) + return candidate; + if (!fallback) + fallback = candidate; + } + + if (!monitor_match && monitor && monitor[0] != '\0' && + strcmp(candidate->entry.monitor, monitor) == 0) { + monitor_match = candidate; + } + + if (!fallback) + fallback = candidate; + } + + return monitor_match ? monitor_match : fallback; +} + +static bool session_resolve_launch_command(SessionRestoreEntry *entry) { + const char *mapped_command; + + if (!entry || entry->app_id[0] == '\0') + return false; + + /* Explicit user mapping wins over best-effort persisted launch data. */ + mapped_command = mango_session_lookup_launch_command(entry->app_id, + entry->title); + if (mapped_command && mapped_command[0] != '\0') { + strncpy(entry->launch_command, mapped_command, + sizeof(entry->launch_command) - 1); + entry->launch_command[sizeof(entry->launch_command) - 1] = '\0'; + return true; + } + + return entry->launch_command[0] != '\0'; +} + +static void session_spawn_restore_entries(void) { + for (size_t i = 0; i < pending_count; ++i) { + SessionRestoreEntry *entry = &pending_entries[i].entry; + + if (!session_resolve_launch_command(entry)) + continue; + + mango_session_spawn_command(entry->launch_command); + } +} + +void session_init(void) {} + +void session_shutdown(void) { + free_pending_entries(); + restore_started = false; +} + +void session_maybe_restore_startup(void) { + if (!session_is_enabled() || restore_started) + return; + + restore_started = true; + if (!load_pending_entries()) + return; + + session_spawn_restore_entries(); +} + +void session_handle_client_mapped(Client *c) { + PendingSessionEntry *match; + const char *appid; + const char *title; + const char *monitor; + + if (!restore_started || pending_count == 0 || !c) + return; + + appid = mango_session_client_appid(c); + if (!appid || appid[0] == '\0' || strcmp(appid, "broken") == 0) + return; + + title = mango_session_client_title(c); + monitor = mango_session_client_monitor(c); + match = find_pending_match(appid, title, monitor); + if (!match) + return; + + mango_session_remember_client_launch_command(c, match->entry.launch_command); + mango_session_apply_restore_entry(c, &match->entry); + match->used = true; +} + +void session_handle_client_destroyed(Client *c) { + (void)c; +} + +void session_save_now(bool is_final_shutdown) { + FILE *out = NULL; + char *dir = NULL, *tmp_path = NULL, *final_path = NULL; + int32_t count = 0; + + if (!session_is_enabled()) + return; + + dir = session_data_dir(); + if (!dir || !mkdir_p(dir)) + goto cleanup; + + tmp_path = string_printf("%s/session.json.tmp", dir); + final_path = string_printf("%s/session.json", dir); + if (!tmp_path || !final_path) + goto cleanup; + + out = fopen(tmp_path, "w"); + if (!out) + goto cleanup; + + count = mango_session_write_snapshot(out); + if (fclose(out) != 0) { + out = NULL; + goto cleanup; + } + out = NULL; + + if (count <= 0 && is_final_shutdown) { + unlink(tmp_path); + goto cleanup; + } + + if (rename(tmp_path, final_path) != 0) + unlink(tmp_path); + +cleanup: + if (out) + fclose(out); + free(dir); + free(tmp_path); + free(final_path); +} + +bool session_is_enabled(void) { return mango_session_is_config_enabled() != 0; } + +bool session_is_restorable_client(Client *c) { + (void)c; + return false; +} diff --git a/src/session/session.h b/src/session/session.h new file mode 100644 index 00000000..7306cad9 --- /dev/null +++ b/src/session/session.h @@ -0,0 +1,39 @@ +#ifndef SESSION_H +#define SESSION_H + +#include +#include + +typedef struct Client Client; + +typedef struct { + int32_t x; + int32_t y; + int32_t width; + int32_t height; +} SessionRect; + +typedef struct { + char app_id[256]; + char title[512]; + char monitor[128]; + char launch_command[1024]; + int32_t pid; + uint32_t tags; + int32_t is_floating; + int32_t is_fullscreen; + int32_t is_minimized; + SessionRect geom; + SessionRect float_geom; +} SessionRestoreEntry; + +void session_init(void); +void session_shutdown(void); +void session_maybe_restore_startup(void); +void session_handle_client_mapped(Client *c); +void session_handle_client_destroyed(Client *c); +void session_save_now(bool is_final_shutdown); +bool session_is_enabled(void); +bool session_is_restorable_client(Client *c); + +#endif From 818f80c0688b638870f9d21669335aeda0a31984 Mon Sep 17 00:00:00 2001 From: cheerfulScumbag <164391367+cheerfulScumbag@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:52:23 +0000 Subject: [PATCH 2/4] Improve automatic session launch command recovery# --- src/dispatch/bind_define.h | 13 +- src/mango.c | 450 ++++++++++++++++++++++++++++++++++++- src/session/session.c | 5 +- 3 files changed, 464 insertions(+), 4 deletions(-) diff --git a/src/dispatch/bind_define.h b/src/dispatch/bind_define.h index f5992e29..05e8f23b 100644 --- a/src/dispatch/bind_define.h +++ b/src/dispatch/bind_define.h @@ -836,10 +836,13 @@ int32_t centerwin(const Arg *arg) { } int32_t spawn_shell(const Arg *arg) { + pid_t pid; + if (!arg->v) return 0; - if (fork() == 0) { + pid = fork(); + if (pid == 0) { // 1. 忽略可能导致 coredump 的信号 signal(SIGSEGV, SIG_IGN); signal(SIGABRT, SIG_IGN); @@ -859,14 +862,18 @@ int32_t spawn_shell(const Arg *arg) { arg->v, strerror(errno)); _exit(EXIT_FAILURE); } + if (pid > 0) + mango_session_track_spawned_command(pid, arg->v); return 0; } int32_t spawn(const Arg *arg) { + pid_t pid; if (!arg->v) return 0; - if (fork() == 0) { + pid = fork(); + if (pid == 0) { // 1. 忽略可能导致 coredump 的信号 signal(SIGSEGV, SIG_IGN); signal(SIGABRT, SIG_IGN); @@ -891,6 +898,8 @@ int32_t spawn(const Arg *arg) { wordfree(&p); // 释放 wordexp 分配的内存 _exit(EXIT_FAILURE); } + if (pid > 0) + mango_session_track_spawned_command(pid, arg->v); return 0; } diff --git a/src/mango.c b/src/mango.c index 53915ca7..9caaecf8 100644 --- a/src/mango.c +++ b/src/mango.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -556,6 +557,13 @@ typedef struct { struct wl_listener destroy; } SessionLock; +typedef struct { + struct wl_list link; + pid_t pid; + time_t created_at; + char command[1024]; +} SessionSpawnCommand; + /* function declarations */ static void applybounds( Client *c, @@ -758,6 +766,16 @@ static void init_fadeout_layers(LayerSurface *l); static void layer_actual_size(LayerSurface *l, int32_t *width, int32_t *height); static void get_layer_target_geometry(LayerSurface *l, struct wlr_box *target_box); +void mango_session_spawn_tracker_init(void); +void mango_session_spawn_tracker_shutdown(void); +void mango_session_track_spawned_command(pid_t pid, const char *command); +void mango_session_attach_spawn_command(Client *c); +void mango_session_remember_client_launch_command(Client *c, + const char *command); +static char *mango_session_recover_process_command(pid_t pid); +static void mango_session_attach_process_command(Client *c); +static char *mango_session_find_desktop_exec(const char *app_id); +static char *mango_session_normalize_launch_command(Client *c, pid_t pid); static void scene_buffer_apply_effect(struct wlr_scene_buffer *buffer, int32_t sx, int32_t sy, void *data); static double find_animation_curve_at(double t, int32_t type); @@ -987,6 +1005,7 @@ static struct wl_listener keyboard_shortcuts_inhibit_new_inhibitor = { .notify = handle_keyboard_shortcuts_inhibit_new_inhibitor}; static struct wl_listener last_cursor_surface_destroy_listener = { .notify = last_cursor_surface_destroy}; +static struct wl_list session_spawn_commands; #ifdef XWAYLAND static void fix_xwayland_unmanaged_coordinate(Client *c); @@ -1056,6 +1075,428 @@ const char *mango_session_client_monitor(Client *c) { return (c && c->mon && c->mon->wlr_output) ? c->mon->wlr_output->name : ""; } +static void mango_session_spawn_tracker_cleanup(bool all) { + SessionSpawnCommand *entry, *tmp; + time_t now = time(NULL); + + wl_list_for_each_safe(entry, tmp, &session_spawn_commands, link) { + if (!all && now - entry->created_at < 300) + continue; + wl_list_remove(&entry->link); + free(entry); + } +} + +void mango_session_spawn_tracker_init(void) { + wl_list_init(&session_spawn_commands); +} + +void mango_session_spawn_tracker_shutdown(void) { + mango_session_spawn_tracker_cleanup(true); +} + +void mango_session_track_spawned_command(pid_t pid, const char *command) { + SessionSpawnCommand *entry; + + if (pid <= 0 || !command || command[0] == '\0') + return; + + mango_session_spawn_tracker_cleanup(false); + + entry = ecalloc(1, sizeof(*entry)); + entry->pid = pid; + entry->created_at = time(NULL); + strncpy(entry->command, command, sizeof(entry->command) - 1); + entry->command[sizeof(entry->command) - 1] = '\0'; + wl_list_insert(&session_spawn_commands, &entry->link); +} + +void mango_session_attach_spawn_command(Client *c) { + SessionSpawnCommand *entry, *match = NULL; + pid_t pid; + + if (!c) + return; + + pid = client_get_pid(c); + if (pid <= 0) + return; + + c->pid = pid; + mango_session_spawn_tracker_cleanup(false); + + wl_list_for_each(entry, &session_spawn_commands, link) { + if (entry->pid == pid || isdescprocess(entry->pid, pid)) { + match = entry; + break; + } + } + + if (!match) + return; + + mango_session_remember_client_launch_command(c, match->command); + wl_list_remove(&match->link); + free(match); +} + +static char *mango_session_shell_quote(const char *arg) { + size_t len = 2; + const char *p; + char *out, *dst; + + if (!arg) + return strdup("''"); + + for (p = arg; *p != '\0'; ++p) { + if (*p == '\'') + len += 4; + else + len += 1; + } + + out = ecalloc(len + 1, sizeof(char)); + dst = out; + *dst++ = '\''; + for (p = arg; *p != '\0'; ++p) { + if (*p == '\'') { + memcpy(dst, "'\\''", 4); + dst += 4; + } else { + *dst++ = *p; + } + } + *dst++ = '\''; + *dst = '\0'; + return out; +} + +static char *mango_session_join_argv(char **argv, size_t argc) { + char *joined; + + if (!argv || argc == 0) + return NULL; + + joined = strdup(""); + for (size_t i = 0; i < argc; ++i) { + char *quoted = mango_session_shell_quote(argv[i]); + char *next = i == 0 ? string_printf("%s", quoted) + : string_printf("%s %s", joined, quoted); + free(quoted); + free(joined); + joined = next; + if (!joined) + return NULL; + } + + return joined; +} + +static void mango_session_trim_newline(char *s) { + size_t len; + + if (!s) + return; + + len = strlen(s); + while (len > 0 && + (s[len - 1] == '\n' || s[len - 1] == '\r' || s[len - 1] == ' ' || + s[len - 1] == '\t')) { + s[--len] = '\0'; + } +} + +static char *mango_session_strip_exec_field_codes(const char *exec) { + char *copy, *token, *saveptr = NULL, *result; + + if (!exec || exec[0] == '\0') + return NULL; + + copy = strdup(exec); + if (!copy) + return NULL; + + result = strdup(""); + for (token = strtok_r(copy, " \t", &saveptr); token != NULL; + token = strtok_r(NULL, " \t", &saveptr)) { + char *next; + + if (token[0] == '%' && token[1] != '\0') + continue; + if (strcmp(token, "%%") == 0) + token = "%"; + + next = result[0] == '\0' ? string_printf("%s", token) + : string_printf("%s %s", result, token); + free(result); + result = next; + if (!result) { + free(copy); + return NULL; + } + } + + free(copy); + + if (result[0] == '\0') { + free(result); + return NULL; + } + + return result; +} + +static bool mango_session_path_exists(const char *path) { + struct stat st; + return path && stat(path, &st) == 0; +} + +static bool mango_session_desktop_matches(const char *desktop_path, + const char *app_id, + char **exec_out) { + FILE *f; + char line[2048]; + char *exec_value = NULL; + char *startup_wm_class = NULL; + char *flatpak_id = NULL; + char *desktop_id = NULL; + bool matched = false; + const char *basename; + char *dot; + + if (!desktop_path || !app_id || app_id[0] == '\0') + return false; + + f = fopen(desktop_path, "r"); + if (!f) + return false; + + while (fgets(line, sizeof(line), f)) { + mango_session_trim_newline(line); + if (strncmp(line, "Exec=", 5) == 0) { + free(exec_value); + exec_value = strdup(line + 5); + } else if (strncmp(line, "StartupWMClass=", 15) == 0) { + free(startup_wm_class); + startup_wm_class = strdup(line + 15); + } else if (strncmp(line, "X-Flatpak=", 10) == 0) { + free(flatpak_id); + flatpak_id = strdup(line + 10); + } + } + fclose(f); + + basename = strrchr(desktop_path, '/'); + basename = basename ? basename + 1 : desktop_path; + desktop_id = strdup(basename); + if (desktop_id) { + dot = strrchr(desktop_id, '.'); + if (dot) + *dot = '\0'; + if (strcmp(desktop_id, app_id) == 0) + matched = true; + free(desktop_id); + } + + if (!matched && startup_wm_class && strcmp(startup_wm_class, app_id) == 0) + matched = true; + if (!matched && flatpak_id && strcmp(flatpak_id, app_id) == 0) + matched = true; + + if (matched && exec_value && exec_out) + *exec_out = mango_session_strip_exec_field_codes(exec_value); + + free(exec_value); + free(startup_wm_class); + free(flatpak_id); + return matched; +} + +static char *mango_session_find_desktop_exec(const char *app_id) { + const char *dirs[] = { + "/home/kenn/.local/share/applications", + "/etc/profiles/per-user/kenn/share/applications", + "/run/current-system/sw/share/applications", + "/var/lib/flatpak/exports/share/applications", + NULL, + }; + + for (size_t i = 0; dirs[i] != NULL; ++i) { + DIR *dir = opendir(dirs[i]); + struct dirent *entry; + + if (!dir) + continue; + + while ((entry = readdir(dir)) != NULL) { + char *path; + char *exec_value = NULL; + bool matched; + + if (!strstr(entry->d_name, ".desktop")) + continue; + + path = string_printf("%s/%s", dirs[i], entry->d_name); + matched = mango_session_desktop_matches(path, app_id, &exec_value); + free(path); + if (!matched) + continue; + + closedir(dir); + return exec_value; + } + + closedir(dir); + } + + return NULL; +} + +static char *mango_session_read_cmdline(pid_t pid, size_t *argc_out) { + FILE *f; + char path[64]; + char *buf = NULL; + size_t size = 0, used = 0; + int ch; + + if (argc_out) + *argc_out = 0; + if (pid <= 0) + return NULL; + + snprintf(path, sizeof(path), "/proc/%d/cmdline", pid); + f = fopen(path, "rb"); + if (!f) + return NULL; + + while ((ch = fgetc(f)) != EOF) { + if (used + 2 > size) { + size_t next_size = size == 0 ? 256 : size * 2; + char *next = realloc(buf, next_size); + if (!next) { + free(buf); + fclose(f); + return NULL; + } + buf = next; + size = next_size; + } + buf[used++] = (char)ch; + } + fclose(f); + + if (!buf || used == 0) { + free(buf); + return NULL; + } + + buf[used] = '\0'; + + if (argc_out) { + size_t argc = 0; + for (size_t i = 0; i < used; ++i) { + if (buf[i] == '\0') + argc++; + } + *argc_out = argc; + } + + return buf; +} + +static char *mango_session_recover_process_command(pid_t pid) { + char *cmdline; + char *argv[64]; + size_t argc = 0, idx = 0; + char *p; + + cmdline = mango_session_read_cmdline(pid, &argc); + if (!cmdline || argc == 0) + return NULL; + + for (p = cmdline; idx < argc && idx < LENGTH(argv); ++idx) { + argv[idx] = p; + p += strlen(p) + 1; + } + argc = idx; + + if (argc >= 3 && + (strcmp(argv[0], "sh") == 0 || strcmp(argv[0], "/bin/sh") == 0 || + strcmp(argv[0], "bash") == 0 || strcmp(argv[0], "/bin/bash") == 0) && + strcmp(argv[1], "-c") == 0) { + char *command = strdup(argv[2]); + free(cmdline); + return command; + } + + if (argc >= 1) { + char *command = mango_session_join_argv(argv, argc); + free(cmdline); + return command; + } + + free(cmdline); + return NULL; +} + +static char *mango_session_normalize_launch_command(Client *c, pid_t pid) { + const char *app_id; + char *desktop_exec; + char *raw_command; + + app_id = c ? client_get_appid(c) : NULL; + if (app_id && app_id[0] != '\0' && strcmp(app_id, "broken") != 0) { + char *flatpak_desktop = string_printf( + "/var/lib/flatpak/exports/share/applications/%s.desktop", app_id); + char *user_flatpak_desktop = string_printf( + "%s/.local/share/flatpak/exports/share/applications/%s.desktop", + getenv("HOME") ? getenv("HOME") : "", app_id); + + if ((flatpak_desktop && mango_session_path_exists(flatpak_desktop)) || + (user_flatpak_desktop && + mango_session_path_exists(user_flatpak_desktop))) { + free(flatpak_desktop); + free(user_flatpak_desktop); + return string_printf("flatpak run %s", app_id); + } + free(flatpak_desktop); + free(user_flatpak_desktop); + + desktop_exec = mango_session_find_desktop_exec(app_id); + if (desktop_exec && desktop_exec[0] != '\0') + return desktop_exec; + free(desktop_exec); + } + + raw_command = mango_session_recover_process_command(pid); + if (raw_command && raw_command[0] != '\0') + return raw_command; + + free(raw_command); + return NULL; +} + +static void mango_session_attach_process_command(Client *c) { + char *command; + pid_t pid; + + if (!c || c->session_launch_command[0] != '\0') + return; + + pid = c->pid > 0 ? c->pid : client_get_pid(c); + if (pid <= 0) + return; + + command = mango_session_normalize_launch_command(c, pid); + if (!command || command[0] == '\0') { + free(command); + return; + } + + mango_session_remember_client_launch_command(c, command); + free(command); +} + const char *mango_session_lookup_launch_command(const char *app_id, const char *title) { if (!app_id || app_id[0] == '\0') @@ -1094,10 +1535,13 @@ void mango_session_remember_client_launch_command(Client *c, const char *command } void mango_session_spawn_command(const char *command) { + pid_t pid; + if (!command || command[0] == '\0') return; - if (fork() == 0) { + pid = fork(); + if (pid == 0) { signal(SIGSEGV, SIG_IGN); signal(SIGABRT, SIG_IGN); signal(SIGILL, SIG_IGN); @@ -1109,6 +1553,8 @@ void mango_session_spawn_command(const char *command) { execlp("bash", "bash", "-c", command, (char *)NULL); _exit(EXIT_FAILURE); } + if (pid > 0) + mango_session_track_spawned_command(pid, command); } static bool session_client_should_save(Client *c) { @@ -4485,6 +4931,8 @@ mapnotify(struct wl_listener *listener, void *data) { // make sure the animation is open type c->is_pending_open_animation = true; resize(c, c->geom, 0); + mango_session_attach_spawn_command(c); + mango_session_attach_process_command(c); session_handle_client_mapped(c); printstatus(); } diff --git a/src/session/session.c b/src/session/session.c index 596349f2..e75e07ec 100644 --- a/src/session/session.c +++ b/src/session/session.c @@ -23,6 +23,8 @@ extern void mango_session_remember_client_launch_command(Client *c, extern void mango_session_spawn_command(const char *command); extern void mango_session_apply_restore_entry(Client *c, const SessionRestoreEntry *entry); +extern void mango_session_spawn_tracker_init(void); +extern void mango_session_spawn_tracker_shutdown(void); typedef struct { SessionRestoreEntry entry; @@ -391,11 +393,12 @@ static void session_spawn_restore_entries(void) { } } -void session_init(void) {} +void session_init(void) { mango_session_spawn_tracker_init(); } void session_shutdown(void) { free_pending_entries(); restore_started = false; + mango_session_spawn_tracker_shutdown(); } void session_maybe_restore_startup(void) { From e2b63a06d73101be3de3f4d87d99ba92d23dfce7 Mon Sep 17 00:00:00 2001 From: cheerfulScumbag <164391367+cheerfulScumbag@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:09:13 +0000 Subject: [PATCH 3/4] Harden session launch command recovery --- assets/config.conf | 6 +- src/mango.c | 430 ++++++++++++++++++++++++++++++++++-------- src/session/session.c | 27 +++ 3 files changed, 380 insertions(+), 83 deletions(-) diff --git a/assets/config.conf b/assets/config.conf index 6101cbc2..3e415ed1 100644 --- a/assets/config.conf +++ b/assets/config.conf @@ -82,10 +82,14 @@ idleinhibit_ignore_visible=0 # Disabled by default to preserve Mango's current behavior. # Minimized restore is currently unsupported. # Restore expects target outputs to already exist before clients map. +# Mango only restores from a trusted session file owned by the current user +# and not writable by group or others. session_restore=0 -# Relaunch mapping used when Mango cannot infer a launch command from a client. +# Optional relaunch override used when Mango cannot recover a suitable launcher. # Format: session_launch=app_id|command # or: session_launch=app_id|title|command +# Mango prefers exact Mango-owned spawn commands, then normalized desktop/Flatpak +# launchers, then raw process argv before consulting session_launch. # session_launch=foot|foot # session_launch=foot|gamma|foot -a foot -T gamma -e sh -lc "sleep 600" # session_launch=org.kde.dolphin|dolphin . diff --git a/src/mango.c b/src/mango.c index 9caaecf8..f2ab2a4a 100644 --- a/src/mango.c +++ b/src/mango.c @@ -775,6 +775,7 @@ void mango_session_remember_client_launch_command(Client *c, static char *mango_session_recover_process_command(pid_t pid); static void mango_session_attach_process_command(Client *c); static char *mango_session_find_desktop_exec(const char *app_id); +static char *mango_session_find_flatpak_command(const char *app_id); static char *mango_session_normalize_launch_command(Client *c, pid_t pid); static void scene_buffer_apply_effect(struct wlr_scene_buffer *buffer, int32_t sx, int32_t sy, void *data); @@ -1207,42 +1208,112 @@ static void mango_session_trim_newline(char *s) { } static char *mango_session_strip_exec_field_codes(const char *exec) { - char *copy, *token, *saveptr = NULL, *result; + char **tokens = NULL; + char *current = NULL; + char *result = NULL; + size_t token_count = 0, token_capacity = 0; + size_t current_len = 0, current_capacity = 0; + bool in_single_quotes = false; + bool in_double_quotes = false; + const char *p; if (!exec || exec[0] == '\0') return NULL; - copy = strdup(exec); - if (!copy) - return NULL; + for (p = exec; *p != '\0'; ++p) { + char ch = *p; - result = strdup(""); - for (token = strtok_r(copy, " \t", &saveptr); token != NULL; - token = strtok_r(NULL, " \t", &saveptr)) { - char *next; + if (!in_single_quotes && !in_double_quotes && + (ch == ' ' || ch == '\t')) { + if (current_len > 0) { + char **next_tokens; - if (token[0] == '%' && token[1] != '\0') + if (token_count + 1 > token_capacity) { + size_t next_capacity = + token_capacity == 0 ? 8 : token_capacity * 2; + + next_tokens = + realloc(tokens, next_capacity * sizeof(*tokens)); + if (!next_tokens) + goto cleanup; + tokens = next_tokens; + token_capacity = next_capacity; + } + + current[current_len] = '\0'; + tokens[token_count++] = current; + current = NULL; + current_len = 0; + current_capacity = 0; + } continue; - if (strcmp(token, "%%") == 0) - token = "%"; - - next = result[0] == '\0' ? string_printf("%s", token) - : string_printf("%s %s", result, token); - free(result); - result = next; - if (!result) { - free(copy); - return NULL; } + + if (!in_double_quotes && ch == '\'') { + in_single_quotes = !in_single_quotes; + continue; + } + if (!in_single_quotes && ch == '"') { + in_double_quotes = !in_double_quotes; + continue; + } + + if (ch == '\\' && p[1] != '\0') { + ch = *++p; + } else if (ch == '%' && p[1] != '\0') { + if (p[1] == '%') { + ch = '%'; + ++p; + } else { + ++p; + continue; + } + } + + if (current_len + 2 > current_capacity) { + size_t next_capacity = current_capacity == 0 ? 32 : current_capacity * 2; + char *next_current = realloc(current, next_capacity); + + if (!next_current) + goto cleanup; + current = next_current; + current_capacity = next_capacity; + } + + current[current_len++] = ch; } - free(copy); + if (current_len > 0) { + char **next_tokens; - if (result[0] == '\0') { + if (token_count + 1 > token_capacity) { + size_t next_capacity = token_capacity == 0 ? 8 : token_capacity * 2; + + next_tokens = realloc(tokens, next_capacity * sizeof(*tokens)); + if (!next_tokens) + goto cleanup; + tokens = next_tokens; + } + + current[current_len] = '\0'; + tokens[token_count++] = current; + current = NULL; + } + + result = mango_session_join_argv(tokens, token_count); + +cleanup: + free(current); + if (tokens) { + for (size_t i = 0; i < token_count; ++i) + free(tokens[i]); + free(tokens); + } + + if (!result || result[0] == '\0') { free(result); return NULL; } - return result; } @@ -1251,43 +1322,211 @@ static bool mango_session_path_exists(const char *path) { return path && stat(path, &st) == 0; } -static bool mango_session_desktop_matches(const char *desktop_path, - const char *app_id, - char **exec_out) { +typedef struct { + char *path; + char *exec_value; + char *startup_wm_class; + char *flatpak_id; +} MangoSessionDesktopEntry; + +static void mango_session_free_desktop_entry(MangoSessionDesktopEntry *entry) { + if (!entry) + return; + + free(entry->path); + free(entry->exec_value); + free(entry->startup_wm_class); + free(entry->flatpak_id); + memset(entry, 0, sizeof(*entry)); +} + +static bool mango_session_append_search_dir(char ***dirs, size_t *count, + size_t *capacity, + const char *dir) { + char **next_dirs; + + if (!dirs || !count || !capacity || !dir || dir[0] == '\0') + return true; + + if (*count > 0 && strcmp((*dirs)[*count - 1], dir) == 0) + return true; + + if (*count + 1 >= *capacity) { + size_t next_capacity = *capacity == 0 ? 8 : *capacity * 2; + + next_dirs = realloc(*dirs, next_capacity * sizeof(**dirs)); + if (!next_dirs) + return false; + *dirs = next_dirs; + *capacity = next_capacity; + } + + (*dirs)[*count] = strdup(dir); + if (!(*dirs)[*count]) + return false; + (*count)++; + (*dirs)[*count] = NULL; + return true; +} + +static bool mango_session_append_data_home_applications(char ***dirs, size_t *count, + size_t *capacity) { + const char *xdg_data_home = getenv("XDG_DATA_HOME"); + const char *home = getenv("HOME"); + char *applications_dir = NULL; + bool ok; + + if (xdg_data_home && xdg_data_home[0] != '\0') + applications_dir = string_printf("%s/applications", xdg_data_home); + else if (home && home[0] != '\0') + applications_dir = string_printf("%s/.local/share/applications", home); + + if (!applications_dir) + return true; + + ok = mango_session_append_search_dir(dirs, count, capacity, applications_dir); + free(applications_dir); + return ok; +} + +static bool mango_session_append_data_dirs_applications(char ***dirs, size_t *count, + size_t *capacity) { + const char *xdg_data_dirs = getenv("XDG_DATA_DIRS"); + const char *cursor; + + if (!xdg_data_dirs || xdg_data_dirs[0] == '\0') + xdg_data_dirs = "/usr/local/share:/usr/share"; + + for (cursor = xdg_data_dirs; cursor && cursor[0] != '\0';) { + const char *separator = strchr(cursor, ':'); + size_t len = separator ? (size_t)(separator - cursor) : strlen(cursor); + char *base_dir; + char *applications_dir; + bool ok; + + if (len == 0) { + cursor = separator ? separator + 1 : NULL; + continue; + } + + base_dir = strndup(cursor, len); + if (!base_dir) + return false; + + applications_dir = string_printf("%s/applications", base_dir); + free(base_dir); + if (!applications_dir) + return false; + + ok = mango_session_append_search_dir(dirs, count, capacity, + applications_dir); + free(applications_dir); + if (!ok) + return false; + + cursor = separator ? separator + 1 : NULL; + } + + return true; +} + +static char **mango_session_desktop_search_dirs(void) { + char **dirs = NULL; + size_t count = 0, capacity = 0; + const char *home = getenv("HOME"); + char *user_flatpak_dir = NULL; + + if (!mango_session_append_data_home_applications(&dirs, &count, &capacity) || + !mango_session_append_data_dirs_applications(&dirs, &count, &capacity)) + goto fail; + + if (!mango_session_append_search_dir(&dirs, &count, &capacity, + "/var/lib/flatpak/exports/share/applications")) + goto fail; + + if (home && home[0] != '\0') { + user_flatpak_dir = string_printf( + "%s/.local/share/flatpak/exports/share/applications", home); + if (!user_flatpak_dir) + goto fail; + if (!mango_session_append_search_dir(&dirs, &count, &capacity, + user_flatpak_dir)) + goto fail; + } + + free(user_flatpak_dir); + return dirs; + +fail: + free(user_flatpak_dir); + if (dirs) { + for (size_t i = 0; i < count; ++i) + free(dirs[i]); + free(dirs); + } + return NULL; +} + +static void mango_session_free_search_dirs(char **dirs) { + size_t i; + + if (!dirs) + return; + + for (i = 0; dirs[i] != NULL; ++i) + free(dirs[i]); + free(dirs); +} + +static bool mango_session_load_desktop_entry(const char *desktop_path, + MangoSessionDesktopEntry *entry) { FILE *f; char line[2048]; - char *exec_value = NULL; - char *startup_wm_class = NULL; - char *flatpak_id = NULL; - char *desktop_id = NULL; - bool matched = false; - const char *basename; - char *dot; - if (!desktop_path || !app_id || app_id[0] == '\0') + if (!desktop_path || !entry) + return false; + + memset(entry, 0, sizeof(*entry)); + entry->path = strdup(desktop_path); + if (!entry->path) return false; f = fopen(desktop_path, "r"); - if (!f) + if (!f) { + mango_session_free_desktop_entry(entry); return false; + } while (fgets(line, sizeof(line), f)) { mango_session_trim_newline(line); if (strncmp(line, "Exec=", 5) == 0) { - free(exec_value); - exec_value = strdup(line + 5); + free(entry->exec_value); + entry->exec_value = strdup(line + 5); } else if (strncmp(line, "StartupWMClass=", 15) == 0) { - free(startup_wm_class); - startup_wm_class = strdup(line + 15); + free(entry->startup_wm_class); + entry->startup_wm_class = strdup(line + 15); } else if (strncmp(line, "X-Flatpak=", 10) == 0) { - free(flatpak_id); - flatpak_id = strdup(line + 10); + free(entry->flatpak_id); + entry->flatpak_id = strdup(line + 10); } } fclose(f); - basename = strrchr(desktop_path, '/'); - basename = basename ? basename + 1 : desktop_path; + return true; +} + +static bool mango_session_desktop_entry_matches( + const MangoSessionDesktopEntry *entry, const char *app_id) { + const char *basename; + char *desktop_id; + char *dot; + bool matched = false; + + if (!entry || !entry->path || !app_id || app_id[0] == '\0') + return false; + + basename = strrchr(entry->path, '/'); + basename = basename ? basename + 1 : entry->path; desktop_id = strdup(basename); if (desktop_id) { dot = strrchr(desktop_id, '.'); @@ -1298,30 +1537,28 @@ static bool mango_session_desktop_matches(const char *desktop_path, free(desktop_id); } - if (!matched && startup_wm_class && strcmp(startup_wm_class, app_id) == 0) + if (!matched && entry->startup_wm_class && + strcmp(entry->startup_wm_class, app_id) == 0) matched = true; - if (!matched && flatpak_id && strcmp(flatpak_id, app_id) == 0) + if (!matched && entry->flatpak_id && strcmp(entry->flatpak_id, app_id) == 0) matched = true; - if (matched && exec_value && exec_out) - *exec_out = mango_session_strip_exec_field_codes(exec_value); - - free(exec_value); - free(startup_wm_class); - free(flatpak_id); return matched; } -static char *mango_session_find_desktop_exec(const char *app_id) { - const char *dirs[] = { - "/home/kenn/.local/share/applications", - "/etc/profiles/per-user/kenn/share/applications", - "/run/current-system/sw/share/applications", - "/var/lib/flatpak/exports/share/applications", - NULL, - }; +static bool mango_session_find_desktop_entry(const char *app_id, + MangoSessionDesktopEntry *result) { + char **dirs = mango_session_desktop_search_dirs(); + bool found = false; - for (size_t i = 0; dirs[i] != NULL; ++i) { + if (!result) + return false; + + memset(result, 0, sizeof(*result)); + if (!dirs) + return false; + + for (size_t i = 0; dirs[i] != NULL && !found; ++i) { DIR *dir = opendir(dirs[i]); struct dirent *entry; @@ -1329,27 +1566,66 @@ static char *mango_session_find_desktop_exec(const char *app_id) { continue; while ((entry = readdir(dir)) != NULL) { + MangoSessionDesktopEntry candidate; char *path; - char *exec_value = NULL; - bool matched; if (!strstr(entry->d_name, ".desktop")) continue; path = string_printf("%s/%s", dirs[i], entry->d_name); - matched = mango_session_desktop_matches(path, app_id, &exec_value); - free(path); - if (!matched) + if (!path) continue; - closedir(dir); - return exec_value; + if (!mango_session_load_desktop_entry(path, &candidate)) { + free(path); + continue; + } + free(path); + + if (!mango_session_desktop_entry_matches(&candidate, app_id)) { + mango_session_free_desktop_entry(&candidate); + continue; + } + + *result = candidate; + found = true; + break; } closedir(dir); } - return NULL; + mango_session_free_search_dirs(dirs); + return found; +} + +static char *mango_session_find_desktop_exec(const char *app_id) { + MangoSessionDesktopEntry entry; + char *exec_value; + + if (!mango_session_find_desktop_entry(app_id, &entry)) + return NULL; + + exec_value = entry.exec_value + ? mango_session_strip_exec_field_codes(entry.exec_value) + : NULL; + mango_session_free_desktop_entry(&entry); + return exec_value; +} + +static char *mango_session_find_flatpak_command(const char *app_id) { + MangoSessionDesktopEntry entry; + char *command = NULL; + + if (!mango_session_find_desktop_entry(app_id, &entry)) + return NULL; + + if (entry.flatpak_id && strcmp(entry.flatpak_id, app_id) == 0) + command = entry.exec_value ? mango_session_strip_exec_field_codes(entry.exec_value) + : NULL; + + mango_session_free_desktop_entry(&entry); + return command; } static char *mango_session_read_cmdline(pid_t pid, size_t *argc_out) { @@ -1441,26 +1717,16 @@ static char *mango_session_recover_process_command(pid_t pid) { static char *mango_session_normalize_launch_command(Client *c, pid_t pid) { const char *app_id; + char *flatpak_command; char *desktop_exec; char *raw_command; app_id = c ? client_get_appid(c) : NULL; if (app_id && app_id[0] != '\0' && strcmp(app_id, "broken") != 0) { - char *flatpak_desktop = string_printf( - "/var/lib/flatpak/exports/share/applications/%s.desktop", app_id); - char *user_flatpak_desktop = string_printf( - "%s/.local/share/flatpak/exports/share/applications/%s.desktop", - getenv("HOME") ? getenv("HOME") : "", app_id); - - if ((flatpak_desktop && mango_session_path_exists(flatpak_desktop)) || - (user_flatpak_desktop && - mango_session_path_exists(user_flatpak_desktop))) { - free(flatpak_desktop); - free(user_flatpak_desktop); - return string_printf("flatpak run %s", app_id); - } - free(flatpak_desktop); - free(user_flatpak_desktop); + flatpak_command = mango_session_find_flatpak_command(app_id); + if (flatpak_command && flatpak_command[0] != '\0') + return flatpak_command; + free(flatpak_command); desktop_exec = mango_session_find_desktop_exec(app_id); if (desktop_exec && desktop_exec[0] != '\0') diff --git a/src/session/session.c b/src/session/session.c index e75e07ec..f4634c4c 100644 --- a/src/session/session.c +++ b/src/session/session.c @@ -82,6 +82,23 @@ static char *session_file_path(void) { return path; } +static bool session_file_is_trusted(const char *path) { + struct stat st; + uid_t uid = getuid(); + + if (!path || stat(path, &st) != 0) + return false; + + if (!S_ISREG(st.st_mode)) + return false; + if (st.st_uid != uid) + return false; + if ((st.st_mode & (S_IWGRP | S_IWOTH)) != 0) + return false; + + return true; +} + static void free_pending_entries(void) { free(pending_entries); pending_entries = NULL; @@ -275,6 +292,16 @@ static bool load_pending_entries(void) { if (!path) return false; + if (access(path, F_OK) != 0) + goto cleanup; + + if (!session_file_is_trusted(path)) { + fprintf(stderr, + "mango session: refusing to restore from untrusted session file: %s\n", + path); + goto cleanup; + } + in = fopen(path, "r"); if (!in) goto cleanup; From f86ec6876a0404891e1daca7243c840134d88b96 Mon Sep 17 00:00:00 2001 From: cheerfulScumbag <164391367+cheerfulScumbag@users.noreply.github.com> Date: Mon, 23 Mar 2026 19:14:43 +0000 Subject: [PATCH 4/4] remove dead helper --- src/mango.c | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/mango.c b/src/mango.c index f2ab2a4a..ace95a8d 100644 --- a/src/mango.c +++ b/src/mango.c @@ -1317,11 +1317,6 @@ cleanup: return result; } -static bool mango_session_path_exists(const char *path) { - struct stat st; - return path && stat(path, &st) == 0; -} - typedef struct { char *path; char *exec_value;