fix compilation
This commit is contained in:
parent
3b50333353
commit
1f34453886
|
|
@ -333,18 +333,26 @@ public class WorkflowsController : ControllerBase
|
|||
[RequireScope("run")]
|
||||
public async Task<IActionResult> Run(Guid id, [FromBody] RunRequest? request, CancellationToken ct)
|
||||
{
|
||||
var workflow = await _db.Workflows
|
||||
.FirstOrDefaultAsync(w =>
|
||||
w.Id == id && w.TenantId == TenantId && w.Status == WorkflowStatus.Compiled, ct);
|
||||
if (workflow == null)
|
||||
return NotFound(new { error = "Workflow not found." });
|
||||
try
|
||||
{
|
||||
var workflow = await _db.Workflows
|
||||
.FirstOrDefaultAsync(w =>
|
||||
w.Id == id && w.TenantId == TenantId && w.Status == WorkflowStatus.Compiled, ct);
|
||||
if (workflow == null)
|
||||
return NotFound(new { error = "Workflow not found." });
|
||||
|
||||
var triggerJson = System.Text.Json.JsonSerializer.Serialize(new { type = TriggerType.Event });
|
||||
var correlation = $"manual:{Guid.NewGuid():N}";
|
||||
var runId = await _launcher.LaunchAsync(
|
||||
new LaunchRequest(TenantId, id, triggerJson, request?.Input, correlation), ct);
|
||||
var triggerJson = System.Text.Json.JsonSerializer.Serialize(new { type = TriggerType.Event });
|
||||
var correlation = $"manual:{Guid.NewGuid():N}";
|
||||
var runId = await _launcher.LaunchAsync(
|
||||
new LaunchRequest(TenantId, id, triggerJson, request?.Input, correlation), ct);
|
||||
|
||||
return Accepted(new { runId });
|
||||
return Accepted(new { runId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to start workflow run {WorkflowId} for tenant {TenantId}", id, TenantId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -391,25 +399,33 @@ public class WorkflowsController : ControllerBase
|
|||
[RequireScope("run")]
|
||||
public async Task<IActionResult> Resume(Guid id, string instanceId, [FromBody] ResumeRequest? request, CancellationToken ct)
|
||||
{
|
||||
var workflow = await _db.Workflows
|
||||
.Include(w => w.Tasks)
|
||||
.FirstOrDefaultAsync(w => w.Id == id && w.TenantId == TenantId && w.Status == WorkflowStatus.Compiled, ct);
|
||||
if (workflow == null)
|
||||
return NotFound(new { error = "Workflow not found." });
|
||||
try
|
||||
{
|
||||
var workflow = await _db.Workflows
|
||||
.Include(w => w.Tasks)
|
||||
.FirstOrDefaultAsync(w => w.Id == id && w.TenantId == TenantId && w.Status == WorkflowStatus.Compiled, ct);
|
||||
if (workflow == null)
|
||||
return NotFound(new { error = "Workflow not found." });
|
||||
|
||||
var checkpoint = await _durable.GetAsync(instanceId, ct);
|
||||
if (checkpoint == null)
|
||||
return NotFound(new { error = $"No durable checkpoint for instance '{instanceId}'." });
|
||||
var checkpoint = await _durable.GetAsync(instanceId, ct);
|
||||
if (checkpoint == null)
|
||||
return NotFound(new { error = $"No durable checkpoint for instance '{instanceId}'." });
|
||||
|
||||
var resumeTask = workflow.Tasks.FirstOrDefault(t => t.Id == checkpoint.TaskId);
|
||||
if (resumeTask == null)
|
||||
return BadRequest(new { error = $"Checkpointed task '{checkpoint.TaskId}' does not belong to this workflow." });
|
||||
var resumeTask = workflow.Tasks.FirstOrDefault(t => t.Id == checkpoint.TaskId);
|
||||
if (resumeTask == null)
|
||||
return BadRequest(new { error = $"Checkpointed task '{checkpoint.TaskId}' does not belong to this workflow." });
|
||||
|
||||
var input = request?.Input ?? checkpoint.StateJson;
|
||||
var correlation = $"resume:{instanceId}:{Guid.NewGuid():N}";
|
||||
var runId = await _launcher.LaunchAsync(
|
||||
new LaunchRequest(TenantId, id, workflow.TriggerJson, input, correlation, checkpoint.TaskId), ct);
|
||||
var input = request?.Input ?? checkpoint.StateJson;
|
||||
var correlation = $"resume:{instanceId}:{Guid.NewGuid():N}";
|
||||
var runId = await _launcher.LaunchAsync(
|
||||
new LaunchRequest(TenantId, id, workflow.TriggerJson, input, correlation, checkpoint.TaskId), ct);
|
||||
|
||||
return Accepted(new { runId, resumedTaskId = checkpoint.TaskId });
|
||||
return Accepted(new { runId, resumedTaskId = checkpoint.TaskId });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to resume workflow {WorkflowId} instance {InstanceId} for tenant {TenantId}", id, instanceId, TenantId);
|
||||
return StatusCode(StatusCodes.Status500InternalServerError, new { error = ex.Message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
Program.cs
16
Program.cs
|
|
@ -223,6 +223,22 @@ builder.Services.AddCors(options =>
|
|||
var app = builder.Build();
|
||||
|
||||
app.UseCors();
|
||||
|
||||
// Global exception handler: catches unhandled exceptions from any controller
|
||||
// or middleware and returns a structured JSON error instead of a raw 500.
|
||||
app.UseExceptionHandler(handler =>
|
||||
{
|
||||
handler.Run(async context =>
|
||||
{
|
||||
context.Response.StatusCode = StatusCodes.Status500InternalServerError;
|
||||
context.Response.ContentType = "application/json";
|
||||
var ex = context.Features.Get<Microsoft.AspNetCore.Diagnostics.IExceptionHandlerFeature>();
|
||||
var message = ex?.Error.Message ?? "An unexpected error occurred.";
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(new { error = message });
|
||||
await context.Response.WriteAsync(json);
|
||||
});
|
||||
});
|
||||
|
||||
app.UseMiddleware<AuthMiddleware>();
|
||||
|
||||
// Resolve the Forgejo-backed workflow source asynchronously after auth.
|
||||
|
|
|
|||
|
|
@ -9,10 +9,14 @@ using Microsoft.CodeAnalysis.CSharp;
|
|||
namespace w4c_workflows.Services.Execution;
|
||||
|
||||
/// <summary>
|
||||
/// Executes multi-file C# with the Roslyn compiler: every <c>.cs</c> file under
|
||||
/// the task's working directory is compiled together into an in-memory assembly
|
||||
/// (one syntax tree per file, so per-file <c>using</c> directives and file-scoped
|
||||
/// namespaces work), then the entry method is invoked reflectively.
|
||||
/// Executes single-file C# with the Roslyn compiler: the entry <c>.cs</c> file
|
||||
/// specified by the task's <c>entry.file</c> is compiled into an in-memory
|
||||
/// assembly, then the entry method is invoked reflectively.
|
||||
///
|
||||
/// Only the entry file is compiled — not every <c>.cs</c> file under the
|
||||
/// working directory. Compiling all files would merge unrelated task sources
|
||||
/// (e.g. <c>Main.cs</c> + <c>tasks/hello-world.cs</c>) into one assembly,
|
||||
/// causing duplicate-type errors when both define a <c>Program</c> class.
|
||||
///
|
||||
/// Each execution uses a <see cref="CollectibleAssemblyLoadContext"/> so the
|
||||
/// loaded assembly and all its types can be garbage-collected after the call
|
||||
|
|
@ -44,12 +48,14 @@ public class CSharpScriptExecutor : IScriptExecutor
|
|||
try
|
||||
{
|
||||
var workingDir = ExecutionHelpers.ResolveWorkingDir(invocation);
|
||||
var sources = EnumerateSources(workingDir);
|
||||
if (sources.Count == 0)
|
||||
return Task.FromResult(Fail("no .cs source files found under the task directory", sw.Elapsed));
|
||||
var entryPath = Path.GetFullPath(Path.Combine(workingDir, invocation.EntryFile));
|
||||
if (!File.Exists(entryPath))
|
||||
return Task.FromResult(Fail($"entry file not found: {invocation.EntryFile}", sw.Elapsed));
|
||||
|
||||
var source = new SourceFile(invocation.EntryFile, File.ReadAllText(entryPath));
|
||||
|
||||
// Compilation is CPU-bound and synchronous; keep it off the request thread.
|
||||
var (assemblyLoadContext, assembly, diagnostics) = Compile(sources);
|
||||
var (assemblyLoadContext, assembly, diagnostics) = Compile(source);
|
||||
if (assembly == null)
|
||||
{
|
||||
assemblyLoadContext?.Unload();
|
||||
|
|
@ -84,27 +90,13 @@ public class CSharpScriptExecutor : IScriptExecutor
|
|||
private static ExecutionResult Fail(string message, TimeSpan duration)
|
||||
=> new(false, -1, string.Empty, string.Empty, null, message, duration);
|
||||
|
||||
private static IReadOnlyList<SourceFile> EnumerateSources(string workingDir)
|
||||
private static (AssemblyLoadContext, Assembly?, ImmutableArray<Diagnostic>) Compile(SourceFile source)
|
||||
{
|
||||
// One syntax tree per file; compilation is order-independent, so a
|
||||
// deterministic sort is all that matters for reproducibility.
|
||||
return Directory.EnumerateFiles(workingDir, "*.cs", SearchOption.AllDirectories)
|
||||
.Select(full => new SourceFile(
|
||||
Path.GetRelativePath(workingDir, full),
|
||||
File.ReadAllText(full)))
|
||||
.OrderBy(s => s.Path, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private static (AssemblyLoadContext, Assembly?, ImmutableArray<Diagnostic>) Compile(IReadOnlyList<SourceFile> sources)
|
||||
{
|
||||
var syntaxTrees = sources
|
||||
.Select(s => CSharpSyntaxTree.ParseText(s.Source, path: s.Path))
|
||||
.ToArray();
|
||||
var syntaxTree = CSharpSyntaxTree.ParseText(source.Source, path: source.Path);
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
$"wf-cs-{Guid.NewGuid():N}",
|
||||
syntaxTrees,
|
||||
[syntaxTree],
|
||||
GetCachedReferences(),
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true));
|
||||
|
||||
|
|
|
|||
|
|
@ -35,8 +35,12 @@ public class CSharpScriptExecutorTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Executes_multi_file_program()
|
||||
public async Task Compiles_only_entry_file_not_other_cs_files()
|
||||
{
|
||||
// Only the entry file (Program.cs) is compiled. Helper.cs exists in the
|
||||
// directory but is NOT included, so referencing Helper.Add causes a
|
||||
// compilation error. This prevents duplicate-type errors when multiple
|
||||
// tasks each define their own Program class in the same workflow directory.
|
||||
using var dir = new TempDir();
|
||||
dir.Write("Helper.cs", """
|
||||
public static class Helper
|
||||
|
|
@ -61,9 +65,8 @@ public class CSharpScriptExecutorTests
|
|||
var result = await _executor.ExecuteAsync(
|
||||
Invocation.For("csharp", "Program.cs", "{\"a\":2,\"b\":3}", dir.Path), default);
|
||||
|
||||
Assert.True(result.Success, result.Error);
|
||||
using var output = JsonDocument.Parse(result.Output!);
|
||||
Assert.Equal(5, output.RootElement.GetProperty("sum").GetInt32());
|
||||
Assert.False(result.Success);
|
||||
Assert.Contains("compilation failed", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Reference in a new issue