Skip to content

Commit 577cc66

Browse files
committed
common(cmd) AsyncTmuxCmd
1 parent c2108ab commit 577cc66

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed

src/libtmux/common.py

+139
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
66
"""
77

8+
import asyncio
89
import logging
910
import re
1011
import shutil
@@ -263,6 +264,144 @@ def __init__(self, *args: t.Any) -> None:
263264
)
264265

265266

267+
class AsyncTmuxCmd:
268+
"""
269+
An asyncio-compatible class for running any tmux command via subprocess.
270+
271+
Attributes
272+
----------
273+
cmd : list[str]
274+
The full command (including the "tmux" binary path).
275+
stdout : list[str]
276+
Lines of stdout output from tmux.
277+
stderr : list[str]
278+
Lines of stderr output from tmux.
279+
returncode : int
280+
The process return code.
281+
282+
Examples
283+
--------
284+
>>> import asyncio
285+
>>>
286+
>>> async def main():
287+
... proc = await AsyncTmuxCmd.run('-V')
288+
... if proc.stderr:
289+
... raise exc.LibTmuxException(
290+
... f"Error invoking tmux: {proc.stderr}"
291+
... )
292+
... print("tmux version:", proc.stdout)
293+
...
294+
>>> asyncio.run(main())
295+
tmux version: [...]
296+
297+
This is equivalent to calling:
298+
299+
.. code-block:: console
300+
301+
$ tmux -V
302+
"""
303+
304+
def __init__(
305+
self,
306+
cmd: list[str],
307+
stdout: list[str],
308+
stderr: list[str],
309+
returncode: int,
310+
) -> None:
311+
"""
312+
Store the results of a completed tmux subprocess run.
313+
314+
Parameters
315+
----------
316+
cmd : list[str]
317+
The command used to invoke tmux.
318+
stdout : list[str]
319+
Captured lines from tmux stdout.
320+
stderr : list[str]
321+
Captured lines from tmux stderr.
322+
returncode : int
323+
Subprocess exit code.
324+
"""
325+
self.cmd: list[str] = cmd
326+
self.stdout: list[str] = stdout
327+
self.stderr: list[str] = stderr
328+
self.returncode: int = returncode
329+
330+
@classmethod
331+
async def run(cls, *args: t.Any) -> "AsyncTmuxCmd":
332+
"""
333+
Execute a tmux command asynchronously and capture its output.
334+
335+
Parameters
336+
----------
337+
*args : str
338+
Arguments to be passed after the "tmux" binary name.
339+
340+
Returns
341+
-------
342+
AsyncTmuxCmd
343+
An instance containing the cmd, stdout, stderr, and returncode.
344+
345+
Raises
346+
------
347+
exc.TmuxCommandNotFound
348+
If no "tmux" executable is found in the user's PATH.
349+
exc.LibTmuxException
350+
If there's any unexpected exception creating or communicating
351+
with the tmux subprocess.
352+
"""
353+
tmux_bin: t.Optional[str] = shutil.which("tmux")
354+
if not tmux_bin:
355+
msg = "tmux executable not found in PATH"
356+
raise exc.TmuxCommandNotFound(
357+
msg,
358+
)
359+
360+
# Convert all arguments to strings, accounting for Python 3.7+ strings
361+
cmd: list[str] = [tmux_bin] + [str_from_console(a) for a in args]
362+
363+
try:
364+
process: asyncio.subprocess.Process = await asyncio.create_subprocess_exec(
365+
*cmd,
366+
stdout=asyncio.subprocess.PIPE,
367+
stderr=asyncio.subprocess.PIPE,
368+
)
369+
raw_stdout, raw_stderr = await process.communicate()
370+
returncode: int = (
371+
process.returncode if process.returncode is not None else -1
372+
)
373+
374+
except Exception as e:
375+
logger.exception("Exception for %s", " ".join(cmd))
376+
msg = f"Exception while running tmux command: {e}"
377+
raise exc.LibTmuxException(
378+
msg,
379+
) from e
380+
381+
stdout_str: str = console_to_str(raw_stdout)
382+
stderr_str: str = console_to_str(raw_stderr)
383+
384+
# Split on newlines, filtering out any trailing empty lines
385+
stdout_split: list[str] = [line for line in stdout_str.split("\n") if line]
386+
stderr_split: list[str] = [line for line in stderr_str.split("\n") if line]
387+
388+
# Workaround for tmux "has-session" command behavior
389+
if "has-session" in cmd and stderr_split and not stdout_split:
390+
# If `has-session` fails, it might output an error on stderr
391+
# with nothing on stdout. We replicate the original logic here:
392+
stdout_split = [stderr_split[0]]
393+
394+
logger.debug("stdout for %s: %s", " ".join(cmd), stdout_split)
395+
logger.debug("stderr for %s: %s", " ".join(cmd), stderr_split)
396+
397+
return cls(
398+
cmd=cmd,
399+
stdout=stdout_split,
400+
stderr=stderr_split,
401+
returncode=returncode,
402+
)
403+
404+
266405
def get_version() -> LooseVersion:
267406
"""Return tmux version.
268407

0 commit comments

Comments
 (0)