22# SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
33# SPDX-License-Identifier: AGPL-3.0-or-later
44#
5+ import atexit
6+ import faulthandler
57import io
68import logging
79import multiprocessing as mp
@@ -114,6 +116,28 @@ def _truncate_capture(text: str) -> tuple[str, bool]:
114116
115117
116118def exception_wrap (fun : Callable | None , * args , resconn : Connection , stdconn : Connection , ** kwargs ):
119+ # --- diagnostic probes: write directly to the real stderr FD so they survive
120+ # Python's stdout/stderr redirection below and even os._exit() won't hide them
121+ # from the parent process's stderr stream.
122+ _diag_fd = os .dup (2 ) # dup before we capture sys.stderr
123+
124+ def _raw_diag (msg : str ) -> None :
125+ with suppress (Exception ):
126+ os .write (_diag_fd , (msg + '\n ' ).encode ())
127+
128+ # Enable faulthandler on the real FD so crash tracebacks (SIGSEGV etc.) appear.
129+ with suppress (Exception ):
130+ faulthandler .enable (file = os .fdopen (os .dup (_diag_fd ), 'w' , closefd = True ), all_threads = True )
131+
132+ # Atexit probe: if this message NEVER appears, it means os._exit() (C-level)
133+ # was called with Python's cleanup phase entirely skipped.
134+ _fun_name = getattr (fun , '__name__' , str (fun ))
135+ atexit .register (
136+ _raw_diag ,
137+ f'[exception_wrap/atexit] pid={ os .getpid ()} target={ _fun_name } '
138+ ': Python atexit reached (normal Python exit)' ,
139+ )
140+
117141 stdout_capture = io .StringIO ()
118142 stderr_capture = io .StringIO ()
119143 orig_stdout = sys .stdout
@@ -124,10 +148,18 @@ def exception_wrap(fun: Callable | None, *args, resconn: Connection, stdconn: Co
124148 try :
125149 if fun is None :
126150 resconn .send ({ 'value' : None , 'error' : None })
151+ _raw_diag (f'[exception_wrap/probe] pid={ os .getpid ()} target={ _fun_name } : result sent (fun=None)' )
127152 else :
128- resconn .send ({ 'value' : fun (* args , ** kwargs ), 'error' : None })
153+ result_value = fun (* args , ** kwargs )
154+ _raw_diag (f'[exception_wrap/probe] pid={ os .getpid ()} target={ _fun_name } : fun() returned, sending result' )
155+ resconn .send ({ 'value' : result_value , 'error' : None })
156+ _raw_diag (f'[exception_wrap/probe] pid={ os .getpid ()} target={ _fun_name } : result pipe send complete' )
129157 except BaseException as e :
130158 tb = traceback .format_exc ()
159+ _raw_diag (
160+ f'[exception_wrap/probe] pid={ os .getpid ()} target={ _fun_name } '
161+ f': caught { type (e ).__name__ } : { e } '
162+ )
131163 payload = {
132164 'value' : None ,
133165 'error' : e ,
@@ -162,6 +194,9 @@ def exception_wrap(fun: Callable | None, *args, resconn: Connection, stdconn: Co
162194 'stdout_truncated' : stdout_truncated ,
163195 'stderr_truncated' : stderr_truncated ,
164196 })
197+ _raw_diag (f'[exception_wrap/probe] pid={ os .getpid ()} target={ _fun_name } : finally block complete' )
198+ with suppress (Exception ):
199+ os .close (_diag_fd )
165200
166201
167202def exec_in_proc (group = None , target = None , name = None , args = (), kwargs = {}, * , daemon = None ): # noqa: B006
0 commit comments