Skip to content

Commit 5438d11

Browse files
committed
chore: Retry Cinema 4D launch if it fails first time on tests.
Signed-off-by: Karthik Bekal Pattathana <133984042+karthikbekalp@users.noreply.github.com>
1 parent f85e8f5 commit 5438d11

1 file changed

Lines changed: 71 additions & 14 deletions

File tree

test/integ/test_cinema4d.py

Lines changed: 71 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import sys
66
import tempfile
77
import time
8+
import warnings
89
from pathlib import Path
910
from shutil import copy2, rmtree
1011
from typing import Callable, Optional
@@ -20,6 +21,7 @@
2021
SharedSubmitterDialog,
2122
find_accessibility_app,
2223
)
24+
2325
from .utils import (
2426
assert_all_images_close,
2527
assert_expected_job_bundle_and_generated_job_bundle_are_equal,
@@ -75,6 +77,10 @@
7577
DialogConfigurator = Callable[[xa11y.Locator], None]
7678

7779

80+
class _Cinema4DStartupError(RuntimeError):
81+
"""Cinema 4D exited before its submitter accessibility app appeared."""
82+
83+
7884
def _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+
229274
def _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

Comments
 (0)