// Package config loads node-agent runtime configuration from environment variables. package config import ( "bufio" "fmt" "os" "path/filepath" "strconv" "strings" ) // LoadEnvFile reads a simple KEY=VALUE file and sets any variables that are not // already present in the environment. This lets the Windows service (installed // via sc.exe, which has no per-service env support) be configured by dropping an // `agent.env` file next to the executable — no registry edits required. // // Lookup order: $AGENT_ENV_FILE, then `agent.env` beside the exe, then `./agent.env`. // Lines starting with # and blank lines are ignored. Existing env vars win, so an // operator can still override any single value at the process level. func LoadEnvFile() { candidates := []string{} if p := os.Getenv("AGENT_ENV_FILE"); p != "" { candidates = append(candidates, p) } if exe, err := os.Executable(); err == nil { candidates = append(candidates, filepath.Join(filepath.Dir(exe), "agent.env")) } candidates = append(candidates, "agent.env") for _, path := range candidates { f, err := os.Open(path) if err != nil { continue } scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } key, val, ok := strings.Cut(line, "=") if !ok { continue } key = strings.TrimSpace(key) val = strings.Trim(strings.TrimSpace(val), `"'`) if _, exists := os.LookupEnv(key); !exists { _ = os.Setenv(key, val) } } f.Close() return // first file found wins } } // Config holds all runtime settings for the node agent. type Config struct { // NodeID is the UUID of this render node, registered in the orchestrator. // Must match a row in render.render_nodes. NodeID string // OrchestratorURL is the base URL of the V2 API gateway (internal network). // Example: http://gateway:8080 or http://172.30.0.5:8088 OrchestratorURL string // NodeHMACSecret is the shared secret sent as X-Node-Signature header. // Must match NODE_HMAC_SECRET in the render-svc environment. NodeHMACSecret string // Region is the datacenter/region label for this node (e.g. "iran-tehran-1"). // The orchestrator uses it to route region-preferred jobs to this node. Region string // AEPath is the full path to the aerender.exe binary. // Example: C:\Program Files\Adobe\Adobe After Effects 2024\Support Files\aerender.exe // Leave empty to use mock rendering (for development / testing without AE). AEPath string // AfterFxPath is the full path to afterfx.exe (the AE app, used to run the // template scanner script). Defaults to afterfx.exe alongside aerender.exe. // Leave AEPath empty too to disable scanning (dev/mock). AfterFxPath string // WorkDir is the scratch directory for render temp files and AE project copies. WorkDir string // RemotionProjectDir is the Remotion project root (package.json + src/index.ts) // used by the code-based render engine. Empty disables Remotion jobs on this node. RemotionProjectDir string // HeartbeatIntervalSec is how often the agent sends a heartbeat to the orchestrator. HeartbeatIntervalSec int // PollIntervalSec is how long the agent waits between job-claim attempts when idle. PollIntervalSec int // AgentVersion is the semantic version string reported to the orchestrator. AgentVersion string // AEVersion is the After Effects version string reported to the orchestrator. // Example: "2024" AEVersion string // ListenPort is the port for the agent's own HTTP health endpoint. ListenPort int } // Load reads configuration from environment variables, returning an error // if any required variable is missing. func Load() (*Config, error) { // Pull in agent.env (if present) before reading the environment. LoadEnvFile() c := &Config{ NodeID: os.Getenv("NODE_ID"), OrchestratorURL: getEnv("ORCHESTRATOR_URL", "http://localhost:8088"), NodeHMACSecret: getEnv("NODE_HMAC_SECRET", "node-secret-change-me"), Region: getEnv("NODE_REGION", ""), AEPath: getEnv("AE_PATH", ""), AfterFxPath: getEnv("AFTERFX_PATH", ""), WorkDir: getEnv("WORK_DIR", os.TempDir()), RemotionProjectDir: getEnv("REMOTION_PROJECT_DIR", ""), AgentVersion: getEnv("AGENT_VERSION", "0.1.0"), AEVersion: getEnv("AE_VERSION", "2024"), HeartbeatIntervalSec: getInt("HEARTBEAT_INTERVAL_SEC", 5), PollIntervalSec: getInt("POLL_INTERVAL_SEC", 3), ListenPort: getInt("LISTEN_PORT", 7777), } if c.NodeID == "" { return nil, fmt.Errorf("NODE_ID environment variable is required") } // Derive afterfx.exe next to aerender.exe when not explicitly set. if c.AfterFxPath == "" && c.AEPath != "" { c.AfterFxPath = filepath.Join(filepath.Dir(c.AEPath), "afterfx.exe") } return c, nil } func getEnv(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } func getInt(key string, fallback int) int { if v := os.Getenv(key); v != "" { if n, err := strconv.Atoi(v); err == nil { return n } } return fallback }