Files
runner/src/Runner.Worker/Handlers/ScriptHandler.cs
Tingluo Huang 8f1c070506 Use Directory.EnumerateFiles instead of Directory.GetFiles in WhichUtil. (#2882)
* Use Directory.EnumerateFiles instead of Directory.GetFiles in WhichUtil.

* .

* .
2023-09-26 20:49:15 +00:00

416 lines
19 KiB
C#

using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Container.ContainerHooks;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Worker.Handlers
{
[ServiceLocator(Default = typeof(ScriptHandler))]
public interface IScriptHandler : IHandler
{
ScriptActionExecutionData Data { get; set; }
}
public sealed class ScriptHandler : Handler, IScriptHandler
{
public ScriptActionExecutionData Data { get; set; }
protected override void PrintActionDetails(ActionRunStage stage)
{
// if we're executing a Job Extension, we won't have an 'Action'
if (!IsActionStep)
{
if (Inputs.TryGetValue("path", out var path))
{
ExecutionContext.Output($"##[group]Run '{path}'");
}
else
{
throw new InvalidOperationException("Inputs 'path' must be set for job extensions");
}
}
else if (Action.Type == Pipelines.ActionSourceType.Script)
{
Inputs.TryGetValue("script", out string contents);
contents = contents ?? string.Empty;
var firstLine = contents.TrimStart(' ', '\t', '\r', '\n');
var firstNewLine = firstLine.IndexOfAny(new[] { '\r', '\n' });
if (firstNewLine >= 0)
{
firstLine = firstLine.Substring(0, firstNewLine);
}
ExecutionContext.Output($"##[group]Run {firstLine}");
var multiLines = contents.Replace("\r\n", "\n").TrimEnd('\n').Split('\n');
foreach (var line in multiLines)
{
// Bright Cyan color
ExecutionContext.Output($"\x1b[36;1m{line}\x1b[0m");
}
}
else
{
throw new InvalidOperationException($"Invalid action type {Action?.Type} for {nameof(ScriptHandler)}");
}
string argFormat;
string shellCommand;
string shellCommandPath = null;
bool validateShellOnHost = !(StepHost is ContainerStepHost);
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.Global.PrependPath.Reverse<string>());
string shell = null;
if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell))
{
// TODO: figure out how defaults interact with template later
// for now, we won't check job.defaults if we are inside a template.
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
{
runDefaults.TryGetValue("shell", out shell);
}
}
if (string.IsNullOrEmpty(shell))
{
#if OS_WINDOWS
shellCommand = "pwsh";
if (validateShellOnHost)
{
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2(shellCommand, require: false, Trace, prependPath);
}
else
{
shellCommandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
}
if (string.IsNullOrEmpty(shellCommandPath))
{
shellCommand = "powershell";
Trace.Info($"Defaulting to {shellCommand}");
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2(shellCommand, require: true, Trace, prependPath);
}
else
{
shellCommandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
}
}
}
#else
shellCommand = "sh";
if (validateShellOnHost)
{
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2("bash", false, Trace, prependPath) ?? WhichUtil.Which2("sh", true, Trace, prependPath);
}
else
{
shellCommandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
}
}
#endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
else
{
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand;
if (validateShellOnHost)
{
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
shellCommandPath = WhichUtil.Which2(parsed.shellCommand, true, Trace, prependPath);
}
else
{
shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace, prependPath);
}
}
argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat))
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
}
if (!string.IsNullOrEmpty(shellCommandPath))
{
ExecutionContext.Output($"shell: {shellCommandPath} {argFormat}");
}
else
{
ExecutionContext.Output($"shell: {shellCommand} {argFormat}");
}
if (this.Environment?.Count > 0)
{
ExecutionContext.Output("env:");
foreach (var env in this.Environment)
{
ExecutionContext.Output($" {env.Key}: {env.Value}");
}
}
ExecutionContext.Output("##[endgroup]");
}
public async Task RunAsync(ActionRunStage stage)
{
// Validate args
Trace.Entering();
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
ArgUtil.NotNull(Inputs, nameof(Inputs));
var githubContext = ExecutionContext.ExpressionValues["github"] as GitHubContext;
ArgUtil.NotNull(githubContext, nameof(githubContext));
var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp);
Inputs.TryGetValue("script", out var contents);
contents = contents ?? string.Empty;
string workingDirectory = null;
if (!Inputs.TryGetValue("workingDirectory", out workingDirectory))
{
// Don't use job level working directories for hooks
if (IsActionStep && string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
{
if (runDefaults.TryGetValue("working-directory", out workingDirectory))
{
ExecutionContext.Debug("Overwrite 'working-directory' base on job defaults.");
}
}
}
var workspaceDir = githubContext["workspace"] as StringContextData;
workingDirectory = Path.Combine(workspaceDir, workingDirectory ?? string.Empty);
string shell = null;
if (!Inputs.TryGetValue("shell", out shell) || string.IsNullOrEmpty(shell))
{
if (string.IsNullOrEmpty(ExecutionContext.ScopeName) && ExecutionContext.Global.JobDefaults.TryGetValue("run", out var runDefaults))
{
if (runDefaults.TryGetValue("shell", out shell))
{
ExecutionContext.Debug("Overwrite 'shell' base on job defaults.");
}
}
}
var isContainerStepHost = StepHost is ContainerStepHost;
string prependPath = string.Join(Path.PathSeparator.ToString(), ExecutionContext.Global.PrependPath.Reverse<string>());
string commandPath, argFormat, shellCommand;
// Set up default command and arguments
if (string.IsNullOrEmpty(shell))
{
#if OS_WINDOWS
shellCommand = "pwsh";
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(shellCommand, require: false, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(shellCommand, require: false, Trace, prependPath);
}
if (string.IsNullOrEmpty(commandPath))
{
shellCommand = "powershell";
Trace.Info($"Defaulting to {shellCommand}");
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(shellCommand, require: true, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(shellCommand, require: true, Trace, prependPath);
}
}
ArgUtil.NotNullOrEmpty(commandPath, "Default Shell");
#else
shellCommand = "sh";
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2("bash", false, Trace, prependPath) ?? WhichUtil.Which2("sh", true, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which("bash", false, Trace, prependPath) ?? WhichUtil.Which("sh", true, Trace, prependPath);
}
#endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
else
{
// For these shells, we want to use system binaries
var systemShells = new string[] { "bash", "sh", "powershell", "pwsh" };
if (!IsActionStep && systemShells.Contains(shell))
{
shellCommand = shell;
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(shell, !isContainerStepHost, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(shell, !isContainerStepHost, Trace, prependPath);
}
if (shell == "bash")
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat("sh");
}
else
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shell);
}
}
else
{
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand;
// For non-ContainerStepHost, the command must be located on the host by Which
if (ExecutionContext.Global.Variables.GetBoolean("DistributedTask.UseWhich2") == true)
{
commandPath = WhichUtil.Which2(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
}
else
{
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace, prependPath);
}
argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat))
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
}
}
// Don't override runner telemetry here
if (!string.IsNullOrEmpty(shellCommand) && IsActionStep)
{
ExecutionContext.StepTelemetry.Action = shellCommand;
}
// No arg format was given, shell must be a built-in
if (string.IsNullOrEmpty(argFormat) || !argFormat.Contains("{0}"))
{
throw new ArgumentException("Invalid shell option. Shell must be a valid built-in (bash, sh, cmd, powershell, pwsh) or a format string containing '{0}'");
}
string scriptFilePath, resolvedScriptPath;
if (IsActionStep)
{
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
resolvedScriptPath = StepHost.ResolvePathForStepHost(ExecutionContext, scriptFilePath).Replace("\"", "\\\"");
}
else
{
// JobExtensionRunners run a script file, we load that from the inputs here
if (!Inputs.ContainsKey("path"))
{
throw new ArgumentException("Expected 'path' input to be set");
}
scriptFilePath = Inputs["path"];
ArgUtil.NotNullOrEmpty(scriptFilePath, "path");
resolvedScriptPath = Inputs["path"].Replace("\"", "\\\"");
}
// Format arg string with script path
var arguments = string.Format(argFormat, resolvedScriptPath);
// Fix up and write the script
contents = ScriptHandlerHelpers.FixUpScriptContents(shellCommand, contents);
#if OS_WINDOWS
// Normalize Windows line endings
contents = contents.Replace("\r\n", "\n").Replace("\n", "\r\n");
var encoding = ExecutionContext.Global.Variables.Retain_Default_Encoding && Console.InputEncoding.CodePage != 65001
? Console.InputEncoding
: new UTF8Encoding(false);
#else
// Don't add a BOM. It causes the script to fail on some operating systems (e.g. on Ubuntu 14).
var encoding = new UTF8Encoding(false);
#endif
if (IsActionStep)
{
// Script is written to local path (ie host) but executed relative to the StepHost, which may be a container
File.WriteAllText(scriptFilePath, contents, encoding);
}
// Prepend PATH
AddPrependPathToEnvironment();
// expose context to environment
foreach (var context in ExecutionContext.ExpressionValues)
{
if (context.Value is IEnvironmentContextData runtimeContext && runtimeContext != null)
{
foreach (var env in runtimeContext.GetRuntimeEnvironmentVariables())
{
Environment[env.Key] = env.Value;
}
}
}
// dump out the command
var fileName = isContainerStepHost ? shellCommand : commandPath;
#if OS_OSX
if (Environment.ContainsKey("DYLD_INSERT_LIBRARIES")) // We don't check `isContainerStepHost` because we don't support container on macOS
{
// launch `node macOSRunInvoker.js shell args` instead of `shell args` to avoid macOS SIP remove `DYLD_INSERT_LIBRARIES` when launch process
string node = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), NodeUtil.GetInternalNodeVersion(), "bin", $"node{IOUtil.ExeExtension}");
string macOSRunInvoker = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Bin), "macos-run-invoker.js");
arguments = $"\"{macOSRunInvoker.Replace("\"", "\\\"")}\" \"{fileName.Replace("\"", "\\\"")}\" {arguments}";
fileName = node;
}
#endif
var systemConnection = ExecutionContext.Global.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
if (systemConnection.Data.TryGetValue("GenerateIdTokenUrl", out var generateIdTokenUrl) && !string.IsNullOrEmpty(generateIdTokenUrl))
{
Environment["ACTIONS_ID_TOKEN_REQUEST_URL"] = generateIdTokenUrl;
Environment["ACTIONS_ID_TOKEN_REQUEST_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken];
}
ExecutionContext.Debug($"{fileName} {arguments}");
Inputs.TryGetValue("standardInInput", out var standardInInput);
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
{
StepHost.OutputDataReceived += stdoutManager.OnDataReceived;
StepHost.ErrorDataReceived += stderrManager.OnDataReceived;
// Execute
int exitCode = await StepHost.ExecuteAsync(ExecutionContext,
workingDirectory: StepHost.ResolvePathForStepHost(ExecutionContext, workingDirectory),
fileName: fileName,
arguments: arguments,
environment: Environment,
requireExitCodeZero: false,
outputEncoding: null,
killProcessOnCancel: false,
inheritConsoleHandler: !ExecutionContext.Global.Variables.Retain_Default_Encoding,
standardInInput: standardInInput,
cancellationToken: ExecutionContext.CancellationToken);
// Error
if (exitCode != 0)
{
ExecutionContext.Error($"Process completed with exit code {exitCode}.");
ExecutionContext.Result = TaskResult.Failed;
}
}
}
}
}