Skip to content

Commit 7f50b7d

Browse files
committed
chore: Add fix for SSM timeout as well.
Signed-off-by: Karthik Bekal Pattathana <133984042+karthikbekalp@users.noreply.github.com>
1 parent 091395f commit 7f50b7d

5 files changed

Lines changed: 195 additions & 38 deletions

File tree

.github/workflows/integ_windows.yml

Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -109,35 +109,86 @@ jobs:
109109
--query 'Reservations[0].Instances[0].InstanceId' `
110110
--profile license --region $region --output text).Trim()
111111
if (-not $bastion -or $bastion -eq "None") { Write-Error "Bastion host not found"; exit 1 }
112+
Write-Host "::add-mask::$bastion"
112113
113114
# Start SSM port forwarding to the bastion (params via file to avoid
114-
# quoting issues). Redirect stdout to a log file so we can capture
115-
# the SessionId for deterministic teardown.
115+
# quoting issues). Retry the complete session once because a failed
116+
# AWS CLI process cannot recover through additional local port probes.
116117
$paramsFile = Join-Path $env:RUNNER_TEMP "ssm_params.json"
117118
@{portNumber=@($rlmPort);localPortNumber=@($rlmPort)} | ConvertTo-Json -Compress | Out-File -FilePath $paramsFile -Encoding ascii
118-
$ssmLog = Join-Path $env:RUNNER_TEMP "ssm.log"
119-
Start-Process -FilePath "aws" -ArgumentList "ssm start-session --target $bastion --document-name DccInteg-PortForwardToLicenseServer --parameters file://$paramsFile --profile license --region $region" -NoNewWindow -RedirectStandardOutput $ssmLog
120-
121-
# Wait for port (retry up to 60s)
122119
$ready = $false
123-
for ($i = 0; $i -lt 12; $i++) {
124-
Start-Sleep -Seconds 5
125-
$t = Test-NetConnection -ComputerName 127.0.0.1 -Port $rlmPort -WarningAction SilentlyContinue
126-
if ($t.TcpTestSucceeded) { $ready = $true; break }
127-
}
128-
if (-not $ready) { Write-Error "SSM port forward not up after 30s"; if (Test-Path $ssmLog) { Get-Content $ssmLog }; exit 1 }
129-
Write-Host "License tunnel up via SSM"
130-
131-
# Capture the SSM session id so the teardown step can terminate the
132-
# session on the (shared, long-lived) license host explicitly, rather
133-
# than relying on SSM's disconnect detection to reap it.
134120
$ssmSessionId = ""
135-
if (Test-Path $ssmLog) {
136-
$m = Select-String -Path $ssmLog -Pattern 'SessionId: ([A-Za-z0-9._-]+)' | Select-Object -First 1
137-
if ($m) { $ssmSessionId = $m.Matches[0].Groups[1].Value }
121+
$lastStatus = "not started"
122+
123+
for ($attempt = 1; $attempt -le 2; $attempt++) {
124+
$ssmOutLog = Join-Path $env:RUNNER_TEMP "ssm-$attempt.out.log"
125+
$ssmErrLog = Join-Path $env:RUNNER_TEMP "ssm-$attempt.err.log"
126+
Write-Host "Starting SSM port forward (attempt $attempt/2)"
127+
$ssmProcess = Start-Process -FilePath "aws" `
128+
-ArgumentList "ssm start-session --target $bastion --document-name DccInteg-PortForwardToLicenseServer --parameters file://$paramsFile --profile license --region $region" `
129+
-NoNewWindow -PassThru `
130+
-RedirectStandardOutput $ssmOutLog -RedirectStandardError $ssmErrLog
131+
132+
# Wait up to 30 seconds for the local listener.
133+
for ($i = 0; $i -lt 6; $i++) {
134+
Start-Sleep -Seconds 5
135+
if ($ssmProcess.HasExited) { break }
136+
$client = [System.Net.Sockets.TcpClient]::new()
137+
try {
138+
$client.Connect("127.0.0.1", [int]$rlmPort)
139+
$ready = $true
140+
break
141+
} catch {
142+
# The listener is not ready yet.
143+
} finally {
144+
$client.Dispose()
145+
}
146+
}
147+
148+
# Capture and mask the session id. It is retained for teardown but
149+
# never printed in the public workflow log.
150+
$attemptSessionId = ""
151+
if (Test-Path $ssmOutLog) {
152+
$m = Select-String -Path $ssmOutLog -Pattern 'SessionId: ([A-Za-z0-9._-]+)' | Select-Object -First 1
153+
if ($m) {
154+
$attemptSessionId = $m.Matches[0].Groups[1].Value
155+
Write-Host "::add-mask::$attemptSessionId"
156+
}
157+
}
158+
159+
if ($ready) {
160+
$ssmSessionId = $attemptSessionId
161+
break
162+
}
163+
164+
$lastStatus = if ($ssmProcess.HasExited) { "exit code $($ssmProcess.ExitCode)" } else { "process still running" }
165+
Write-Warning "SSM attempt $attempt/2 did not open the local port ($lastStatus)"
166+
167+
# Tear down a partial session before retrying.
168+
if ($attemptSessionId) {
169+
aws ssm terminate-session --session-id $attemptSessionId --profile license --region $region 2>$null | Out-Null
170+
$global:LASTEXITCODE = 0
171+
}
172+
if (-not $ssmProcess.HasExited) {
173+
Stop-Process -Id $ssmProcess.Id -Force -ErrorAction SilentlyContinue
174+
}
175+
Get-Process -Name "session-manager-plugin" -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
176+
177+
if ($attempt -lt 2) {
178+
Write-Host "Retrying SSM port forward in 5 seconds"
179+
Start-Sleep -Seconds 5
180+
}
138181
}
182+
139183
"SSM_SESSION_ID=$ssmSessionId" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
140-
if ($ssmSessionId) { Write-Host "SSM session id: $ssmSessionId" } else { Write-Host "SSM session id: <not captured>" }
184+
185+
if (-not $ready) {
186+
Write-Error "SSM port forward failed after 2 attempts ($lastStatus)"
187+
exit 1
188+
}
189+
190+
Write-Host "License tunnel up via SSM"
191+
if ($ssmSessionId) { Write-Host "SSM session id captured for teardown" } else { Write-Host "SSM session id was not captured" }
141192
142193
hatch run integ:test
143194

PR_DESCRIPTION.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
### What was the problem/requirement? (What/Why)
2+
3+
The Windows integration workflow exposed two startup failure modes:
4+
5+
1. Cinema 4D 2025 encountered a rare native startup crash and exited with
6+
`0xC0000005` (`STATUS_ACCESS_VIOLATION`) before its submitter accessibility
7+
application appeared. The test continued waiting for UI Automation, which
8+
obscured the native process failure.
9+
2. A later run never opened the local SSM license-forwarding port. The workflow
10+
did not retain the AWS CLI process or capture its stderr, so it could only
11+
report a port timeout. `Test-NetConnection` also made the nominal timeout
12+
substantially longer than the message indicated.
13+
14+
Failed jobs:
15+
16+
- Cinema 4D startup crash:
17+
https://github.com/aws-deadline/deadline-cloud-for-cinema-4d/actions/runs/31621623631/job/94197613785
18+
- SSM port-forward startup failure:
19+
https://github.com/aws-deadline/deadline-cloud-for-cinema-4d/actions/runs/31669886824/job/94352428414
20+
21+
### What was the solution? (How)
22+
23+
The Windows integration test now monitors the launched Cinema 4D process before
24+
each UI Automation scan:
25+
26+
- If the accessibility application appears, the test continues normally.
27+
- If Cinema 4D exits first, the test reports its decimal and hexadecimal exit
28+
codes and restarts Cinema 4D once.
29+
- The failed process is cleaned up before the second launch.
30+
- Each attempt logs its PID and has a separate plugin diagnostic log.
31+
- A warning records the first startup failure even when the second launch
32+
succeeds.
33+
- If the second launch also exits early, the test fails with the native exit
34+
code.
35+
36+
Only an early process exit is retried. Accessibility timeouts and failures after
37+
startup are not retried.
38+
39+
The integration tests now run in-process with uncaptured output so Cinema 4D
40+
and xa11y progress is visible immediately. If a test runs for ten minutes,
41+
pytest's faulthandler dumps every Python thread and exits rather than waiting
42+
for the GitHub Actions job timeout.
43+
44+
The Windows SSM setup now:
45+
46+
- Retains the `aws ssm start-session` process and reports an early exit code.
47+
- Uses a direct TCP connection to check the local listener six times at
48+
five-second intervals.
49+
- If the first session does not open the port, cleans up the partial process and
50+
session, waits five seconds, and starts one fresh SSM session.
51+
- After both attempts fail, reports whether the AWS CLI exited without printing
52+
raw SSM logs.
53+
- Captures the SSM session ID for deterministic teardown.
54+
55+
### What is the impact of this change?
56+
57+
This change only affects the integration test harness and Windows integration
58+
workflow. It makes the suite resilient to a rare, transient Cinema 4D startup
59+
crash, retries a failed SSM tunnel once, and reports whether the AWS CLI exited
60+
if both tunnel attempts fail.
61+
62+
There is no change to the Cinema 4D submitter, adaptor, customer workflows, or
63+
production behavior.
64+
65+
### How was this change tested?
66+
67+
- Unit tests: `358 passed, 6 skipped`
68+
- `hatch run lint`: passed
69+
- Workflow YAML parsing: passed
70+
- `git diff --check`: passed
71+
72+
- Have you run the unit tests?
73+
74+
Yes. `hatch run test` completed with `358 passed, 6 skipped`.
75+
76+
- Have you run the integration tests? (Add your integration test report below)
77+
78+
Not locally. The full integration suite requires a Windows environment with
79+
Cinema 4D installed, GitHub OIDC credentials, and access to the license
80+
infrastructure. End-to-end validation must run in the versioned Windows
81+
integration workflow.
82+
83+
- Have you made changes to the submitter?
84+
85+
No. The changes are limited to the integration test harness, its Hatch
86+
command, the Windows workflow, and test documentation.
87+
88+
### Was this change documented?
89+
90+
The modified integration-test functions include updated docstrings, and
91+
`test/AGENTS.md` documents the in-process execution and hang diagnostics. No
92+
README, schema, or customer-facing documentation changes are required.
93+
94+
### Is this a breaking change?
95+
96+
No. This change is limited to integration test behavior and does not modify any
97+
public contract or customer-facing functionality.
98+
99+
----
100+
101+
*By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.*

hatch.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ pre-install-commands = [
4343
PYTHONIOENCODING = "utf-8"
4444

4545
[envs.integ.scripts]
46-
test = "pytest --no-cov {args:test/integ} -vvv --numprocesses=1"
46+
test = "pytest --no-cov {args:test/integ} -vvv --numprocesses=0 -s -o faulthandler_timeout=600 -o faulthandler_exit_on_timeout=true"
4747

4848
[envs.installer.scripts]
4949
build-installer = "python {root}/scripts/build_installer_cli.py --installer-source-path {root}/installer/DeadlineCloudForCinema4dSubmitter.xml {args:}"

test/AGENTS.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -280,13 +280,13 @@ macOS. All six jobs run in parallel. Local runs default to Cinema 4D 2026.
280280
Set `C4D_VERSION` for an older version and set `C4D_LOCATION` only when its
281281
installation is outside that version's default path.
282282

283-
The `integ:test` script hardcodes the `test/integ` path and
284-
`--numprocesses=1`. Beware: any args you pass *replace* the path (hatch
285-
`{args:test/integ}` falls back to the global `testpaths = ["test"]`), so
286-
`hatch run integ:test -k physical` would scan the whole `test/` tree. To
287-
filter or run in-process (e.g. to see C4D/xa11y stdout, which xdist hides), call
288-
pytest directly with an explicit path — the test spawns its own subprocesses
289-
regardless of `--numprocesses`:
283+
The `integ:test` script runs in-process with output capture disabled so C4D and
284+
xa11y progress reaches CI as it happens. If one test runs for 10 minutes, pytest
285+
dumps all Python thread stacks and exits instead of waiting for the CI job
286+
timeout. Beware: any args you pass *replace* the `test/integ` path (hatch
287+
`{args:test/integ}` falls back to the global `testpaths = ["test"]`), so `hatch
288+
run integ:test -k physical` would scan the whole `test/` tree. To filter, call
289+
pytest with an explicit path:
290290

291291
```bash
292292
hatch -e integ run pytest --no-cov test/integ/test_cinema4d.py \

test/integ/test_cinema4d.py

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,18 @@ def _find_submitter_app_while_process_runs(
284284
last_error: Exception | None = None
285285

286286
while time.monotonic() < deadline:
287+
# Avoid entering Windows UIA after C4D has already exited. Enumerating
288+
# every top-level app can block on an unresponsive accessibility
289+
# provider, which would prevent both this process check and the outer
290+
# startup timeout from running.
291+
returncode = proc.poll()
292+
if returncode is not None:
293+
unsigned_returncode = returncode & 0xFFFFFFFF
294+
raise _Cinema4DStartupError(
295+
"Cinema 4D exited before its accessibility app appeared "
296+
f"(exit code {returncode} / 0x{unsigned_returncode:08X})"
297+
)
298+
287299
try:
288300
app = next(
289301
(
@@ -299,15 +311,6 @@ def _find_submitter_app_while_process_runs(
299311
if app is not None:
300312
return app
301313

302-
# Check after the app scan so an app registered at the same instant the
303-
# process exits is not incorrectly reported as a startup crash.
304-
returncode = proc.poll()
305-
if returncode is not None:
306-
unsigned_returncode = returncode & 0xFFFFFFFF
307-
raise _Cinema4DStartupError(
308-
"Cinema 4D exited before its accessibility app appeared "
309-
f"(exit code {returncode} / 0x{unsigned_returncode:08X})"
310-
)
311314
time.sleep(0.25)
312315

313316
message = (
@@ -599,7 +602,9 @@ def _export_job_bundle_via_submitter(
599602
deadline_farm["env_overlay"],
600603
extra_env=extra_env,
601604
)
605+
log(f"launching Cinema 4D (attempt {attempt + 1}/2)")
602606
proc = _launch_cinema4d(cinema4d_gui_exe, scene_path, env)
607+
log(f"Cinema 4D launched (attempt {attempt + 1}/2, pid={proc.pid})")
603608
try:
604609
staged_bundle = _drive_submitter_ui(proc, history_dir, configure=configure)
605610
_copy_bundle_files(staged_bundle, job_bundle_generated)

0 commit comments

Comments
 (0)