99 lines
3.7 KiB
C#
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;
|
|
}
|