Skip to content

Commit bb3259a

Browse files
committed
Implement an emergency crash handler
1 parent 7384ac5 commit bb3259a

File tree

1 file changed

+78
-4
lines changed

1 file changed

+78
-4
lines changed

src/gptcmd/cli.py

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,23 @@
88
"""
99

1010
import argparse
11+
import atexit
1112
import cmd
1213
import concurrent.futures
1314
import dataclasses
15+
import datetime
1416
import json
1517
import os
1618
import re
1719
import shlex
1820
import subprocess
1921
import sys
2022
import tempfile
23+
import traceback
2124
from ast import literal_eval
2225
from textwrap import shorten
2326
from typing import (
27+
Any,
2428
Callable,
2529
Dict,
2630
List,
@@ -958,7 +962,12 @@ def do_unsticky(self, arg):
958962
mp = "message" if len(t) == 1 else "messages"
959963
print(f"{len(t)} {mp} unstickied")
960964

961-
def do_save(self, arg):
965+
def do_save(
966+
self,
967+
arg: str,
968+
_extra_metadata: Optional[Dict[str, Any]] = None,
969+
_print_on_success: bool = True,
970+
):
962971
"""
963972
Save all named threads to the specified json file. With no argument,
964973
save to the most recently loaded/saved JSON file in this session.
@@ -984,7 +993,11 @@ def do_save(self, arg):
984993
else:
985994
path = args[0]
986995
res = {}
987-
res["_meta"] = {"version": __version__}
996+
if _extra_metadata is None:
997+
res["_meta"] = {}
998+
else:
999+
res["_meta"] = _extra_metadata.copy()
1000+
res["_meta"]["version"] = __version__
9881001
res["threads"] = {k: v.to_dict() for k, v in self._threads.items()}
9891002
try:
9901003
with open(path, "w", encoding="utf-8") as cam:
@@ -994,7 +1007,8 @@ def do_save(self, arg):
9941007
return
9951008
for thread in self._threads.values():
9961009
thread.dirty = False
997-
print(f"{os.path.abspath(path)} saved")
1010+
if _print_on_success:
1011+
print(f"{os.path.abspath(path)} saved")
9981012
self.last_path = path
9991013

10001014
def do_load(self, arg, _print_on_success=True):
@@ -1282,6 +1296,48 @@ def do_quit(self, arg):
12821296
return can_exit # Truthy return values cause the cmdloop to stop
12831297

12841298

1299+
def _write_crash_dump(shell: Gptcmd, exc: Exception) -> Optional[str]:
1300+
"""
1301+
Serialize the current shell into a JSON file and return its absolute
1302+
path.
1303+
"""
1304+
detached_added = False
1305+
try:
1306+
ts = (
1307+
datetime.datetime.now()
1308+
.isoformat(timespec="seconds")
1309+
.replace(":", "-")
1310+
)
1311+
filename = f"gptcmd-{ts}.json"
1312+
tb_text = "".join(
1313+
traceback.format_exception(type(exc), exc, exc.__traceback__)
1314+
)
1315+
if shell._detached:
1316+
original_dirty = shell._detached.dirty
1317+
detached_base = "__detached__"
1318+
detached_key = detached_base
1319+
i = 1
1320+
while detached_key in shell._threads:
1321+
i += 1
1322+
detached_key = f"{detached_base}{i}"
1323+
shell._detached.dirty = False
1324+
shell._threads[detached_key] = shell._detached
1325+
detached_added = True
1326+
shell.do_save(
1327+
filename,
1328+
_extra_metadata={"crash_traceback": tb_text},
1329+
_print_on_success=False,
1330+
)
1331+
return os.path.abspath(filename)
1332+
except Exception as e:
1333+
print(f"Failed to write crash dump: {e}", file=sys.stderr)
1334+
return None
1335+
finally:
1336+
if detached_added:
1337+
shell._detached.dirty = original_dirty
1338+
shell._threads.pop(detached_key, None)
1339+
1340+
12851341
def main() -> bool:
12861342
"""
12871343
Setuptools requires a callable entry point to build an installable script
@@ -1336,7 +1392,25 @@ def main() -> bool:
13361392
shell.do_account(args.account, _print_on_success=False)
13371393
if args.model:
13381394
shell.do_model(args.model, _print_on_success=False)
1339-
shell.cmdloop()
1395+
try:
1396+
shell.cmdloop()
1397+
except Exception as e:
1398+
# Does any thread contain messages?
1399+
should_save = (shell._detached and shell._detached.dirty) or any(
1400+
t and t.dirty for t in shell._threads.values()
1401+
)
1402+
if should_save:
1403+
dump_path = _write_crash_dump(shell, e)
1404+
if dump_path:
1405+
# Hack: Print the "crash dump" notice after the traceback
1406+
atexit.register(
1407+
lambda p=dump_path: print(
1408+
f"Crash dump written to {p}",
1409+
file=sys.stderr,
1410+
flush=True,
1411+
)
1412+
)
1413+
raise
13401414
return True
13411415

13421416

0 commit comments

Comments
 (0)