aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoruser <user@clank>2026-08-22 18:30:19 +0200
committeruser <user@clank>2026-08-22 18:30:19 +0200
commit063d43f7702fa74071e8555c6e9dabba1a914c74 (patch)
tree08703e3200f9e4977726c09fc2f22a023dad52cd
parentslosh: paint the padding ring, so a float cannot be seen through (diff)
downloadslosh-063d43f7702fa74071e8555c6e9dabba1a914c74.tar.gz
slosh-063d43f7702fa74071e8555c6e9dabba1a914c74.tar.xz
slosh-063d43f7702fa74071e8555c6e9dabba1a914c74.zip
slosh: refuse to attach to the session this pane is inside
A new client displaces the current display, so `slosh` typed into a pane of "main" displaced the very client showing that pane -- you watched your own session detach, from inside it. SLOSH_SESSION names the pane's own session; that one name is refused with the way out spelled in the message. Any other name is deliberate nesting, and cmd/ls are untouched: they are how a program in a pane is supposed to reach its own session.
-rw-r--r--src/main.c20
-rw-r--r--tests/test_nested.py87
2 files changed, 107 insertions, 0 deletions
diff --git a/src/main.c b/src/main.c
index b488e56..e3e621e 100644
--- a/src/main.c
+++ b/src/main.c
@@ -249,6 +249,26 @@ int main(int argc, char **argv) {
if (isatty(STDOUT_FILENO)) term_size(&cols, &rows);
if (server) return server_run(name, cmd_argv, cols, rows, layout, watch);
+ /* Not to the session this process is already inside. A new client
+ * displaces the current display (EXIT_REPLACED), so `slosh` typed into a
+ * pane of "main" attaches to main, displaces the very client showing that
+ * pane, and the user watches their own session detach from inside it.
+ * SLOSH_SESSION names the pane's own session; asking for any *other* name
+ * is deliberate nesting and none of our business. The one legitimate shape
+ * this refuses — a fresh terminal launched from a pane, inheriting the
+ * variables, attaching to take over as the display — gets its escape hatch
+ * named in the message rather than a flag: it is exactly what unsetting
+ * the variable means. */
+ const char *inside = getenv("SLOSH") ? getenv("SLOSH_SESSION") : NULL;
+ if (inside && *inside && strcmp(inside, name) == 0) {
+ fprintf(stderr,
+ "slosh: this pane is already inside session %s\n"
+ " (C-a d detaches; `slosh -s NAME` nests another session;\n"
+ " unset SLOSH_SESSION to attach anyway, displacing this view)\n",
+ name);
+ return 1;
+ }
+
/* attach, creating the session if nobody is home */
int fd = server_connect(name);
if (fd < 0) fd = server_spawn(name, cmd_argv, cols, rows, layout, watch);
diff --git a/tests/test_nested.py b/tests/test_nested.py
new file mode 100644
index 0000000..16c10ed
--- /dev/null
+++ b/tests/test_nested.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python3
+"""Running slosh from inside slosh.
+
+A new client displaces the current display, so `slosh` typed into a pane of
+the very session it would attach used to displace the client showing that
+pane — you watched your own session detach, from inside it. The pane's
+environment (SLOSH, SLOSH_SESSION) says which session this is, so the attach
+path refuses that one name and only that one name: any other is deliberate
+nesting, and the control verbs (`cmd`, `ls`) are how a program in a pane is
+*supposed* to reach its own session, so they are untouched.
+"""
+
+import os
+import shutil
+import subprocess
+import sys
+import tempfile
+
+from harness import BIN, check, report
+
+
+def run(args, session=None, timeout=10, **env_extra):
+ """Run the binary with a clean pane-environment: no inherited SLOSH_*,
+ sockets in a private runtime dir, stdin not a tty."""
+ env = {k: v for k, v in os.environ.items() if not k.startswith("SLOSH")}
+ env.update(env_extra)
+ if session is not None:
+ env["SLOSH"] = "1"
+ env["SLOSH_SESSION"] = session
+ return subprocess.run(
+ [BIN] + args,
+ capture_output=True,
+ text=True,
+ env=env,
+ stdin=subprocess.DEVNULL,
+ timeout=timeout,
+ )
+
+
+def test_attaching_to_your_own_session_is_refused():
+ rt = tempfile.mkdtemp()
+ try:
+ r = run(["-s", "mine"], session="mine", XDG_RUNTIME_DIR=rt)
+ check("it refuses", r.returncode == 1, str(r.returncode))
+ check("and names the session", "inside session mine" in r.stderr, r.stderr)
+ check("and offers the detach key", "C-a d" in r.stderr, r.stderr)
+ socks = (
+ os.listdir(os.path.join(rt, "slosh"))
+ if os.path.isdir(os.path.join(rt, "slosh"))
+ else []
+ )
+ check("no session was spawned", not socks, str(socks))
+ finally:
+ shutil.rmtree(rt, ignore_errors=True)
+
+
+def test_any_other_session_is_deliberate_nesting():
+ rt = tempfile.mkdtemp()
+ try:
+ # Asked for a *different* session from inside a pane: the guard steps
+ # aside. Without a tty the attach then fails on its own terms, but the
+ # session was created — which is the proof the refusal did not fire.
+ r = run(["-s", "nest"], session="outer", XDG_RUNTIME_DIR=rt)
+ check("no refusal", "already inside" not in r.stderr, r.stderr)
+
+ # The control verbs from inside the *same* session: the documented
+ # interface for a program in a pane, and exactly what must keep working.
+ r = run(["-s", "nest", "cmd", "alive"], session="nest", XDG_RUNTIME_DIR=rt)
+ check("cmd still answers", r.returncode == 0 and "true" in r.stdout, str(r))
+ r = run(["ls"], session="nest", XDG_RUNTIME_DIR=rt)
+ check("ls still answers", r.returncode == 0 and "nest" in r.stdout, str(r))
+
+ # A --script pane leaves SLOSH_SESSION empty: there is nothing this
+ # pane is inside, so nothing to refuse.
+ r = run(["-s", "nest"], session="", XDG_RUNTIME_DIR=rt)
+ check(
+ "an empty name refuses nothing", "already inside" not in r.stderr, r.stderr
+ )
+ finally:
+ run(["-s", "nest", "cmd", "quit"], XDG_RUNTIME_DIR=rt)
+ shutil.rmtree(rt, ignore_errors=True)
+
+
+if __name__ == "__main__":
+ test_attaching_to_your_own_session_is_refused()
+ test_any_other_session_is_deliberate_nesting()
+ sys.exit(report())