ghosttrap-cli 0.3.19__tar.gz → 0.3.21__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.
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/PKG-INFO +1 -1
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli/cli.py +62 -9
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli.egg-info/PKG-INFO +1 -1
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/pyproject.toml +1 -1
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/README.md +0 -0
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli/__init__.py +0 -0
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli.egg-info/SOURCES.txt +0 -0
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli.egg-info/dependency_links.txt +0 -0
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli.egg-info/entry_points.txt +0 -0
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli.egg-info/requires.txt +0 -0
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/ghosttrap_cli.egg-info/top_level.txt +0 -0
- {ghosttrap_cli-0.3.19 → ghosttrap_cli-0.3.21}/setup.cfg +0 -0
|
@@ -4,6 +4,7 @@ import argparse
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import json
|
|
6
6
|
import os
|
|
7
|
+
import signal
|
|
7
8
|
import subprocess
|
|
8
9
|
import sys
|
|
9
10
|
import tempfile
|
|
@@ -13,7 +14,20 @@ import urllib.request
|
|
|
13
14
|
|
|
14
15
|
import websockets
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
|
|
18
|
+
def _harden_signals():
|
|
19
|
+
"""Explicitly ignore SIGURG so no process supervisor can nudge peek out
|
|
20
|
+
with an out-of-band signal. POSIX default is already ignore; this makes
|
|
21
|
+
it defensive against layers that change the disposition.
|
|
22
|
+
"""
|
|
23
|
+
s = getattr(signal, "SIGURG", None)
|
|
24
|
+
if s is not None:
|
|
25
|
+
try:
|
|
26
|
+
signal.signal(s, signal.SIG_IGN)
|
|
27
|
+
except (OSError, ValueError):
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
__version__ = "0.3.21"
|
|
17
31
|
|
|
18
32
|
GHOSTTRAP_SERVER = "wss://ghosttrap.io/stream/"
|
|
19
33
|
CONFIG_DIR = os.path.expanduser("~/.ghosttrap")
|
|
@@ -231,7 +245,10 @@ def _get_repo_token(config, requested=None):
|
|
|
231
245
|
|
|
232
246
|
|
|
233
247
|
async def _connect_and_handle(server_url, token, config, once=False):
|
|
234
|
-
"""Core WebSocket loop. If once=True,
|
|
248
|
+
"""Core WebSocket loop. If once=True, returns True after the first error event.
|
|
249
|
+
Returns False if the server closed the socket without sending an error
|
|
250
|
+
(e.g. idle timeout) so callers can distinguish 'job done' from 'reconnect me'.
|
|
251
|
+
"""
|
|
235
252
|
since = config.get("cursor")
|
|
236
253
|
url = f"{server_url}?token={token}"
|
|
237
254
|
if since is not None:
|
|
@@ -293,7 +310,8 @@ async def _connect_and_handle(server_url, token, config, once=False):
|
|
|
293
310
|
print(f"{'='*60}", file=sys.stderr)
|
|
294
311
|
|
|
295
312
|
if once:
|
|
296
|
-
return
|
|
313
|
+
return True
|
|
314
|
+
return False
|
|
297
315
|
|
|
298
316
|
|
|
299
317
|
def _require_setup():
|
|
@@ -412,6 +430,31 @@ async def setup(server_url, token):
|
|
|
412
430
|
sys.exit(1)
|
|
413
431
|
|
|
414
432
|
|
|
433
|
+
# Exception types we knowingly retry on. All represent a transient network/transport
|
|
434
|
+
# blip rather than a semantic error from the server:
|
|
435
|
+
# - ConnectionClosed: peer closed the WebSocket (either side of the handshake)
|
|
436
|
+
# - InvalidStatus: non-101 HTTP response during upgrade (e.g. 502 from a proxy)
|
|
437
|
+
# - ConnectionError: builtin — refused, reset, unreachable
|
|
438
|
+
# - OSError: DNS failure, transient socket errors (gaierror is a subclass)
|
|
439
|
+
# Anything else escapes and prints a diagnostic line first so we can add it here
|
|
440
|
+
# in the next release. Semantic rejections from the server ({"type": "rejected"})
|
|
441
|
+
# raise SystemExit, which we deliberately do NOT catch — those are real errors.
|
|
442
|
+
_RETRYABLE = (
|
|
443
|
+
websockets.ConnectionClosed,
|
|
444
|
+
websockets.InvalidStatus,
|
|
445
|
+
ConnectionError,
|
|
446
|
+
OSError,
|
|
447
|
+
)
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _log_unexpected(e):
|
|
451
|
+
print(
|
|
452
|
+
f"unexpected {type(e).__module__}.{type(e).__name__}: {e} — "
|
|
453
|
+
f"not currently in the retry list; please report so we can add it.",
|
|
454
|
+
file=sys.stderr,
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
|
|
415
458
|
async def watch(server_url, token):
|
|
416
459
|
config = _load_config()
|
|
417
460
|
print(f"connecting to {server_url}...", file=sys.stderr)
|
|
@@ -419,9 +462,13 @@ async def watch(server_url, token):
|
|
|
419
462
|
while True:
|
|
420
463
|
try:
|
|
421
464
|
await _connect_and_handle(server_url, token, config, once=False)
|
|
422
|
-
|
|
465
|
+
print("connection closed by server, reconnecting...", file=sys.stderr)
|
|
466
|
+
except _RETRYABLE:
|
|
423
467
|
print("connection lost, reconnecting...", file=sys.stderr)
|
|
424
|
-
|
|
468
|
+
except Exception as e:
|
|
469
|
+
_log_unexpected(e)
|
|
470
|
+
raise
|
|
471
|
+
await asyncio.sleep(60)
|
|
425
472
|
|
|
426
473
|
|
|
427
474
|
async def peek(server_url, token):
|
|
@@ -429,11 +476,16 @@ async def peek(server_url, token):
|
|
|
429
476
|
_check_cli_version(config)
|
|
430
477
|
while True:
|
|
431
478
|
try:
|
|
432
|
-
await _connect_and_handle(server_url, token, config, once=True)
|
|
433
|
-
|
|
434
|
-
|
|
479
|
+
got_error = await _connect_and_handle(server_url, token, config, once=True)
|
|
480
|
+
if got_error:
|
|
481
|
+
return
|
|
482
|
+
print("connection closed by server, reconnecting...", file=sys.stderr)
|
|
483
|
+
except _RETRYABLE:
|
|
435
484
|
print("connection lost, reconnecting...", file=sys.stderr)
|
|
436
|
-
|
|
485
|
+
except Exception as e:
|
|
486
|
+
_log_unexpected(e)
|
|
487
|
+
raise
|
|
488
|
+
await asyncio.sleep(60)
|
|
437
489
|
|
|
438
490
|
|
|
439
491
|
def _advance_cursor(config, token):
|
|
@@ -669,6 +721,7 @@ def last(do_clear=False, requested=None):
|
|
|
669
721
|
|
|
670
722
|
|
|
671
723
|
def main():
|
|
724
|
+
_harden_signals()
|
|
672
725
|
parser = argparse.ArgumentParser(prog="ghosttrap", description="Watch for errors from ghosttrap.io")
|
|
673
726
|
sub = parser.add_subparsers(dest="command")
|
|
674
727
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|