Skip to content

Commit 93f658d

Browse files
committed
fix(libtmux[rust-backend]) honor config startup semantics
why: Config-backed servers should start and load config without breaking liveness checks. what: - run first command with -f via tmux bin when server is down - align rust-backend server helpers to new signature - refine rust helper error handling and typing
1 parent 2b51cf4 commit 93f658d

3 files changed

Lines changed: 139 additions & 28 deletions

File tree

src/libtmux/_rust.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,22 @@
1313
_NATIVE: Any | None = None
1414

1515

16+
class RustBackendImportError(ImportError):
17+
"""Raise when the Rust backend cannot be imported."""
18+
19+
def __init__(self) -> None:
20+
super().__init__(
21+
"libtmux rust backend requires the vibe_tmux extension to be installed"
22+
)
23+
24+
1625
def _load_native() -> Any:
1726
global _NATIVE
1827
if _NATIVE is None:
1928
try:
2029
_NATIVE = importlib.import_module("vibe_tmux")
2130
except Exception as exc: # pragma: no cover - import path is env-dependent
22-
raise ImportError(
23-
"libtmux rust backend requires the vibe_tmux extension to be installed"
24-
) from exc
31+
raise RustBackendImportError() from exc
2532
return _NATIVE
2633

2734

src/libtmux/common.py

Lines changed: 100 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77

88
from __future__ import annotations
99

10+
import contextlib
1011
import logging
1112
import os
13+
import pathlib
1214
import re
1315
import shlex
1416
import shutil
@@ -41,6 +43,53 @@
4143
_RUST_SERVER_CONFIG: dict[tuple[str | None, str | None, int | None], set[str]] = {}
4244

4345

46+
def _resolve_rust_socket_path(socket_path: str | None, socket_name: str | None) -> str:
47+
if socket_path:
48+
return socket_path
49+
uid = os.geteuid()
50+
name = socket_name or "default"
51+
base = (
52+
os.getenv("TMUX_TMPDIR")
53+
or os.getenv("XDG_RUNTIME_DIR")
54+
or f"/run/user/{uid}"
55+
or "/tmp"
56+
)
57+
base_path = pathlib.Path(base)
58+
socket_dir = base_path / f"tmux-{uid}"
59+
socket_dir.mkdir(parents=True, exist_ok=True)
60+
with contextlib.suppress(OSError):
61+
socket_dir.chmod(0o700)
62+
return str(socket_dir / name)
63+
64+
65+
def _rust_run_with_config(
66+
socket_path: str | None,
67+
socket_name: str | None,
68+
config_file: str,
69+
cmd_parts: list[str],
70+
cmd_list: list[str],
71+
) -> tuple[list[str], list[str], int, list[str]]:
72+
tmux_bin = shutil.which("tmux")
73+
if not tmux_bin:
74+
raise exc.TmuxCommandNotFound
75+
resolved_socket = _resolve_rust_socket_path(socket_path, socket_name)
76+
process = subprocess.Popen(
77+
[tmux_bin, "-S", resolved_socket, "-f", config_file, *cmd_parts],
78+
stdout=subprocess.PIPE,
79+
stderr=subprocess.PIPE,
80+
text=True,
81+
errors="backslashreplace",
82+
)
83+
stdout_raw, stderr_raw = process.communicate()
84+
stdout_lines = stdout_raw.split("\n") if stdout_raw else []
85+
while stdout_lines and stdout_lines[-1] == "":
86+
stdout_lines.pop()
87+
stderr_lines = list(filter(None, stderr_raw.split("\n"))) if stderr_raw else []
88+
if "has-session" in cmd_list and stderr_lines and not stdout_lines:
89+
stdout_lines = [stderr_lines[0]]
90+
return stdout_lines, stderr_lines, process.returncode, cmd_list
91+
92+
4493
def _parse_tmux_args(args: tuple[t.Any, ...]) -> tuple[
4594
str | None, str | None, str | None, int | None, list[str]
4695
]:
@@ -102,7 +151,6 @@ def _rust_server(
102151
socket_name: str | None,
103152
socket_path: str | None,
104153
colors: int | None,
105-
config_file: str | None,
106154
) -> t.Any:
107155
key = (socket_name, socket_path, colors)
108156
server = _RUST_SERVER_CACHE.get(key)
@@ -121,17 +169,12 @@ def _rust_server(
121169
_RUST_SERVER_CACHE[key] = server
122170
_RUST_SERVER_CONFIG[key] = set()
123171

124-
if config_file:
125-
loaded = _RUST_SERVER_CONFIG.setdefault(key, set())
126-
if config_file not in loaded:
127-
quoted = shlex.quote(config_file)
128-
server.cmd(f"source-file {quoted}")
129-
loaded.add(config_file)
130-
131172
return server
132173

133174

134-
def _rust_cmd_result(args: tuple[t.Any, ...]) -> tuple[list[str], list[str], int, list[str]]:
175+
def _rust_cmd_result(
176+
args: tuple[t.Any, ...],
177+
) -> tuple[list[str], list[str], int, list[str]]:
135178
socket_name, socket_path, config_file, colors, cmd_parts = _parse_tmux_args(args)
136179
cmd_list = [str(c) for c in args]
137180
if not cmd_parts:
@@ -159,18 +202,53 @@ def _rust_cmd_result(args: tuple[t.Any, ...]) -> tuple[list[str], list[str], int
159202
cmd_parts = ["-f", config_file, *cmd_parts]
160203
config_file = None
161204

162-
server = _rust_server(socket_name, socket_path, colors, config_file)
205+
server = _rust_server(socket_name, socket_path, colors)
206+
key = (socket_name, socket_path, colors)
207+
if config_file:
208+
loaded = _RUST_SERVER_CONFIG.setdefault(key, set())
209+
if config_file not in loaded:
210+
if not server.is_alive():
211+
stdout_lines, stderr_lines, exit_code, cmd_args = _rust_run_with_config(
212+
socket_path,
213+
socket_name,
214+
config_file,
215+
cmd_parts,
216+
cmd_list,
217+
)
218+
if exit_code == 0:
219+
loaded.add(config_file)
220+
return stdout_lines, stderr_lines, exit_code, cmd_args
221+
quoted = shlex.quote(config_file)
222+
try:
223+
server.cmd(f"source-file {quoted}")
224+
except Exception as err:
225+
message = str(err)
226+
error_stdout: list[str] = []
227+
error_stderr = [message] if message else []
228+
if "has-session" in cmd_list and error_stderr and not error_stdout:
229+
error_stdout = [error_stderr[0]]
230+
return error_stdout, error_stderr, 1, cmd_list
231+
loaded.add(config_file)
163232

164-
cmd = " ".join(shlex.quote(part) for part in cmd_parts)
165-
result = server.cmd(cmd)
166-
stdout = result.stdout.split("\n") if result.stdout else []
167-
while stdout and stdout[-1] == "":
168-
stdout.pop()
233+
cmd_line = " ".join(shlex.quote(part) for part in cmd_parts)
234+
try:
235+
result = server.cmd(cmd_line)
236+
except Exception as err:
237+
message = str(err)
238+
error_stdout_lines: list[str] = []
239+
error_stderr_lines = [message] if message else []
240+
if "has-session" in cmd_list and error_stderr_lines and not error_stdout_lines:
241+
error_stdout_lines = [error_stderr_lines[0]]
242+
return error_stdout_lines, error_stderr_lines, 1, cmd_list
243+
244+
stdout_lines = result.stdout.split("\n") if result.stdout else []
245+
while stdout_lines and stdout_lines[-1] == "":
246+
stdout_lines.pop()
169247
stderr_raw = getattr(result, "exit_message", None) or ""
170-
stderr = list(filter(None, stderr_raw.split("\n"))) if stderr_raw else []
171-
if "has-session" in cmd_list and stderr and not stdout:
172-
stdout = [stderr[0]]
173-
return stdout, stderr, result.exit_code, cmd_list
248+
stderr_lines = list(filter(None, stderr_raw.split("\n"))) if stderr_raw else []
249+
if "has-session" in cmd_list and stderr_lines and not stdout_lines:
250+
stdout_lines = [stderr_lines[0]]
251+
return stdout_lines, stderr_lines, result.exit_code, cmd_list
174252

175253

176254
class CmdProtocol(t.Protocol):
@@ -413,20 +491,20 @@ def __init__(self, *args: t.Any) -> None:
413491
text=True,
414492
errors="backslashreplace",
415493
)
416-
stdout, stderr = self.process.communicate()
494+
stdout_text, stderr_text = self.process.communicate()
417495
returncode = self.process.returncode
418496
except Exception:
419497
logger.exception(f"Exception for {subprocess.list2cmdline(cmd)}")
420498
raise
421499

422500
self.returncode = returncode
423501

424-
stdout_split = stdout.split("\n")
502+
stdout_split = stdout_text.split("\n")
425503
# remove trailing newlines from stdout
426504
while stdout_split and stdout_split[-1] == "":
427505
stdout_split.pop()
428506

429-
stderr_split = stderr.split("\n")
507+
stderr_split = stderr_text.split("\n")
430508
self.stderr = list(filter(None, stderr_split)) # filter empty values
431509

432510
if "has-session" in cmd and len(self.stderr) and not stdout_split:

src/libtmux/server.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
PaneDict,
3030
SessionDict,
3131
WindowDict,
32+
_rust_server,
3233
session_check_name,
3334
)
3435
from .options import OptionsMixin
@@ -204,6 +205,17 @@ def is_alive(self) -> bool:
204205
>>> tmux = Server(socket_name="no_exist")
205206
>>> assert not tmux.is_alive()
206207
"""
208+
if os.getenv("LIBTMUX_BACKEND") == "rust":
209+
try:
210+
socket_path = (
211+
str(self.socket_path)
212+
if isinstance(self.socket_path, pathlib.Path)
213+
else self.socket_path
214+
)
215+
server = _rust_server(self.socket_name, socket_path, self.colors)
216+
return bool(server.is_alive())
217+
except Exception:
218+
return False
207219
try:
208220
res = self.cmd("list-sessions")
209221
except Exception:
@@ -221,9 +233,23 @@ def raise_if_dead(self) -> None:
221233
<class 'subprocess.CalledProcessError'>
222234
"""
223235
if os.getenv("LIBTMUX_BACKEND") == "rust":
224-
proc = self.cmd("list-sessions")
225-
if proc.returncode != 0:
226-
raise subprocess.CalledProcessError(proc.returncode, proc.cmd)
236+
rust_cmd_args: list[str] = ["list-sessions"]
237+
if self.socket_name:
238+
rust_cmd_args.insert(0, f"-L{self.socket_name}")
239+
if self.socket_path:
240+
rust_cmd_args.insert(0, f"-S{self.socket_path}")
241+
if self.config_file:
242+
rust_cmd_args.insert(0, f"-f{self.config_file}")
243+
try:
244+
socket_path = (
245+
str(self.socket_path)
246+
if isinstance(self.socket_path, pathlib.Path)
247+
else self.socket_path
248+
)
249+
server = _rust_server(self.socket_name, socket_path, self.colors)
250+
server.require_server()
251+
except Exception as err:
252+
raise subprocess.CalledProcessError(1, rust_cmd_args) from err
227253
return
228254
tmux_bin = shutil.which("tmux")
229255
if tmux_bin is None:

0 commit comments

Comments
 (0)