w4c-workflows-api/Services/Nodes/Executors/CodeNodeExecutor.cs
2026-09-12 23:29:19 +03:00

99 lines
3.7 KiB
C#

using System.Globalization;
using System.Text.Json.Nodes;
using w4c_workflows.Models.Nodes;
using w4c_workflows.Services.Execution;
namespace w4c_workflows.Services.Nodes.Executors;
/// <summary>
/// Runs a workflow-repository script as a node. This is the bridge between the
/// node kernel and the existing language runtimes: it resolves the
/// <see cref="IScriptExecutor"/> 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 <c>language</c>+<c>entry</c>
/// script step becomes a synthetic <c>core.code</c> node), so script workflows
/// can eventually run through the same edge-driven kernel.
/// </summary>
public sealed class CodeNodeExecutor : INodeExecutor
{
private readonly RuntimeRegistry _runtimes;
public CodeNodeExecutor(RuntimeRegistry runtimes)
{
_runtimes = runtimes;
}
public string Type => "core.code";
public async Task<NodeExecutionOutcome> 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) when (ex is not OperationCanceledException)
{
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));
}
/// <summary>Stringifies the workflow variables so they reach the child process env.</summary>
private static IReadOnlyDictionary<string, string> BuildEnvironment(JsonObject environment)
{
var env = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var (key, value) in environment)
{
if (value is null)
continue;
env[key] = value is JsonValue jsonValue && jsonValue.TryGetValue<string>(out var text)
? text
: value.ToJsonString();
}
return env;
}
private static string? ReadString(NodeExecutionContext context, string name)
=> context.Parameters[name] is JsonValue value && value.TryGetValue<string>(out var text)
? text
: null;
}