akashcli 4.4.0__tar.gz → 4.5.0__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.
- {akashcli-4.4.0 → akashcli-4.5.0}/PKG-INFO +1 -1
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/cli.py +130 -50
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/relay.py +192 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/PKG-INFO +1 -1
- {akashcli-4.4.0 → akashcli-4.5.0}/pyproject.toml +1 -1
- {akashcli-4.4.0 → akashcli-4.5.0}/README.md +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/__init__.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/auth.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/client.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/config.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/crypto.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/env_manager.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/exceptions.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/exec.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/integrity.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/logger.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/models.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/p2p.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/packages.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/restore.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/snapshot_crypto.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/storage.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/terminal.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/uploader.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli/utils.py +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/SOURCES.txt +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/dependency_links.txt +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/entry_points.txt +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/requires.txt +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/akashcli.egg-info/top_level.txt +0 -0
- {akashcli-4.4.0 → akashcli-4.5.0}/setup.cfg +0 -0
|
@@ -179,9 +179,11 @@ def _print_help():
|
|
|
179
179
|
f" {C}pull{RS} {DM}<CODE>{RS} restore on any machine — no login needed",
|
|
180
180
|
f"",
|
|
181
181
|
f" {DM}REMOTE SHELL{RS}",
|
|
182
|
-
f" {C}shell{RS} {DM}[--device NAME]{RS} open a shell here,
|
|
182
|
+
f" {C}shell{RS} {DM}[--device NAME]{RS} open a shell here, on a device, or local",
|
|
183
|
+
f" {C}shell{RS} {DM}--code A3F7B2{RS} connect via one-time pairing code (no login)",
|
|
183
184
|
f" {C}run{RS} {DM}<CMD> [--device NAME]{RS} run a command locally or on a device",
|
|
184
|
-
f" {C}serve{RS} {DM}[--
|
|
185
|
+
f" {C}serve{RS} {DM}[--share]{RS} share this shell (prints a pairing code)",
|
|
186
|
+
f" {C}serve{RS} {DM}[--label NAME]{RS} register device permanently (needs login)",
|
|
185
187
|
f" {C}devices{RS} {DM}[--revoke NAME]{RS} list or revoke enrolled devices",
|
|
186
188
|
f"",
|
|
187
189
|
f" {DM}FILES{RS}",
|
|
@@ -966,27 +968,37 @@ def run(
|
|
|
966
968
|
reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
|
|
967
969
|
save: bool = typer.Option(False, "--save", help="Upload command output to cloud"),
|
|
968
970
|
save_name: Optional[str] = typer.Option(None, "--save-name", help="Filename for saved output"),
|
|
969
|
-
device: Optional[str] = typer.Option(None, "--device","-d",
|
|
971
|
+
device: Optional[str] = typer.Option(None, "--device", "-d",
|
|
972
|
+
help="Run on a device (set up with: akash serve --label NAME)"),
|
|
970
973
|
):
|
|
971
|
-
"""Run a command — on the server or on
|
|
974
|
+
"""Run a command — locally, on the server, or on an enrolled device.
|
|
972
975
|
|
|
973
|
-
|
|
976
|
+
No login needed:
|
|
977
|
+
akash run "df -h" runs locally
|
|
978
|
+
akash run "ls" --save runs locally, uploads output
|
|
974
979
|
|
|
975
|
-
|
|
976
|
-
akash run "
|
|
977
|
-
akash run "df -h" --device termux
|
|
978
|
-
akash run "
|
|
979
|
-
|
|
980
|
+
With login:
|
|
981
|
+
akash run "df -h" runs on server
|
|
982
|
+
akash run "df -h" --device termux runs on your phone
|
|
983
|
+
akash run "pip list" --save runs on server, saves output
|
|
984
|
+
|
|
985
|
+
To make a device reachable:
|
|
986
|
+
1. On that device: akash login --label termux && akash serve
|
|
987
|
+
2. From anywhere: akash run "cmd" --device termux
|
|
980
988
|
"""
|
|
981
989
|
from .exec import acquire_exec_token, run_command, _clear_cached_token
|
|
982
990
|
|
|
983
991
|
try:
|
|
984
992
|
api_key, base = _resolve_client(url)
|
|
985
993
|
|
|
986
|
-
# ──
|
|
994
|
+
# ── --device but no key ───────────────────────────────────────────
|
|
987
995
|
if device is not None and not api_key:
|
|
988
|
-
|
|
989
|
-
|
|
996
|
+
_warn("relay requires an API key.")
|
|
997
|
+
_p()
|
|
998
|
+
_info(f"To run on {_cy(device.strip() or 'a device')} you need to log in first:")
|
|
999
|
+
_info(f" {_cy('akash login')} save your API key on this machine")
|
|
1000
|
+
_info(f" {_cy('akash serve')} run on the target device to make it reachable")
|
|
1001
|
+
_p()
|
|
990
1002
|
raise typer.Exit(1)
|
|
991
1003
|
|
|
992
1004
|
# ── Device relay path ─────────────────────────────────────────────
|
|
@@ -1130,39 +1142,68 @@ def _build_ws_url_for_run(token: str, base_url: str) -> str:
|
|
|
1130
1142
|
def shell(
|
|
1131
1143
|
url: Optional[str] = typer.Option(None, "--url", help="Override server URL"),
|
|
1132
1144
|
reauth: bool = typer.Option(False, "--reauth", help="Force re-authentication"),
|
|
1133
|
-
device: Optional[str] = typer.Option(None, "--device","-d",
|
|
1145
|
+
device: Optional[str] = typer.Option(None, "--device", "-d",
|
|
1146
|
+
help="Device label to connect to (set up with: akash serve --label NAME)"),
|
|
1147
|
+
code: Optional[str] = typer.Option(None, "--code", "-c",
|
|
1148
|
+
help="Pairing code from `akash serve` — no login needed"),
|
|
1134
1149
|
):
|
|
1135
|
-
"""Open
|
|
1150
|
+
"""Open a shell — local, on a device, or via a one-time pairing code.
|
|
1136
1151
|
|
|
1137
|
-
|
|
1138
|
-
|
|
1152
|
+
No login needed:
|
|
1153
|
+
akash shell open a local bash shell
|
|
1154
|
+
akash serve get a pairing code, share it
|
|
1155
|
+
akash shell --code A3F7B2 connect using that code
|
|
1139
1156
|
|
|
1140
|
-
|
|
1141
|
-
akash shell
|
|
1142
|
-
akash shell --device termux
|
|
1143
|
-
akash shell --device
|
|
1144
|
-
akash shell --device # auto (works if only 1 device enrolled)
|
|
1157
|
+
With login:
|
|
1158
|
+
akash shell full server shell
|
|
1159
|
+
akash shell --device termux relay shell on enrolled device
|
|
1160
|
+
akash shell --device auto-pick enrolled device
|
|
1145
1161
|
"""
|
|
1146
1162
|
from .exec import acquire_exec_token, run_shell, _clear_cached_token
|
|
1163
|
+
import shutil as _sh, subprocess as _sp
|
|
1147
1164
|
|
|
1148
1165
|
try:
|
|
1149
1166
|
api_key, base = _resolve_client(url)
|
|
1150
1167
|
|
|
1151
|
-
# ──
|
|
1168
|
+
# ── Pairing code path (no login needed) ───────────────────────────
|
|
1169
|
+
if code is not None:
|
|
1170
|
+
from .relay import guest_connect_shell
|
|
1171
|
+
c = code.strip().upper()
|
|
1172
|
+
_p()
|
|
1173
|
+
_info(f"connecting with code {_cy(c)}…")
|
|
1174
|
+
try:
|
|
1175
|
+
exit_code = guest_connect_shell(base, c)
|
|
1176
|
+
except RuntimeError as e:
|
|
1177
|
+
_err(str(e))
|
|
1178
|
+
raise typer.Exit(1)
|
|
1179
|
+
_p()
|
|
1180
|
+
_info("session closed")
|
|
1181
|
+
if exit_code != 0:
|
|
1182
|
+
raise typer.Exit(exit_code)
|
|
1183
|
+
return
|
|
1184
|
+
|
|
1185
|
+
# ── --device flag but no key ──────────────────────────────────────
|
|
1152
1186
|
if device is not None and not api_key:
|
|
1153
|
-
|
|
1154
|
-
|
|
1187
|
+
_warn("relay requires an API key.")
|
|
1188
|
+
_p()
|
|
1189
|
+
_info(f"To connect to {_cy(device.strip() or 'a device')} you need to log in first:")
|
|
1190
|
+
_info(f" {_cy('akash login')} save your API key on this machine")
|
|
1191
|
+
_info(f" Or use a pairing code: {_cy('akash serve')} → {_cy('akash shell --code <CODE>')}")
|
|
1192
|
+
_p()
|
|
1155
1193
|
raise typer.Exit(1)
|
|
1156
1194
|
|
|
1157
1195
|
# ── Device relay path ─────────────────────────────────────────────
|
|
1158
1196
|
if device is not None:
|
|
1159
1197
|
from .relay import connect_shell
|
|
1160
1198
|
label = device.strip() if device.strip() else None
|
|
1161
|
-
|
|
1199
|
+
_p()
|
|
1200
|
+
_info(f"connecting to {_cy(label or 'auto')}…")
|
|
1162
1201
|
try:
|
|
1163
1202
|
exit_code = connect_shell(api_key, base, target=label)
|
|
1164
1203
|
except RuntimeError as e:
|
|
1165
1204
|
_err(str(e))
|
|
1205
|
+
_p()
|
|
1206
|
+
_info(f"Make sure the device is running: {_cy('akash serve')}")
|
|
1166
1207
|
raise typer.Exit(1)
|
|
1167
1208
|
_p()
|
|
1168
1209
|
_info("session closed")
|
|
@@ -1170,20 +1211,17 @@ def shell(
|
|
|
1170
1211
|
raise typer.Exit(exit_code)
|
|
1171
1212
|
return
|
|
1172
1213
|
|
|
1173
|
-
# ── No key → local
|
|
1214
|
+
# ── No key → silent local shell ───────────────────────────────────
|
|
1174
1215
|
if not api_key:
|
|
1175
|
-
import shutil as _sh
|
|
1176
|
-
print(_GUEST_WARNING)
|
|
1177
|
-
_p()
|
|
1178
1216
|
shell_bin = os.environ.get("SHELL") or _sh.which("bash") or _sh.which("sh") or "/bin/sh"
|
|
1179
|
-
_info(f"
|
|
1217
|
+
_info(f"local shell {_dim(shell_bin)} {_dim('· tip: run akash login to unlock remote shells')}")
|
|
1180
1218
|
_p()
|
|
1181
|
-
import subprocess as _sp
|
|
1182
1219
|
result = _sp.run([shell_bin])
|
|
1183
1220
|
_p()
|
|
1184
1221
|
_info("session closed")
|
|
1185
1222
|
raise typer.Exit(result.returncode)
|
|
1186
1223
|
|
|
1224
|
+
# ── Server shell ──────────────────────────────────────────────────
|
|
1187
1225
|
if reauth:
|
|
1188
1226
|
_clear_cached_token()
|
|
1189
1227
|
_info("cleared cached token — re-authenticating…")
|
|
@@ -1215,23 +1253,43 @@ def devices(
|
|
|
1215
1253
|
revoke: Optional[str] = typer.Option(None, "--revoke", "-r", help="Revoke (unenroll) a device by label"),
|
|
1216
1254
|
raw: bool = typer.Option(False, "--json", help="Raw JSON output"),
|
|
1217
1255
|
):
|
|
1218
|
-
"""List enrolled devices or revoke one.
|
|
1256
|
+
"""List enrolled devices — or revoke one.
|
|
1219
1257
|
|
|
1220
|
-
Each device you
|
|
1221
|
-
|
|
1258
|
+
Each device you run `akash serve` on appears here.
|
|
1259
|
+
Connect from anywhere with: akash shell --device NAME
|
|
1222
1260
|
|
|
1223
1261
|
Examples:
|
|
1224
1262
|
akash devices list all enrolled devices
|
|
1225
1263
|
akash devices --revoke laptop remove a device
|
|
1264
|
+
|
|
1265
|
+
Setup:
|
|
1266
|
+
akash login --label phone name this device
|
|
1267
|
+
akash serve register it as reachable
|
|
1226
1268
|
"""
|
|
1227
1269
|
try:
|
|
1228
|
-
api_key, base = _resolve_client(require_key=
|
|
1270
|
+
api_key, base = _resolve_client(require_key=False)
|
|
1229
1271
|
except typer.Exit:
|
|
1230
1272
|
raise
|
|
1231
1273
|
except Exception as e:
|
|
1232
1274
|
_err(str(e))
|
|
1233
1275
|
raise typer.Exit(1)
|
|
1234
1276
|
|
|
1277
|
+
if not api_key:
|
|
1278
|
+
_p()
|
|
1279
|
+
_info("no devices yet — here's how to set up remote access:")
|
|
1280
|
+
_p()
|
|
1281
|
+
print(f" {_DM}Step 1 — on each device you want to reach:{_RS}")
|
|
1282
|
+
print(f" {_cy('akash login --label termux')} {_DM}(name this device){_RS}")
|
|
1283
|
+
print(f" {_cy('akash serve')} {_DM}(keep running — makes it reachable){_RS}")
|
|
1284
|
+
_p()
|
|
1285
|
+
print(f" {_DM}Step 2 — from anywhere:{_RS}")
|
|
1286
|
+
print(f" {_cy('akash shell --device termux')} {_DM}(open a shell on it){_RS}")
|
|
1287
|
+
print(f" {_cy('akash run --device termux \"cmd\"')} {_DM}(run a command on it){_RS}")
|
|
1288
|
+
_p()
|
|
1289
|
+
_info(f"Run {_cy('akash login')} first to save your API key.")
|
|
1290
|
+
_p()
|
|
1291
|
+
raise typer.Exit(0)
|
|
1292
|
+
|
|
1235
1293
|
_hdrs = {"x-api-key": api_key, "Origin": "https://akash.cli"}
|
|
1236
1294
|
|
|
1237
1295
|
if revoke:
|
|
@@ -1297,22 +1355,33 @@ def serve(
|
|
|
1297
1355
|
daemon: bool = typer.Option(False, "--daemon", help="Run in background"),
|
|
1298
1356
|
status: bool = typer.Option(False, "--status", help="Show serve daemon status"),
|
|
1299
1357
|
stop: bool = typer.Option(False, "--stop", help="Stop background serve daemon"),
|
|
1358
|
+
share: bool = typer.Option(False, "--share", help="Guest mode: get a one-time code, no login needed"),
|
|
1300
1359
|
):
|
|
1301
|
-
"""Make this device reachable
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
akash serve
|
|
1309
|
-
akash serve --daemon
|
|
1310
|
-
akash serve --
|
|
1311
|
-
akash serve --
|
|
1312
|
-
akash serve --
|
|
1360
|
+
"""Make this device reachable for remote shell — with or without login.
|
|
1361
|
+
|
|
1362
|
+
No login needed (one-time pairing code):
|
|
1363
|
+
akash serve --share prints a code, share it with anyone
|
|
1364
|
+
akash shell --code A3F7B2 they connect using that code
|
|
1365
|
+
|
|
1366
|
+
With login (permanent, device enrolled):
|
|
1367
|
+
akash serve register this device permanently
|
|
1368
|
+
akash serve --daemon run as background daemon
|
|
1369
|
+
akash serve --label myphone use a custom label
|
|
1370
|
+
akash serve --status check daemon status
|
|
1371
|
+
akash serve --stop stop daemon
|
|
1313
1372
|
"""
|
|
1314
1373
|
from .relay import serve as _serve, serve_status, serve_stop
|
|
1315
1374
|
|
|
1375
|
+
# ── Guest / no-login mode ─────────────────────────────────────────────
|
|
1376
|
+
if share:
|
|
1377
|
+
from .relay import guest_serve
|
|
1378
|
+
_, base = _resolve_client(url)
|
|
1379
|
+
try:
|
|
1380
|
+
guest_serve(base)
|
|
1381
|
+
except KeyboardInterrupt:
|
|
1382
|
+
pass
|
|
1383
|
+
return
|
|
1384
|
+
|
|
1316
1385
|
if status:
|
|
1317
1386
|
st = serve_status()
|
|
1318
1387
|
if st["running"]:
|
|
@@ -1320,7 +1389,8 @@ def serve(
|
|
|
1320
1389
|
_ok(f"running {_dim('pid ' + pid_str)}")
|
|
1321
1390
|
_info(f"logs {st['log']}")
|
|
1322
1391
|
else:
|
|
1323
|
-
_info("not running
|
|
1392
|
+
_info("not running")
|
|
1393
|
+
_info(f"start with: {_cy('akash serve')} or {_cy('akash serve --daemon')}")
|
|
1324
1394
|
return
|
|
1325
1395
|
|
|
1326
1396
|
if stop:
|
|
@@ -1331,13 +1401,23 @@ def serve(
|
|
|
1331
1401
|
return
|
|
1332
1402
|
|
|
1333
1403
|
try:
|
|
1334
|
-
api_key, base = _resolve_client(url, require_key=
|
|
1404
|
+
api_key, base = _resolve_client(url, require_key=False)
|
|
1335
1405
|
except typer.Exit:
|
|
1336
1406
|
raise
|
|
1337
1407
|
except Exception as e:
|
|
1338
1408
|
_err(str(e))
|
|
1339
1409
|
raise typer.Exit(1)
|
|
1340
1410
|
|
|
1411
|
+
if not api_key:
|
|
1412
|
+
_p()
|
|
1413
|
+
_warn("not logged in.")
|
|
1414
|
+
_p()
|
|
1415
|
+
_info(f"For a quick one-time session (no login): {_cy('akash serve --share')}")
|
|
1416
|
+
_p()
|
|
1417
|
+
_info(f"To set up permanently: {_cy('akash login --label myphone')} then {_cy('akash serve')}")
|
|
1418
|
+
_p()
|
|
1419
|
+
raise typer.Exit(1)
|
|
1420
|
+
|
|
1341
1421
|
# Resolve label: flag > config > "default"
|
|
1342
1422
|
from .exec import _device_label as _cfg_label
|
|
1343
1423
|
resolved_label = (label or _cfg_label() or "default").strip()
|
|
@@ -535,3 +535,195 @@ def serve_stop() -> bool:
|
|
|
535
535
|
return True
|
|
536
536
|
except Exception:
|
|
537
537
|
return False
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
# ── Guest relay (no API key — pairing code) ───────────────────────────────────
|
|
541
|
+
|
|
542
|
+
def _guest_code_url(base: str) -> str:
|
|
543
|
+
return f"{base.rstrip('/')}/api/guest/relay/code"
|
|
544
|
+
|
|
545
|
+
def _guest_host_url(base: str, code: str) -> str:
|
|
546
|
+
return f"{_ws_base(base)}/api/guest/relay/host?code={code}"
|
|
547
|
+
|
|
548
|
+
def _guest_join_url(base: str, code: str) -> str:
|
|
549
|
+
return f"{_ws_base(base)}/api/guest/relay/join?code={code}"
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def guest_serve(base_url: str) -> None:
|
|
553
|
+
"""
|
|
554
|
+
Host a shell session with no API key.
|
|
555
|
+
Requests a pairing code from the server, prints it, then waits for
|
|
556
|
+
someone to connect with: akash shell --code <CODE>
|
|
557
|
+
"""
|
|
558
|
+
import httpx
|
|
559
|
+
import websockets.sync.client as _ws
|
|
560
|
+
|
|
561
|
+
# 1. Get a pairing code
|
|
562
|
+
try:
|
|
563
|
+
r = httpx.post(_guest_code_url(base_url), timeout=10)
|
|
564
|
+
r.raise_for_status()
|
|
565
|
+
code = r.json()["code"]
|
|
566
|
+
ttl = r.json().get("expires_in", 600)
|
|
567
|
+
except Exception as e:
|
|
568
|
+
print(f" {_R}✗{_RS} could not get pairing code: {e}", file=sys.stderr)
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
print()
|
|
572
|
+
print(f" {_G}◉{_RS} guest relay ready")
|
|
573
|
+
print()
|
|
574
|
+
print(f" Your pairing code: {_C}{_Y}{code}{_RS}")
|
|
575
|
+
print()
|
|
576
|
+
print(f" {_DM}On another machine, run:{_RS}")
|
|
577
|
+
print(f" {_C}akash shell --code {code}{_RS}")
|
|
578
|
+
print()
|
|
579
|
+
print(f" {_DM}Code expires in {ttl // 60} min · Ctrl-C to stop{_RS}")
|
|
580
|
+
print()
|
|
581
|
+
|
|
582
|
+
# 2. Connect to host endpoint and serve shell frames
|
|
583
|
+
while True:
|
|
584
|
+
try:
|
|
585
|
+
ws_url = _guest_host_url(base_url, code)
|
|
586
|
+
with _ws.connect(ws_url, open_timeout=15, close_timeout=5) as ws:
|
|
587
|
+
ack = json.loads(ws.recv())
|
|
588
|
+
if ack.get("type") == "error":
|
|
589
|
+
print(f" {_R}✗{_RS} {ack.get('message')}", file=sys.stderr)
|
|
590
|
+
return
|
|
591
|
+
print(f" {_DM}waiting for connection…{_RS}")
|
|
592
|
+
_run_relay_device(ws, f"guest:{code}")
|
|
593
|
+
print(f"\n {_DM}session ended — waiting for next connection…{_RS}\n")
|
|
594
|
+
except KeyboardInterrupt:
|
|
595
|
+
print(f"\n {_DM}guest serve stopped{_RS}")
|
|
596
|
+
break
|
|
597
|
+
except Exception as exc:
|
|
598
|
+
print(f" {_Y}!{_RS} relay disconnected ({exc}) — reconnecting…")
|
|
599
|
+
time.sleep(3)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def guest_connect_shell(base_url: str, code: str) -> int:
|
|
603
|
+
"""
|
|
604
|
+
Connect to a guest host by pairing code — no API key needed.
|
|
605
|
+
Returns the remote exit code.
|
|
606
|
+
"""
|
|
607
|
+
import websockets.sync.client as _ws
|
|
608
|
+
import select as _sel, signal
|
|
609
|
+
|
|
610
|
+
code = code.strip().upper()
|
|
611
|
+
ws_url = _guest_join_url(base_url, code)
|
|
612
|
+
|
|
613
|
+
from .exec import _get_terminal_size
|
|
614
|
+
rows, cols = _get_terminal_size()
|
|
615
|
+
|
|
616
|
+
_raw_ok = False
|
|
617
|
+
_old_settings = None
|
|
618
|
+
_stdin_fd = -1
|
|
619
|
+
try:
|
|
620
|
+
import tty, termios as _term
|
|
621
|
+
_stdin_fd = sys.stdin.fileno()
|
|
622
|
+
_old_settings = _term.tcgetattr(_stdin_fd)
|
|
623
|
+
tty.setraw(_stdin_fd)
|
|
624
|
+
_raw_ok = True
|
|
625
|
+
except Exception:
|
|
626
|
+
pass
|
|
627
|
+
|
|
628
|
+
def _restore():
|
|
629
|
+
if _raw_ok and _old_settings is not None:
|
|
630
|
+
try:
|
|
631
|
+
import termios as _t
|
|
632
|
+
_t.tcsetattr(_stdin_fd, _t.TCSADRAIN, _old_settings)
|
|
633
|
+
except Exception:
|
|
634
|
+
pass
|
|
635
|
+
|
|
636
|
+
exit_code = 1
|
|
637
|
+
try:
|
|
638
|
+
with _ws.connect(ws_url, open_timeout=15, close_timeout=10) as ws:
|
|
639
|
+
ack = json.loads(ws.recv())
|
|
640
|
+
if ack.get("type") == "error":
|
|
641
|
+
_restore()
|
|
642
|
+
raise RuntimeError(ack.get("message", "relay error"))
|
|
643
|
+
|
|
644
|
+
ws.send(json.dumps({
|
|
645
|
+
"type": "pty_run",
|
|
646
|
+
"command": os.environ.get("SHELL", "/bin/bash"),
|
|
647
|
+
"rows": rows, "cols": cols,
|
|
648
|
+
}))
|
|
649
|
+
|
|
650
|
+
stop = threading.Event()
|
|
651
|
+
|
|
652
|
+
def _stdin_pump():
|
|
653
|
+
try:
|
|
654
|
+
while not stop.is_set():
|
|
655
|
+
if _stdin_fd >= 0:
|
|
656
|
+
r = _sel.select([_stdin_fd], [], [], 0.05)[0]
|
|
657
|
+
if not r:
|
|
658
|
+
continue
|
|
659
|
+
chunk = os.read(_stdin_fd, 256)
|
|
660
|
+
else:
|
|
661
|
+
chunk = sys.stdin.buffer.read(1)
|
|
662
|
+
if not chunk:
|
|
663
|
+
break
|
|
664
|
+
ws.send(json.dumps({
|
|
665
|
+
"type": "pty_in",
|
|
666
|
+
"data": base64.b64encode(chunk).decode(),
|
|
667
|
+
}))
|
|
668
|
+
except Exception:
|
|
669
|
+
pass
|
|
670
|
+
|
|
671
|
+
def _on_resize(signum, _frame):
|
|
672
|
+
r, c = _get_terminal_size()
|
|
673
|
+
try:
|
|
674
|
+
ws.send(json.dumps({"type": "resize", "rows": r, "cols": c}))
|
|
675
|
+
except Exception:
|
|
676
|
+
pass
|
|
677
|
+
try:
|
|
678
|
+
signal.signal(signal.SIGWINCH, _on_resize)
|
|
679
|
+
except Exception:
|
|
680
|
+
pass
|
|
681
|
+
|
|
682
|
+
t = threading.Thread(target=_stdin_pump, daemon=True)
|
|
683
|
+
t.start()
|
|
684
|
+
|
|
685
|
+
while True:
|
|
686
|
+
try:
|
|
687
|
+
raw = ws.recv()
|
|
688
|
+
except Exception:
|
|
689
|
+
break
|
|
690
|
+
try:
|
|
691
|
+
frame = json.loads(raw)
|
|
692
|
+
except Exception:
|
|
693
|
+
continue
|
|
694
|
+
ftype = frame.get("type")
|
|
695
|
+
if ftype == "pty_out":
|
|
696
|
+
data = base64.b64decode(frame["data"])
|
|
697
|
+
if data:
|
|
698
|
+
sys.stdout.buffer.write(data)
|
|
699
|
+
sys.stdout.buffer.flush()
|
|
700
|
+
elif ftype == "exit":
|
|
701
|
+
exit_code = frame.get("code", 0)
|
|
702
|
+
stop.set()
|
|
703
|
+
break
|
|
704
|
+
elif ftype == "error":
|
|
705
|
+
stop.set()
|
|
706
|
+
_restore()
|
|
707
|
+
print(f"\r\n{_R}[error]{_RS} {frame.get('message','?')}", file=sys.stderr)
|
|
708
|
+
exit_code = 1
|
|
709
|
+
break
|
|
710
|
+
elif ftype == "disconnected":
|
|
711
|
+
stop.set()
|
|
712
|
+
break
|
|
713
|
+
elif ftype == "ping":
|
|
714
|
+
try:
|
|
715
|
+
ws.send(json.dumps({"type": "pong"}))
|
|
716
|
+
except Exception:
|
|
717
|
+
break
|
|
718
|
+
|
|
719
|
+
except KeyboardInterrupt:
|
|
720
|
+
pass
|
|
721
|
+
finally:
|
|
722
|
+
stop.set() if 'stop' in dir() else None
|
|
723
|
+
_restore()
|
|
724
|
+
try:
|
|
725
|
+
signal.signal(signal.SIGWINCH, signal.SIG_DFL)
|
|
726
|
+
except Exception:
|
|
727
|
+
pass
|
|
728
|
+
|
|
729
|
+
return exit_code
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|