55import sys
66import tempfile
77import time
8+ import warnings
89from pathlib import Path
910from shutil import copy2 , rmtree
1011from typing import Callable , Optional
2021 SharedSubmitterDialog ,
2122 find_accessibility_app ,
2223)
24+
2325from .utils import (
2426 assert_all_images_close ,
2527 assert_expected_job_bundle_and_generated_job_bundle_are_equal ,
7577DialogConfigurator = Callable [[xa11y .Locator ], None ]
7678
7779
80+ class _Cinema4DStartupError (RuntimeError ):
81+ """Cinema 4D exited before its submitter accessibility app appeared."""
82+
83+
7884def _prepend (new : str , existing : str , sep : str ) -> str :
7985 """Prepend `new` to `existing` using `sep`, without leaving a dangling
8086 separator when `existing` is empty.
@@ -226,6 +232,45 @@ def _dump_dialog_discovery_failure(app) -> None:
226232 log (f"App.list() failed: { e !r} " )
227233
228234
235+ def _find_submitter_app_while_process_runs (
236+ proc : subprocess .Popen ,
237+ timeout : float ,
238+ ) -> xa11y .App :
239+ """Wait for the submitter UIA app while also monitoring Cinema 4D."""
240+ deadline = time .monotonic () + timeout
241+
242+ while time .monotonic () < deadline :
243+ try :
244+ app = next (
245+ (
246+ app
247+ for app in xa11y .App .list ()
248+ if app .pid == proc .pid and app .name and app .name .startswith (_DIALOG_NAME_PREFIX )
249+ ),
250+ None ,
251+ )
252+ except Exception : # noqa: BLE001 - providers can fail transiently during startup
253+ app = None
254+ if app is not None :
255+ return app
256+
257+ # Check after the app scan so an app registered at the same instant the
258+ # process exits is not incorrectly reported as a startup crash.
259+ returncode = proc .poll ()
260+ if returncode is not None :
261+ unsigned_returncode = returncode & 0xFFFFFFFF
262+ raise _Cinema4DStartupError (
263+ "Cinema 4D exited before its accessibility app appeared "
264+ f"(exit code { returncode } / 0x{ unsigned_returncode :08X} )"
265+ )
266+ time .sleep (0.25 )
267+
268+ raise TimeoutError (
269+ f"No accessibility app appeared for PID { proc .pid } "
270+ f"and name prefix { _DIALOG_NAME_PREFIX !r} "
271+ )
272+
273+
229274def _resolve_dialog_app (proc : subprocess .Popen ):
230275 """Attach xa11y to the launched Cinema 4D process and return the
231276 accessibility app that hosts the submitter dialog.
@@ -237,13 +282,13 @@ def _resolve_dialog_app(proc: subprocess.Popen):
237282 app sharing the C4D pid.
238283 - macOS AX: dialogs are child windows of the host app.
239284 """
240- log (f"waiting up to { _C4D_BOOT_TIMEOUT_S :.0f} s for xa11y to attach by pid" )
241285 if sys .platform == "win32" :
286+ app_timeout = _C4D_BOOT_TIMEOUT_S + _DIALOG_VISIBLE_TIMEOUT_S
287+ log (f"waiting up to { app_timeout :.0f} s for the submitter UIA app" )
242288 try :
243- dialog_app = find_accessibility_app (
244- proc .pid ,
245- timeout = _C4D_BOOT_TIMEOUT_S + _DIALOG_VISIBLE_TIMEOUT_S ,
246- name_prefix = _DIALOG_NAME_PREFIX ,
289+ dialog_app = _find_submitter_app_while_process_runs (
290+ proc ,
291+ timeout = app_timeout ,
247292 )
248293 except TimeoutError :
249294 try :
@@ -260,6 +305,7 @@ def _resolve_dialog_app(proc: subprocess.Popen):
260305 log (f"submitter dialog UIA app: { dialog_app .name !r} " )
261306 else :
262307 # On macOS the dialog is a child window of the C4D app.
308+ log (f"waiting up to { _C4D_BOOT_TIMEOUT_S :.0f} s for xa11y to attach by pid" )
263309 dialog_app = find_accessibility_app (
264310 proc .pid ,
265311 timeout = _C4D_BOOT_TIMEOUT_S ,
@@ -481,8 +527,9 @@ def _export_job_bundle_via_submitter(
481527 the C4D subprocess), so the bundle lands under that dir; we then copy its
482528 files flat into `job_bundle_generated` for validation.
483529
484- Owns all cleanup: the C4D subprocess is always killed and its diagnostic
485- log echoed before the staging dir is removed, even on failure.
530+ If Cinema 4D exits before its submitter accessibility app appears, launch
531+ it once more. Owns all cleanup: each C4D subprocess is killed and its
532+ diagnostic log echoed before the staging dir is removed, even on failure.
486533 """
487534 cinema4d_gui_exe = resolve_c4d_exe (cinema4d_location , "Cinema 4D" )
488535
@@ -492,13 +539,23 @@ def _export_job_bundle_via_submitter(
492539 log (f"bundle staging dir: { bundle_staging } ; job history dir: { history_dir } " )
493540 try :
494541 env = _build_launch_env (scene_path , plugin_diag_log , deadline_farm ["env_overlay" ])
495- proc = _launch_cinema4d (cinema4d_gui_exe , scene_path , env )
496- try :
497- staged_bundle = _drive_submitter_ui (proc , history_dir , configure = configure )
498- _copy_bundle_files (staged_bundle , job_bundle_generated )
499- finally :
500- kill_proc (proc )
501- _dump_plugin_diag_log (plugin_diag_log )
542+ for attempt in range (2 ):
543+ proc = _launch_cinema4d (cinema4d_gui_exe , scene_path , env )
544+ try :
545+ staged_bundle = _drive_submitter_ui (proc , history_dir , configure = configure )
546+ _copy_bundle_files (staged_bundle , job_bundle_generated )
547+ return
548+ except _Cinema4DStartupError as error :
549+ if attempt == 1 :
550+ raise
551+ warnings .warn (
552+ f"{ error } ; restarting Cinema 4D once" ,
553+ RuntimeWarning ,
554+ stacklevel = 2 ,
555+ )
556+ finally :
557+ kill_proc (proc )
558+ _dump_plugin_diag_log (plugin_diag_log )
502559 finally :
503560 rmtree (bundle_staging , ignore_errors = True )
504561 log (f"removed staging dir: { bundle_staging } " )
0 commit comments