This is an automated email from the git hooks/post-receive script.
git pushed a commit to reference refs/pull/29/head
in repository terminology.
View the commit online.
commit 39bff5194cca5f16d6188982856127c08274961c
Author: [email protected] <[email protected]>
AuthorDate: Sat Mar 14 19:07:36 2026 -0600
session: implement session_load with two-pass tree restore and fallback
Adds _build_mux_cmd, _restore_stub, _restore_fill_tabs, _win_is_pristine,
_session_file_free, and session_load. Uses two-pass approach: pass 1 builds
the TC stub tree (deferring extra Tabs solos via Tabs_Pending list), pass 2
fills deferred solos after win_tc_set installs the tree so tabs_tc->parent
is valid. Includes pristine-window reuse, per-window and global fallback
terminals, and version guard. Fixes eina_list_nth_data (non-existent API)
to eina_list_nth, and replaces eet_data_descriptor_decode_free (absent in
EET 1.28) with a manual _session_file_free walker.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
---
src/bin/session.c | 422 +++++++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 419 insertions(+), 3 deletions(-)
diff --git a/src/bin/session.c b/src/bin/session.c
index deba224d..f92835f5 100644
--- a/src/bin/session.c
+++ b/src/bin/session.c
@@ -584,9 +584,425 @@ cleanup:
return ok;
}
-Eina_Bool session_load(const char *name EINA_UNUSED,
- Win *wn EINA_UNUSED)
-{ return EINA_FALSE; }
+/* Free a Session_File decoded by eet_data_read.
+ * EET uses eina_stringshare for EET_T_STRING fields, so _node_free handles
+ * them correctly. The Session_File shell itself is plain malloc. */
+static void
+_session_file_free(Session_File *sf)
+{
+ Session_Node *n;
+ if (!sf) return;
+ EINA_LIST_FREE(sf->windows, n)
+ _node_free(n);
+ free(sf);
+}
+
+/* Returns a malloc'd command string or NULL for plain shell.
+ * Caller must free(). */
+static char *
+_build_mux_cmd(const Session_Node *solo)
+{
+ char buf[512];
+
+ if (!solo->mux_type || !solo->mux_session)
+ return NULL;
+
+ /* Validate mux_session to prevent shell injection from crafted files */
+ if (!_session_name_valid(solo->mux_session))
+ {
+ WRN("session_load: mux_session '%s' fails validation, skipping mux attach",
+ solo->mux_session);
+ return NULL;
+ }
+
+ if (!strcmp(solo->mux_type, "tmux"))
+ {
+ int n = snprintf(buf, sizeof(buf),
+ "tmux new-session -A -s %s", solo->mux_session);
+ if (n < 0 || (size_t)n >= sizeof(buf)) return NULL;
+ return strdup(buf);
+ }
+ else if (!strcmp(solo->mux_type, "screen"))
+ {
+ int n = snprintf(buf, sizeof(buf),
+ "screen -R %s", solo->mux_session);
+ if (n < 0 || (size_t)n >= sizeof(buf)) return NULL;
+ return strdup(buf);
+ }
+ return NULL;
+}
+
+typedef struct {
+ Session_Node *node;
+ Term_Container *tc;
+} Tabs_Pending;
+
+/* Close a partially-built TC tree and all its terms.
+ * Called when _restore_stub fails after creating child TCs. */
+static void
+_tc_tree_close(Term_Container *tc, Win *wn)
+{
+ if (!tc) return;
+
+ switch (tc->type)
+ {
+ case TERM_CONTAINER_TYPE_SOLO:
+ {
+ Solo *solo = (Solo*)tc;
+ if (solo->term)
+ term_close(win_evas_object_get(wn),
+ term_termio_get(solo->term), EINA_FALSE);
+ }
+ break;
+
+ case TERM_CONTAINER_TYPE_SPLIT:
+ {
+ Split *split = (Split*)tc;
+ _tc_tree_close(split->tc1, wn);
+ _tc_tree_close(split->tc2, wn);
+ }
+ break;
+
+ case TERM_CONTAINER_TYPE_TABS:
+ {
+ Tabs *tabs = (Tabs*)tc;
+ Tab_Item *tab_item;
+ Eina_List *l;
+ EINA_LIST_FOREACH(tabs->tabs, l, tab_item)
+ {
+ if (tab_item && tab_item->tc)
+ _tc_tree_close(tab_item->tc, wn);
+ }
+ }
+ break;
+
+ default:
+ break;
+ }
+}
+
+/* Builds the full TC stub tree. For TABS, only the first Solo is added;
+ * additional Solos are deferred via pending_tabs list.
+ * Returns the root Term_Container* or NULL on complete failure. */
+static Term_Container *
+_restore_stub(Session_Node *node, Win *wn,
+ Eina_List **pending_tabs, int *success)
+{
+ switch (node->type)
+ {
+ case SESSION_NODE_SPLIT:
+ {
+ Term_Container *tc1 = NULL, *tc2 = NULL;
+ Session_Node *child1, *child2;
+
+ child1 = eina_list_data_get(node->children);
+ child2 = eina_list_nth(node->children, 1);
+ if (!child1 || !child2) return NULL;
+
+ tc1 = _restore_stub(child1, wn, pending_tabs, success);
+ if (!tc1) return NULL;
+ tc2 = _restore_stub(child2, wn, pending_tabs, success);
+ if (!tc2)
+ {
+ _tc_tree_close(tc1, wn);
+ return NULL;
+ }
+
+ return win_split_new(tc1, tc2, node->split_ratio,
+ node->is_horizontal);
+ }
+
+ case SESSION_NODE_TABS:
+ {
+ Session_Node *first = eina_list_data_get(node->children);
+ if (!first) return NULL;
+
+ Term_Container *first_solo = _restore_stub(first, wn,
+ pending_tabs, success);
+ if (!first_solo) return NULL;
+
+ Term_Container *tabs_tc = win_tabs_create(first_solo);
+ if (!tabs_tc)
+ {
+ _tc_tree_close(first_solo, wn);
+ return NULL;
+ }
+
+ /* Defer the remaining solos — they need tabs_tc to be
+ * installed in the window before _tabs_attach can run */
+ if (eina_list_count(node->children) > 1)
+ {
+ Tabs_Pending *tp = malloc(sizeof(Tabs_Pending));
+ if (tp)
+ {
+ tp->node = node;
+ tp->tc = tabs_tc;
+ *pending_tabs = eina_list_append(*pending_tabs, tp);
+ }
+ }
+ return tabs_tc;
+ }
+
+ case SESSION_NODE_SOLO:
+ {
+ char *cmd = _build_mux_cmd(node);
+ Config *cfg = main_config_get();
+ Term *term;
+
+ term = term_new(wn, cfg, cmd,
+ cfg->login_shell,
+ node->cwd,
+ cfg->cg_width > 0 ? cfg->cg_width : 80,
+ cfg->cg_height > 0 ? cfg->cg_height : 24,
+ EINA_FALSE, NULL);
+ free(cmd);
+
+ if (!term) return NULL;
+ (*success)++;
+
+ /* Set tab title */
+ {
+ const char *title = node->mux_session
+ ? node->mux_session
+ : node->tab_title;
+ if (title)
+ termio_user_title_set(term_termio_get(term), title);
+ }
+
+ return win_solo_new(term, wn);
+ }
+
+ default:
+ return NULL;
+ }
+}
+
+/* Second pass: fill remaining Solos into Tabs nodes now that the
+ * Tabs container is installed in the window (has a valid parent). */
+static void
+_restore_fill_tabs(Eina_List *pending_tabs, Win *wn, int *success)
+{
+ Tabs_Pending *tp;
+ EINA_LIST_FREE(pending_tabs, tp)
+ {
+ Session_Node *child;
+ Eina_List *l;
+ int i = 0;
+
+ EINA_LIST_FOREACH(tp->node->children, l, child)
+ {
+ if (i++ == 0) continue; /* first already done in _restore_stub */
+ char *cmd = _build_mux_cmd(child);
+ Config *cfg = main_config_get();
+ Term *term = term_new(wn, cfg, cmd,
+ cfg->login_shell,
+ child->cwd,
+ cfg->cg_width > 0 ? cfg->cg_width : 80,
+ cfg->cg_height > 0 ? cfg->cg_height : 24,
+ EINA_FALSE, NULL);
+ free(cmd);
+ if (!term) continue;
+ (*success)++;
+
+ const char *title = child->mux_session
+ ? child->mux_session
+ : child->tab_title;
+ if (title)
+ termio_user_title_set(term_termio_get(term), title);
+
+ win_tabs_term_append(tp->tc, term);
+ }
+ win_tabs_focus_index(tp->tc, tp->node->active_tab);
+ free(tp);
+ }
+}
+
+/* Returns EINA_TRUE if the window has a single shell terminal at prompt
+ * with no active child process (best-effort). */
+static Eina_Bool
+_win_is_pristine(Win *wn)
+{
+ Eina_List *terms;
+ Term *term;
+ Evas_Object *termio;
+ pid_t shell_pid, fg_pid;
+ int pty_fd;
+
+ terms = win_terms_get(wn);
+ if (!terms || eina_list_count(terms) != 1) return EINA_FALSE;
+ term = eina_list_data_get(terms);
+ if (!term) return EINA_FALSE;
+
+ termio = term_termio_get(term);
+ shell_pid = termio_pid_get(termio);
+ pty_fd = termpty_fd_get(termio_pty_get(termio));
+ if (pty_fd < 0) return EINA_FALSE;
+
+ fg_pid = tcgetpgrp(pty_fd);
+ if (fg_pid < 0) return EINA_FALSE; /* conservative: non-pristine */
+
+ return (fg_pid == shell_pid) ? EINA_TRUE : EINA_FALSE;
+}
+
+Eina_Bool
+session_load(const char *name, Win *caller_wn)
+{
+ char path[PATH_MAX];
+ Eet_File *ef;
+ Session_File *sf = NULL;
+ Session_Node *win_node;
+ Eina_List *l;
+ int total_success = 0;
+ Eina_Bool ok = EINA_FALSE;
+ Eina_Bool pristine = EINA_FALSE;
+
+ if (!_session_name_valid(name))
+ {
+ ERR("session_load: invalid name '%s'", name);
+ return EINA_FALSE;
+ }
+
+ if (!_session_path_get(name, path, sizeof(path)))
+ return EINA_FALSE;
+
+ ef = eet_open(path, EET_FILE_MODE_READ);
+ if (!ef)
+ {
+ ERR("session_load: cannot open '%s'", path);
+ return EINA_FALSE;
+ }
+
+ sf = eet_data_read(ef, _edd_file, "session");
+ eet_close(ef);
+
+ if (!sf)
+ {
+ ERR("session_load: EET read failed for '%s'", path);
+ return EINA_FALSE;
+ }
+
+ if (sf->version > SESSION_VERSION)
+ {
+ ERR("session_load: version %d > compiled %d, rejecting",
+ sf->version, SESSION_VERSION);
+ _session_file_free(sf);
+ return EINA_FALSE;
+ }
+ if (sf->version < SESSION_VERSION)
+ WRN("session_load: file version %d < compiled %d, some data may be missing",
+ sf->version, SESSION_VERSION);
+
+ /* Determine pristine-replace path */
+ if (caller_wn && _win_is_pristine(caller_wn))
+ pristine = EINA_TRUE;
+
+ EINA_LIST_FOREACH(sf->windows, l, win_node)
+ {
+ Win *wn;
+ Term_Container *root_tc = NULL;
+ Eina_List *pending_tabs = NULL;
+ int success = 0;
+
+ if (win_node->type != SESSION_NODE_WIN) continue;
+
+ if (pristine && caller_wn)
+ {
+ /* Reuse the existing window — close its current terminal */
+ Eina_List *terms = eina_list_clone(win_terms_get(caller_wn));
+ Term *t;
+ EINA_LIST_FREE(terms, t)
+ term_close(win_evas_object_get(caller_wn),
+ term_termio_get(t), EINA_FALSE);
+ wn = caller_wn;
+ pristine = EINA_FALSE; /* only first win replaces */
+ }
+ else
+ {
+ wn = win_new(NULL, NULL, NULL, NULL, main_config_get(),
+ EINA_FALSE, EINA_FALSE, EINA_FALSE, EINA_FALSE,
+ EINA_FALSE);
+ if (!wn) continue;
+ }
+
+ /* Build stub tree from the WIN node's child */
+ {
+ Session_Node *child = eina_list_data_get(win_node->children);
+ if (child)
+ root_tc = _restore_stub(child, wn, &pending_tabs, &success);
+ }
+
+ if (!root_tc)
+ goto win_fallback;
+
+ /* Install the tree into the window */
+ win_tc_set(wn, root_tc);
+
+ /* Fill deferred Tabs solos (need live parent now) */
+ _restore_fill_tabs(pending_tabs, wn, &success);
+ pending_tabs = NULL;
+
+ total_success += success;
+
+ win_sizing_handle(wn);
+ evas_object_show(win_evas_object_get(wn));
+ ok = EINA_TRUE;
+ continue;
+
+win_fallback:
+ {
+ /* Clean up any pending_tabs */
+ Tabs_Pending *tp;
+ EINA_LIST_FREE(pending_tabs, tp) free(tp);
+ }
+ if (success == 0)
+ {
+ /* No terminal opened at all — create one default terminal */
+ ERR("session '%s': restore failed for a window, "
+ "falling back to default terminal", name);
+ Term *fallback = term_new(wn, main_config_get(), NULL,
+ main_config_get()->login_shell,
+ NULL, 80, 24, EINA_FALSE, NULL);
+ if (fallback)
+ {
+ win_term_set(wn, fallback);
+ total_success++;
+ ok = EINA_TRUE;
+ }
+ }
+ else
+ {
+ /* Partial success — some terms were created */
+ total_success += success;
+ ok = EINA_TRUE;
+ }
+ win_sizing_handle(wn);
+ evas_object_show(win_evas_object_get(wn));
+ }
+
+ /* Global fallback: zero terminals across all windows */
+ if (total_success == 0)
+ {
+ ERR("session '%s': complete restore failure, "
+ "creating default terminal", name);
+ Win *fallback_wn = caller_wn;
+ if (!fallback_wn)
+ fallback_wn = win_new(NULL, NULL, NULL, NULL, main_config_get(),
+ EINA_FALSE, EINA_FALSE, EINA_FALSE,
+ EINA_FALSE, EINA_FALSE);
+ if (fallback_wn)
+ {
+ Term *t = term_new(fallback_wn, main_config_get(), NULL,
+ main_config_get()->login_shell,
+ NULL, 80, 24, EINA_FALSE, NULL);
+ if (t) win_term_set(fallback_wn, t);
+ win_sizing_handle(fallback_wn);
+ evas_object_show(win_evas_object_get(fallback_wn));
+ }
+ }
+
+ _session_file_free(sf);
+ return ok;
+}
static const char *
_session_dir_get(void)
--
To stop receiving notification emails like this one, please contact
the administrator of this repository.