nullgate 1.1.0__py3-none-any.whl
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.
- nullgate/__init__.py +1 -0
- nullgate/account.py +23 -0
- nullgate/bridge.py +162 -0
- nullgate/client_config.py +116 -0
- nullgate/commands.py +172 -0
- nullgate/gateway.py +897 -0
- nullgate/ingress.py +206 -0
- nullgate/runtime.py +236 -0
- nullgate/session.py +509 -0
- nullgate/transports.py +406 -0
- nullgate/wsroute.py +483 -0
- nullgate-1.1.0.dist-info/METADATA +10 -0
- nullgate-1.1.0.dist-info/RECORD +17 -0
- nullgate-1.1.0.dist-info/WHEEL +5 -0
- nullgate-1.1.0.dist-info/entry_points.txt +2 -0
- nullgate-1.1.0.dist-info/licenses/LICENSE +21 -0
- nullgate-1.1.0.dist-info/top_level.txt +1 -0
nullgate/session.py
ADDED
|
@@ -0,0 +1,509 @@
|
|
|
1
|
+
"""Session lifecycle orchestration for Nullgate.
|
|
2
|
+
|
|
3
|
+
Provides handlers for open, shut, inspect, enter, trace, cycle, and upgrade.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import argparse
|
|
9
|
+
import importlib.metadata
|
|
10
|
+
import os
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from nullgate import __version__, transports, wsroute
|
|
19
|
+
from nullgate.account import current_username
|
|
20
|
+
import nullgate.runtime as st
|
|
21
|
+
|
|
22
|
+
DEFAULT_PORT = 4822
|
|
23
|
+
DEFAULT_SLOT = 1
|
|
24
|
+
DISTRIBUTION_URL = "https://shadowdocks.github.io/nullgate-dist"
|
|
25
|
+
INSTALLER_URL = f"{DISTRIBUTION_URL}/install.sh"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _version() -> str:
|
|
29
|
+
try:
|
|
30
|
+
return importlib.metadata.version("nullgate")
|
|
31
|
+
except Exception:
|
|
32
|
+
return __version__
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def die(message: str) -> None:
|
|
36
|
+
raise SystemExit(message)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _resolve_launch_settings(
|
|
40
|
+
args: Any, saved: dict[str, Any] | None = None
|
|
41
|
+
) -> tuple[Path, int, int]:
|
|
42
|
+
saved = saved or {}
|
|
43
|
+
root_arg = getattr(args, "workspace", None) or saved.get("root") or "."
|
|
44
|
+
port_arg = getattr(args, "port", None) or saved.get("port") or DEFAULT_PORT
|
|
45
|
+
slot_arg = getattr(args, "slot", None) or saved.get("slot") or DEFAULT_SLOT
|
|
46
|
+
|
|
47
|
+
port_text = str(port_arg)
|
|
48
|
+
if not port_text.isdigit() or not 1 <= int(port_text) <= 65535:
|
|
49
|
+
die("Port must be an integer between 1 and 65535")
|
|
50
|
+
port = int(port_text)
|
|
51
|
+
|
|
52
|
+
if not str(slot_arg).isdigit() or int(slot_arg) < 1:
|
|
53
|
+
die("Slot must be a positive integer")
|
|
54
|
+
slot = int(slot_arg)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
root = Path(root_arg).expanduser().resolve(strict=True)
|
|
58
|
+
except OSError:
|
|
59
|
+
die(f"Workspace root directory does not exist: {root_arg}")
|
|
60
|
+
|
|
61
|
+
return root, port, slot
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class _Color:
|
|
65
|
+
def __init__(self, enabled: bool) -> None:
|
|
66
|
+
self._on = enabled
|
|
67
|
+
|
|
68
|
+
def _wrap(self, code: str, text: str) -> str:
|
|
69
|
+
return f"\x1b[{code}m{text}\x1b[0m" if self._on else text
|
|
70
|
+
|
|
71
|
+
def green(self, text: str) -> str:
|
|
72
|
+
return self._wrap("32", text)
|
|
73
|
+
|
|
74
|
+
def red(self, text: str) -> str:
|
|
75
|
+
return self._wrap("31", text)
|
|
76
|
+
|
|
77
|
+
def dim(self, text: str) -> str:
|
|
78
|
+
return self._wrap("2", text)
|
|
79
|
+
|
|
80
|
+
def bold(self, text: str) -> str:
|
|
81
|
+
return self._wrap("1", text)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _colorizer() -> _Color:
|
|
85
|
+
return _Color(sys.stdout.isatty())
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _proc_row(color: _Color, running: bool, pid: int | None) -> str:
|
|
89
|
+
if running:
|
|
90
|
+
return color.green(f"running pid {pid}")
|
|
91
|
+
return color.red("stopped")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _print_url_row(color: _Color, known: bool, url: str) -> None:
|
|
95
|
+
del color
|
|
96
|
+
if known:
|
|
97
|
+
print(f" url {url}")
|
|
98
|
+
else:
|
|
99
|
+
print(" url pending " + _Color(sys.stdout.isatty()).dim("(nullgate trace transport -f)"))
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _print_auth_rows(color: _Color, state: Path) -> None:
|
|
103
|
+
accept = st.manifest_get("accept")
|
|
104
|
+
allow = st.manifest_get("allow_tcp_forwarding")
|
|
105
|
+
if accept == "1":
|
|
106
|
+
print(" auth none " + color.dim("(--accept); anyone can connect"))
|
|
107
|
+
else:
|
|
108
|
+
password_file = state / "password"
|
|
109
|
+
if password_file.is_file():
|
|
110
|
+
print(f" auth {current_username()} / {password_file.read_text()}")
|
|
111
|
+
keys = Path(os.path.expanduser("~/.ssh/authorized_keys"))
|
|
112
|
+
if keys.is_file() and keys.stat().st_size > 0:
|
|
113
|
+
print(" keys enabled")
|
|
114
|
+
else:
|
|
115
|
+
print(" keys disabled " + color.dim("(add keys to ~/.ssh/authorized_keys)"))
|
|
116
|
+
if allow == "1":
|
|
117
|
+
print(" forward enabled")
|
|
118
|
+
else:
|
|
119
|
+
print(" forward disabled " + color.dim("(--allow-tcp-forwarding)"))
|
|
120
|
+
confine = st.manifest_get("confine_sftp")
|
|
121
|
+
if not confine:
|
|
122
|
+
confine = st.read_settings().get("confine_sftp")
|
|
123
|
+
if confine == "1":
|
|
124
|
+
print(" sftp confined " + color.dim("(--confine-sftp)"))
|
|
125
|
+
else:
|
|
126
|
+
print(" sftp host")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _setup_line(host: str, proxy_command: str) -> str:
|
|
130
|
+
lines = [
|
|
131
|
+
"Host " + host,
|
|
132
|
+
" ProxyCommand " + proxy_command,
|
|
133
|
+
" StrictHostKeyChecking no",
|
|
134
|
+
" UserKnownHostsFile /dev/null",
|
|
135
|
+
" LogLevel ERROR",
|
|
136
|
+
]
|
|
137
|
+
printed = " ".join(f"'{line}'" for line in lines)
|
|
138
|
+
if "*" in host:
|
|
139
|
+
grep_pattern = "^Host " + host.replace("*", "\\*")
|
|
140
|
+
else:
|
|
141
|
+
grep_pattern = f"^Host {host}$"
|
|
142
|
+
return (
|
|
143
|
+
f"grep -qs '{grep_pattern}' ~/.ssh/config || "
|
|
144
|
+
f"printf '%s\\n' {printed} >> ~/.ssh/config"
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _fallback_line(ssh_user: str, host: str, proxy_command: str) -> str:
|
|
149
|
+
return (
|
|
150
|
+
f"ssh {ssh_user}@{host} -o 'ProxyCommand={proxy_command}' "
|
|
151
|
+
"-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "
|
|
152
|
+
"-o LogLevel=ERROR"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _tail_stderr(path: Path, count: int) -> None:
|
|
157
|
+
try:
|
|
158
|
+
lines = path.read_text(errors="replace").splitlines()
|
|
159
|
+
except OSError:
|
|
160
|
+
return
|
|
161
|
+
for line in lines[-count:]:
|
|
162
|
+
print(line, file=sys.stderr)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _tail_file(path: Path, follow: bool) -> None:
|
|
166
|
+
if not follow:
|
|
167
|
+
try:
|
|
168
|
+
lines = path.read_text(errors="replace").splitlines()[-100:]
|
|
169
|
+
except OSError:
|
|
170
|
+
lines = []
|
|
171
|
+
for line in lines:
|
|
172
|
+
print(line)
|
|
173
|
+
return
|
|
174
|
+
with open(path, "rb") as handle:
|
|
175
|
+
handle.seek(0, os.SEEK_END)
|
|
176
|
+
try:
|
|
177
|
+
while True:
|
|
178
|
+
line = handle.readline()
|
|
179
|
+
if line:
|
|
180
|
+
sys.stdout.buffer.write(line)
|
|
181
|
+
sys.stdout.buffer.flush()
|
|
182
|
+
else:
|
|
183
|
+
time.sleep(0.2)
|
|
184
|
+
except KeyboardInterrupt:
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _show_status() -> int:
|
|
189
|
+
color = _colorizer()
|
|
190
|
+
state = st.state_dir()
|
|
191
|
+
gateway_pid_file = state / "gateway.pid"
|
|
192
|
+
transport_pid_file = state / "transport.pid"
|
|
193
|
+
gateway_running = st.is_running(gateway_pid_file)
|
|
194
|
+
transport_running = st.is_running(transport_pid_file)
|
|
195
|
+
transport = st.manifest_get("transport") or transports.DEFAULT_TRANSPORT
|
|
196
|
+
target = transports.resolve_target(transport, state)
|
|
197
|
+
|
|
198
|
+
print(f"nullgate {_version()} \u00b7 {transport}")
|
|
199
|
+
print()
|
|
200
|
+
print(f" gateway {_proc_row(color, gateway_running, st.read_pid(gateway_pid_file))}")
|
|
201
|
+
print(f" transport {_proc_row(color, transport_running, st.read_pid(transport_pid_file))}")
|
|
202
|
+
_print_url_row(color, target is not None, target.url if target else "")
|
|
203
|
+
_print_auth_rows(color, state)
|
|
204
|
+
if target is not None:
|
|
205
|
+
print()
|
|
206
|
+
print(" enter " + color.bold(f"ssh {target.user or current_username()}@{target.host}"))
|
|
207
|
+
print(" " + color.dim("first time here: nullgate enter"))
|
|
208
|
+
return 0 if (gateway_running and transport_running) else 1
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def cmd_open(args: Any) -> int:
|
|
212
|
+
saved = st.read_settings()
|
|
213
|
+
transport = getattr(args, "transport", None)
|
|
214
|
+
if transport is None:
|
|
215
|
+
transport = saved.get("transport") or transports.DEFAULT_TRANSPORT
|
|
216
|
+
if transport not in transports.VALID_TRANSPORTS:
|
|
217
|
+
die(
|
|
218
|
+
f"Unrecognized transport: {transport} "
|
|
219
|
+
f"(supported: {', '.join(transports.VALID_TRANSPORTS)})"
|
|
220
|
+
)
|
|
221
|
+
hostname = getattr(args, "hostname", None) or saved.get("hostname") or ""
|
|
222
|
+
endpoint = getattr(args, "endpoint", None) or saved.get("endpoint") or ""
|
|
223
|
+
token = (
|
|
224
|
+
getattr(args, "token", None)
|
|
225
|
+
or os.environ.get("NULLGATE_CLOUDFLARED_TOKEN")
|
|
226
|
+
or saved.get("token")
|
|
227
|
+
or ""
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
root, port, slot = _resolve_launch_settings(args, saved)
|
|
231
|
+
state = st.setup_state()
|
|
232
|
+
gateway_pid_file = state / "gateway.pid"
|
|
233
|
+
transport_pid_file = state / "transport.pid"
|
|
234
|
+
gateway_log = state / "gateway.log"
|
|
235
|
+
transport_log = state / "transport.log"
|
|
236
|
+
|
|
237
|
+
if st.is_running(gateway_pid_file):
|
|
238
|
+
die("Gateway service is already running")
|
|
239
|
+
if st.is_running(transport_pid_file):
|
|
240
|
+
die("Transport service is already running")
|
|
241
|
+
for pid_file in (gateway_pid_file, transport_pid_file):
|
|
242
|
+
if not pid_file.is_dir():
|
|
243
|
+
pid_file.unlink(missing_ok=True)
|
|
244
|
+
if st.port_open(port):
|
|
245
|
+
die(f"Port {port} is already in use")
|
|
246
|
+
|
|
247
|
+
accept_arg = getattr(args, "accept", None)
|
|
248
|
+
accept = saved.get("accept") == "1" if accept_arg is None else bool(accept_arg)
|
|
249
|
+
forwarding_arg = getattr(args, "allow_tcp_forwarding", None)
|
|
250
|
+
allow_tcp_forwarding = (
|
|
251
|
+
saved.get("allow_tcp_forwarding") == "1"
|
|
252
|
+
if forwarding_arg is None
|
|
253
|
+
else bool(forwarding_arg)
|
|
254
|
+
)
|
|
255
|
+
confine_arg = getattr(args, "confine_sftp", None)
|
|
256
|
+
if confine_arg is not None:
|
|
257
|
+
confine_sftp = bool(confine_arg)
|
|
258
|
+
else:
|
|
259
|
+
raw_env = os.environ.get("NULLGATE_CONFINE_SFTP")
|
|
260
|
+
if raw_env is not None:
|
|
261
|
+
confine_sftp = raw_env.strip().lower() in ("1", "true", "yes")
|
|
262
|
+
else:
|
|
263
|
+
confine_sftp = saved.get("confine_sftp") == "1"
|
|
264
|
+
|
|
265
|
+
if accept:
|
|
266
|
+
(state / "password").unlink(missing_ok=True)
|
|
267
|
+
password = ""
|
|
268
|
+
elif transport == "upterm":
|
|
269
|
+
transports.prompt_authorized_key()
|
|
270
|
+
keys = Path(os.path.expanduser("~/.ssh/authorized_keys"))
|
|
271
|
+
if not keys.is_file() or not keys.stat().st_size:
|
|
272
|
+
die("The upterm transport requires an SSH authorized key or the --accept flag")
|
|
273
|
+
(state / "password").unlink(missing_ok=True)
|
|
274
|
+
password = ""
|
|
275
|
+
else:
|
|
276
|
+
transports.prompt_authorized_key()
|
|
277
|
+
password = st.generate_password()
|
|
278
|
+
(state / "password").write_text(password)
|
|
279
|
+
|
|
280
|
+
session_id = st.get_or_create_session()
|
|
281
|
+
gateway_log.write_text("")
|
|
282
|
+
transport_log.write_text("")
|
|
283
|
+
|
|
284
|
+
gateway_proc = transports.launch_gateway(
|
|
285
|
+
transport=transport,
|
|
286
|
+
root=root,
|
|
287
|
+
port=port,
|
|
288
|
+
password=password,
|
|
289
|
+
accept=accept,
|
|
290
|
+
allow_tcp_forwarding=allow_tcp_forwarding,
|
|
291
|
+
confine_sftp=confine_sftp,
|
|
292
|
+
state=state,
|
|
293
|
+
)
|
|
294
|
+
if not st.write_pid(gateway_pid_file, gateway_proc.pid):
|
|
295
|
+
st.kill_untracked(gateway_proc.pid)
|
|
296
|
+
die("Unable to track gateway process")
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
ready = False
|
|
300
|
+
for _ in range(120):
|
|
301
|
+
if not st.is_running(gateway_pid_file):
|
|
302
|
+
break
|
|
303
|
+
if st.port_open(port):
|
|
304
|
+
ready = True
|
|
305
|
+
break
|
|
306
|
+
time.sleep(0.5)
|
|
307
|
+
if not ready:
|
|
308
|
+
st.stop_one(gateway_pid_file)
|
|
309
|
+
print("Gateway service failed to launch. Recent logs:", file=sys.stderr)
|
|
310
|
+
_tail_stderr(gateway_log, 100)
|
|
311
|
+
return 1
|
|
312
|
+
|
|
313
|
+
transport_proc = transports.launch_transport(
|
|
314
|
+
transport=transport,
|
|
315
|
+
port=port,
|
|
316
|
+
slot=slot,
|
|
317
|
+
session=session_id,
|
|
318
|
+
state=state,
|
|
319
|
+
endpoint=endpoint,
|
|
320
|
+
hostname=hostname,
|
|
321
|
+
token=token,
|
|
322
|
+
accept=accept,
|
|
323
|
+
)
|
|
324
|
+
if not st.write_pid(transport_pid_file, transport_proc.pid):
|
|
325
|
+
st.kill_untracked(transport_proc.pid)
|
|
326
|
+
st.stop_one(gateway_pid_file)
|
|
327
|
+
die("Unable to track transport process")
|
|
328
|
+
|
|
329
|
+
time.sleep(2)
|
|
330
|
+
if not st.is_running(transport_pid_file):
|
|
331
|
+
st.stop_one(gateway_pid_file)
|
|
332
|
+
transport_pid_file.unlink(missing_ok=True)
|
|
333
|
+
print("Transport service failed to launch. Recent logs:", file=sys.stderr)
|
|
334
|
+
_tail_stderr(transport_log, 100)
|
|
335
|
+
return 1
|
|
336
|
+
|
|
337
|
+
if transport == "srvus":
|
|
338
|
+
transports.wait_for_srvus_host(transport_pid_file, transport_log)
|
|
339
|
+
elif transport == "upterm":
|
|
340
|
+
if not transports.wait_for_upterm_session(transport_pid_file, state / "upterm.json"):
|
|
341
|
+
st.stop_one(transport_pid_file)
|
|
342
|
+
st.stop_one(gateway_pid_file)
|
|
343
|
+
print("Upterm session initialization timed out. Recent logs:", file=sys.stderr)
|
|
344
|
+
_tail_stderr(transport_log, 100)
|
|
345
|
+
return 1
|
|
346
|
+
|
|
347
|
+
manifest: dict[str, Any] = {
|
|
348
|
+
"transport": transport,
|
|
349
|
+
"port": str(port),
|
|
350
|
+
"accept": "1" if accept else "0",
|
|
351
|
+
"allow_tcp_forwarding": "1" if allow_tcp_forwarding else "0",
|
|
352
|
+
"confine_sftp": "1" if confine_sftp else "0",
|
|
353
|
+
"sftp_mode": "confined" if confine_sftp else "host",
|
|
354
|
+
"sftp_root": str(root),
|
|
355
|
+
}
|
|
356
|
+
if transport == "cloudflare":
|
|
357
|
+
manifest["endpoint"] = endpoint
|
|
358
|
+
manifest["session"] = session_id
|
|
359
|
+
elif transport == "cloudflared":
|
|
360
|
+
manifest["hostname"] = hostname
|
|
361
|
+
elif transport == "upterm":
|
|
362
|
+
manifest["endpoint"] = endpoint or wsroute.DEFAULT_ENDPOINT
|
|
363
|
+
st.write_manifest(manifest)
|
|
364
|
+
|
|
365
|
+
st.write_settings(
|
|
366
|
+
{
|
|
367
|
+
"transport": transport,
|
|
368
|
+
"root": str(root),
|
|
369
|
+
"port": str(port),
|
|
370
|
+
"slot": str(slot),
|
|
371
|
+
"hostname": hostname,
|
|
372
|
+
"endpoint": endpoint,
|
|
373
|
+
"token": token,
|
|
374
|
+
"accept": "1" if accept else "0",
|
|
375
|
+
"allow_tcp_forwarding": "1" if allow_tcp_forwarding else "0",
|
|
376
|
+
"confine_sftp": "1" if confine_sftp else "0",
|
|
377
|
+
}
|
|
378
|
+
)
|
|
379
|
+
except BaseException:
|
|
380
|
+
st.stop_one(transport_pid_file)
|
|
381
|
+
st.stop_one(gateway_pid_file)
|
|
382
|
+
raise
|
|
383
|
+
|
|
384
|
+
_show_status()
|
|
385
|
+
return 0
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def cmd_shut(args: Any = None) -> int:
|
|
389
|
+
del args
|
|
390
|
+
st.setup_state()
|
|
391
|
+
state = st.state_dir()
|
|
392
|
+
st.stop_one(state / "transport.pid")
|
|
393
|
+
st.stop_one(state / "gateway.pid")
|
|
394
|
+
(state / "manifest.json").unlink(missing_ok=True)
|
|
395
|
+
print("Nullgate stopped.")
|
|
396
|
+
return 0
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def cmd_inspect(args: Any = None) -> int:
|
|
400
|
+
del args
|
|
401
|
+
st.setup_state()
|
|
402
|
+
return _show_status()
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def cmd_enter(args: Any = None) -> int:
|
|
406
|
+
del args
|
|
407
|
+
st.setup_state()
|
|
408
|
+
state = st.state_dir()
|
|
409
|
+
transport = st.manifest_get("transport") or transports.DEFAULT_TRANSPORT
|
|
410
|
+
target = transports.resolve_target(transport, state)
|
|
411
|
+
if target is None:
|
|
412
|
+
die("No published address yet; check nullgate inspect.")
|
|
413
|
+
color = _colorizer()
|
|
414
|
+
ssh_user = target.user or current_username()
|
|
415
|
+
print("Run once on the machine you are connecting from:")
|
|
416
|
+
print()
|
|
417
|
+
print(" " + _setup_line(target.setup_host, target.proxy_command))
|
|
418
|
+
print()
|
|
419
|
+
print("Then, from now on:")
|
|
420
|
+
print()
|
|
421
|
+
print(" " + color.bold(f"ssh {ssh_user}@{target.host}"))
|
|
422
|
+
print()
|
|
423
|
+
print("Or, without changing any config:")
|
|
424
|
+
print()
|
|
425
|
+
print(" " + _fallback_line(ssh_user, target.host, target.proxy_command))
|
|
426
|
+
if target.note:
|
|
427
|
+
print()
|
|
428
|
+
print(color.dim(target.note))
|
|
429
|
+
return 0
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def cmd_trace(args: Any) -> int:
|
|
433
|
+
st.setup_state()
|
|
434
|
+
state = st.state_dir()
|
|
435
|
+
gateway_log = state / "gateway.log"
|
|
436
|
+
transport_log = state / "transport.log"
|
|
437
|
+
for log in (gateway_log, transport_log):
|
|
438
|
+
if not log.is_file():
|
|
439
|
+
log.write_text("")
|
|
440
|
+
if args.target == "all":
|
|
441
|
+
_tail_file(gateway_log, follow=args.f)
|
|
442
|
+
_tail_file(transport_log, follow=args.f)
|
|
443
|
+
elif args.target == "gateway":
|
|
444
|
+
_tail_file(gateway_log, follow=args.f)
|
|
445
|
+
else:
|
|
446
|
+
_tail_file(transport_log, follow=args.f)
|
|
447
|
+
return 0
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def cmd_cycle(args: Any = None) -> int:
|
|
451
|
+
del args
|
|
452
|
+
st.setup_state()
|
|
453
|
+
if not st.read_settings():
|
|
454
|
+
die("No saved configuration found to cycle; run open with desired options first.")
|
|
455
|
+
cmd_shut()
|
|
456
|
+
return cmd_open(
|
|
457
|
+
argparse.Namespace(
|
|
458
|
+
workspace=None,
|
|
459
|
+
port=None,
|
|
460
|
+
slot=None,
|
|
461
|
+
transport=None,
|
|
462
|
+
endpoint=None,
|
|
463
|
+
hostname=None,
|
|
464
|
+
token=None,
|
|
465
|
+
accept=None,
|
|
466
|
+
allow_tcp_forwarding=None,
|
|
467
|
+
confine_sftp=None,
|
|
468
|
+
)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def cmd_upgrade(args: Any) -> int:
|
|
473
|
+
if not shutil.which("uv"):
|
|
474
|
+
die("uv is required; install it and re-run `nullgate upgrade`")
|
|
475
|
+
if shutil.which("curl") is None:
|
|
476
|
+
die("curl is required to upgrade")
|
|
477
|
+
|
|
478
|
+
current_version = _version()
|
|
479
|
+
requested_version = args.version or os.environ.get("NULLGATE_VERSION")
|
|
480
|
+
distribution_url = (
|
|
481
|
+
os.environ.get("NULLGATE_DIST_URL") or DISTRIBUTION_URL
|
|
482
|
+
).rstrip("/")
|
|
483
|
+
install_url = os.environ.get("NULLGATE_INSTALL_URL") or (
|
|
484
|
+
f"{distribution_url}/install.sh"
|
|
485
|
+
)
|
|
486
|
+
separator = "&" if "?" in install_url else "?"
|
|
487
|
+
install_request = f"{install_url}{separator}ts={int(time.time())}"
|
|
488
|
+
try:
|
|
489
|
+
installer = subprocess.run(
|
|
490
|
+
["curl", "-fsSL", install_request],
|
|
491
|
+
check=True,
|
|
492
|
+
capture_output=True,
|
|
493
|
+
)
|
|
494
|
+
except subprocess.CalledProcessError:
|
|
495
|
+
die(f"Failed to fetch installer from {install_url}")
|
|
496
|
+
destination = requested_version or "latest stable release"
|
|
497
|
+
print(f"nullgate {current_version} upgrading to {destination}")
|
|
498
|
+
environment = dict(os.environ)
|
|
499
|
+
environment.pop("NULLGATE_PACKAGE", None)
|
|
500
|
+
if requested_version:
|
|
501
|
+
environment["NULLGATE_VERSION"] = requested_version
|
|
502
|
+
else:
|
|
503
|
+
environment.pop("NULLGATE_VERSION", None)
|
|
504
|
+
completed = subprocess.run(
|
|
505
|
+
["sh"],
|
|
506
|
+
input=installer.stdout,
|
|
507
|
+
env=environment,
|
|
508
|
+
)
|
|
509
|
+
return completed.returncode
|