Skip to content

Commit e07935a

Browse files
tyvsmithclaude
andcommitted
Replace shell=True with shell=False throughout
- _host_cmd(str) -> _host_prefix() -> list[str]: returns ["flatpak-spawn", "--host"] inside Flatpak, [] outside - _run() now takes list[str] args, passes _host_prefix() + args to subprocess.run with shell=False - _has_command() uses shutil.which() outside Flatpak (no subprocess), falls back to _run(["which", name]) inside Flatpak - launch() uses shlex.split() to tokenise the command string before passing to Popen with shell=False - Update all tests to assert list args; add Flatpak launch test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2e0603d commit e07935a

2 files changed

Lines changed: 50 additions & 42 deletions

File tree

lan_mouse.py

Lines changed: 23 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import functools
66
import re
7+
import shlex
8+
import shutil
79
import subprocess
810
import time
911
from pathlib import Path
@@ -28,18 +30,16 @@ class Client(TypedDict):
2830
ips: list[str]
2931

3032

31-
def _host_cmd(command: str) -> str:
32-
"""Prefix a command with flatpak-spawn --host when running inside flatpak."""
33-
if _IN_FLATPAK:
34-
return f"flatpak-spawn --host {command}"
35-
return command
33+
def _host_prefix() -> list[str]:
34+
"""Return the flatpak-spawn --host prefix when running inside the Flatpak sandbox."""
35+
return ["flatpak-spawn", "--host"] if _IN_FLATPAK else []
3636

3737

38-
def _run(command: str, timeout: float = 5.0) -> subprocess.CompletedProcess[str]:
38+
def _run(args: list[str], timeout: float = 5.0) -> subprocess.CompletedProcess[str]:
3939
"""Run a host command and return the CompletedProcess."""
4040
return subprocess.run(
41-
_host_cmd(command),
42-
shell=True,
41+
_host_prefix() + args,
42+
shell=False,
4343
capture_output=True,
4444
text=True,
4545
timeout=timeout,
@@ -55,8 +55,10 @@ def _bin(bin_path: str = "") -> str:
5555

5656
def _has_command(name: str) -> bool:
5757
"""Check if a command exists on the host."""
58+
if not _IN_FLATPAK:
59+
return shutil.which(name) is not None
5860
try:
59-
result = _run(f"command -v {name}")
61+
result = _run(["which", name])
6062
return result.returncode == 0
6163
except (subprocess.TimeoutExpired, OSError):
6264
return False
@@ -66,7 +68,7 @@ def is_running(bin_path: str = "") -> bool:
6668
"""Check if the lan-mouse process is running."""
6769
try:
6870
name = Path(_bin(bin_path)).name
69-
result = _run(f"pgrep -x {name}")
71+
result = _run(["pgrep", "-x", name])
7072
return result.returncode == 0
7173
except (subprocess.TimeoutExpired, OSError):
7274
return False
@@ -108,7 +110,7 @@ def list_clients(bin_path: str = "") -> list[Client]:
108110
Returns an empty list if lan-mouse is not running or the command fails.
109111
"""
110112
try:
111-
result = _run(f"{_bin(bin_path)} cli list")
113+
result = _run([_bin(bin_path), "cli", "list"])
112114
if result.returncode != 0:
113115
return []
114116
except (subprocess.TimeoutExpired, OSError):
@@ -132,7 +134,7 @@ def get_status(bin_path: str = "") -> Status:
132134
distinguish "not running" from "running but no clients".
133135
"""
134136
try:
135-
result = _run(f"{_bin(bin_path)} cli list")
137+
result = _run([_bin(bin_path), "cli", "list"])
136138
if result.returncode == 0:
137139
return Status(running=True, clients=_parse_clients(result.stdout))
138140
except (subprocess.TimeoutExpired, OSError):
@@ -145,7 +147,7 @@ def get_status(bin_path: str = "") -> Status:
145147
def activate(client_id: int, bin_path: str = "") -> bool:
146148
"""Activate a client connection. Returns True on success."""
147149
try:
148-
result = _run(f"{_bin(bin_path)} cli activate {client_id}")
150+
result = _run([_bin(bin_path), "cli", "activate", str(client_id)])
149151
return result.returncode == 0
150152
except (subprocess.TimeoutExpired, OSError):
151153
return False
@@ -154,7 +156,7 @@ def activate(client_id: int, bin_path: str = "") -> bool:
154156
def deactivate(client_id: int, bin_path: str = "") -> bool:
155157
"""Deactivate a client connection. Returns True on success."""
156158
try:
157-
result = _run(f"{_bin(bin_path)} cli deactivate {client_id}")
159+
result = _run([_bin(bin_path), "cli", "deactivate", str(client_id)])
158160
return result.returncode == 0
159161
except (subprocess.TimeoutExpired, OSError):
160162
return False
@@ -181,10 +183,12 @@ def launch(command: str = "", bin_path: str = "") -> None:
181183
command: Custom launch command. If empty, auto-detects the best method.
182184
bin_path: Custom path to the lan-mouse binary.
183185
"""
184-
cmd = _host_cmd(command.strip() or _resolve_launch_cmd(bin_path))
186+
args = _host_prefix() + shlex.split(
187+
command.strip() or _resolve_launch_cmd(bin_path)
188+
)
185189
subprocess.Popen(
186-
cmd,
187-
shell=True,
190+
args,
191+
shell=False,
188192
start_new_session=True,
189193
stdin=subprocess.DEVNULL,
190194
stdout=subprocess.DEVNULL,
@@ -199,7 +203,7 @@ def wait_for_ready(bin_path: str = "", timeout: float = 5.0) -> bool:
199203
while time.monotonic() < deadline:
200204
if is_running(bin_path):
201205
try:
202-
result = _run(f"{_bin(bin_path)} cli list")
206+
result = _run([_bin(bin_path), "cli", "list"])
203207
if result.returncode == 0:
204208
return True
205209
except (subprocess.TimeoutExpired, OSError):
@@ -212,7 +216,7 @@ def kill(bin_path: str = "") -> bool:
212216
"""Kill the lan-mouse daemon and wait for it to exit. Returns True on success."""
213217
try:
214218
name = Path(_bin(bin_path)).name
215-
result = _run(f"pkill -x {name}")
219+
result = _run(["pkill", "-x", name])
216220
if result.returncode != 0:
217221
return False
218222
for _ in range(20):

tests/test_lan_mouse.py

Lines changed: 27 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,18 @@ def test_custom_path_stripped(self):
2626

2727

2828
# ---------------------------------------------------------------------------
29-
# _host_cmd()
29+
# _host_prefix()
3030
# ---------------------------------------------------------------------------
3131

3232

33-
class TestHostCmd:
33+
class TestHostPrefix:
3434
@patch("lan_mouse._IN_FLATPAK", False)
3535
def test_outside_flatpak(self):
36-
assert lan_mouse._host_cmd("pgrep -x lan-mouse") == "pgrep -x lan-mouse"
36+
assert lan_mouse._host_prefix() == []
3737

3838
@patch("lan_mouse._IN_FLATPAK", True)
3939
def test_inside_flatpak(self):
40-
assert (
41-
lan_mouse._host_cmd("pgrep -x lan-mouse")
42-
== "flatpak-spawn --host pgrep -x lan-mouse"
43-
)
40+
assert lan_mouse._host_prefix() == ["flatpak-spawn", "--host"]
4441

4542

4643
# ---------------------------------------------------------------------------
@@ -185,7 +182,7 @@ def test_skips_malformed_lines(self, mock_run):
185182
def test_custom_bin_path(self, mock_run):
186183
mock_run.return_value = _make_run_result("")
187184
lan_mouse.list_clients("/custom/lan-mouse")
188-
mock_run.assert_called_once_with("/custom/lan-mouse cli list")
185+
mock_run.assert_called_once_with(["/custom/lan-mouse", "cli", "list"])
189186

190187

191188
# ---------------------------------------------------------------------------
@@ -275,7 +272,7 @@ def test_oserror(self, mock_run):
275272
def test_uses_basename_of_custom_path(self, mock_run):
276273
mock_run.return_value = _make_run_result("", returncode=0)
277274
lan_mouse.is_running("/usr/local/bin/lan-mouse")
278-
mock_run.assert_called_once_with("pgrep -x lan-mouse")
275+
mock_run.assert_called_once_with(["pgrep", "-x", "lan-mouse"])
279276

280277

281278
# ---------------------------------------------------------------------------
@@ -288,7 +285,7 @@ class TestActivate:
288285
def test_success(self, mock_run):
289286
mock_run.return_value = _make_run_result("", returncode=0)
290287
assert lan_mouse.activate(0) is True
291-
mock_run.assert_called_once_with("lan-mouse cli activate 0")
288+
mock_run.assert_called_once_with(["lan-mouse", "cli", "activate", "0"])
292289

293290
@patch("lan_mouse._run")
294291
def test_failure(self, mock_run):
@@ -304,15 +301,15 @@ def test_timeout(self, mock_run):
304301
def test_custom_bin_path(self, mock_run):
305302
mock_run.return_value = _make_run_result("", returncode=0)
306303
lan_mouse.activate(3, "/opt/lm")
307-
mock_run.assert_called_once_with("/opt/lm cli activate 3")
304+
mock_run.assert_called_once_with(["/opt/lm", "cli", "activate", "3"])
308305

309306

310307
class TestDeactivate:
311308
@patch("lan_mouse._run")
312309
def test_success(self, mock_run):
313310
mock_run.return_value = _make_run_result("", returncode=0)
314311
assert lan_mouse.deactivate(1) is True
315-
mock_run.assert_called_once_with("lan-mouse cli deactivate 1")
312+
mock_run.assert_called_once_with(["lan-mouse", "cli", "deactivate", "1"])
316313

317314
@patch("lan_mouse._run")
318315
def test_failure(self, mock_run):
@@ -361,26 +358,33 @@ def test_custom_bin_without_uwsm(self, _mock):
361358

362359
class TestLaunch:
363360
@patch("subprocess.Popen")
364-
@patch("lan_mouse._host_cmd", side_effect=lambda c: c)
361+
@patch("lan_mouse._IN_FLATPAK", False)
365362
@patch("lan_mouse._resolve_launch_cmd", return_value="uwsm-app -- lan-mouse")
366-
def test_auto_detect(self, _resolve, _host, mock_popen):
363+
def test_auto_detect(self, _resolve, mock_popen):
367364
lan_mouse.launch()
368365
mock_popen.assert_called_once()
369-
assert mock_popen.call_args[0][0] == "uwsm-app -- lan-mouse"
366+
assert mock_popen.call_args[0][0] == ["uwsm-app", "--", "lan-mouse"]
370367

371368
@patch("subprocess.Popen")
372-
@patch("lan_mouse._host_cmd", side_effect=lambda c: c)
373-
def test_custom_command(self, _host, mock_popen):
369+
@patch("lan_mouse._IN_FLATPAK", False)
370+
def test_custom_command(self, mock_popen):
374371
lan_mouse.launch("my-custom-launcher")
375372
mock_popen.assert_called_once()
376-
assert mock_popen.call_args[0][0] == "my-custom-launcher"
373+
assert mock_popen.call_args[0][0] == ["my-custom-launcher"]
377374

378375
@patch("subprocess.Popen")
379-
@patch("lan_mouse._host_cmd", side_effect=lambda c: c)
376+
@patch("lan_mouse._IN_FLATPAK", False)
380377
@patch("lan_mouse._resolve_launch_cmd", return_value="lan-mouse")
381-
def test_whitespace_command_uses_auto(self, _resolve, _host, mock_popen):
378+
def test_whitespace_command_uses_auto(self, _resolve, mock_popen):
382379
lan_mouse.launch(" ")
383-
assert mock_popen.call_args[0][0] == "lan-mouse"
380+
assert mock_popen.call_args[0][0] == ["lan-mouse"]
381+
382+
@patch("subprocess.Popen")
383+
@patch("lan_mouse._IN_FLATPAK", True)
384+
@patch("lan_mouse._resolve_launch_cmd", return_value="lan-mouse")
385+
def test_flatpak_prepends_host_prefix(self, _resolve, mock_popen):
386+
lan_mouse.launch()
387+
assert mock_popen.call_args[0][0] == ["flatpak-spawn", "--host", "lan-mouse"]
384388

385389

386390
# ---------------------------------------------------------------------------
@@ -395,7 +399,7 @@ class TestKill:
395399
def test_kill_success_immediate(self, mock_run, mock_sleep, mock_running):
396400
mock_run.return_value = _make_run_result("", returncode=0)
397401
assert lan_mouse.kill() is True
398-
mock_run.assert_called_once_with("pkill -x lan-mouse")
402+
mock_run.assert_called_once_with(["pkill", "-x", "lan-mouse"])
399403
mock_sleep.assert_not_called()
400404

401405
@patch("time.sleep")
@@ -433,7 +437,7 @@ def test_kill_process_never_exits(self, mock_run, mock_sleep, mock_running):
433437
def test_kill_custom_bin_path(self, mock_run, mock_sleep, mock_running):
434438
mock_run.return_value = _make_run_result("", returncode=0)
435439
lan_mouse.kill("/usr/local/bin/lan-mouse")
436-
mock_run.assert_called_once_with("pkill -x lan-mouse")
440+
mock_run.assert_called_once_with(["pkill", "-x", "lan-mouse"])
437441

438442

439443
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)