From f0ce286527d3bf84ce363beb1d0ab91d00bc5f7f Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Thu, 4 Jun 2026 21:27:34 +0330 Subject: [PATCH] fix(scan): force-kill stale AE processes before each launch (fresh start) PrepareFreshAE = taskkill AfterFX/aerender/AfterFXLib/dynamiclinkmanager/QT32 + 2s settle + clear crash markers, then launch. A hung/zombie AE from a prior job would otherwise block or corrupt the new run. RunScan now calls it. Co-Authored-By: Claude Opus 4.8 --- .../node-agent/internal/runner/aecrash.go | 51 +++++++++++++------ services/node-agent/internal/runner/scan.go | 2 +- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/services/node-agent/internal/runner/aecrash.go b/services/node-agent/internal/runner/aecrash.go index c0028c1..fec8b26 100644 --- a/services/node-agent/internal/runner/aecrash.go +++ b/services/node-agent/internal/runner/aecrash.go @@ -4,27 +4,50 @@ import ( "os" "os/exec" "path/filepath" + "time" ) +// aeProcesses are the AE-related executables to force-kill before a fresh launch. +var aeProcesses = []string{ + "AfterFX.exe", // the AE app + "aerender.exe", // the headless renderer + "AfterFXLib.exe", // AE render engine + "dynamiclinkmanager.exe", // Dynamic Link + "Adobe QT32 Server.exe", // QuickTime/media server +} + +// PrepareFreshAE guarantees a clean AE start before a scan/render: it force-kills +// any leftover AE process (a hung/zombie instance from a prior job would otherwise +// block or corrupt the new launch), waits for them to release file locks, then +// clears the crash/Safe-Mode markers. Call this right before launching afterfx/aerender. +func PrepareFreshAE() { + if os.Getenv("APPDATA") == "" { + return // non-Windows / dev + } + KillAEProcesses() + time.Sleep(2 * time.Second) // let the OS reap processes + release locks + ClearAECrashState() +} + +// KillAEProcesses force-terminates every AE-related process tree (taskkill is a +// Windows built-in — no external dep). Errors (e.g. "process not found") are ignored. +func KillAEProcesses() { + for _, name := range aeProcesses { + _ = exec.Command("taskkill", "/F", "/T", "/IM", name).Run() + } +} + // ClearAECrashState removes the markers After Effects uses to decide it crashed, -// so the blocking "Crash Repair Options" (Safe Mode) dialog never appears on a -// headless launch. Two parts: -// +// so the blocking "Crash Repair Options" (Safe Mode) dialog never appears: // 1. SCRPriorState.json in each AE prefs version dir (session crash-recovery state). -// 2. HKCU\Software\Adobe\After Effects\AppStates — AE writes a per-session GUID -// subkey on startup and removes it on a clean exit; a leftover one (after a -// kill/crash) triggers Safe Mode. reg.exe is a Windows built-in (no external -// dep / cgo), so we shell out to it. -// -// Targeted (vs. wiping all prefs) so the node keeps its AE preferences. Safe no-op -// on non-Windows (APPDATA unset). +// 2. HKCU\Software\Adobe\After Effects\AppStates — a leftover per-session GUID +// (after a kill/crash) trips Safe Mode. reg.exe is a Windows built-in. +// Targeted (vs. wiping all prefs) so the node keeps its AE preferences. func ClearAECrashState() { appData := os.Getenv("APPDATA") if appData == "" { - return // non-Windows / dev + return } - - // 1. session crash-recovery files base := filepath.Join(appData, "Adobe", "After Effects") if entries, err := os.ReadDir(base); err == nil { for _, e := range entries { @@ -33,7 +56,5 @@ func ClearAECrashState() { } } } - - // 2. registry session/crash state _ = exec.Command("reg", "delete", `HKCU\Software\Adobe\After Effects\AppStates`, "/f").Run() } diff --git a/services/node-agent/internal/runner/scan.go b/services/node-agent/internal/runner/scan.go index d1158e9..8248ec5 100644 --- a/services/node-agent/internal/runner/scan.go +++ b/services/node-agent/internal/runner/scan.go @@ -33,7 +33,7 @@ func WriteScanScript(workDir string) (string, error) { // afterfx -r runs the script and the script calls app.quit(); we still poll for the // output file because afterfx can return before the file is flushed. func RunScan(ctx context.Context, afterfxPath, aepPath, workDir, outPath, mode string) ([]byte, error) { - ClearAECrashState() // avoid the "Crash Repair Options" dialog hanging a headless launch + PrepareFreshAE() // kill any stale AE + clear crash/Safe-Mode markers → guaranteed fresh launch scriptPath, err := WriteScanScript(workDir) if err != nil { return nil, fmt.Errorf("write scan script: %w", err)