fix compilation
This commit is contained in:
parent
3b50333353
commit
1f34453886
|
|
@ -332,6 +332,8 @@ public class WorkflowsController : ControllerBase
|
||||||
[HttpPost("{id:guid}/run")]
|
[HttpPost("{id:guid}/run")]
|
||||||
[RequireScope("run")]
|
[RequireScope("run")]
|
||||||
public async Task<IActionResult> Run(Guid id, [FromBody] RunRequest? request, CancellationToken ct)
|
public async Task<IActionResult> Run(Guid id, [FromBody] RunRequest? request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var workflow = await _db.Workflows
|
var workflow = await _db.Workflows
|
||||||
.FirstOrDefaultAsync(w =>
|
.FirstOrDefaultAsync(w =>
|
||||||
|
|
@ -346,6 +348,12 @@ public class WorkflowsController : ControllerBase
|
||||||
|
|
||||||
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>
|
/// <summary>
|
||||||
/// Enables or disables the workflow's auto-trigger (cron/interval/webhook/
|
/// Enables or disables the workflow's auto-trigger (cron/interval/webhook/
|
||||||
|
|
@ -390,6 +398,8 @@ public class WorkflowsController : ControllerBase
|
||||||
[HttpPost("{id:guid}/instances/{instanceId}/resume")]
|
[HttpPost("{id:guid}/instances/{instanceId}/resume")]
|
||||||
[RequireScope("run")]
|
[RequireScope("run")]
|
||||||
public async Task<IActionResult> Resume(Guid id, string instanceId, [FromBody] ResumeRequest? request, CancellationToken ct)
|
public async Task<IActionResult> Resume(Guid id, string instanceId, [FromBody] ResumeRequest? request, CancellationToken ct)
|
||||||
|
{
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var workflow = await _db.Workflows
|
var workflow = await _db.Workflows
|
||||||
.Include(w => w.Tasks)
|
.Include(w => w.Tasks)
|
||||||
|
|
@ -412,4 +422,10 @@ public class WorkflowsController : ControllerBase
|
||||||
|
|
||||||
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();
|
var app = builder.Build();
|
||||||
|
|
||||||
app.UseCors();
|
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>();
|
app.UseMiddleware<AuthMiddleware>();
|
||||||
|
|
||||||
// Resolve the Forgejo-backed workflow source asynchronously after auth.
|
// Resolve the Forgejo-backed workflow source asynchronously after auth.
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,14 @@ using Microsoft.CodeAnalysis.CSharp;
|
||||||
namespace w4c_workflows.Services.Execution;
|
namespace w4c_workflows.Services.Execution;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Executes multi-file C# with the Roslyn compiler: every <c>.cs</c> file under
|
/// Executes single-file C# with the Roslyn compiler: the entry <c>.cs</c> file
|
||||||
/// the task's working directory is compiled together into an in-memory assembly
|
/// specified by the task's <c>entry.file</c> is compiled into an in-memory
|
||||||
/// (one syntax tree per file, so per-file <c>using</c> directives and file-scoped
|
/// assembly, then the entry method is invoked reflectively.
|
||||||
/// namespaces work), 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
|
/// Each execution uses a <see cref="CollectibleAssemblyLoadContext"/> so the
|
||||||
/// loaded assembly and all its types can be garbage-collected after the call
|
/// loaded assembly and all its types can be garbage-collected after the call
|
||||||
|
|
@ -44,12 +48,14 @@ public class CSharpScriptExecutor : IScriptExecutor
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var workingDir = ExecutionHelpers.ResolveWorkingDir(invocation);
|
var workingDir = ExecutionHelpers.ResolveWorkingDir(invocation);
|
||||||
var sources = EnumerateSources(workingDir);
|
var entryPath = Path.GetFullPath(Path.Combine(workingDir, invocation.EntryFile));
|
||||||
if (sources.Count == 0)
|
if (!File.Exists(entryPath))
|
||||||
return Task.FromResult(Fail("no .cs source files found under the task directory", sw.Elapsed));
|
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.
|
// 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)
|
if (assembly == null)
|
||||||
{
|
{
|
||||||
assemblyLoadContext?.Unload();
|
assemblyLoadContext?.Unload();
|
||||||
|
|
@ -84,27 +90,13 @@ public class CSharpScriptExecutor : IScriptExecutor
|
||||||
private static ExecutionResult Fail(string message, TimeSpan duration)
|
private static ExecutionResult Fail(string message, TimeSpan duration)
|
||||||
=> new(false, -1, string.Empty, string.Empty, null, message, 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
|
var syntaxTree = CSharpSyntaxTree.ParseText(source.Source, path: source.Path);
|
||||||
// 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 compilation = CSharpCompilation.Create(
|
var compilation = CSharpCompilation.Create(
|
||||||
$"wf-cs-{Guid.NewGuid():N}",
|
$"wf-cs-{Guid.NewGuid():N}",
|
||||||
syntaxTrees,
|
[syntaxTree],
|
||||||
GetCachedReferences(),
|
GetCachedReferences(),
|
||||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true));
|
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, allowUnsafe: true));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,12 @@ public class CSharpScriptExecutorTests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[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();
|
using var dir = new TempDir();
|
||||||
dir.Write("Helper.cs", """
|
dir.Write("Helper.cs", """
|
||||||
public static class Helper
|
public static class Helper
|
||||||
|
|
@ -61,9 +65,8 @@ public class CSharpScriptExecutorTests
|
||||||
var result = await _executor.ExecuteAsync(
|
var result = await _executor.ExecuteAsync(
|
||||||
Invocation.For("csharp", "Program.cs", "{\"a\":2,\"b\":3}", dir.Path), default);
|
Invocation.For("csharp", "Program.cs", "{\"a\":2,\"b\":3}", dir.Path), default);
|
||||||
|
|
||||||
Assert.True(result.Success, result.Error);
|
Assert.False(result.Success);
|
||||||
using var output = JsonDocument.Parse(result.Output!);
|
Assert.Contains("compilation failed", result.Error, StringComparison.OrdinalIgnoreCase);
|
||||||
Assert.Equal(5, output.RootElement.GetProperty("sum").GetInt32());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue