44
55import io
66import logging
7+ import subprocess
8+ import sys
79import threading
10+ import time
811import traceback
912from typing import Any , Callable
1013
14+ import django .utils .autoreload
1115from django .conf import settings
1216from django .http import HttpResponse
1317
@@ -31,33 +35,74 @@ def add_threading_except_hook():
3135
3236 original_threading_excepthook = threading .excepthook
3337
34-
3538 def tidewave_excepthook (args ):
3639 if args .thread is not None and args .thread .name == "django-main-thread" :
3740 formatted = "" .join (
3841 traceback .format_exception (args .exc_type , args .exc_value , args .exc_traceback )
3942 )
40- message = "Django terminated with exception:\n " + formatted
41- record = logging .LogRecord (
42- name = "tidewave.exceptionhook" ,
43- level = logging .ERROR ,
44- pathname = "" ,
45- lineno = 0 ,
46- msg = message ,
47- args = (),
48- exc_info = None ,
49- )
50-
51- file_handler .emit (record )
52- file_handler .flush ()
43+ emit_error_log ("Django terminated with exception:\n " + formatted )
5344
5445 if original_threading_excepthook is not None :
5546 original_threading_excepthook (args )
5647
57-
5848 threading .excepthook = tidewave_excepthook
5949
50+
51+ def patch_django_autoreload ():
52+ original_restart_with_reloader = django .utils .autoreload .restart_with_reloader
53+
54+ def new_restart_with_reloader ():
55+ while True :
56+ # With autoreload enabled, Django has a top-level process,
57+ # which then starts a child server process. The child
58+ # process also runs a watcher and calls sys.exit(3) whenever
59+ # files change. The top-level process then tries to start
60+ # a new child. This loop is defined in restart_with_reloader [1].
61+ # If booting the child fails (for example, exception in
62+ # settings.py), the loop stops and all processes terminate.
63+ # We want to keep the process running, so we patch the
64+ # loop, such that on boot failure, we wait 1 second and
65+ # try booting again. We want to enable the LLM to see the
66+ # exception, so we run "python manage.py shell", which
67+ # should fail with the exception in stderr, and then we
68+ # write that exception to our log file.
69+ #
70+ # [1]: https://github.com/django/django/blob/5.2.6/django/utils/autoreload.py#L269-L275
71+ original_restart_with_reloader ()
72+
73+ manage_py_path = sys .argv [0 ]
74+ process = subprocess .run (
75+ [sys .executable , manage_py_path , "shell" ], capture_output = True , text = True , input = ""
76+ )
77+
78+ if process .returncode != 0 :
79+ print ("[Tidewave] Django failed to boot, retrying in 2 seconds" )
80+ emit_error_log (
81+ "Django failed to boot, retrying in 2 seconds. Error:" + process .stderr
82+ )
83+ time .sleep (2 )
84+
85+ django .utils .autoreload .restart_with_reloader = new_restart_with_reloader
86+
87+
88+ def emit_error_log (message : str ):
89+ record = logging .LogRecord (
90+ name = "tidewave" ,
91+ level = logging .ERROR ,
92+ pathname = "" ,
93+ lineno = 0 ,
94+ msg = message ,
95+ args = (),
96+ exc_info = None ,
97+ )
98+
99+ file_handler .emit (record )
100+ file_handler .flush ()
101+
102+
60103add_threading_except_hook ()
104+ patch_django_autoreload ()
105+
61106
62107class Middleware :
63108 """Django middleware for Tidewave MCP integration
0 commit comments