using System.Globalization; using System.Text.Json.Nodes; using w4c_workflows.Models.Nodes; using w4c_workflows.Services.Execution; namespace w4c_workflows.Services.Nodes.Executors; /// /// Runs a workflow-repository script as a node. This is the bridge between the /// node kernel and the existing language runtimes: it resolves the /// for the configured language, feeds the input /// items to it as JSON, and maps the script's JSON output back to items. /// /// It is also the target of legacy lowering (a language+entry /// script step becomes a synthetic core.code node), so script workflows /// can eventually run through the same edge-driven kernel. /// public sealed class CodeNodeExecutor : INodeExecutor { private readonly RuntimeRegistry _runtimes; public CodeNodeExecutor(RuntimeRegistry runtimes) { _runtimes = runtimes; } public string Type => "core.code"; public async Task RunAsync(NodeExecutionContext context, CancellationToken ct) { var language = ReadString(context, "language"); if (string.IsNullOrWhiteSpace(language)) return NodeExecutionOutcome.Failed("core.code requires a 'language' parameter", "invalid_parameter"); var entryFile = ReadString(context, "entryFile"); if (string.IsNullOrWhiteSpace(entryFile)) return NodeExecutionOutcome.Failed("core.code requires an 'entryFile' parameter", "invalid_parameter"); var executor = _runtimes.Resolve(language); if (executor == null) return NodeExecutionOutcome.Failed($"unknown language '{language}'", "unknown_language"); if (!executor.IsAvailable()) return NodeExecutionOutcome.Failed( $"the runtime for language '{language}' is unavailable on this host", "runtime_unavailable"); var input = context.Input(0); var invocation = new TaskInvocation( TaskInvocation.TypeValue, context.RunId, context.TaskId, context.NodeName ?? context.TaskId, language, entryFile, ReadString(context, "entryFunction"), BuildEnvironment(context.Environment), FlowItemJson.Serialize(input) ?? "{}", ReadString(context, "workingDirectory") ?? context.WorkingDirectory, context.TenantId, 1); ExecutionResult result; try { result = await executor.ExecuteAsync(invocation, ct); } catch (Exception ex) { return NodeExecutionOutcome.Failed(ex.Message, "script_failed"); } if (!result.Success) return NodeExecutionOutcome.Failed( result.Error ?? $"script exited with code {result.ExitCode}", "script_failed"); return NodeExecutionOutcome.Single(FlowItemJson.Parse(result.Output)); } /// Stringifies the workflow variables so they reach the child process env. private static IReadOnlyDictionary BuildEnvironment(JsonObject environment) { var env = new Dictionary(StringComparer.Ordinal); foreach (var (key, value) in environment) { if (value is null) continue; env[key] = value is JsonValue jsonValue && jsonValue.TryGetValue(out var text) ? text : value.ToJsonString(); } return env; } private static string? ReadString(NodeExecutionContext context, string name) => context.Parameters[name] is JsonValue value && value.TryGetValue(out var text) ? text : null; }