GitHub Actions Runner

This commit is contained in:
Tingluo Huang
2019-10-10 00:52:42 -04:00
commit c8afc84840
1255 changed files with 198670 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System;
using GitHub.Runner.Worker.Container;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.DistributedTask.WebApi;
using GitHub.DistributedTask.Pipelines.ContextData;
using System.Linq;
namespace GitHub.Runner.Worker.Handlers
{
[ServiceLocator(Default = typeof(ContainerActionHandler))]
public interface IContainerActionHandler : IHandler
{
ContainerActionExecutionData Data { get; set; }
}
public sealed class ContainerActionHandler : Handler, IContainerActionHandler
{
public ContainerActionExecutionData Data { get; set; }
public async Task RunAsync(ActionRunStage stage)
{
// Validate args.
Trace.Entering();
ArgUtil.NotNull(Data, nameof(Data));
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
#if OS_WINDOWS || OS_OSX
throw new NotSupportedException($"Container action is only supported on Linux");
#else
// Update the env dictionary.
AddInputsToEnvironment();
var dockerManger = HostContext.GetService<IDockerCommandManager>();
// container image haven't built/pull
if (Data.Image.StartsWith("docker://", StringComparison.OrdinalIgnoreCase))
{
Data.Image = Data.Image.Substring("docker://".Length);
}
else if (Data.Image.EndsWith("Dockerfile") || Data.Image.EndsWith("dockerfile"))
{
// ensure docker file exist
var dockerFile = Path.Combine(ActionDirectory, Data.Image);
ArgUtil.File(dockerFile, nameof(Data.Image));
ExecutionContext.Output($"Dockerfile for action: '{dockerFile}'.");
var imageName = $"{dockerManger.DockerInstanceLabel}:{ExecutionContext.Id.ToString("N")}";
var buildExitCode = await dockerManger.DockerBuild(ExecutionContext, ExecutionContext.GetGitHubContext("workspace"), Directory.GetParent(dockerFile).FullName, imageName);
if (buildExitCode != 0)
{
throw new InvalidOperationException($"Docker build failed with exit code {buildExitCode}");
}
Data.Image = imageName;
}
// run container
var container = new ContainerInfo()
{
ContainerImage = Data.Image,
ContainerName = ExecutionContext.Id.ToString("N"),
ContainerDisplayName = $"{Pipelines.Validation.NameValidation.Sanitize(Data.Image)}_{Guid.NewGuid().ToString("N").Substring(0, 6)}",
};
if (stage == ActionRunStage.Main)
{
if (!string.IsNullOrEmpty(Data.EntryPoint))
{
// use entrypoint from action.yml
container.ContainerEntryPoint = Data.EntryPoint;
}
else
{
// use entrypoint input, this is for action v1 which doesn't have action.yml
container.ContainerEntryPoint = Inputs.GetValueOrDefault("entryPoint");
}
}
else if (stage == ActionRunStage.Post)
{
container.ContainerEntryPoint = Data.Cleanup;
}
// create inputs context for template evaluation
var inputsContext = new DictionaryContextData();
if (this.Inputs != null)
{
foreach (var input in Inputs)
{
inputsContext.Add(input.Key, new StringContextData(input.Value));
}
}
var evaluateContext = new Dictionary<string, PipelineContextData>(StringComparer.OrdinalIgnoreCase);
evaluateContext["inputs"] = inputsContext;
var manifestManager = HostContext.GetService<IActionManifestManager>();
if (Data.Arguments != null)
{
container.ContainerEntryPointArgs = "";
var evaluatedArgs = manifestManager.EvaluateContainerArguments(ExecutionContext, Data.Arguments, evaluateContext);
foreach (var arg in evaluatedArgs)
{
if (!string.IsNullOrEmpty(arg))
{
container.ContainerEntryPointArgs = container.ContainerEntryPointArgs + $" \"{arg.Replace("\"", "\\\"")}\"";
}
else
{
container.ContainerEntryPointArgs = container.ContainerEntryPointArgs + " \"\"";
}
}
}
else
{
container.ContainerEntryPointArgs = Inputs.GetValueOrDefault("args");
}
if (Data.Environment != null)
{
var evaluatedEnv = manifestManager.EvaluateContainerEnvironment(ExecutionContext, Data.Environment, evaluateContext);
foreach (var env in evaluatedEnv)
{
if (!this.Environment.ContainsKey(env.Key))
{
this.Environment[env.Key] = env.Value;
}
}
}
if (ExecutionContext.JobContext.Container.TryGetValue("network", out var networkContextData) && networkContextData is StringContextData networkStringData)
{
container.ContainerNetwork = networkStringData.ToString();
}
var defaultWorkingDirectory = ExecutionContext.GetGitHubContext("workspace");
var tempDirectory = HostContext.GetDirectory(WellKnownDirectory.Temp);
ArgUtil.NotNullOrEmpty(defaultWorkingDirectory, nameof(defaultWorkingDirectory));
ArgUtil.NotNullOrEmpty(tempDirectory, nameof(tempDirectory));
var tempHomeDirectory = Path.Combine(tempDirectory, "_github_home");
Directory.CreateDirectory(tempHomeDirectory);
this.Environment["HOME"] = tempHomeDirectory;
var tempWorkflowDirectory = Path.Combine(tempDirectory, "_github_workflow");
ArgUtil.Directory(tempWorkflowDirectory, nameof(tempWorkflowDirectory));
container.MountVolumes.Add(new MountVolume("/var/run/docker.sock", "/var/run/docker.sock"));
container.MountVolumes.Add(new MountVolume(tempHomeDirectory, "/github/home"));
container.MountVolumes.Add(new MountVolume(tempWorkflowDirectory, "/github/workflow"));
container.MountVolumes.Add(new MountVolume(defaultWorkingDirectory, "/github/workspace"));
container.AddPathTranslateMapping(tempHomeDirectory, "/github/home");
container.AddPathTranslateMapping(tempWorkflowDirectory, "/github/workflow");
container.AddPathTranslateMapping(defaultWorkingDirectory, "/github/workspace");
container.ContainerWorkDirectory = "/github/workspace";
// 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;
}
}
}
// Add Actions Runtime server info
var systemConnection = ExecutionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
Environment["ACTIONS_RUNTIME_URL"] = systemConnection.Url.AbsoluteUri;
Environment["ACTIONS_RUNTIME_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken];
if (systemConnection.Data.TryGetValue("CacheServerUrl", out var cacheUrl) && !string.IsNullOrEmpty(cacheUrl))
{
Environment["ACTIONS_CACHE_URL"] = cacheUrl;
}
foreach (var variable in this.Environment)
{
container.ContainerEnvironmentVariables[variable.Key] = container.TranslateToContainerPath(variable.Value);
}
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
{
var runExitCode = await dockerManger.DockerRun(ExecutionContext, container, stdoutManager.OnDataReceived, stderrManager.OnDataReceived);
if (runExitCode != 0)
{
ExecutionContext.Error($"Docker run failed with exit code {runExitCode}");
ExecutionContext.Result = TaskResult.Failed;
}
}
#endif
}
}
}

View File

@@ -0,0 +1,177 @@
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using System.IO;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Worker.Handlers
{
public interface IHandler : IRunnerService
{
Pipelines.ActionStepDefinitionReference Action { get; set; }
Dictionary<string, string> Environment { get; set; }
IExecutionContext ExecutionContext { get; set; }
Variables RuntimeVariables { get; set; }
IStepHost StepHost { get; set; }
Dictionary<string, string> Inputs { get; set; }
string ActionDirectory { get; set; }
Task RunAsync(ActionRunStage stage);
void PrintActionDetails(ActionRunStage stage);
}
public abstract class Handler : RunnerService
{
#if OS_WINDOWS
// In windows OS the maximum supported size of a environment variable value is 32k.
// You can set environment variable greater then 32K, but that variable will not be able to read in node.exe.
private const int _environmentVariableMaximumSize = 32766;
#endif
protected IActionCommandManager ActionCommandManager { get; private set; }
public Pipelines.ActionStepDefinitionReference Action { get; set; }
public Dictionary<string, string> Environment { get; set; }
public Variables RuntimeVariables { get; set; }
public IExecutionContext ExecutionContext { get; set; }
public IStepHost StepHost { get; set; }
public Dictionary<string, string> Inputs { get; set; }
public string ActionDirectory { get; set; }
public virtual void PrintActionDetails(ActionRunStage stage)
{
if (stage == ActionRunStage.Post)
{
ExecutionContext.Output($"Post job cleanup.");
return;
}
string groupName = "";
if (Action.Type == Pipelines.ActionSourceType.ContainerRegistry)
{
var registryAction = Action as Pipelines.ContainerRegistryReference;
groupName = $"Run docker://{registryAction.Image}";
}
else if (Action.Type == Pipelines.ActionSourceType.Repository)
{
var repoAction = Action as Pipelines.RepositoryPathReference;
if (string.Equals(repoAction.RepositoryType, Pipelines.PipelineConstants.SelfAlias, StringComparison.OrdinalIgnoreCase))
{
groupName = $"Run {repoAction.Path}";
}
else
{
if (string.IsNullOrEmpty(repoAction.Path))
{
groupName = $"Run {repoAction.Name}@{repoAction.Ref}";
}
else
{
groupName = $"Run {repoAction.Name}/{repoAction.Path}@{repoAction.Ref}";
}
}
}
else
{
// this should never happen
Trace.Error($"Can't generate default folding group name for action {Action.Type.ToString()}");
groupName = "Action details";
}
ExecutionContext.Output($"##[group]{groupName}");
if (this.Inputs?.Count > 0)
{
ExecutionContext.Output("with:");
foreach (var input in this.Inputs)
{
if (!string.IsNullOrEmpty(input.Value))
{
ExecutionContext.Output($" {input.Key}: {input.Value}");
}
}
}
if (this.Environment?.Count > 0)
{
ExecutionContext.Output("env:");
foreach (var env in this.Environment)
{
ExecutionContext.Output($" {env.Key}: {env.Value}");
}
}
ExecutionContext.Output("##[endgroup]");
}
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
ActionCommandManager = hostContext.CreateService<IActionCommandManager>();
}
protected void AddInputsToEnvironment()
{
// Validate args.
Trace.Entering();
ArgUtil.NotNull(Inputs, nameof(Inputs));
// Add the inputs to the environment variable dictionary.
foreach (KeyValuePair<string, string> pair in Inputs)
{
AddEnvironmentVariable(
key: $"INPUT_{pair.Key?.Replace(' ', '_').ToUpperInvariant()}",
value: pair.Value);
}
}
protected void AddEnvironmentVariable(string key, string value)
{
ArgUtil.NotNullOrEmpty(key, nameof(key));
Trace.Verbose($"Setting env '{key}' to '{value}'.");
Environment[key] = value ?? string.Empty;
#if OS_WINDOWS
if (Environment[key].Length > _environmentVariableMaximumSize)
{
ExecutionContext.Warning($"Environment variable '{key}' exceeds the maximum supported length. Environment variable length: {value.Length} , Maximum supported length: {_environmentVariableMaximumSize}");
}
#endif
}
protected void AddPrependPathToEnvironment()
{
// Validate args.
Trace.Entering();
ArgUtil.NotNull(ExecutionContext.PrependPath, nameof(ExecutionContext.PrependPath));
if (ExecutionContext.PrependPath.Count == 0)
{
return;
}
// Prepend path.
string prepend = string.Join(Path.PathSeparator.ToString(), ExecutionContext.PrependPath.Reverse<string>());
var containerStepHost = StepHost as ContainerStepHost;
if (containerStepHost != null)
{
containerStepHost.PrependPath = prepend;
}
else
{
string taskEnvPATH;
Environment.TryGetValue(Constants.PathVariable, out taskEnvPATH);
string originalPath = RuntimeVariables.Get(Constants.PathVariable) ?? // Prefer a job variable.
taskEnvPATH ?? // Then a task-environment variable.
System.Environment.GetEnvironmentVariable(Constants.PathVariable) ?? // Then an environment variable.
string.Empty;
string newPath = PathUtil.PrependPath(prepend, originalPath);
AddEnvironmentVariable(Constants.PathVariable, newPath);
}
}
}
}

View File

@@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Worker.Handlers
{
[ServiceLocator(Default = typeof(HandlerFactory))]
public interface IHandlerFactory : IRunnerService
{
IHandler Create(
IExecutionContext executionContext,
Pipelines.ActionStepDefinitionReference action,
IStepHost stepHost,
ActionExecutionData data,
Dictionary<string, string> inputs,
Dictionary<string, string> environment,
Variables runtimeVariables,
string actionDirectory);
}
public sealed class HandlerFactory : RunnerService, IHandlerFactory
{
public IHandler Create(
IExecutionContext executionContext,
Pipelines.ActionStepDefinitionReference action,
IStepHost stepHost,
ActionExecutionData data,
Dictionary<string, string> inputs,
Dictionary<string, string> environment,
Variables runtimeVariables,
string actionDirectory)
{
// Validate args.
Trace.Entering();
ArgUtil.NotNull(executionContext, nameof(executionContext));
ArgUtil.NotNull(stepHost, nameof(stepHost));
ArgUtil.NotNull(data, nameof(data));
ArgUtil.NotNull(inputs, nameof(inputs));
ArgUtil.NotNull(environment, nameof(environment));
ArgUtil.NotNull(runtimeVariables, nameof(runtimeVariables));
// Create the handler.
IHandler handler;
if (data.ExecutionType == ActionExecutionType.Container)
{
handler = HostContext.CreateService<IContainerActionHandler>();
(handler as IContainerActionHandler).Data = data as ContainerActionExecutionData;
}
else if (data.ExecutionType == ActionExecutionType.NodeJS)
{
handler = HostContext.CreateService<INodeScriptActionHandler>();
(handler as INodeScriptActionHandler).Data = data as NodeJSActionExecutionData;
}
else if (data.ExecutionType == ActionExecutionType.Script)
{
handler = HostContext.CreateService<IScriptHandler>();
(handler as IScriptHandler).Data = data as ScriptActionExecutionData;
}
else if (data.ExecutionType == ActionExecutionType.Plugin)
{
// Agent plugin
handler = HostContext.CreateService<IRunnerPluginHandler>();
(handler as IRunnerPluginHandler).Data = data as PluginActionExecutionData;
}
else
{
// This should never happen.
throw new NotSupportedException(data.ExecutionType.ToString());
}
handler.Action = action;
handler.Environment = environment;
handler.RuntimeVariables = runtimeVariables;
handler.ExecutionContext = executionContext;
handler.StepHost = stepHost;
handler.Inputs = inputs;
handler.ActionDirectory = actionDirectory;
return handler;
}
}
}

View File

@@ -0,0 +1,134 @@
using System.IO;
using System.Text;
using System.Threading.Tasks;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.DistributedTask.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
using System;
using System.Linq;
namespace GitHub.Runner.Worker.Handlers
{
[ServiceLocator(Default = typeof(NodeScriptActionHandler))]
public interface INodeScriptActionHandler : IHandler
{
NodeJSActionExecutionData Data { get; set; }
}
public sealed class NodeScriptActionHandler : Handler, INodeScriptActionHandler
{
public NodeJSActionExecutionData Data { get; set; }
public async Task RunAsync(ActionRunStage stage)
{
// Validate args.
Trace.Entering();
ArgUtil.NotNull(Data, nameof(Data));
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
ArgUtil.NotNull(Inputs, nameof(Inputs));
ArgUtil.Directory(ActionDirectory, nameof(ActionDirectory));
// Update the env dictionary.
AddInputsToEnvironment();
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;
}
}
}
// Add Actions Runtime server info
var systemConnection = ExecutionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
Environment["ACTIONS_RUNTIME_URL"] = systemConnection.Url.AbsoluteUri;
Environment["ACTIONS_RUNTIME_TOKEN"] = systemConnection.Authorization.Parameters[EndpointAuthorizationParameters.AccessToken];
if (systemConnection.Data.TryGetValue("CacheServerUrl", out var cacheUrl) && !string.IsNullOrEmpty(cacheUrl))
{
Environment["ACTIONS_CACHE_URL"] = cacheUrl;
}
// Resolve the target script.
string target = null;
if (stage == ActionRunStage.Main)
{
target = Data.Script;
}
else if (stage == ActionRunStage.Post)
{
target = Data.Cleanup;
}
ArgUtil.NotNullOrEmpty(target, nameof(target));
target = Path.Combine(ActionDirectory, target);
ArgUtil.File(target, nameof(target));
// Resolve the working directory.
string workingDirectory = ExecutionContext.GetGitHubContext("workspace");
if (string.IsNullOrEmpty(workingDirectory))
{
workingDirectory = HostContext.GetDirectory(WellKnownDirectory.Work);
}
var nodeRuntimeVersion = await StepHost.DetermineNodeRuntimeVersion(ExecutionContext);
string file = Path.Combine(HostContext.GetDirectory(WellKnownDirectory.Externals), nodeRuntimeVersion, "bin", $"node{IOUtil.ExeExtension}");
// Format the arguments passed to node.
// 1) Wrap the script file path in double quotes.
// 2) Escape double quotes within the script file path. Double-quote is a valid
// file name character on Linux.
string arguments = StepHost.ResolvePathForStepHost(StringUtil.Format(@"""{0}""", target.Replace(@"""", @"\""")));
#if OS_WINDOWS
// It appears that node.exe outputs UTF8 when not in TTY mode.
Encoding outputEncoding = Encoding.UTF8;
#else
// Let .NET choose the default.
Encoding outputEncoding = null;
#endif
using (var stdoutManager = new OutputManager(ExecutionContext, ActionCommandManager))
using (var stderrManager = new OutputManager(ExecutionContext, ActionCommandManager))
{
StepHost.OutputDataReceived += stdoutManager.OnDataReceived;
StepHost.ErrorDataReceived += stderrManager.OnDataReceived;
// Execute the process. Exit code 0 should always be returned.
// A non-zero exit code indicates infrastructural failure.
// Task failure should be communicated over STDOUT using ## commands.
Task<int> step = StepHost.ExecuteAsync(workingDirectory: StepHost.ResolvePathForStepHost(workingDirectory),
fileName: StepHost.ResolvePathForStepHost(file),
arguments: arguments,
environment: Environment,
requireExitCodeZero: false,
outputEncoding: outputEncoding,
killProcessOnCancel: false,
inheritConsoleHandler: !ExecutionContext.Variables.Retain_Default_Encoding,
cancellationToken: ExecutionContext.CancellationToken);
// Wait for either the node exit or force finish through ##vso command
await System.Threading.Tasks.Task.WhenAny(step, ExecutionContext.ForceCompleted);
if (ExecutionContext.ForceCompleted.IsCompleted)
{
ExecutionContext.Debug("The task was marked as \"done\", but the process has not closed after 5 seconds. Treating the task as complete.");
}
else
{
var exitCode = await step;
if (exitCode != 0)
{
ExecutionContext.Error($"Node run failed with exit code {exitCode}");
ExecutionContext.Result = TaskResult.Failed;
}
}
}
}
}
}

View File

@@ -0,0 +1,319 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using DTWebApi = GitHub.DistributedTask.WebApi;
namespace GitHub.Runner.Worker.Handlers
{
public sealed class OutputManager : IDisposable
{
private const string _colorCodePrefix = "\033[";
private const int _maxAttempts = 3;
private const string _timeoutKey = "GITHUB_ACTIONS_RUNNER_ISSUE_MATCHER_TIMEOUT";
private static readonly Regex _colorCodeRegex = new Regex(@"\x0033\[[0-9;]*m?", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private readonly IActionCommandManager _commandManager;
private readonly IExecutionContext _executionContext;
private readonly object _matchersLock = new object();
private readonly TimeSpan _timeout;
private IssueMatcher[] _matchers = Array.Empty<IssueMatcher>();
public OutputManager(IExecutionContext executionContext, IActionCommandManager commandManager)
{
//executionContext.Debug("ENTERING OutputManager ctor");
_executionContext = executionContext;
_commandManager = commandManager;
//_executionContext.Debug("OutputManager ctor - determine timeout from variable");
// Determine the timeout
var timeoutStr = _executionContext.Variables.Get(_timeoutKey);
if (string.IsNullOrEmpty(timeoutStr) ||
!TimeSpan.TryParse(timeoutStr, CultureInfo.InvariantCulture, out _timeout) ||
_timeout <= TimeSpan.Zero)
{
//_executionContext.Debug("OutputManager ctor - determine timeout from env var");
timeoutStr = Environment.GetEnvironmentVariable(_timeoutKey);
if (string.IsNullOrEmpty(timeoutStr) ||
!TimeSpan.TryParse(timeoutStr, CultureInfo.InvariantCulture, out _timeout) ||
_timeout <= TimeSpan.Zero)
{
//_executionContext.Debug("OutputManager ctor - set timeout to default");
_timeout = TimeSpan.FromSeconds(1);
}
}
//_executionContext.Debug("OutputManager ctor - adding matchers");
// Lock
lock (_matchersLock)
{
//_executionContext.Debug("OutputManager ctor - adding OnMatcherChanged");
_executionContext.Add(OnMatcherChanged);
//_executionContext.Debug("OutputManager ctor - getting matchers");
_matchers = _executionContext.GetMatchers().Select(x => new IssueMatcher(x, _timeout)).ToArray();
}
//_executionContext.Debug("LEAVING OutputManager ctor");
}
public void Dispose()
{
try
{
_executionContext.Remove(OnMatcherChanged);
}
catch
{
}
}
public void OnDataReceived(object sender, ProcessDataReceivedEventArgs e)
{
//_executionContext.Debug("ENTERING OutputManager OnDataReceived");
var line = e.Data;
// ## commands
if (!String.IsNullOrEmpty(line) &&
(line.IndexOf(ActionCommand.Prefix) >= 0 || line.IndexOf(ActionCommand._commandKey) >= 0))
{
// This does not need to be inside of a critical section.
// The logging queues and command handlers are thread-safe.
if (_commandManager.TryProcessCommand(_executionContext, line))
{
//_executionContext.Debug("LEAVING OutputManager OnDataReceived - command processed");
return;
}
}
// Problem matchers
if (_matchers.Length > 0)
{
// Copy the reference
var matchers = _matchers;
// Strip color codes
var stripped = line.Contains(_colorCodePrefix) ? _colorCodeRegex.Replace(line, string.Empty) : line;
foreach (var matcher in matchers)
{
IssueMatch match = null;
for (var attempt = 1; attempt <= _maxAttempts; attempt++)
{
// Match
try
{
match = matcher.Match(stripped);
break;
}
catch (RegexMatchTimeoutException ex)
{
if (attempt < _maxAttempts)
{
// Debug
_executionContext.Debug($"Timeout processing issue matcher '{matcher.Owner}' against line '{stripped}'. Exception: {ex.ToString()}");
}
else
{
// Warn
_executionContext.Warning($"Removing issue matcher '{matcher.Owner}'. Matcher failed {_maxAttempts} times. Error: {ex.Message}");
// Remove
Remove(matcher);
}
}
}
if (match != null)
{
// Reset other matchers
foreach (var otherMatcher in matchers.Where(x => !object.ReferenceEquals(x, matcher)))
{
otherMatcher.Reset();
}
// Convert to issue
var issue = ConvertToIssue(match);
if (issue != null)
{
// Log issue
_executionContext.AddIssue(issue, stripped);
//_executionContext.Debug("LEAVING OutputManager OnDataReceived - issue logged");
return;
}
}
}
}
// Regular output
_executionContext.Output(line);
//_executionContext.Debug("LEAVING OutputManager OnDataReceived");
}
private void OnMatcherChanged(object sender, MatcherChangedEventArgs e)
{
// Lock
lock (_matchersLock)
{
var newMatchers = new List<IssueMatcher>();
// Prepend
if (e.Config.Patterns.Length > 0)
{
newMatchers.Add(new IssueMatcher(e.Config, _timeout));
}
// Add existing non-matching
newMatchers.AddRange(_matchers.Where(x => !string.Equals(x.Owner, e.Config.Owner, StringComparison.OrdinalIgnoreCase)));
// Store
_matchers = newMatchers.ToArray();
}
}
private void Remove(IssueMatcher matcher)
{
// Lock
lock (_matchersLock)
{
var newMatchers = new List<IssueMatcher>();
// Match by object reference, not by owner name
newMatchers.AddRange(_matchers.Where(x => !object.ReferenceEquals(x, matcher)));
// Store
_matchers = newMatchers.ToArray();
}
}
private DTWebApi.Issue ConvertToIssue(IssueMatch match)
{
// Validate the message
if (string.IsNullOrWhiteSpace(match.Message))
{
_executionContext.Debug("Skipping logging an issue for the matched line because the message is empty.");
return null;
}
// Validate the severity
DTWebApi.IssueType issueType;
if (string.IsNullOrEmpty(match.Severity) || string.Equals(match.Severity, "error", StringComparison.OrdinalIgnoreCase))
{
issueType = DTWebApi.IssueType.Error;
}
else if (string.Equals(match.Severity, "warning", StringComparison.OrdinalIgnoreCase))
{
issueType = DTWebApi.IssueType.Warning;
}
else
{
_executionContext.Debug($"Skipped logging an issue for the matched line because the severity '{match.Severity}' is not supported.");
return null;
}
var issue = new DTWebApi.Issue
{
Message = match.Message,
Type = issueType,
};
// Line
if (!string.IsNullOrEmpty(match.Line))
{
if (int.TryParse(match.Line, NumberStyles.None, CultureInfo.InvariantCulture, out var line))
{
issue.Data["line"] = line.ToString(CultureInfo.InvariantCulture);
}
else
{
_executionContext.Debug($"Unable to parse line number '{match.Line}'");
}
}
// Column
if (!string.IsNullOrEmpty(match.Column))
{
if (int.TryParse(match.Column, NumberStyles.None, CultureInfo.InvariantCulture, out var column))
{
issue.Data["col"] = column.ToString(CultureInfo.InvariantCulture);
}
else
{
_executionContext.Debug($"Unable to parse column number '{match.Column}'");
}
}
// Code
if (!string.IsNullOrWhiteSpace(match.Code))
{
issue.Data["code"] = match.Code.Trim();
}
// File
try
{
if (!string.IsNullOrWhiteSpace(match.File))
{
var file = match.File;
// Root using fromPath
if (!string.IsNullOrWhiteSpace(match.FromPath) && !Path.IsPathRooted(file))
{
file = Path.Combine(match.FromPath, file);
}
// Root using system.defaultWorkingDirectory
if (!Path.IsPathRooted(file))
{
var githubContext = _executionContext.ExpressionValues["github"] as GitHubContext;
ArgUtil.NotNull(githubContext, nameof(githubContext));
var workspace = githubContext["workspace"].ToString();
ArgUtil.NotNullOrEmpty(workspace, "workspace");
file = Path.Combine(workspace, file);
}
// Normalize slashes
file = file.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
// File exists
if (File.Exists(file))
{
// Repository path
var repositoryPath = _executionContext.GetGitHubContext("workspace");
ArgUtil.NotNullOrEmpty(repositoryPath, nameof(repositoryPath));
// Normalize slashes
repositoryPath = repositoryPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
if (!file.StartsWith(repositoryPath, IOUtil.FilePathStringComparison))
{
// File is not under repo
_executionContext.Debug($"Dropping file value '{file}'. Path is not under the repo.");
}
else
{
// prefer `/` on all platforms
issue.Data["file"] = file.Substring(repositoryPath.Length).TrimStart(Path.DirectorySeparatorChar).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
}
// File does not exist
else
{
_executionContext.Debug($"Dropping file value '{file}'. Path does not exist");
}
}
}
catch (Exception ex)
{
_executionContext.Debug($"Dropping file value '{match.File}' and fromPath value '{match.FromPath}'. Exception during validation: {ex.ToString()}");
}
return issue;
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Threading.Tasks;
using System;
using GitHub.Runner.Sdk;
using GitHub.Runner.Common;
using Pipelines = GitHub.DistributedTask.Pipelines;
namespace GitHub.Runner.Worker.Handlers
{
[ServiceLocator(Default = typeof(RunnerPluginHandler))]
public interface IRunnerPluginHandler : IHandler
{
PluginActionExecutionData Data { get; set; }
}
public sealed class RunnerPluginHandler : Handler, IRunnerPluginHandler
{
public PluginActionExecutionData Data { get; set; }
public async Task RunAsync(ActionRunStage stage)
{
// Validate args.
Trace.Entering();
ArgUtil.NotNull(Data, nameof(Data));
ArgUtil.NotNull(ExecutionContext, nameof(ExecutionContext));
ArgUtil.NotNull(Inputs, nameof(Inputs));
string plugin = null;
if (stage == ActionRunStage.Main)
{
plugin = Data.Plugin;
}
else if (stage == ActionRunStage.Post)
{
plugin = Data.Cleanup;
}
ArgUtil.NotNullOrEmpty(plugin, nameof(plugin));
// Update the env dictionary.
AddPrependPathToEnvironment();
// Make sure only particular task get run as runner plugin.
var runnerPlugin = HostContext.GetService<IRunnerPluginManager>();
using (var outputManager = new OutputManager(ExecutionContext, ActionCommandManager))
{
ActionCommandManager.EnablePluginInternalCommand();
try
{
await runnerPlugin.RunPluginActionAsync(ExecutionContext, plugin, Inputs, Environment, RuntimeVariables, outputManager.OnDataReceived);
}
finally
{
ActionCommandManager.DisablePluginInternalCommand();
}
}
}
}
}

View File

@@ -0,0 +1,241 @@
using System;
using System.IO;
using System.Text;
using System.Threading.Tasks;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.DistributedTask.WebApi;
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; }
public override void PrintActionDetails(ActionRunStage stage)
{
if (stage == ActionRunStage.Post)
{
throw new NotSupportedException("Script action should not have 'Post' job action.");
}
Inputs.TryGetValue("script", out string contents);
contents = contents ?? string.Empty;
if (Action.Type == Pipelines.ActionSourceType.Script)
{
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}");
}
else
{
throw new InvalidOperationException($"Invalid action type {Action.Type} for {nameof(ScriptHandler)}");
}
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");
}
string argFormat;
string shellCommand;
string shellCommandPath = null;
bool validateShellOnHost = !(StepHost is ContainerStepHost);
Inputs.TryGetValue("shell", out var shell);
if (string.IsNullOrEmpty(shell))
{
#if OS_WINDOWS
shellCommand = "cmd";
if(validateShellOnHost)
{
shellCommandPath = System.Environment.GetEnvironmentVariable("ComSpec");
}
#else
shellCommand = "sh";
if (validateShellOnHost)
{
shellCommandPath = WhichUtil.Which("bash") ?? WhichUtil.Which("sh", true, Trace);
}
#endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
else
{
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand;
if (validateShellOnHost)
{
shellCommandPath = WhichUtil.Which(parsed.shellCommand, true, Trace);
}
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)
{
if (stage == ActionRunStage.Post)
{
throw new NotSupportedException("Script action should not have 'Post' job action.");
}
// 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;
Inputs.TryGetValue("workingDirectory", out var workingDirectory);
var workspaceDir = githubContext["workspace"] as StringContextData;
workingDirectory = Path.Combine(workspaceDir, workingDirectory ?? string.Empty);
Inputs.TryGetValue("shell", out var shell);
var isContainerStepHost = StepHost is ContainerStepHost;
string commandPath, argFormat, shellCommand;
// Set up default command and arguments
if (string.IsNullOrEmpty(shell))
{
#if OS_WINDOWS
shellCommand = "cmd";
commandPath = System.Environment.GetEnvironmentVariable("ComSpec");
ArgUtil.NotNullOrEmpty(commandPath, "%ComSpec%");
#else
shellCommand = "sh";
commandPath = WhichUtil.Which("bash", false, Trace) ?? WhichUtil.Which("sh", true, Trace);
#endif
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(shellCommand);
}
else
{
var parsed = ScriptHandlerHelpers.ParseShellOptionString(shell);
shellCommand = parsed.shellCommand;
// For non-ContainerStepHost, the command must be located on the host by Which
commandPath = WhichUtil.Which(parsed.shellCommand, !isContainerStepHost, Trace);
argFormat = $"{parsed.shellArgs}".TrimStart();
if (string.IsNullOrEmpty(argFormat))
{
argFormat = ScriptHandlerHelpers.GetScriptArgumentsFormat(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}'");
}
// We do not not the full path until we know what shell is being used, so that we can determine the file extension
var scriptFilePath = Path.Combine(tempDirectory, $"{Guid.NewGuid()}{ScriptHandlerHelpers.GetScriptFileExtension(shellCommand)}");
var resolvedScriptPath = $"{StepHost.ResolvePathForStepHost(scriptFilePath).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.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
// 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;
ExecutionContext.Debug($"{fileName} {arguments}");
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(workingDirectory: StepHost.ResolvePathForStepHost(workingDirectory),
fileName: fileName,
arguments: arguments,
environment: Environment,
requireExitCodeZero: false,
outputEncoding: null,
killProcessOnCancel: false,
inheritConsoleHandler: !ExecutionContext.Variables.Retain_Default_Encoding,
cancellationToken: ExecutionContext.CancellationToken);
// Error
if (exitCode != 0)
{
ExecutionContext.Error($"Process completed with exit code {exitCode}.");
ExecutionContext.Result = TaskResult.Failed;
}
}
}
}
}

View File

@@ -0,0 +1,83 @@
using System;
using System.Collections.Generic;
namespace GitHub.Runner.Worker.Handlers
{
internal class ScriptHandlerHelpers
{
private static readonly Dictionary<string, string> _defaultArguments = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["cmd"] = "/D /E:ON /V:OFF /S /C \"CALL \"{0}\"\"",
["pwsh"] = "-command \". '{0}'\"",
["powershell"] = "-command \". '{0}'\"",
["bash"] = "--noprofile --norc -e -o pipefail {0}",
["sh"] = "-e {0}",
["python"] = "{0}"
};
private static readonly Dictionary<string, string> _extensions = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["cmd"] = ".cmd",
["pwsh"] = ".ps1",
["powershell"] = ".ps1",
["bash"] = ".sh",
["sh"] = ".sh",
["python"] = ".py"
};
internal static string GetScriptArgumentsFormat(string scriptType)
{
if (_defaultArguments.TryGetValue(scriptType, out var argFormat))
{
return argFormat;
}
return "";
}
internal static string GetScriptFileExtension(string scriptType)
{
if (_extensions.TryGetValue(scriptType, out var extension))
{
return extension;
}
return "";
}
internal static string FixUpScriptContents(string scriptType, string contents)
{
switch (scriptType)
{
case "cmd":
// Note, use @echo off instead of using the /Q command line switch.
// When /Q is used, echo can't be turned on.
contents = $"@echo off{Environment.NewLine}{contents}";
break;
case "powershell":
case "pwsh":
var prepend = "$ErrorActionPreference = 'stop'";
var append = @"if ((Test-Path -LiteralPath variable:\LASTEXITCODE)) { exit $LASTEXITCODE }";
contents = $"{prepend}{Environment.NewLine}{contents}{Environment.NewLine}{append}";
break;
}
return contents;
}
internal static (string shellCommand, string shellArgs) ParseShellOptionString(string shellOption)
{
var shellStringParts = shellOption.Split(" ", 2);
if (shellStringParts.Length == 2)
{
return (shellCommand: shellStringParts[0], shellArgs: shellStringParts[1]);
}
else if (shellStringParts.Length == 1)
{
return (shellCommand: shellStringParts[0], shellArgs: "");
}
else
{
throw new ArgumentException($"Failed to parse COMMAND [..ARGS] from {shellOption}");
}
}
}
}

View File

@@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Worker.Container;
using GitHub.Services.WebApi;
using Newtonsoft.Json;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using System.Linq;
namespace GitHub.Runner.Worker.Handlers
{
public interface IStepHost : IRunnerService
{
event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
string ResolvePathForStepHost(string path);
Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext);
Task<int> ExecuteAsync(string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
bool inheritConsoleHandler,
CancellationToken cancellationToken);
}
[ServiceLocator(Default = typeof(ContainerStepHost))]
public interface IContainerStepHost : IStepHost
{
ContainerInfo Container { get; set; }
string PrependPath { get; set; }
}
[ServiceLocator(Default = typeof(DefaultStepHost))]
public interface IDefaultStepHost : IStepHost
{
}
public sealed class DefaultStepHost : RunnerService, IDefaultStepHost
{
public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
public string ResolvePathForStepHost(string path)
{
return path;
}
public Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext)
{
return Task.FromResult<string>("node12");
}
public async Task<int> ExecuteAsync(string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
bool inheritConsoleHandler,
CancellationToken cancellationToken)
{
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
{
processInvoker.OutputDataReceived += OutputDataReceived;
processInvoker.ErrorDataReceived += ErrorDataReceived;
return await processInvoker.ExecuteAsync(workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding,
killProcessOnCancel: killProcessOnCancel,
redirectStandardIn: null,
inheritConsoleHandler: inheritConsoleHandler,
cancellationToken: cancellationToken);
}
}
}
public sealed class ContainerStepHost : RunnerService, IContainerStepHost
{
public ContainerInfo Container { get; set; }
public string PrependPath { get; set; }
public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
public string ResolvePathForStepHost(string path)
{
// make sure container exist.
ArgUtil.NotNull(Container, nameof(Container));
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId));
// remove double quotes around the path
path = path.Trim('\"');
// try to resolve path inside container if the request path is part of the mount volume
#if OS_WINDOWS
if (Container.MountVolumes.Exists(x => path.StartsWith(x.SourceVolumePath, StringComparison.OrdinalIgnoreCase)))
#else
if (Container.MountVolumes.Exists(x => path.StartsWith(x.SourceVolumePath)))
#endif
{
return Container.TranslateToContainerPath(path);
}
else
{
return path;
}
}
public async Task<string> DetermineNodeRuntimeVersion(IExecutionContext executionContext)
{
// Best effort to determine a compatible node runtime
// There may be more variation in which libraries are linked than just musl/glibc,
// so determine based on known distribtutions instead
var osReleaseIdCmd = "sh -c \"cat /etc/*release | grep ^ID\"";
var dockerManager = HostContext.GetService<IDockerCommandManager>();
var output = new List<string>();
var execExitCode = await dockerManager.DockerExec(executionContext, Container.ContainerId, string.Empty, osReleaseIdCmd, output);
string nodeExternal;
if (execExitCode == 0)
{
foreach (var line in output)
{
executionContext.Debug(line);
if (line.ToLower().Contains("alpine"))
{
nodeExternal = "node12_alpine";
executionContext.Output($"Container distribution is alpine. Running JavaScript Action with external tool: {nodeExternal}");
return nodeExternal;
}
}
}
// Optimistically use the default
nodeExternal = "node12";
executionContext.Output($"Running JavaScript Action with default external tool: {nodeExternal}");
return nodeExternal;
}
public async Task<int> ExecuteAsync(string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
bool inheritConsoleHandler,
CancellationToken cancellationToken)
{
// make sure container exist.
ArgUtil.NotNull(Container, nameof(Container));
ArgUtil.NotNullOrEmpty(Container.ContainerId, nameof(Container.ContainerId));
var dockerManager = HostContext.GetService<IDockerCommandManager>();
string dockerClientPath = dockerManager.DockerPath;
// Usage: docker exec [OPTIONS] CONTAINER COMMAND [ARG...]
IList<string> dockerCommandArgs = new List<string>();
dockerCommandArgs.Add($"exec");
// [OPTIONS]
dockerCommandArgs.Add($"-i");
dockerCommandArgs.Add($"--workdir {workingDirectory}");
foreach (var env in environment)
{
// e.g. -e MY_SECRET maps the value into the exec'ed process without exposing
// the value directly in the command
dockerCommandArgs.Add($"-e {env.Key}");
}
if (!string.IsNullOrEmpty(PrependPath))
{
// Prepend tool paths to container's PATH
var fullPath = !string.IsNullOrEmpty(Container.ContainerRuntimePath) ? $"{PrependPath}:{Container.ContainerRuntimePath}" : PrependPath;
dockerCommandArgs.Add($"-e PATH=\"{fullPath}\"");
}
// CONTAINER
dockerCommandArgs.Add($"{Container.ContainerId}");
// COMMAND
dockerCommandArgs.Add(fileName);
// [ARG...]
dockerCommandArgs.Add(arguments);
string dockerCommandArgstring = string.Join(" ", dockerCommandArgs);
// make sure all env are using container path
foreach (var envKey in environment.Keys.ToList())
{
environment[envKey] = this.Container.TranslateToContainerPath(environment[envKey]);
}
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
{
processInvoker.OutputDataReceived += OutputDataReceived;
processInvoker.ErrorDataReceived += ErrorDataReceived;
#if OS_WINDOWS
// It appears that node.exe outputs UTF8 when not in TTY mode.
outputEncoding = Encoding.UTF8;
#else
// Let .NET choose the default.
outputEncoding = null;
#endif
return await processInvoker.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Work),
fileName: dockerClientPath,
arguments: dockerCommandArgstring,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding,
killProcessOnCancel: killProcessOnCancel,
redirectStandardIn: null,
inheritConsoleHandler: inheritConsoleHandler,
cancellationToken: cancellationToken);
}
}
}
}