From 9d601791ae042cae72341f79c9d1756ee8200bc5 Mon Sep 17 00:00:00 2001 From: Vitali sharp8n Date: Fri, 11 Sep 2026 15:06:06 +0300 Subject: [PATCH] fix --- .../Execution/SubprocessScriptExecutor.cs | 55 ++++++++++++++++++- appsettings.Development.json | 11 ++++ .../SubprocessScriptExecutorTests.cs | 22 ++++++++ 3 files changed, 86 insertions(+), 2 deletions(-) create mode 100644 appsettings.Development.json diff --git a/Services/Execution/SubprocessScriptExecutor.cs b/Services/Execution/SubprocessScriptExecutor.cs index 617ac20..e62870d 100644 --- a/Services/Execution/SubprocessScriptExecutor.cs +++ b/Services/Execution/SubprocessScriptExecutor.cs @@ -32,9 +32,12 @@ public class SubprocessScriptExecutor : IScriptExecutor var workingDir = ExecutionHelpers.ResolveWorkingDir(invocation); var entryPath = Path.Combine(workingDir, invocation.EntryFile); + var (executable, args) = ResolveInterpreter(entryPath); + args.Add(entryPath); + var output = await ProcessRunner.RunAsync( - _executable, - new[] { entryPath }, + executable, + args, workingDir, ExecutionHelpers.BuildEnvironment(invocation), invocation.Input, @@ -50,6 +53,54 @@ public class SubprocessScriptExecutor : IScriptExecutor } } + /// + /// Chooses the interpreter for a shell script. A shell script may declare a + /// specific shell in its shebang (e.g. #!/usr/bin/env bash); running it + /// under the registered POSIX sh instead would reject bash-only syntax + /// such as set -o pipefail. When the shebang names an interpreter that + /// is available on PATH we honour it; otherwise (no shebang, unknown + /// interpreter) we fall back to the registered default so behaviour is + /// unchanged for plain POSIX scripts. + /// + private (string Executable, List Args) ResolveInterpreter(string entryPath) + { + var fallback = (_executable, new List()); + if (!string.Equals(_language, "shell", StringComparison.Ordinal) || !File.Exists(entryPath)) + return fallback; + + try + { + string? firstLine; + using (var reader = new StreamReader(entryPath)) + firstLine = reader.ReadLine(); + + if (firstLine is null || !firstLine.StartsWith("#!", StringComparison.Ordinal)) + return fallback; + + var parts = firstLine.Substring(2).Trim() + .Split(new[] { ' ', '\t' }, StringSplitOptions.RemoveEmptyEntries); + if (parts.Length == 0) + return fallback; + + var candidate = parts[0]; + var extra = parts.Skip(1).ToList(); + + // "#!/usr/bin/env bash" -> run "bash" (env resolves it via PATH). + if (string.Equals(Path.GetFileName(candidate), "env", StringComparison.Ordinal) && extra.Count > 0) + { + candidate = extra[0]; + extra.RemoveAt(0); + } + + var resolved = ProcessRunner.FindInPath(candidate); + return resolved is null ? fallback : (resolved, extra); + } + catch (IOException) + { + return fallback; + } + } + private static int ParseInt(string? text, int fallback) => int.TryParse(text, out var value) && value > 0 ? value : fallback; } diff --git a/appsettings.Development.json b/appsettings.Development.json new file mode 100644 index 0000000..3214c4c --- /dev/null +++ b/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "Workflows": { + "ChatApiUrl": "http://localhost:5258" + } +} diff --git a/w4c-workflows-api.Tests/SubprocessScriptExecutorTests.cs b/w4c-workflows-api.Tests/SubprocessScriptExecutorTests.cs index 8a97fec..e8d7a81 100644 --- a/w4c-workflows-api.Tests/SubprocessScriptExecutorTests.cs +++ b/w4c-workflows-api.Tests/SubprocessScriptExecutorTests.cs @@ -82,6 +82,28 @@ public class SubprocessScriptExecutorTests Assert.Contains("time limit", result.Error); } + [Fact] + public async Task Shell_honours_bash_shebang_for_bash_only_scripts() + { + if (ProcessRunner.FindInPath("bash") is null) + return; // no bash on this host — the fallback path is covered elsewhere + + using var dir = new TempDir(); + dir.Write("task.sh", """ + #!/usr/bin/env bash + set -euo pipefail + cat >/dev/null || true + echo '{"ok":true}' + """); + + var result = await Shell().ExecuteAsync( + Invocation.For("shell", "task.sh", "{}", dir.Path), default); + + Assert.True(result.Success, result.Error); + using var output = JsonDocument.Parse(result.Output!); + Assert.True(output.RootElement.GetProperty("ok").GetBoolean()); + } + [Fact] public async Task Python_reads_stdin_and_returns_json_output() {