Skip to content

Commit 8507fbb

Browse files
committed
Improve Windows SSH
1 parent 2968176 commit 8507fbb

2 files changed

Lines changed: 37 additions & 19 deletions

File tree

ptrlib/connection/ssh.py

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"""This package provides SSH function.
22
"""
3-
import shlex
43
import os
54
from ptrlib.os import which
65
from .proc import Process
@@ -10,11 +9,11 @@ def SSH(host: str,
109
username: str,
1110
*,
1211
port: int = 22,
13-
password: str | None=None,
14-
identity: str | None=None,
15-
ssh_path: str | None=None,
12+
password: str | None = None,
13+
identity: str | None = None,
14+
ssh_path: str | None = None,
1615
options: list[str] | None = None,
17-
command: str=''):
16+
command: str | None = None):
1817
"""Create an SSH shell
1918
2019
Create a new process to connect to SSH server
@@ -35,8 +34,7 @@ def SSH(host: str,
3534
FileNotFoundError: If SSH executable is not found.
3635
"""
3736
assert isinstance(port, int)
38-
if identity is None and password is None:
39-
raise ValueError("You must provide either password or identity")
37+
# Allow agent-based auth when neither password nor identity is provided.
4038
if ssh_path is None:
4139
ssh_path = which('ssh')
4240
if ssh_path is None or not os.path.isfile(ssh_path):
@@ -47,14 +45,19 @@ def SSH(host: str,
4745
if identity is not None:
4846
options += ['-i', os.path.realpath(os.path.expanduser(identity))]
4947

50-
sess = Process([
48+
argv: list[str] = [
5149
ssh_path,
5250
'-oStrictHostKeyChecking=no', '-oCheckHostIP=no',
53-
f'{shlex.quote(username)}@{shlex.quote(host)}',
51+
f'{username}@{host}',
5452
'-p', str(port),
5553
*options,
56-
command
57-
])
54+
]
55+
# Only append command if provided (avoid passing an empty argument which
56+
# would make ssh execute an empty remote command and close immediately).
57+
if command:
58+
argv.append(command)
59+
60+
sess = Process(argv)
5861
sess.prompt = ""
5962
if password is not None:
6063
sess.sendlineafter("password: ", password)

ptrlib/os/path.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,44 @@
11
import os
2+
import shutil
23
import subprocess
34

45

56
def which(s: str) -> str | None:
67
"""Cross-platform executable resolver.
78
8-
On POSIX, uses `which`; on Windows, uses `where.exe`.
9-
Returns the absolute path if found, otherwise None. If a path-like string
10-
with '/' (or '\\' on Windows) is provided, validates its existence.
9+
Prefer Python's shutil.which for robust PATH/PATHEXT handling.
10+
Falls back to platform utilities only if necessary.
11+
12+
Behavior:
13+
- If ``s`` contains a path separator, return it if it points to a file.
14+
- Else, search PATH using shutil.which and return the first resolved file.
15+
- On Windows, handles PATHEXT and avoids the multi-line pitfalls of ``where``.
1116
"""
1217
is_windows = os.name == 'nt'
1318

19+
# If caller provides a path-like string, just validate it
1420
if ('/' in s) or ('\\' in s):
1521
return s if os.path.isfile(s) else None
1622

23+
# Primary: shutil.which
24+
path = shutil.which(s)
25+
if path and os.path.isfile(path):
26+
return path
27+
28+
# Fallback: platform utilities (best-effort)
1729
try:
1830
if is_windows:
1931
out = subprocess.check_output(["where.exe", s])
2032
else:
2133
out = subprocess.check_output(["which", s])
22-
path = out.decode(errors='ignore').strip()
23-
return path if os.path.isfile(path) else None
24-
except subprocess.CalledProcessError:
25-
return None
34+
# Take the first non-empty existing path
35+
for line in out.decode(errors='ignore').splitlines():
36+
cand = line.strip().strip('\"')
37+
if cand and os.path.isfile(cand):
38+
return cand
39+
except Exception:
40+
pass
41+
return None
2642

2743

2844
__all__ = ['which']
29-

0 commit comments

Comments
 (0)