rrun-cli 0.1.1__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/PKG-INFO +1 -1
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/pyproject.toml +1 -1
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun/__init__.py +1 -1
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun/__main__.py +78 -72
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun/executor.py +12 -12
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun/registry.py +3 -3
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun/setup.py +8 -8
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun_cli.egg-info/PKG-INFO +1 -1
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/LICENSE +0 -0
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/README.md +0 -0
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/setup.cfg +0 -0
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun/remote-requirements.txt +0 -0
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun_cli.egg-info/SOURCES.txt +0 -0
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun_cli.egg-info/dependency_links.txt +0 -0
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun_cli.egg-info/entry_points.txt +0 -0
- {rrun_cli-0.1.1 → rrun_cli-0.1.2}/src/rrun_cli.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rrun-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner.
|
|
5
5
|
Author: waqiju
|
|
6
6
|
License-Expression: MIT
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "rrun-cli"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
|
-
"""rrun —
|
|
2
|
+
"""rrun CLI — run local scripts on remote machines over SSH stdin pipes.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
Core contract: script content (UTF-8, CJK welcome) is piped to the remote
|
|
5
|
+
interpreter via stdin, never via command-line arguments — sidestepping the
|
|
6
|
+
bash -> ssh -> cmd quoting/encoding gauntlet.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
Usage:
|
|
8
9
|
rrun exec <host> <script.py|.ps1|.sh> [ascii_args...]
|
|
9
|
-
rrun exec <host> -c "Write-Output
|
|
10
|
+
rrun exec <host> -c "Write-Output hello" # lang inferred from remote OS
|
|
10
11
|
rrun exec pc_build temp/x.py --timeout 60
|
|
11
12
|
rrun exec mac_mini --lang python temp/x.py
|
|
12
|
-
rrun setup <host|--all> [--force] #
|
|
13
|
-
rrun pip <host> -- list #
|
|
14
|
-
rrun machines #
|
|
15
|
-
rrun config #
|
|
16
|
-
rrun close [<host>|--all] #
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
13
|
+
rrun setup <host|--all> [--force] # provision the unified python env (3.12 venv)
|
|
14
|
+
rrun pip <host> -- list # run pip in the unified venv
|
|
15
|
+
rrun machines # list machines (redacted, with source)
|
|
16
|
+
rrun config # diagnose the machines.json source chain
|
|
17
|
+
rrun close [<host>|--all] # close ssh multiplexed connections
|
|
18
|
+
|
|
19
|
+
Conventions:
|
|
20
|
+
- Language inference: file extension (.py/.ps1/.sh) first, then remote OS
|
|
21
|
+
(Windows=powershell, others=bash).
|
|
22
|
+
- Command-line args are ASCII-only, passed through remotely
|
|
23
|
+
(python=sys.argv; bash=$@; powershell=$args); put CJK/special characters
|
|
24
|
+
in the script content or a JSON file instead.
|
|
25
|
+
- Remote python is pinned to 3.12: probes the unified venv (preferred),
|
|
26
|
+
the standalone base, then existing installs; all missing -> run `rrun setup`
|
|
27
|
+
first (exec never installs implicitly on the hot path).
|
|
28
|
+
- Exit codes: the remote exit code passes through unchanged;
|
|
29
|
+
255=ssh transport error; 124=local timeout.
|
|
30
|
+
- Inline -c content is archived under ~/.rrun/drops/ for replay.
|
|
31
|
+
- Audit: every execution appends to ~/.rrun/log/remote-exec.jsonl.
|
|
32
|
+
|
|
33
|
+
Remote stdout -> local stdout, stderr -> stderr; the exit code is the remote one.
|
|
29
34
|
"""
|
|
30
35
|
|
|
31
36
|
import argparse
|
|
@@ -45,7 +50,7 @@ def _parse_env(pairs) -> dict:
|
|
|
45
50
|
env = {}
|
|
46
51
|
for p in pairs or []:
|
|
47
52
|
if "=" not in p:
|
|
48
|
-
raise SystemExit(f"[remote-exec] --env
|
|
53
|
+
raise SystemExit(f"[remote-exec] --env expects KEY=VAL, got: {p!r}")
|
|
49
54
|
k, v = p.split("=", 1)
|
|
50
55
|
env[k] = v
|
|
51
56
|
return env
|
|
@@ -70,9 +75,9 @@ def cmd_exec(ns) -> int:
|
|
|
70
75
|
file = ns.script or ""
|
|
71
76
|
lang = ns.lang or ""
|
|
72
77
|
if content and file:
|
|
73
|
-
raise SystemExit("[remote-exec]
|
|
78
|
+
raise SystemExit("[remote-exec] give either a script file or -c/--content, not both")
|
|
74
79
|
if not content and not file:
|
|
75
|
-
raise SystemExit("[remote-exec]
|
|
80
|
+
raise SystemExit("[remote-exec] nothing to run: pass a script file or -c/--content")
|
|
76
81
|
|
|
77
82
|
# content 模式先确定 lang 再落盘(扩展名需要 lang)
|
|
78
83
|
script_for_log = file
|
|
@@ -81,7 +86,7 @@ def cmd_exec(ns) -> int:
|
|
|
81
86
|
lang = machine.default_lang
|
|
82
87
|
dropped = _drop_inline(machine.name, lang, content)
|
|
83
88
|
script_for_log = str(dropped)
|
|
84
|
-
print(f"[remote-exec]
|
|
89
|
+
print(f"[remote-exec] inline content archived to {dropped}", file=sys.stderr)
|
|
85
90
|
|
|
86
91
|
args = ns.args
|
|
87
92
|
|
|
@@ -106,11 +111,11 @@ def cmd_exec(ns) -> int:
|
|
|
106
111
|
sys.stderr.buffer.flush()
|
|
107
112
|
|
|
108
113
|
if result.timed_out:
|
|
109
|
-
print(f"[remote-exec]
|
|
114
|
+
print(f"[remote-exec] local timeout ({ns.timeout}s), ssh client killed", file=sys.stderr)
|
|
110
115
|
elif result.transport_error:
|
|
111
|
-
print(f"[remote-exec] ssh
|
|
116
|
+
print(f"[remote-exec] ssh transport error (unreachable / auth failure / dropped), exit=255", file=sys.stderr)
|
|
112
117
|
if not ns.quiet:
|
|
113
|
-
print(f"[remote-exec] exit={result.exit_code}
|
|
118
|
+
print(f"[remote-exec] exit={result.exit_code} took {result.duration:.1f}s", file=sys.stderr)
|
|
114
119
|
return result.exit_code
|
|
115
120
|
|
|
116
121
|
|
|
@@ -138,19 +143,20 @@ def cmd_machines(ns) -> int:
|
|
|
138
143
|
|
|
139
144
|
def cmd_config(ns) -> int:
|
|
140
145
|
infos = scan_sources()
|
|
141
|
-
print("machines.json
|
|
146
|
+
print("machines.json source chain (high -> low priority; same-name machines are")
|
|
147
|
+
print("overridden by higher-priority sources):")
|
|
142
148
|
raw_total = 0
|
|
143
149
|
for i, info in enumerate(infos, 1):
|
|
144
150
|
if not info.exists:
|
|
145
|
-
state = "-
|
|
151
|
+
state = "- missing"
|
|
146
152
|
elif info.error:
|
|
147
|
-
state = f"
|
|
153
|
+
state = f"x read failed: {info.error}"
|
|
148
154
|
else:
|
|
149
|
-
state = f"
|
|
155
|
+
state = f"ok, {info.machine_count} machines"
|
|
150
156
|
raw_total += info.machine_count
|
|
151
157
|
print(f" [{i}] {info.label:<22} {info.path} {state}")
|
|
152
158
|
merged = load_machines()
|
|
153
|
-
print(f"
|
|
159
|
+
print(f"{len(merged)} machines total ({raw_total} across all sources before same-name overrides)")
|
|
154
160
|
return 0
|
|
155
161
|
|
|
156
162
|
|
|
@@ -160,10 +166,10 @@ def cmd_setup(ns) -> int:
|
|
|
160
166
|
elif ns.host:
|
|
161
167
|
hosts = [ns.host]
|
|
162
168
|
else:
|
|
163
|
-
raise SystemExit("[setup]
|
|
169
|
+
raise SystemExit("[setup] specify a host or --all")
|
|
164
170
|
results = []
|
|
165
171
|
if len(hosts) == 1:
|
|
166
|
-
print(f"[setup] {hosts[0]}
|
|
172
|
+
print(f"[setup] provisioning {hosts[0]} ...", file=sys.stderr)
|
|
167
173
|
results.append(setup_machine(hosts[0], force=ns.force))
|
|
168
174
|
else:
|
|
169
175
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
@@ -173,17 +179,17 @@ def cmd_setup(ns) -> int:
|
|
|
173
179
|
r = f.result()
|
|
174
180
|
results.append(r)
|
|
175
181
|
print(f"[setup] {r.host}: {'ok' if r.ok else 'FAIL'} ({r.duration:.0f}s)", file=sys.stderr)
|
|
176
|
-
print(f"\n{'host':<24} {'
|
|
182
|
+
print(f"\n{'host':<24} {'result':<6} {'python':<10} {'venv':<46} note")
|
|
177
183
|
for r in sorted(results, key=lambda x: x.host):
|
|
178
184
|
if r.ok:
|
|
179
185
|
note = []
|
|
180
186
|
if r.installed_standalone:
|
|
181
|
-
note.append("
|
|
187
|
+
note.append("standalone installed")
|
|
182
188
|
if r.created_venv:
|
|
183
|
-
note.append("
|
|
189
|
+
note.append("venv created")
|
|
184
190
|
if not note:
|
|
185
|
-
note.append("
|
|
186
|
-
print(f"{r.host:<24} {'ok':<6} {r.version:<10} {r.venv_python:<46} {'
|
|
191
|
+
note.append("already present, verified/topped-up deps")
|
|
192
|
+
print(f"{r.host:<24} {'ok':<6} {r.version:<10} {r.venv_python:<46} {', '.join(note)}")
|
|
187
193
|
else:
|
|
188
194
|
print(f"{r.host:<24} {'FAIL':<6} {'':<10} {'':<46} {r.message[:80]}")
|
|
189
195
|
return 0 if all(r.ok for r in results) else 1
|
|
@@ -196,9 +202,9 @@ def cmd_pip(ns) -> int:
|
|
|
196
202
|
raise SystemExit(f"[pip] {e}")
|
|
197
203
|
args = list(ns.pargs or []) + list(getattr(ns, "args", []) or [])
|
|
198
204
|
if not args:
|
|
199
|
-
raise SystemExit("[pip]
|
|
205
|
+
raise SystemExit("[pip] missing pip args, e.g.: rrun pip <host> -- list")
|
|
200
206
|
try:
|
|
201
|
-
_check_ascii(args, "pip
|
|
207
|
+
_check_ascii(args, "pip args")
|
|
202
208
|
py = venv_python_or_die(ns.host)
|
|
203
209
|
except (ValueError, RuntimeError) as e:
|
|
204
210
|
raise SystemExit(f"[pip] {e}")
|
|
@@ -213,7 +219,7 @@ def cmd_pip(ns) -> int:
|
|
|
213
219
|
sys.stdout.buffer.flush()
|
|
214
220
|
sys.stderr.buffer.write(err)
|
|
215
221
|
sys.stderr.buffer.flush()
|
|
216
|
-
print(f"[pip] {machine.name} exit={rc}
|
|
222
|
+
print(f"[pip] {machine.name} exit={rc} took {time.time() - t0:.1f}s", file=sys.stderr)
|
|
217
223
|
return rc
|
|
218
224
|
|
|
219
225
|
|
|
@@ -223,7 +229,7 @@ def cmd_close(ns) -> int:
|
|
|
223
229
|
elif ns.host:
|
|
224
230
|
hosts = [ns.host]
|
|
225
231
|
else:
|
|
226
|
-
raise SystemExit("[remote-exec] close
|
|
232
|
+
raise SystemExit("[remote-exec] close needs a host or --all")
|
|
227
233
|
rc = 0
|
|
228
234
|
for h in hosts:
|
|
229
235
|
try:
|
|
@@ -241,47 +247,47 @@ def main() -> None:
|
|
|
241
247
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
242
248
|
sub = ap.add_subparsers(dest="subcmd", required=True)
|
|
243
249
|
|
|
244
|
-
ep = sub.add_parser("exec", help="
|
|
245
|
-
ep.add_argument("host", help="
|
|
246
|
-
ep.add_argument("script", nargs="?", help="
|
|
250
|
+
ep = sub.add_parser("exec", help="execute a local script / inline content on a remote machine")
|
|
251
|
+
ep.add_argument("host", help="machine name/ip/hostname (see the machines subcommand)")
|
|
252
|
+
ep.add_argument("script", nargs="?", help="local script path (.py/.ps1/.sh, UTF-8)")
|
|
247
253
|
ep.add_argument("--lang", choices=["bash", "powershell", "python"],
|
|
248
|
-
help="
|
|
249
|
-
ep.add_argument("-c", "--content", help="
|
|
254
|
+
help="remote interpreter language (default: by extension, else by remote OS)")
|
|
255
|
+
ep.add_argument("-c", "--content", help="inline script content (archived to ~/.rrun/drops/)")
|
|
250
256
|
ep.add_argument("args", nargs="*",
|
|
251
|
-
help="
|
|
252
|
-
ep.add_argument("--workdir", help="
|
|
257
|
+
help="script arguments (ASCII only; use -- to separate from options)")
|
|
258
|
+
ep.add_argument("--workdir", help="remote working directory (bash/powershell only)")
|
|
253
259
|
ep.add_argument("--env", action="append", metavar="KEY=VAL",
|
|
254
|
-
help="
|
|
255
|
-
ep.add_argument("--timeout", type=float, help="
|
|
256
|
-
ep.add_argument("--python", dest="python", help="
|
|
257
|
-
ep.add_argument("--no-utf8", action="store_true", help="
|
|
258
|
-
ep.add_argument("--no-mux", action="store_true", help="
|
|
259
|
-
ep.add_argument("-q", "--quiet", action="store_true", help="
|
|
260
|
+
help="remote environment variable (repeatable; ASCII only)")
|
|
261
|
+
ep.add_argument("--timeout", type=float, help="local timeout in seconds (exit=124 on expiry)")
|
|
262
|
+
ep.add_argument("--python", dest="python", help="remote python path (skips auto-detection)")
|
|
263
|
+
ep.add_argument("--no-utf8", action="store_true", help="do not pass -X utf8 to remote python")
|
|
264
|
+
ep.add_argument("--no-mux", action="store_true", help="disable ssh ControlMaster multiplexing")
|
|
265
|
+
ep.add_argument("-q", "--quiet", action="store_true", help="suppress [remote-exec] info lines")
|
|
260
266
|
ep.set_defaults(func=cmd_exec)
|
|
261
267
|
|
|
262
|
-
mp = sub.add_parser("machines", help="
|
|
268
|
+
mp = sub.add_parser("machines", help="list machines merged from all sources (redacted, with source)")
|
|
263
269
|
mp.add_argument("--json", action="store_true")
|
|
264
270
|
mp.set_defaults(func=cmd_machines)
|
|
265
271
|
|
|
266
|
-
cf = sub.add_parser("config", help="
|
|
272
|
+
cf = sub.add_parser("config", help="show the machines.json source chain (which files apply, how many machines each)")
|
|
267
273
|
cf.set_defaults(func=cmd_config)
|
|
268
274
|
|
|
269
|
-
sp = sub.add_parser("setup", help="
|
|
270
|
-
sp.add_argument("host", nargs="?", help="
|
|
271
|
-
sp.add_argument("--all", action="store_true", help="
|
|
272
|
-
sp.add_argument("--force", action="store_true", help="
|
|
273
|
-
sp.add_argument("--jobs", type=int, default=6, help="--all
|
|
275
|
+
sp = sub.add_parser("setup", help="provision the unified remote python environment (3.12 venv)")
|
|
276
|
+
sp.add_argument("host", nargs="?", help="machine name/ip; use --all for every machine")
|
|
277
|
+
sp.add_argument("--all", action="store_true", help="run against all machines in machines.json")
|
|
278
|
+
sp.add_argument("--force", action="store_true", help="recreate the venv (keeps the python base)")
|
|
279
|
+
sp.add_argument("--jobs", type=int, default=6, help="concurrency for --all (default 6)")
|
|
274
280
|
sp.set_defaults(func=cmd_setup)
|
|
275
281
|
|
|
276
|
-
pp = sub.add_parser("pip", help="
|
|
277
|
-
pp.add_argument("host", help="
|
|
278
|
-
pp.add_argument("pargs", nargs="*", help="pip
|
|
279
|
-
pp.add_argument("--timeout", type=float, default=300.0, help="
|
|
282
|
+
pp = sub.add_parser("pip", help="run pip inside the remote unified venv (ad-hoc installs)")
|
|
283
|
+
pp.add_argument("host", help="machine name/ip")
|
|
284
|
+
pp.add_argument("pargs", nargs="*", help="pip arguments; put options starting with - after --")
|
|
285
|
+
pp.add_argument("--timeout", type=float, default=300.0, help="local timeout in seconds (default 300)")
|
|
280
286
|
pp.set_defaults(func=cmd_pip)
|
|
281
287
|
|
|
282
|
-
cp = sub.add_parser("close", help="
|
|
283
|
-
cp.add_argument("host", nargs="?", help="
|
|
284
|
-
cp.add_argument("--all", action="store_true", help="
|
|
288
|
+
cp = sub.add_parser("close", help="close ssh ControlMaster multiplexed connections")
|
|
289
|
+
cp.add_argument("host", nargs="?", help="machine name/ip; omit with --all")
|
|
290
|
+
cp.add_argument("--all", action="store_true", help="close connections for all machines")
|
|
285
291
|
cp.set_defaults(func=cmd_close)
|
|
286
292
|
|
|
287
293
|
# argparse 对 -- 的处理与子解析器/位置参数组合有 quirk,手动切分更可靠:
|
|
@@ -113,7 +113,7 @@ class ExecResult:
|
|
|
113
113
|
def _check_ascii(values, what: str) -> None:
|
|
114
114
|
for v in values:
|
|
115
115
|
if not all(ord(c) < 128 for c in v):
|
|
116
|
-
raise ValueError(f"{what}
|
|
116
|
+
raise ValueError(f"{what} must be ASCII-only, got: {v!r}; put CJK/special characters in the script content or a JSON file, not on the command line")
|
|
117
117
|
|
|
118
118
|
|
|
119
119
|
def _ps_quote(s: str) -> str:
|
|
@@ -123,7 +123,7 @@ def _ps_quote(s: str) -> str:
|
|
|
123
123
|
|
|
124
124
|
def build_ps_wrapper(code: str, args=(), workdir: str = "", env: "dict | None" = None) -> bytes:
|
|
125
125
|
"""把 PowerShell 脚本编码为单行纯 ASCII wrapper(-Command - 按行执行,禁止多行)。"""
|
|
126
|
-
_check_ascii(args, "
|
|
126
|
+
_check_ascii(args, "arguments")
|
|
127
127
|
parts = [
|
|
128
128
|
"[Console]::OutputEncoding=[Text.Encoding]::UTF8",
|
|
129
129
|
"$OutputEncoding=[Text.Encoding]::UTF8",
|
|
@@ -157,7 +157,7 @@ def _posix_prefix(workdir: str = "", env: "dict | None" = None) -> str:
|
|
|
157
157
|
|
|
158
158
|
|
|
159
159
|
def build_bash_command(args=(), workdir: str = "", env: "dict | None" = None) -> str:
|
|
160
|
-
_check_ascii(args, "
|
|
160
|
+
_check_ascii(args, "arguments")
|
|
161
161
|
cmd = _posix_prefix(workdir, env) + "bash -s"
|
|
162
162
|
if args:
|
|
163
163
|
cmd += " -- " + " ".join(shlex.quote(a) for a in args)
|
|
@@ -169,7 +169,7 @@ def _ssh_run(machine: Machine, remote_cmd: str, stdin: bytes, timeout: "float |
|
|
|
169
169
|
"""返回 (exit_code, stdout, stderr, timed_out)。exit 255 即传输层错误。"""
|
|
170
170
|
sshpass = shutil.which("sshpass")
|
|
171
171
|
if not sshpass:
|
|
172
|
-
raise RuntimeError("
|
|
172
|
+
raise RuntimeError("sshpass not found on this machine (install: sudo apt install sshpass / brew install hudochenkov/sshpass/sshpass)")
|
|
173
173
|
if mux:
|
|
174
174
|
CONTROL_PATH_DIR.mkdir(mode=0o700, exist_ok=True)
|
|
175
175
|
args = [sshpass, "-p", machine.password, "ssh", *SSH_BASE_OPTS]
|
|
@@ -181,7 +181,7 @@ def _ssh_run(machine: Machine, remote_cmd: str, stdin: bytes, timeout: "float |
|
|
|
181
181
|
timeout=timeout if timeout and timeout > 0 else None)
|
|
182
182
|
return p.returncode, p.stdout, p.stderr, False
|
|
183
183
|
except subprocess.TimeoutExpired as e:
|
|
184
|
-
return EXIT_TIMEOUT, e.stdout or b"", (e.stderr or b"") + f"\n[remote-exec]
|
|
184
|
+
return EXIT_TIMEOUT, e.stdout or b"", (e.stderr or b"") + f"\n[remote-exec] local timeout {timeout}s\n".encode(), True
|
|
185
185
|
|
|
186
186
|
|
|
187
187
|
# 注意:代码内不得出现任何引号(bash 探测用单引号包裹此代码;chr(46)='.')
|
|
@@ -259,8 +259,8 @@ def detect_remote_python(machine: Machine, override: str = "", mux: bool = True)
|
|
|
259
259
|
found = probe_python(machine, candidates, mux)
|
|
260
260
|
if not found:
|
|
261
261
|
raise RuntimeError(
|
|
262
|
-
f"[{machine.name}]
|
|
263
|
-
f"
|
|
262
|
+
f"[{machine.name}] no python {REQUIRED_PY_MAJOR_MINOR[0]}.{REQUIRED_PY_MAJOR_MINOR[1]}"
|
|
263
|
+
f" found (baseline is pinned). Provision first: rrun setup {machine.name}"
|
|
264
264
|
)
|
|
265
265
|
result = found
|
|
266
266
|
_python_cache[key] = result
|
|
@@ -278,9 +278,9 @@ def run(host: str, lang: str = "", file: str = "", content: str = "", args=(),
|
|
|
278
278
|
"""
|
|
279
279
|
machine = resolve_machine(host)
|
|
280
280
|
if file and content:
|
|
281
|
-
raise ValueError("file
|
|
281
|
+
raise ValueError("pass either file or content, not both")
|
|
282
282
|
if not file and not content:
|
|
283
|
-
raise ValueError("
|
|
283
|
+
raise ValueError("file or content is required")
|
|
284
284
|
|
|
285
285
|
if file:
|
|
286
286
|
script_path = str(Path(file).resolve())
|
|
@@ -293,8 +293,8 @@ def run(host: str, lang: str = "", file: str = "", content: str = "", args=(),
|
|
|
293
293
|
if not lang:
|
|
294
294
|
lang = EXT_TO_LANG.get(suffix, machine.default_lang)
|
|
295
295
|
if lang not in LANGS:
|
|
296
|
-
raise ValueError(f"
|
|
297
|
-
_check_ascii(args, "
|
|
296
|
+
raise ValueError(f"unsupported language: {lang} (choose from {LANGS})")
|
|
297
|
+
_check_ascii(args, "arguments")
|
|
298
298
|
_check_ascii([x for kv in (env or {}).items() for x in kv], "env")
|
|
299
299
|
|
|
300
300
|
t0 = time.time()
|
|
@@ -313,7 +313,7 @@ def run(host: str, lang: str = "", file: str = "", content: str = "", args=(),
|
|
|
313
313
|
py_cmd += " " + " ".join(shlex.quote(a) for a in args)
|
|
314
314
|
if env or workdir:
|
|
315
315
|
if machine.is_windows:
|
|
316
|
-
raise ValueError("Windows
|
|
316
|
+
raise ValueError("workdir/env are not supported for Windows+python yet; use os.chdir/os.environ inside the script")
|
|
317
317
|
remote_cmd = _posix_prefix(workdir, env) + py_cmd
|
|
318
318
|
else:
|
|
319
319
|
remote_cmd = py_cmd
|
|
@@ -158,6 +158,6 @@ def resolve_machine(host: str, path: "str | os.PathLike | None" = None) -> Machi
|
|
|
158
158
|
for m in machines:
|
|
159
159
|
if host and host in (m.name, m.ip, m.hostname):
|
|
160
160
|
return m
|
|
161
|
-
available = ", ".join(f"{m.name}({m.ip})" for m in machines) or "
|
|
162
|
-
sources = ", ".join(str(p) for _lbl, p in candidate_sources() if p.is_file()) or "
|
|
163
|
-
raise KeyError(f"
|
|
161
|
+
available = ", ".join(f"{m.name}({m.ip})" for m in machines) or "<none>"
|
|
162
|
+
sources = ", ".join(str(p) for _lbl, p in candidate_sources() if p.is_file()) or "<no sources available>"
|
|
163
|
+
raise KeyError(f"machine [{host}] not found. Scanned sources: {sources}. Available: {available}")
|
|
@@ -103,7 +103,7 @@ def _ensure_local_tarball(os_name: str, arch: str) -> Path:
|
|
|
103
103
|
return path
|
|
104
104
|
CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
105
105
|
url = PBS_DOWNLOAD_BASE + name
|
|
106
|
-
print(f"[setup]
|
|
106
|
+
print(f"[setup] local cache miss, downloading {url} ...", file=sys.stderr)
|
|
107
107
|
tmp = path.with_suffix(".part")
|
|
108
108
|
urllib.request.urlretrieve(url, tmp) # noqa: S310 - 固定官方源
|
|
109
109
|
tmp.rename(path)
|
|
@@ -128,7 +128,7 @@ def _install_standalone(machine: Machine, mux: bool, timeout: float) -> str:
|
|
|
128
128
|
arch = out.decode("utf-8", "replace").strip()
|
|
129
129
|
os_name = "Mac"
|
|
130
130
|
if (os_name, arch) not in PBS_ASSETS:
|
|
131
|
-
raise RuntimeError(f"[{machine.name}]
|
|
131
|
+
raise RuntimeError(f"[{machine.name}] unsupported architecture: {arch}")
|
|
132
132
|
extract_cmd = (
|
|
133
133
|
'tools="$HOME/.remote-machine"; mkdir -p "$tools" '
|
|
134
134
|
'&& rm -rf "$tools/python312" '
|
|
@@ -137,15 +137,15 @@ def _install_standalone(machine: Machine, mux: bool, timeout: float) -> str:
|
|
|
137
137
|
)
|
|
138
138
|
base_py = BASE_PY_POSIX
|
|
139
139
|
tarball = _ensure_local_tarball(os_name, arch)
|
|
140
|
-
print(f"[setup] [{machine.name}]
|
|
140
|
+
print(f"[setup] [{machine.name}] streaming standalone python ({tarball.name}, "
|
|
141
141
|
f"{tarball.stat().st_size // 1024 // 1024}MB) ...", file=sys.stderr)
|
|
142
142
|
rc, _, err, timed_out = _ssh_run(machine, extract_cmd, tarball.read_bytes(), timeout, mux)
|
|
143
143
|
if rc != 0:
|
|
144
|
-
raise RuntimeError(f"[{machine.name}] standalone
|
|
144
|
+
raise RuntimeError(f"[{machine.name}] standalone push/extract failed (exit={rc}): "
|
|
145
145
|
f"{err.decode('utf-8', 'replace')[:300]}")
|
|
146
146
|
ver = probe_python_version(machine, base_py, mux)
|
|
147
147
|
if not ver:
|
|
148
|
-
raise RuntimeError(f"[{machine.name}] standalone
|
|
148
|
+
raise RuntimeError(f"[{machine.name}] standalone python failed verification after extraction: {base_py}")
|
|
149
149
|
return base_py
|
|
150
150
|
|
|
151
151
|
|
|
@@ -230,11 +230,11 @@ def setup_machine(host: str, force: bool = False, mux: bool = True,
|
|
|
230
230
|
rc, out, err, _ = _ssh_run(machine, "bash -s", payload, timeout, mux)
|
|
231
231
|
if rc != 0:
|
|
232
232
|
tail = (err or out).decode("utf-8", "replace").strip()[-400:]
|
|
233
|
-
raise RuntimeError(f"setup
|
|
233
|
+
raise RuntimeError(f"setup script failed (exit={rc}): {tail}")
|
|
234
234
|
|
|
235
235
|
final = probe_python(machine, [venv_py], mux)
|
|
236
236
|
if not final:
|
|
237
|
-
raise RuntimeError("setup
|
|
237
|
+
raise RuntimeError("setup finished but the venv python probe failed")
|
|
238
238
|
result.venv_python, result.version = final
|
|
239
239
|
result.packages = reqs
|
|
240
240
|
result.ok = True
|
|
@@ -272,5 +272,5 @@ def venv_python_or_die(host: str, mux: bool = True) -> str:
|
|
|
272
272
|
venv_py = VENV_PY_WIN if machine.is_windows else VENV_PY_POSIX
|
|
273
273
|
hit = probe_python(machine, [venv_py], mux)
|
|
274
274
|
if not hit:
|
|
275
|
-
raise RuntimeError(f"[{machine.name}]
|
|
275
|
+
raise RuntimeError(f"[{machine.name}] unified venv not provisioned yet; run first: rrun setup {machine.name}")
|
|
276
276
|
return hit[0]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: rrun-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Run local scripts on remote machines over SSH via stdin pipes — no escaping/encoding hell. Supports python/powershell/bash, with a unified remote Python 3.12 venv provisioner.
|
|
5
5
|
Author: waqiju
|
|
6
6
|
License-Expression: MIT
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|