package runner import ( "context" _ "embed" "fmt" "os" "os/exec" "path/filepath" "time" ) //go:embed scan.jsx var scanScript []byte // WriteScanScript writes the embedded scanner JSX into workDir and returns its path. func WriteScanScript(workDir string) (string, error) { dir := filepath.Join(workDir, "scripts") if err := os.MkdirAll(dir, 0o755); err != nil { return "", err } p := filepath.Join(dir, "scan.jsx") if err := os.WriteFile(p, scanScript, 0o644); err != nil { return "", err } return p, nil } // RunScan runs the After Effects template scanner against aepPath and returns the // JSON the script writes to outPath. afterfxPath must be afterfx.exe (the AE app, // not aerender.exe — only the app can run ScriptUI/ExtendScript). // // 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 string) ([]byte, error) { scriptPath, err := WriteScanScript(workDir) if err != nil { return nil, fmt.Errorf("write scan script: %w", err) } if err := os.MkdirAll(filepath.Dir(outPath), 0o755); err != nil { return nil, err } _ = os.Remove(outPath) _ = os.Remove(outPath + ".error") cmd := exec.CommandContext(ctx, afterfxPath, "-r", scriptPath) cmd.Env = append(os.Environ(), "FR_SCAN_AEP="+aepPath, "FR_SCAN_OUT="+outPath) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr // afterfx may exit non-zero on app.quit() — don't treat the exit code as fatal; // success is determined by the presence of the output JSON file. _ = cmd.Run() for { if eb, rerr := os.ReadFile(outPath + ".error"); rerr == nil && len(eb) > 0 { return nil, fmt.Errorf("scan script error: %s", string(eb)) } if b, rerr := os.ReadFile(outPath); rerr == nil && len(b) > 0 { return b, nil } select { case <-ctx.Done(): return nil, fmt.Errorf("scan timed out waiting for %s", outPath) case <-time.After(2 * time.Second): } } }