From 1bfedd1124533e07148498a30fe521b47835c0be Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 5 Feb 2026 17:18:46 -0600 Subject: [PATCH 1/2] tests(test_server[race_condition]): Deterministically reproduce #624 as strict xfail why: The previous test looped new_session() 10 times hoping to trigger a timing race, but the bug is deterministic in PyInstaller + Python 3.13 + Docker environments (LD_LIBRARY_PATH contamination crashes tmux server after new-session returns, causing list-sessions to miss the session). what: - Replace loop-based test with monkeypatch that intercepts neo.fetch_objs - Return empty list on first list-sessions call, simulating stale response - Add strict=True so XPASS signals when retry-based fix is added - Keep real tmux new-session execution to validate integration - Add precise type annotations matching neo.fetch_objs signature --- tests/test_server.py | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_server.py b/tests/test_server.py index 9b85d279c..52da777d9 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -11,6 +11,7 @@ import pytest +from libtmux import exc from libtmux.server import Server if t.TYPE_CHECKING: @@ -182,6 +183,50 @@ def test_new_session_environmental_variables( assert my_session.show_environment()["FOO"] == "HI" +@pytest.mark.xfail( + raises=exc.TmuxObjectDoesNotExist, + reason="Race condition: new_session() may fail when list-sessions " + "doesn't yet see the session created by new-session. " + "See https://github.com/tmux-python/libtmux/issues/624", + strict=True, +) +def test_new_session_race_condition( + server: Server, + monkeypatch: pytest.MonkeyPatch, +) -> None: + """Simulate race between new-session and list-sessions (#624). + + In PyInstaller + Python 3.13 + Docker, LD_LIBRARY_PATH contamination + can crash the tmux server after new-session returns, causing the + subsequent list-sessions to not find the newly created session. + + This test monkeypatches fetch_objs to return an empty list on the + first list-sessions call, reproducing that exact failure window. + """ + from libtmux import neo + + original_fetch_objs = neo.fetch_objs + first_list_sessions_call = True + + def fetch_objs_stale_first_call( + server: Server, + list_cmd: t.Literal["list-sessions", "list-windows", "list-panes"], + list_extra_args: t.Iterable[str] | None = None, + ) -> list[dict[str, t.Any]]: + nonlocal first_list_sessions_call + if list_cmd == "list-sessions" and first_list_sessions_call: + first_list_sessions_call = False + return [] + return original_fetch_objs( + server=server, + list_cmd=list_cmd, + list_extra_args=list_extra_args, + ) + + monkeypatch.setattr(neo, "fetch_objs", fetch_objs_stale_first_call) + server.new_session(session_name="race_test") + + def test_no_server_sessions() -> None: """Verify ``Server.sessions`` returns empty list without tmux server.""" server = Server(socket_name="test_attached_session_no_server") From e88a2a892f4e92c63ed9162714bb1569b5c5c2a3 Mon Sep 17 00:00:00 2001 From: Tony Narlock Date: Thu, 5 Feb 2026 17:19:16 -0600 Subject: [PATCH 2/2] tests(test_server[race_condition]): Reproduce #624 via real server crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit why: Previous test monkeypatched fetch_objs to return [], bypassing all real tmux interaction. This version exercises the full code path with real tmux commands — only crash timing is controlled. what: - Kill real tmux server inside fetch_objs wrapper, start replacement - Bumper session ensures session_id mismatch (original $1 vs replacement $0) - All 7 code paths exercised: new-session, kill-server, replacement new-session, list-sessions, fetch_objs parsing, fetch_obj search, TmuxObjectDoesNotExist --- tests/test_server.py | 47 ++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/tests/test_server.py b/tests/test_server.py index 52da777d9..73d40a28b 100644 --- a/tests/test_server.py +++ b/tests/test_server.py @@ -194,36 +194,53 @@ def test_new_session_race_condition( server: Server, monkeypatch: pytest.MonkeyPatch, ) -> None: - """Simulate race between new-session and list-sessions (#624). - - In PyInstaller + Python 3.13 + Docker, LD_LIBRARY_PATH contamination - can crash the tmux server after new-session returns, causing the - subsequent list-sessions to not find the newly created session. - - This test monkeypatches fetch_objs to return an empty list on the - first list-sessions call, reproducing that exact failure window. + """Reproduce server crash between new-session and list-sessions (#624). + + In PyInstaller + Docker, LD_LIBRARY_PATH contamination from the + bootloader (pyi_utils_posix.c:384-412) causes the tmux server to load + incompatible bundled libraries instead of system libraries. The server + (forked via proc_fork_and_daemon in proc.c:358-380) crashes after + the client returns the session_id from new-session. + + This test injects a real server kill at the exact point between + new-session and list-sessions, then starts a replacement server. + All tmux commands and libtmux parsing run against real tmux — only + the crash timing is controlled. """ from libtmux import neo original_fetch_objs = neo.fetch_objs - first_list_sessions_call = True + server_crashed = False - def fetch_objs_stale_first_call( + def fetch_objs_with_server_crash( server: Server, list_cmd: t.Literal["list-sessions", "list-windows", "list-panes"], list_extra_args: t.Iterable[str] | None = None, ) -> list[dict[str, t.Any]]: - nonlocal first_list_sessions_call - if list_cmd == "list-sessions" and first_list_sessions_call: - first_list_sessions_call = False - return [] + nonlocal server_crashed + if list_cmd == "list-sessions" and not server_crashed: + server_crashed = True + # Simulate server crash: kill and start replacement + server.cmd("kill-server") + server.cmd("new-session", "-d", "-s", "_replacement") + # Call the REAL fetch_objs — runs real list-sessions against + # the replacement server, parses real output return original_fetch_objs( server=server, list_cmd=list_cmd, list_extra_args=list_extra_args, ) - monkeypatch.setattr(neo, "fetch_objs", fetch_objs_stale_first_call) + monkeypatch.setattr(neo, "fetch_objs", fetch_objs_with_server_crash) + + # Bumper session: advances session IDs so race_test gets $1+, + # avoiding collision with replacement server's $0 + server.cmd("new-session", "-d", "-s", "_bumper") + + # This calls real new-session ($1), then from_session_id → + # fetch_obj → our patched fetch_objs (kills server, starts + # replacement) → real list-sessions (finds $0, not $1) → + # TmuxObjectDoesNotExist server.new_session(session_name="race_test")