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,253 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System;
using System.Collections.Generic;
namespace GitHub.Runner.Common
{
public sealed class ActionCommand
{
private static readonly EscapeMapping[] _escapeMappings = new[]
{
new EscapeMapping(token: "%", replacement: "%25"),
new EscapeMapping(token: ";", replacement: "%3B"),
new EscapeMapping(token: "\r", replacement: "%0D"),
new EscapeMapping(token: "\n", replacement: "%0A"),
new EscapeMapping(token: "]", replacement: "%5D"),
};
private static readonly EscapeMapping[] _escapeDataMappings = new[]
{
new EscapeMapping(token: "\r", replacement: "%0D"),
new EscapeMapping(token: "\n", replacement: "%0A"),
};
private static readonly EscapeMapping[] _escapePropertyMappings = new[]
{
new EscapeMapping(token: "%", replacement: "%25"),
new EscapeMapping(token: "\r", replacement: "%0D"),
new EscapeMapping(token: "\n", replacement: "%0A"),
new EscapeMapping(token: ":", replacement: "%3A"),
new EscapeMapping(token: ",", replacement: "%2C"),
};
private readonly Dictionary<string, string> _properties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
public const string Prefix = "##[";
public const string _commandKey = "::";
public ActionCommand(string command)
{
ArgUtil.NotNullOrEmpty(command, nameof(command));
Command = command;
}
public string Command { get; }
public Dictionary<string, string> Properties => _properties;
public string Data { get; set; }
public static bool TryParseV2(string message, HashSet<string> registeredCommands, out ActionCommand command)
{
command = null;
if (string.IsNullOrEmpty(message))
{
return false;
}
try
{
// the message needs to start with the keyword after trim leading space.
message = message.TrimStart();
if (!message.StartsWith(_commandKey))
{
return false;
}
// Get the index of the separator between the command info and the data.
int endIndex = message.IndexOf(_commandKey, _commandKey.Length);
if (endIndex < 0)
{
return false;
}
// Get the command info (command and properties).
int cmdIndex = _commandKey.Length;
string cmdInfo = message.Substring(cmdIndex, endIndex - cmdIndex);
// Get the command name
int spaceIndex = cmdInfo.IndexOf(' ');
string commandName =
spaceIndex < 0
? cmdInfo
: cmdInfo.Substring(0, spaceIndex);
if (registeredCommands.Contains(commandName))
{
// Initialize the command.
command = new ActionCommand(commandName);
}
else
{
return false;
}
// Set the properties.
if (spaceIndex > 0)
{
string propertiesStr = cmdInfo.Substring(spaceIndex + 1).Trim();
string[] splitProperties = propertiesStr.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string propertyStr in splitProperties)
{
string[] pair = propertyStr.Split(new[] { '=' }, count: 2, options: StringSplitOptions.RemoveEmptyEntries);
if (pair.Length == 2)
{
command.Properties[pair[0]] = UnescapeProperty(pair[1]);
}
}
}
command.Data = UnescapeData(message.Substring(endIndex + _commandKey.Length));
return true;
}
catch
{
command = null;
return false;
}
}
public static bool TryParse(string message, HashSet<string> registeredCommands, out ActionCommand command)
{
command = null;
if (string.IsNullOrEmpty(message))
{
return false;
}
try
{
// Get the index of the prefix.
int prefixIndex = message.IndexOf(Prefix);
if (prefixIndex < 0)
{
return false;
}
// Get the index of the separator between the command info and the data.
int rbIndex = message.IndexOf(']', prefixIndex);
if (rbIndex < 0)
{
return false;
}
// Get the command info (command and properties).
int cmdIndex = prefixIndex + Prefix.Length;
string cmdInfo = message.Substring(cmdIndex, rbIndex - cmdIndex);
// Get the command name
int spaceIndex = cmdInfo.IndexOf(' ');
string commandName =
spaceIndex < 0
? cmdInfo
: cmdInfo.Substring(0, spaceIndex);
if (registeredCommands.Contains(commandName))
{
// Initialize the command.
command = new ActionCommand(commandName);
}
else
{
return false;
}
// Set the properties.
if (spaceIndex > 0)
{
string propertiesStr = cmdInfo.Substring(spaceIndex + 1);
string[] splitProperties = propertiesStr.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries);
foreach (string propertyStr in splitProperties)
{
string[] pair = propertyStr.Split(new[] { '=' }, count: 2, options: StringSplitOptions.RemoveEmptyEntries);
if (pair.Length == 2)
{
command.Properties[pair[0]] = Unescape(pair[1]);
}
}
}
command.Data = Unescape(message.Substring(rbIndex + 1));
return true;
}
catch
{
command = null;
return false;
}
}
private static string Unescape(string escaped)
{
if (string.IsNullOrEmpty(escaped))
{
return string.Empty;
}
string unescaped = escaped;
foreach (EscapeMapping mapping in _escapeMappings)
{
unescaped = unescaped.Replace(mapping.Replacement, mapping.Token);
}
return unescaped;
}
private static string UnescapeProperty(string escaped)
{
if (string.IsNullOrEmpty(escaped))
{
return string.Empty;
}
string unescaped = escaped;
foreach (EscapeMapping mapping in _escapePropertyMappings)
{
unescaped = unescaped.Replace(mapping.Replacement, mapping.Token);
}
return unescaped;
}
private static string UnescapeData(string escaped)
{
if (string.IsNullOrEmpty(escaped))
{
return string.Empty;
}
string unescaped = escaped;
foreach (EscapeMapping mapping in _escapeDataMappings)
{
unescaped = unescaped.Replace(mapping.Replacement, mapping.Token);
}
return unescaped;
}
private sealed class EscapeMapping
{
public string Replacement { get; }
public string Token { get; }
public EscapeMapping(string token, string replacement)
{
ArgUtil.NotNullOrEmpty(token, nameof(token));
ArgUtil.NotNullOrEmpty(replacement, nameof(replacement));
Token = token;
Replacement = replacement;
}
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace GitHub.Runner.Common
{
public enum ActionResult
{
Success = 0,
Failure = 1,
Cancelled = 2,
Skipped = 3
}
}

View File

@@ -0,0 +1,33 @@
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Runner.Common
{
//Stephen Toub: http://blogs.msdn.com/b/pfxteam/archive/2012/02/11/10266920.aspx
public class AsyncManualResetEvent
{
private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>();
public Task WaitAsync() { return m_tcs.Task; }
public void Set()
{
var tcs = m_tcs;
Task.Factory.StartNew(s => ((TaskCompletionSource<bool>)s).TrySetResult(true),
tcs, CancellationToken.None, TaskCreationOptions.PreferFairness, TaskScheduler.Default);
tcs.Task.Wait();
}
public void Reset()
{
while (true)
{
var tcs = m_tcs;
if (!tcs.Task.IsCompleted ||
Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs)
return;
}
}
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common.Capabilities
{
[ServiceLocator(Default = typeof(CapabilitiesManager))]
public interface ICapabilitiesManager : IRunnerService
{
Task<Dictionary<string, string>> GetCapabilitiesAsync(RunnerSettings settings, CancellationToken token);
}
public sealed class CapabilitiesManager : RunnerService, ICapabilitiesManager
{
public async Task<Dictionary<string, string>> GetCapabilitiesAsync(RunnerSettings settings, CancellationToken cancellationToken)
{
Trace.Entering();
ArgUtil.NotNull(settings, nameof(settings));
// Initialize a dictionary of capabilities.
var capabilities = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (settings.SkipCapabilitiesScan)
{
Trace.Info("Skip capabilities scan.");
return capabilities;
}
// Get the providers.
var extensionManager = HostContext.GetService<IExtensionManager>();
IEnumerable<ICapabilitiesProvider> providers =
extensionManager
.GetExtensions<ICapabilitiesProvider>()
?.OrderBy(x => x.Order);
// Add each capability returned from each provider.
foreach (ICapabilitiesProvider provider in providers ?? new ICapabilitiesProvider[0])
{
foreach (Capability capability in await provider.GetCapabilitiesAsync(settings, cancellationToken) ?? new List<Capability>())
{
// Make sure we mask secrets in capabilities values.
capabilities[capability.Name] = HostContext.SecretMasker.MaskSecrets(capability.Value);
}
}
return capabilities;
}
}
public interface ICapabilitiesProvider : IExtension
{
int Order { get; }
Task<List<Capability>> GetCapabilitiesAsync(RunnerSettings settings, CancellationToken cancellationToken);
}
public sealed class Capability
{
public string Name { get; }
public string Value { get; }
public Capability(string name, string value)
{
ArgUtil.NotNullOrEmpty(name, nameof(name));
Name = name;
Value = value ?? string.Empty;
}
}
}

View File

@@ -0,0 +1,86 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Runner.Common.Capabilities
{
public sealed class RunnerCapabilitiesProvider : RunnerService, ICapabilitiesProvider
{
public Type ExtensionType => typeof(ICapabilitiesProvider);
public int Order => 99; // Process last to override prior.
public Task<List<Capability>> GetCapabilitiesAsync(RunnerSettings settings, CancellationToken cancellationToken)
{
ArgUtil.NotNull(settings, nameof(settings));
var capabilities = new List<Capability>();
Add(capabilities, "Runner.Name", settings.AgentName ?? string.Empty);
Add(capabilities, "Runner.OS", VarUtil.OS);
Add(capabilities, "Runner.OSArchitecture", VarUtil.OSArchitecture);
#if OS_WINDOWS
Add(capabilities, "Runner.OSVersion", GetOSVersionString());
#endif
Add(capabilities, "InteractiveSession", (HostContext.StartupType != StartupType.Service).ToString());
Add(capabilities, "Runner.Version", BuildConstants.RunnerPackage.Version);
Add(capabilities, "Runner.ComputerName", Environment.MachineName ?? string.Empty);
Add(capabilities, "Runner.HomeDirectory", HostContext.GetDirectory(WellKnownDirectory.Root));
return Task.FromResult(capabilities);
}
private void Add(List<Capability> capabilities, string name, string value)
{
Trace.Info($"Adding '{name}': '{value}'");
capabilities.Add(new Capability(name, value));
}
private object GetHklmValue(string keyName, string valueName)
{
keyName = $@"HKEY_LOCAL_MACHINE\{keyName}";
object value = Registry.GetValue(keyName, valueName, defaultValue: null);
if (object.ReferenceEquals(value, null))
{
Trace.Info($"Key name '{keyName}', value name '{valueName}' is null.");
return null;
}
Trace.Info($"Key name '{keyName}', value name '{valueName}': '{value}'");
return value;
}
private string GetOSVersionString()
{
// Do not use System.Environment.OSVersion.Version to resolve the OS version number.
// It leverages the GetVersionEx function which may report an incorrect version
// depending on the app's manifest. For details, see:
// https://msdn.microsoft.com/library/windows/desktop/ms724451(v=vs.85).aspx
// Attempt to retrieve the major/minor version from the new registry values added in
// in Windows 10.
//
// The registry value "CurrentVersion" is unreliable in Windows 10. It contains the
// value "6.3" instead of "10.0".
object major = GetHklmValue(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentMajorVersionNumber");
object minor = GetHklmValue(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentMinorVersionNumber");
string majorMinorString;
if (major != null && minor != null)
{
majorMinorString = StringUtil.Format("{0}.{1}", major, minor);
}
else
{
// Fallback to the registry value "CurrentVersion".
majorMinorString = GetHklmValue(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentVersion") as string;
}
// Opted to use the registry value "CurrentBuildNumber" over "CurrentBuild". Based on brief
// internet investigation, the only difference appears to be that on Windows XP "CurrentBuild"
// was unreliable and "CurrentBuildNumber" was the correct choice.
string build = GetHklmValue(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentBuildNumber") as string;
return StringUtil.Format("{0}.{1}", majorMinorString, build);
}
}
}

View File

@@ -0,0 +1,128 @@
using GitHub.Runner.Common.Util;
using System;
using System.Collections.Generic;
using GitHub.DistributedTask.Logging;
using GitHub.Runner.Sdk;
//
// Pattern:
// cmd1 cmd2 --arg1 arg1val --aflag --arg2 arg2val
//
namespace GitHub.Runner.Common
{
public sealed class CommandLineParser
{
private ISecretMasker _secretMasker;
private Tracing _trace;
public List<string> Commands { get; }
public HashSet<string> Flags { get; }
public Dictionary<string, string> Args { get; }
public HashSet<string> SecretArgNames { get; }
private bool HasArgs { get; set; }
public CommandLineParser(IHostContext hostContext, string[] secretArgNames)
{
_secretMasker = hostContext.SecretMasker;
_trace = hostContext.GetTrace(nameof(CommandLineParser));
Commands = new List<string>();
Flags = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
Args = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
SecretArgNames = new HashSet<string>(secretArgNames ?? new string[0], StringComparer.OrdinalIgnoreCase);
}
public bool IsCommand(string name)
{
bool result = false;
if (Commands.Count > 0)
{
result = String.Equals(name, Commands[0], StringComparison.CurrentCultureIgnoreCase);
}
return result;
}
public void Parse(string[] args)
{
_trace.Info(nameof(Parse));
ArgUtil.NotNull(args, nameof(args));
_trace.Info("Parsing {0} args", args.Length);
string argScope = null;
foreach (string arg in args)
{
_trace.Info("parsing argument");
HasArgs = HasArgs || arg.StartsWith("--");
_trace.Info("HasArgs: {0}", HasArgs);
if (string.Equals(arg, "/?", StringComparison.Ordinal))
{
Flags.Add("help");
}
else if (!HasArgs)
{
_trace.Info("Adding Command: {0}", arg);
Commands.Add(arg.Trim());
}
else
{
// it's either an arg, an arg value or a flag
if (arg.StartsWith("--") && arg.Length > 2)
{
string argVal = arg.Substring(2);
_trace.Info("arg: {0}", argVal);
// this means two --args in a row which means previous was a flag
if (argScope != null)
{
_trace.Info("Adding flag: {0}", argScope);
Flags.Add(argScope.Trim());
}
argScope = argVal;
}
else if (!arg.StartsWith("-"))
{
// we found a value - check if we're in scope of an arg
if (argScope != null && !Args.ContainsKey(argScope = argScope.Trim()))
{
if (SecretArgNames.Contains(argScope))
{
_secretMasker.AddValue(arg);
}
_trace.Info("Adding option '{0}': '{1}'", argScope, arg);
// ignore duplicates - first wins - below will be val1
// --arg1 val1 --arg1 val1
Args.Add(argScope, arg);
argScope = null;
}
}
else
{
//
// ignoring the second value for an arg (val2 below)
// --arg val1 val2
// ignoring invalid things like empty - and --
// --arg val1 -- --flag
_trace.Info("Ignoring arg");
}
}
}
_trace.Verbose("done parsing arguments");
// handle last arg being a flag
if (argScope != null)
{
Flags.Add(argScope);
}
_trace.Verbose("Exiting parse");
}
}
}

View File

@@ -0,0 +1,252 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System.IO;
using System.Runtime.Serialization;
using System.Text;
using System.Threading;
namespace GitHub.Runner.Common
{
//
// Settings are persisted in this structure
//
[DataContract]
public sealed class RunnerSettings
{
[DataMember(EmitDefaultValue = false)]
public bool AcceptTeeEula { get; set; }
[DataMember(EmitDefaultValue = false)]
public int AgentId { get; set; }
[DataMember(EmitDefaultValue = false)]
public string AgentName { get; set; }
[DataMember(EmitDefaultValue = false)]
public string NotificationPipeName { get; set; }
[DataMember(EmitDefaultValue = false)]
public string NotificationSocketAddress { get; set; }
[DataMember(EmitDefaultValue = false)]
public bool SkipCapabilitiesScan { get; set; }
[DataMember(EmitDefaultValue = false)]
public bool SkipSessionRecover { get; set; }
[DataMember(EmitDefaultValue = false)]
public int PoolId { get; set; }
[DataMember(EmitDefaultValue = false)]
public string PoolName { get; set; }
[DataMember(EmitDefaultValue = false)]
public string ServerUrl { get; set; }
[DataMember(EmitDefaultValue = false)]
public string GitHubUrl { get; set; }
[DataMember(EmitDefaultValue = false)]
public string WorkFolder { get; set; }
[DataMember(EmitDefaultValue = false)]
public string MonitorSocketAddress { get; set; }
}
[DataContract]
public sealed class RunnerRuntimeOptions
{
#if OS_WINDOWS
[DataMember(EmitDefaultValue = false)]
public bool GitUseSecureChannel { get; set; }
#endif
}
[ServiceLocator(Default = typeof(ConfigurationStore))]
public interface IConfigurationStore : IRunnerService
{
bool IsConfigured();
bool IsServiceConfigured();
bool HasCredentials();
CredentialData GetCredentials();
RunnerSettings GetSettings();
void SaveCredential(CredentialData credential);
void SaveSettings(RunnerSettings settings);
void DeleteCredential();
void DeleteSettings();
RunnerRuntimeOptions GetRunnerRuntimeOptions();
void SaveRunnerRuntimeOptions(RunnerRuntimeOptions options);
void DeleteRunnerRuntimeOptions();
}
public sealed class ConfigurationStore : RunnerService, IConfigurationStore
{
private string _binPath;
private string _configFilePath;
private string _credFilePath;
private string _serviceConfigFilePath;
private string _runtimeOptionsFilePath;
private CredentialData _creds;
private RunnerSettings _settings;
private RunnerRuntimeOptions _runtimeOptions;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
var currentAssemblyLocation = System.Reflection.Assembly.GetEntryAssembly().Location;
Trace.Info("currentAssemblyLocation: {0}", currentAssemblyLocation);
_binPath = HostContext.GetDirectory(WellKnownDirectory.Bin);
Trace.Info("binPath: {0}", _binPath);
RootFolder = HostContext.GetDirectory(WellKnownDirectory.Root);
Trace.Info("RootFolder: {0}", RootFolder);
_configFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Runner);
Trace.Info("ConfigFilePath: {0}", _configFilePath);
_credFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Credentials);
Trace.Info("CredFilePath: {0}", _credFilePath);
_serviceConfigFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Service);
Trace.Info("ServiceConfigFilePath: {0}", _serviceConfigFilePath);
_runtimeOptionsFilePath = hostContext.GetConfigFile(WellKnownConfigFile.Options);
Trace.Info("RuntimeOptionsFilePath: {0}", _runtimeOptionsFilePath);
}
public string RootFolder { get; private set; }
public bool HasCredentials()
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
Trace.Info("HasCredentials()");
bool credsStored = (new FileInfo(_credFilePath)).Exists;
Trace.Info("stored {0}", credsStored);
return credsStored;
}
public bool IsConfigured()
{
Trace.Info("IsConfigured()");
bool configured = HostContext.RunMode == RunMode.Local || (new FileInfo(_configFilePath)).Exists;
Trace.Info("IsConfigured: {0}", configured);
return configured;
}
public bool IsServiceConfigured()
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
Trace.Info("IsServiceConfigured()");
bool serviceConfigured = (new FileInfo(_serviceConfigFilePath)).Exists;
Trace.Info($"IsServiceConfigured: {serviceConfigured}");
return serviceConfigured;
}
public CredentialData GetCredentials()
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
if (_creds == null)
{
_creds = IOUtil.LoadObject<CredentialData>(_credFilePath);
}
return _creds;
}
public RunnerSettings GetSettings()
{
if (_settings == null)
{
RunnerSettings configuredSettings = null;
if (File.Exists(_configFilePath))
{
string json = File.ReadAllText(_configFilePath, Encoding.UTF8);
Trace.Info($"Read setting file: {json.Length} chars");
configuredSettings = StringUtil.ConvertFromJson<RunnerSettings>(json);
}
ArgUtil.NotNull(configuredSettings, nameof(configuredSettings));
_settings = configuredSettings;
}
return _settings;
}
public void SaveCredential(CredentialData credential)
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
Trace.Info("Saving {0} credential @ {1}", credential.Scheme, _credFilePath);
if (File.Exists(_credFilePath))
{
// Delete existing credential file first, since the file is hidden and not able to overwrite.
Trace.Info("Delete exist runner credential file.");
IOUtil.DeleteFile(_credFilePath);
}
IOUtil.SaveObject(credential, _credFilePath);
Trace.Info("Credentials Saved.");
File.SetAttributes(_credFilePath, File.GetAttributes(_credFilePath) | FileAttributes.Hidden);
}
public void SaveSettings(RunnerSettings settings)
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
Trace.Info("Saving runner settings.");
if (File.Exists(_configFilePath))
{
// Delete existing runner settings file first, since the file is hidden and not able to overwrite.
Trace.Info("Delete exist runner settings file.");
IOUtil.DeleteFile(_configFilePath);
}
IOUtil.SaveObject(settings, _configFilePath);
Trace.Info("Settings Saved.");
File.SetAttributes(_configFilePath, File.GetAttributes(_configFilePath) | FileAttributes.Hidden);
}
public void DeleteCredential()
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
IOUtil.Delete(_credFilePath, default(CancellationToken));
}
public void DeleteSettings()
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
IOUtil.Delete(_configFilePath, default(CancellationToken));
}
public RunnerRuntimeOptions GetRunnerRuntimeOptions()
{
if (_runtimeOptions == null && File.Exists(_runtimeOptionsFilePath))
{
_runtimeOptions = IOUtil.LoadObject<RunnerRuntimeOptions>(_runtimeOptionsFilePath);
}
return _runtimeOptions;
}
public void SaveRunnerRuntimeOptions(RunnerRuntimeOptions options)
{
Trace.Info("Saving runtime options.");
if (File.Exists(_runtimeOptionsFilePath))
{
// Delete existing runtime options file first, since the file is hidden and not able to overwrite.
Trace.Info("Delete exist runtime options file.");
IOUtil.DeleteFile(_runtimeOptionsFilePath);
}
IOUtil.SaveObject(options, _runtimeOptionsFilePath);
Trace.Info("Options Saved.");
File.SetAttributes(_runtimeOptionsFilePath, File.GetAttributes(_runtimeOptionsFilePath) | FileAttributes.Hidden);
}
public void DeleteRunnerRuntimeOptions()
{
IOUtil.Delete(_runtimeOptionsFilePath, default(CancellationToken));
}
}
}

View File

@@ -0,0 +1,343 @@
using System;
namespace GitHub.Runner.Common
{
public enum RunMode
{
Normal, // Keep "Normal" first (default value).
Local,
}
public enum WellKnownDirectory
{
Bin,
Diag,
Externals,
Root,
Actions,
Temp,
Tools,
Update,
Work,
}
public enum WellKnownConfigFile
{
Runner,
Credentials,
RSACredentials,
Service,
CredentialStore,
Certificates,
Proxy,
ProxyCredentials,
ProxyBypass,
Options,
}
public static class Constants
{
/// <summary>Path environment variable name.</summary>
#if OS_WINDOWS
public static readonly string PathVariable = "Path";
#else
public static readonly string PathVariable = "PATH";
#endif
public static string ProcessTrackingId = "RUNNER_TRACKING_ID";
public static string PluginTracePrefix = "##[plugin.trace]";
public static readonly int RunnerDownloadRetryMaxAttempts = 3;
// This enum is embedded within the Constants class to make it easier to reference and avoid
// ambiguous type reference with System.Runtime.InteropServices.OSPlatform and System.Runtime.InteropServices.Architecture
public enum OSPlatform
{
OSX,
Linux,
Windows
}
public enum Architecture
{
X86,
X64,
Arm,
Arm64
}
public static class Runner
{
#if OS_LINUX
public static readonly OSPlatform Platform = OSPlatform.Linux;
#elif OS_OSX
public static readonly OSPlatform Platform = OSPlatform.OSX;
#elif OS_WINDOWS
public static readonly OSPlatform Platform = OSPlatform.Windows;
#endif
#if X86
public static readonly Architecture PlatformArchitecture = Architecture.X86;
#elif X64
public static readonly Architecture PlatformArchitecture = Architecture.X64;
#elif ARM
public static readonly Architecture PlatformArchitecture = Architecture.Arm;
#elif ARM64
public static readonly Architecture PlatformArchitecture = Architecture.Arm64;
#endif
public static readonly TimeSpan ExitOnUnloadTimeout = TimeSpan.FromSeconds(30);
public static class CommandLine
{
//if you are adding a new arg, please make sure you update the
//validArgs array as well present in the CommandSettings.cs
public static class Args
{
public static readonly string Agent = "agent";
public static readonly string Auth = "auth";
public static readonly string CollectionName = "collectionname";
public static readonly string DeploymentGroupName = "deploymentgroupname";
public static readonly string DeploymentPoolName = "deploymentpoolname";
public static readonly string DeploymentGroupTags = "deploymentgrouptags";
public static readonly string MachineGroupName = "machinegroupname";
public static readonly string MachineGroupTags = "machinegrouptags";
public static readonly string Matrix = "matrix";
public static readonly string MonitorSocketAddress = "monitorsocketaddress";
public static readonly string NotificationPipeName = "notificationpipename";
public static readonly string NotificationSocketAddress = "notificationsocketaddress";
public static readonly string Pool = "pool";
public static readonly string ProjectName = "projectname";
public static readonly string ProxyUrl = "proxyurl";
public static readonly string ProxyUserName = "proxyusername";
public static readonly string SslCACert = "sslcacert";
public static readonly string SslClientCert = "sslclientcert";
public static readonly string SslClientCertKey = "sslclientcertkey";
public static readonly string SslClientCertArchive = "sslclientcertarchive";
public static readonly string SslClientCertPassword = "sslclientcertpassword";
public static readonly string StartupType = "startuptype";
public static readonly string Url = "url";
public static readonly string UserName = "username";
public static readonly string WindowsLogonAccount = "windowslogonaccount";
public static readonly string Work = "work";
public static readonly string Yml = "yml";
// Secret args. Must be added to the "Secrets" getter as well.
public static readonly string Password = "password";
public static readonly string ProxyPassword = "proxypassword";
public static readonly string Token = "token";
public static readonly string WindowsLogonPassword = "windowslogonpassword";
public static string[] Secrets => new[]
{
Password,
ProxyPassword,
SslClientCertPassword,
Token,
WindowsLogonPassword,
};
}
public static class Commands
{
public static readonly string Configure = "configure";
public static readonly string LocalRun = "localRun";
public static readonly string Remove = "remove";
public static readonly string Run = "run";
public static readonly string Warmup = "warmup";
}
//if you are adding a new flag, please make sure you update the
//validFlags array as well present in the CommandSettings.cs
public static class Flags
{
public static readonly string AcceptTeeEula = "acceptteeeula";
public static readonly string AddDeploymentGroupTags = "adddeploymentgrouptags";
public static readonly string AddMachineGroupTags = "addmachinegrouptags";
public static readonly string Commit = "commit";
public static readonly string DeploymentGroup = "deploymentgroup";
public static readonly string DeploymentPool = "deploymentpool";
public static readonly string OverwriteAutoLogon = "overwriteautologon";
public static readonly string GitUseSChannel = "gituseschannel";
public static readonly string Help = "help";
public static readonly string MachineGroup = "machinegroup";
public static readonly string Replace = "replace";
public static readonly string NoRestart = "norestart";
public static readonly string LaunchBrowser = "launchbrowser";
public static readonly string Once = "once";
public static readonly string RunAsAutoLogon = "runasautologon";
public static readonly string RunAsService = "runasservice";
public static readonly string SslSkipCertValidation = "sslskipcertvalidation";
public static readonly string Unattended = "unattended";
public static readonly string Version = "version";
public static readonly string WhatIf = "whatif";
}
}
public static class ReturnCode
{
public const int Success = 0;
public const int TerminatedError = 1;
public const int RetryableError = 2;
public const int RunnerUpdating = 3;
public const int RunOnceRunnerUpdating = 4;
}
}
public static class Pipeline
{
public static class Path
{
public static readonly string PipelineMappingDirectory = "_PipelineMapping";
public static readonly string TrackingConfigFile = "PipelineFolder.json";
}
}
public static class Configuration
{
public static readonly string AAD = "AAD";
public static readonly string OAuthAccessToken = "OAuthAccessToken";
public static readonly string PAT = "PAT";
public static readonly string OAuth = "OAuth";
}
public static class Expressions
{
public static readonly string Always = "always";
public static readonly string Canceled = "canceled";
public static readonly string Cancelled = "cancelled";
public static readonly string Failed = "failed";
public static readonly string Failure = "failure";
public static readonly string Success = "success";
public static readonly string Succeeded = "succeeded";
public static readonly string SucceededOrFailed = "succeededOrFailed";
public static readonly string Variables = "variables";
}
public static class Path
{
public static readonly string ActionsDirectory = "_actions";
public static readonly string ActionManifestFile = "action.yml";
public static readonly string BinDirectory = "bin";
public static readonly string DiagDirectory = "_diag";
public static readonly string ExternalsDirectory = "externals";
public static readonly string RunnerDiagnosticLogPrefix = "Runner_";
public static readonly string TempDirectory = "_temp";
public static readonly string TeeDirectory = "tee";
public static readonly string ToolDirectory = "_tool";
public static readonly string TaskJsonFile = "task.json";
public static readonly string UpdateDirectory = "_update";
public static readonly string WorkDirectory = "_work";
public static readonly string WorkerDiagnosticLogPrefix = "Worker_";
}
// Related to definition variables.
public static class Variables
{
public static readonly string MacroPrefix = "$(";
public static readonly string MacroSuffix = ")";
public static class Actions
{
//
// Keep alphabetical
//
public static readonly string RunnerDebug = "ACTIONS_RUNNER_DEBUG";
public static readonly string StepDebug = "ACTIONS_STEP_DEBUG";
}
public static class Agent
{
//
// Keep alphabetical
//
public static readonly string AcceptTeeEula = "agent.acceptteeeula";
public static readonly string AllowAllEndpoints = "agent.allowAllEndpoints"; // remove after sprint 120 or so.
public static readonly string AllowAllSecureFiles = "agent.allowAllSecureFiles"; // remove after sprint 121 or so.
public static readonly string BuildDirectory = "agent.builddirectory";
public static readonly string ContainerId = "agent.containerid";
public static readonly string ContainerNetwork = "agent.containernetwork";
public static readonly string HomeDirectory = "agent.homedirectory";
public static readonly string Id = "agent.id";
public static readonly string GitUseSChannel = "agent.gituseschannel";
public static readonly string JobName = "agent.jobname";
public static readonly string MachineName = "agent.machinename";
public static readonly string Name = "agent.name";
public static readonly string OS = "agent.os";
public static readonly string OSArchitecture = "agent.osarchitecture";
public static readonly string OSVersion = "agent.osversion";
public static readonly string ProxyUrl = "agent.proxyurl";
public static readonly string ProxyUsername = "agent.proxyusername";
public static readonly string ProxyPassword = "agent.proxypassword";
public static readonly string ProxyBypassList = "agent.proxybypasslist";
public static readonly string RetainDefaultEncoding = "agent.retainDefaultEncoding";
public static readonly string RootDirectory = "agent.RootDirectory";
public static readonly string RunMode = "agent.runmode";
public static readonly string ServerOMDirectory = "agent.ServerOMDirectory";
public static readonly string ServicePortPrefix = "agent.services";
public static readonly string SslCAInfo = "agent.cainfo";
public static readonly string SslClientCert = "agent.clientcert";
public static readonly string SslClientCertKey = "agent.clientcertkey";
public static readonly string SslClientCertArchive = "agent.clientcertarchive";
public static readonly string SslClientCertPassword = "agent.clientcertpassword";
public static readonly string SslSkipCertValidation = "agent.skipcertvalidation";
public static readonly string TempDirectory = "agent.TempDirectory";
public static readonly string ToolsDirectory = "agent.ToolsDirectory";
public static readonly string Version = "agent.version";
public static readonly string WorkFolder = "agent.workfolder";
public static readonly string WorkingDirectory = "agent.WorkingDirectory";
}
public static class Build
{
//
// Keep alphabetical
//
public static readonly string ArtifactStagingDirectory = "build.artifactstagingdirectory";
public static readonly string BinariesDirectory = "build.binariesdirectory";
public static readonly string Number = "build.buildNumber";
public static readonly string Clean = "build.clean";
public static readonly string DefinitionName = "build.definitionname";
public static readonly string GatedRunCI = "build.gated.runci";
public static readonly string GatedShelvesetName = "build.gated.shelvesetname";
public static readonly string RepoClean = "build.repository.clean";
public static readonly string RepoGitSubmoduleCheckout = "build.repository.git.submodulecheckout";
public static readonly string RepoId = "build.repository.id";
public static readonly string RepoLocalPath = "build.repository.localpath";
public static readonly string RepoName = "build.Repository.name";
public static readonly string RepoProvider = "build.repository.provider";
public static readonly string RepoTfvcWorkspace = "build.repository.tfvc.workspace";
public static readonly string RepoUri = "build.repository.uri";
public static readonly string SourceBranch = "build.sourcebranch";
public static readonly string SourceTfvcShelveset = "build.sourcetfvcshelveset";
public static readonly string SourceVersion = "build.sourceversion";
public static readonly string SourcesDirectory = "build.sourcesdirectory";
public static readonly string StagingDirectory = "build.stagingdirectory";
public static readonly string SyncSources = "build.syncSources";
}
public static class System
{
//
// Keep alphabetical
//
public static readonly string AccessToken = "system.accessToken";
public static readonly string ArtifactsDirectory = "system.artifactsdirectory";
public static readonly string CollectionId = "system.collectionid";
public static readonly string Culture = "system.culture";
public static readonly string DefaultWorkingDirectory = "system.defaultworkingdirectory";
public static readonly string DefinitionId = "system.definitionid";
public static readonly string EnableAccessToken = "system.enableAccessToken";
public static readonly string HostType = "system.hosttype";
public static readonly string PhaseDisplayName = "system.phaseDisplayName";
public static readonly string PreferGitFromPath = "system.prefergitfrompath";
public static readonly string PullRequestTargetBranchName = "system.pullrequest.targetbranch";
public static readonly string SelfManageGitCreds = "system.selfmanagegitcreds";
public static readonly string ServerType = "system.servertype";
public static readonly string TFServerUrl = "system.TeamFoundationServerUri"; // back compat variable, do not document
public static readonly string TeamProject = "system.teamproject";
public static readonly string TeamProjectId = "system.teamProjectId";
public static readonly string WorkFolder = "system.workfolder";
}
}
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace GitHub.Runner.Common
{
public sealed class CredentialData
{
public string Scheme { get; set; }
public Dictionary<string, string> Data
{
get
{
if (_data == null)
{
_data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
return _data;
}
}
private Dictionary<string, string> _data;
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace GitHub.Runner.Common
{
public class NonRetryableException : Exception
{
public NonRetryableException()
: base()
{ }
public NonRetryableException(string message)
: base(message)
{ }
public NonRetryableException(string message, Exception inner)
: base(message, inner)
{ }
}
}

View File

@@ -0,0 +1,80 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(ExtensionManager))]
public interface IExtensionManager : IRunnerService
{
List<T> GetExtensions<T>() where T : class, IExtension;
}
public sealed class ExtensionManager : RunnerService, IExtensionManager
{
private readonly ConcurrentDictionary<Type, List<IExtension>> _cache = new ConcurrentDictionary<Type, List<IExtension>>();
public List<T> GetExtensions<T>() where T : class, IExtension
{
Trace.Info("Getting extensions for interface: '{0}'", typeof(T).FullName);
List<IExtension> extensions = _cache.GetOrAdd(
key: typeof(T),
valueFactory: (Type key) =>
{
return LoadExtensions<T>();
});
return extensions.Select(x => x as T).ToList();
}
//
// We will load extensions from assembly
// once AssemblyLoadContext.Resolving event is able to
// resolve dependency recursively
//
private List<IExtension> LoadExtensions<T>() where T : class, IExtension
{
var extensions = new List<IExtension>();
switch (typeof(T).FullName)
{
// Listener capabilities providers.
case "GitHub.Runner.Common.Capabilities.ICapabilitiesProvider":
Add<T>(extensions, "GitHub.Runner.Common.Capabilities.RunnerCapabilitiesProvider, Runner.Common");
break;
// Action command extensions.
case "GitHub.Runner.Worker.IActionCommandExtension":
Add<T>(extensions, "GitHub.Runner.Worker.InternalPluginSetRepoPathCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.SetOutputCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.SaveStateCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.AddPathCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.AddMaskCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.AddMatcherCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.RemoveMatcherCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.WarningCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.ErrorCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.DebugCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.GroupCommandExtension, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.EndGroupCommandExtension, Runner.Worker");
break;
default:
// This should never happen.
throw new NotSupportedException($"Unexpected extension type: '{typeof(T).FullName}'");
}
return extensions;
}
private void Add<T>(List<IExtension> extensions, string assemblyQualifiedName) where T : class, IExtension
{
Trace.Info($"Creating instance: {assemblyQualifiedName}");
Type type = Type.GetType(assemblyQualifiedName, throwOnError: true);
var extension = Activator.CreateInstance(type) as T;
ArgUtil.NotNull(extension, nameof(extension));
extension.Initialize(HostContext);
extensions.Add(extension);
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Runner.Common
{
//this code is documented on http://blogs.msdn.com/b/pfxteam/archive/2012/10/05/how-do-i-cancel-non-cancelable-async-operations.aspx
public static class Extensions
{
public static async Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(
s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
return await task;
}
public static async Task WithCancellation(this Task task, CancellationToken cancellationToken)
{
var tcs = new TaskCompletionSource<bool>();
using (cancellationToken.Register(
s => ((TaskCompletionSource<bool>)s).TrySetResult(true), tcs))
if (task != await Task.WhenAny(task, tcs.Task))
throw new OperationCanceledException(cancellationToken);
await task;
}
}
}

View File

@@ -0,0 +1,597 @@
using GitHub.Runner.Common.Util;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.Loader;
using System.Threading;
using System.Threading.Tasks;
using System.Diagnostics;
using System.Net.Http;
using System.Diagnostics.Tracing;
using GitHub.DistributedTask.Logging;
using System.Net.Http.Headers;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
public interface IHostContext : IDisposable
{
RunMode RunMode { get; set; }
StartupType StartupType { get; set; }
CancellationToken RunnerShutdownToken { get; }
ShutdownReason RunnerShutdownReason { get; }
ISecretMasker SecretMasker { get; }
ProductInfoHeaderValue UserAgent { get; }
string GetDirectory(WellKnownDirectory directory);
string GetConfigFile(WellKnownConfigFile configFile);
Tracing GetTrace(string name);
Task Delay(TimeSpan delay, CancellationToken cancellationToken);
T CreateService<T>() where T : class, IRunnerService;
T GetService<T>() where T : class, IRunnerService;
void SetDefaultCulture(string name);
event EventHandler Unloading;
void ShutdownRunner(ShutdownReason reason);
void WritePerfCounter(string counter);
}
public enum StartupType
{
Manual,
Service,
AutoStartup
}
public sealed class HostContext : EventListener, IObserver<DiagnosticListener>, IObserver<KeyValuePair<string, object>>, IHostContext, IDisposable
{
private const int _defaultLogPageSize = 8; //MB
private static int _defaultLogRetentionDays = 30;
private static int[] _vssHttpMethodEventIds = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 24 };
private static int[] _vssHttpCredentialEventIds = new int[] { 11, 13, 14, 15, 16, 17, 18, 20, 21, 22, 27, 29 };
private readonly ConcurrentDictionary<Type, object> _serviceInstances = new ConcurrentDictionary<Type, object>();
private readonly ConcurrentDictionary<Type, Type> _serviceTypes = new ConcurrentDictionary<Type, Type>();
private readonly ISecretMasker _secretMasker = new SecretMasker();
private readonly ProductInfoHeaderValue _userAgent = new ProductInfoHeaderValue($"GitHubActionsRunner-{BuildConstants.RunnerPackage.PackageName}", BuildConstants.RunnerPackage.Version);
private CancellationTokenSource _runnerShutdownTokenSource = new CancellationTokenSource();
private object _perfLock = new object();
private RunMode _runMode = RunMode.Normal;
private Tracing _trace;
private Tracing _vssTrace;
private Tracing _httpTrace;
private ITraceManager _traceManager;
private AssemblyLoadContext _loadContext;
private IDisposable _httpTraceSubscription;
private IDisposable _diagListenerSubscription;
private StartupType _startupType;
private string _perfFile;
public event EventHandler Unloading;
public CancellationToken RunnerShutdownToken => _runnerShutdownTokenSource.Token;
public ShutdownReason RunnerShutdownReason { get; private set; }
public ISecretMasker SecretMasker => _secretMasker;
public ProductInfoHeaderValue UserAgent => _userAgent;
public HostContext(string hostType, string logFile = null)
{
// Validate args.
ArgUtil.NotNullOrEmpty(hostType, nameof(hostType));
_loadContext = AssemblyLoadContext.GetLoadContext(typeof(HostContext).GetTypeInfo().Assembly);
_loadContext.Unloading += LoadContext_Unloading;
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift1);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift2);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift3);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift4);
this.SecretMasker.AddValueEncoder(ValueEncoders.Base64StringEscapeShift5);
this.SecretMasker.AddValueEncoder(ValueEncoders.ExpressionStringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.XmlDataEscape);
// Create the trace manager.
if (string.IsNullOrEmpty(logFile))
{
int logPageSize;
string logSizeEnv = Environment.GetEnvironmentVariable($"{hostType.ToUpperInvariant()}_LOGSIZE");
if (!string.IsNullOrEmpty(logSizeEnv) || !int.TryParse(logSizeEnv, out logPageSize))
{
logPageSize = _defaultLogPageSize;
}
int logRetentionDays;
string logRetentionDaysEnv = Environment.GetEnvironmentVariable($"{hostType.ToUpperInvariant()}_LOGRETENTION");
if (!string.IsNullOrEmpty(logRetentionDaysEnv) || !int.TryParse(logRetentionDaysEnv, out logRetentionDays))
{
logRetentionDays = _defaultLogRetentionDays;
}
// this should give us _diag folder under runner root directory
string diagLogDirectory = Path.Combine(new DirectoryInfo(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)).Parent.FullName, Constants.Path.DiagDirectory);
_traceManager = new TraceManager(new HostTraceListener(diagLogDirectory, hostType, logPageSize, logRetentionDays), this.SecretMasker);
}
else
{
_traceManager = new TraceManager(new HostTraceListener(logFile), this.SecretMasker);
}
_trace = GetTrace(nameof(HostContext));
_vssTrace = GetTrace("GitHubActionsRunner"); // VisualStudioService
// Enable Http trace
bool enableHttpTrace;
if (bool.TryParse(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_HTTPTRACE"), out enableHttpTrace) && enableHttpTrace)
{
_trace.Warning("*****************************************************************************************");
_trace.Warning("** **");
_trace.Warning("** Http trace is enabled, all your http traffic will be dumped into runner diag log. **");
_trace.Warning("** DO NOT share the log in public place! The trace may contains secrets in plain text. **");
_trace.Warning("** **");
_trace.Warning("*****************************************************************************************");
_httpTrace = GetTrace("HttpTrace");
_diagListenerSubscription = DiagnosticListener.AllListeners.Subscribe(this);
}
// Enable perf counter trace
string perfCounterLocation = Environment.GetEnvironmentVariable("RUNNER_PERFLOG");
if (!string.IsNullOrEmpty(perfCounterLocation))
{
try
{
Directory.CreateDirectory(perfCounterLocation);
_perfFile = Path.Combine(perfCounterLocation, $"{hostType}.perf");
}
catch (Exception ex)
{
_trace.Error(ex);
}
}
}
public RunMode RunMode
{
get
{
return _runMode;
}
set
{
_trace.Info($"Set run mode: {value}");
_runMode = value;
}
}
public string GetDirectory(WellKnownDirectory directory)
{
string path;
switch (directory)
{
case WellKnownDirectory.Bin:
path = Path.GetDirectoryName(Assembly.GetEntryAssembly().Location);
break;
case WellKnownDirectory.Diag:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
Constants.Path.DiagDirectory);
break;
case WellKnownDirectory.Externals:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
Constants.Path.ExternalsDirectory);
break;
case WellKnownDirectory.Root:
path = new DirectoryInfo(GetDirectory(WellKnownDirectory.Bin)).Parent.FullName;
break;
case WellKnownDirectory.Temp:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Work),
Constants.Path.TempDirectory);
break;
case WellKnownDirectory.Actions:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Work),
Constants.Path.ActionsDirectory);
break;
case WellKnownDirectory.Tools:
// TODO: Coallesce to just check RUNNER_TOOL_CACHE when images stabilize
path = Environment.GetEnvironmentVariable("RUNNER_TOOL_CACHE") ?? Environment.GetEnvironmentVariable("RUNNER_TOOLSDIRECTORY") ?? Environment.GetEnvironmentVariable("AGENT_TOOLSDIRECTORY") ?? Environment.GetEnvironmentVariable(Constants.Variables.Agent.ToolsDirectory);
if (string.IsNullOrEmpty(path))
{
path = Path.Combine(
GetDirectory(WellKnownDirectory.Work),
Constants.Path.ToolDirectory);
}
break;
case WellKnownDirectory.Update:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Work),
Constants.Path.UpdateDirectory);
break;
case WellKnownDirectory.Work:
var configurationStore = GetService<IConfigurationStore>();
RunnerSettings settings = configurationStore.GetSettings();
ArgUtil.NotNull(settings, nameof(settings));
ArgUtil.NotNullOrEmpty(settings.WorkFolder, nameof(settings.WorkFolder));
path = Path.GetFullPath(Path.Combine(
GetDirectory(WellKnownDirectory.Root),
settings.WorkFolder));
break;
default:
throw new NotSupportedException($"Unexpected well known directory: '{directory}'");
}
_trace.Info($"Well known directory '{directory}': '{path}'");
return path;
}
public string GetConfigFile(WellKnownConfigFile configFile)
{
string path;
switch (configFile)
{
case WellKnownConfigFile.Runner:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".runner");
break;
case WellKnownConfigFile.Credentials:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".credentials");
break;
case WellKnownConfigFile.RSACredentials:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".credentials_rsaparams");
break;
case WellKnownConfigFile.Service:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".service");
break;
case WellKnownConfigFile.CredentialStore:
#if OS_OSX
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".credential_store.keychain");
#else
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".credential_store");
#endif
break;
case WellKnownConfigFile.Certificates:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".certificates");
break;
case WellKnownConfigFile.Proxy:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".proxy");
break;
case WellKnownConfigFile.ProxyCredentials:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".proxycredentials");
break;
case WellKnownConfigFile.ProxyBypass:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".proxybypass");
break;
case WellKnownConfigFile.Options:
path = Path.Combine(
GetDirectory(WellKnownDirectory.Root),
".options");
break;
default:
throw new NotSupportedException($"Unexpected well known config file: '{configFile}'");
}
_trace.Info($"Well known config file '{configFile}': '{path}'");
return path;
}
public Tracing GetTrace(string name)
{
return _traceManager[name];
}
public async Task Delay(TimeSpan delay, CancellationToken cancellationToken)
{
await Task.Delay(delay, cancellationToken);
}
/// <summary>
/// Creates a new instance of T.
/// </summary>
public T CreateService<T>() where T : class, IRunnerService
{
Type target;
if (!_serviceTypes.TryGetValue(typeof(T), out target))
{
// Infer the concrete type from the ServiceLocatorAttribute.
CustomAttributeData attribute = typeof(T)
.GetTypeInfo()
.CustomAttributes
.FirstOrDefault(x => x.AttributeType == typeof(ServiceLocatorAttribute));
if (attribute != null)
{
foreach (CustomAttributeNamedArgument arg in attribute.NamedArguments)
{
if (string.Equals(arg.MemberName, ServiceLocatorAttribute.DefaultPropertyName, StringComparison.Ordinal))
{
target = arg.TypedValue.Value as Type;
}
}
}
if (target == null)
{
throw new KeyNotFoundException(string.Format(CultureInfo.InvariantCulture, "Service mapping not found for key '{0}'.", typeof(T).FullName));
}
_serviceTypes.TryAdd(typeof(T), target);
target = _serviceTypes[typeof(T)];
}
// Create a new instance.
T svc = Activator.CreateInstance(target) as T;
svc.Initialize(this);
return svc;
}
/// <summary>
/// Gets or creates an instance of T.
/// </summary>
public T GetService<T>() where T : class, IRunnerService
{
// Return the cached instance if one already exists.
object instance;
if (_serviceInstances.TryGetValue(typeof(T), out instance))
{
return instance as T;
}
// Otherwise create a new instance and try to add it to the cache.
_serviceInstances.TryAdd(typeof(T), CreateService<T>());
// Return the instance from the cache.
return _serviceInstances[typeof(T)] as T;
}
public void SetDefaultCulture(string name)
{
ArgUtil.NotNull(name, nameof(name));
_trace.Verbose($"Setting default culture and UI culture to: '{name}'");
CultureInfo.DefaultThreadCurrentCulture = new CultureInfo(name);
CultureInfo.DefaultThreadCurrentUICulture = new CultureInfo(name);
}
public void ShutdownRunner(ShutdownReason reason)
{
ArgUtil.NotNull(reason, nameof(reason));
_trace.Info($"Runner will be shutdown for {reason.ToString()}");
RunnerShutdownReason = reason;
_runnerShutdownTokenSource.Cancel();
}
public override void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
public StartupType StartupType
{
get
{
return _startupType;
}
set
{
_startupType = value;
}
}
public void WritePerfCounter(string counter)
{
if (!string.IsNullOrEmpty(_perfFile))
{
string normalizedCounter = counter.Replace(':', '_');
lock (_perfLock)
{
try
{
File.AppendAllLines(_perfFile, new[] { $"{normalizedCounter}:{DateTime.UtcNow.ToString("O")}" });
}
catch (Exception ex)
{
_trace.Error(ex);
}
}
}
}
private void Dispose(bool disposing)
{
// TODO: Dispose the trace listener also.
if (disposing)
{
if (_loadContext != null)
{
_loadContext.Unloading -= LoadContext_Unloading;
_loadContext = null;
}
_httpTraceSubscription?.Dispose();
_diagListenerSubscription?.Dispose();
_traceManager?.Dispose();
_traceManager = null;
_runnerShutdownTokenSource?.Dispose();
_runnerShutdownTokenSource = null;
base.Dispose();
}
}
private void LoadContext_Unloading(AssemblyLoadContext obj)
{
if (Unloading != null)
{
Unloading(this, null);
}
}
void IObserver<DiagnosticListener>.OnCompleted()
{
_httpTrace.Info("DiagListeners finished transmitting data.");
}
void IObserver<DiagnosticListener>.OnError(Exception error)
{
_httpTrace.Error(error);
}
void IObserver<DiagnosticListener>.OnNext(DiagnosticListener listener)
{
if (listener.Name == "HttpHandlerDiagnosticListener" && _httpTraceSubscription == null)
{
_httpTraceSubscription = listener.Subscribe(this);
}
}
void IObserver<KeyValuePair<string, object>>.OnCompleted()
{
_httpTrace.Info("HttpHandlerDiagnosticListener finished transmitting data.");
}
void IObserver<KeyValuePair<string, object>>.OnError(Exception error)
{
_httpTrace.Error(error);
}
void IObserver<KeyValuePair<string, object>>.OnNext(KeyValuePair<string, object> value)
{
_httpTrace.Info($"Trace {value.Key} event:{Environment.NewLine}{value.Value.ToString()}");
}
protected override void OnEventSourceCreated(EventSource source)
{
if (source.Name.Equals("Microsoft-VSS-Http"))
{
EnableEvents(source, EventLevel.Verbose);
}
}
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (eventData == null)
{
return;
}
string message = eventData.Message;
object[] payload = new object[0];
if (eventData.Payload != null && eventData.Payload.Count > 0)
{
payload = eventData.Payload.ToArray();
}
try
{
if (_vssHttpMethodEventIds.Contains(eventData.EventId))
{
payload[0] = Enum.Parse(typeof(VssHttpMethod), ((int)payload[0]).ToString());
}
else if (_vssHttpCredentialEventIds.Contains(eventData.EventId))
{
payload[0] = Enum.Parse(typeof(GitHub.Services.Common.VssCredentialsType), ((int)payload[0]).ToString());
}
if (payload.Length > 0)
{
message = String.Format(eventData.Message.Replace("%n", Environment.NewLine), payload);
}
switch (eventData.Level)
{
case EventLevel.Critical:
case EventLevel.Error:
_vssTrace.Error(message);
break;
case EventLevel.Warning:
_vssTrace.Warning(message);
break;
case EventLevel.Informational:
_vssTrace.Info(message);
break;
default:
_vssTrace.Verbose(message);
break;
}
}
catch (Exception ex)
{
_vssTrace.Error(ex);
_vssTrace.Info(eventData.Message);
_vssTrace.Info(string.Join(", ", eventData.Payload?.ToArray() ?? new string[0]));
}
}
// Copied from pipelines server code base, used for EventData translation.
internal enum VssHttpMethod
{
UNKNOWN,
DELETE,
HEAD,
GET,
OPTIONS,
PATCH,
POST,
PUT,
}
}
public static class HostContextExtension
{
public static HttpClientHandler CreateHttpClientHandler(this IHostContext context)
{
HttpClientHandler clientHandler = new HttpClientHandler();
var runnerWebProxy = context.GetService<IRunnerWebProxy>();
clientHandler.Proxy = runnerWebProxy.WebProxy;
return clientHandler;
}
}
public enum ShutdownReason
{
UserCancelled = 0,
OperatingSystemShutdown = 1,
}
}

View File

@@ -0,0 +1,202 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
namespace GitHub.Runner.Common
{
public sealed class HostTraceListener : TextWriterTraceListener
{
private const string _logFileNamingPattern = "{0}_{1:yyyyMMdd-HHmmss}-utc.log";
private string _logFileDirectory;
private string _logFilePrefix;
private bool _enablePageLog = false;
private bool _enableLogRetention = false;
private int _currentPageSize;
private int _pageSizeLimit;
private int _retentionDays;
public HostTraceListener(string logFileDirectory, string logFilePrefix, int pageSizeLimit, int retentionDays)
: base()
{
ArgUtil.NotNullOrEmpty(logFileDirectory, nameof(logFileDirectory));
ArgUtil.NotNullOrEmpty(logFilePrefix, nameof(logFilePrefix));
_logFileDirectory = logFileDirectory;
_logFilePrefix = logFilePrefix;
Directory.CreateDirectory(_logFileDirectory);
if (pageSizeLimit > 0)
{
_enablePageLog = true;
_pageSizeLimit = pageSizeLimit * 1024 * 1024;
_currentPageSize = 0;
}
if (retentionDays > 0)
{
_enableLogRetention = true;
_retentionDays = retentionDays;
}
Writer = CreatePageLogWriter();
}
public HostTraceListener(string logFile)
: base()
{
ArgUtil.NotNullOrEmpty(logFile, nameof(logFile));
Directory.CreateDirectory(Path.GetDirectoryName(logFile));
Stream logStream = new FileStream(logFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 4096);
Writer = new StreamWriter(logStream);
}
// Copied and modified slightly from .Net Core source code. Modification was required to make it compile.
// There must be some TraceFilter extension class that is missing in this source code.
public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
{
if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, message, null, null, null))
{
return;
}
WriteHeader(source, eventType, id);
WriteLine(message);
WriteFooter(eventCache);
}
public override void WriteLine(string message)
{
base.WriteLine(message);
if (_enablePageLog)
{
int messageSize = UTF8Encoding.UTF8.GetByteCount(message);
_currentPageSize += messageSize;
if (_currentPageSize > _pageSizeLimit)
{
Flush();
if (Writer != null)
{
Writer.Dispose();
Writer = null;
}
Writer = CreatePageLogWriter();
_currentPageSize = 0;
}
}
Flush();
}
public override void Write(string message)
{
base.Write(message);
if (_enablePageLog)
{
int messageSize = UTF8Encoding.UTF8.GetByteCount(message);
_currentPageSize += messageSize;
}
Flush();
}
internal bool IsEnabled(TraceOptions opts)
{
return (opts & TraceOutputOptions) != 0;
}
// Altered from the original .Net Core implementation.
private void WriteHeader(string source, TraceEventType eventType, int id)
{
string type = null;
switch (eventType)
{
case TraceEventType.Critical:
type = "CRIT";
break;
case TraceEventType.Error:
type = "ERR ";
break;
case TraceEventType.Warning:
type = "WARN";
break;
case TraceEventType.Information:
type = "INFO";
break;
case TraceEventType.Verbose:
type = "VERB";
break;
default:
type = eventType.ToString();
break;
}
Write(StringUtil.Format("[{0:u} {1} {2}] ", DateTime.UtcNow, type, source));
}
// Copied and modified slightly from .Net Core source code to make it compile. The original code
// accesses a private indentLevel field. In this code it has been modified to use the getter/setter.
private void WriteFooter(TraceEventCache eventCache)
{
if (eventCache == null)
return;
IndentLevel++;
if (IsEnabled(TraceOptions.ProcessId))
WriteLine("ProcessId=" + eventCache.ProcessId);
if (IsEnabled(TraceOptions.ThreadId))
WriteLine("ThreadId=" + eventCache.ThreadId);
if (IsEnabled(TraceOptions.DateTime))
WriteLine("DateTime=" + eventCache.DateTime.ToString("o", CultureInfo.InvariantCulture));
if (IsEnabled(TraceOptions.Timestamp))
WriteLine("Timestamp=" + eventCache.Timestamp);
IndentLevel--;
}
private StreamWriter CreatePageLogWriter()
{
if (_enableLogRetention)
{
DirectoryInfo diags = new DirectoryInfo(_logFileDirectory);
var logs = diags.GetFiles($"{_logFilePrefix}*.log");
foreach (var log in logs)
{
if (log.LastWriteTimeUtc.AddDays(_retentionDays) < DateTime.UtcNow)
{
try
{
log.Delete();
}
catch (Exception)
{
// catch Exception and continue
// we shouldn't block logging and fail the runner if the runner can't delete an older log file.
}
}
}
}
string fileName = StringUtil.Format(_logFileNamingPattern, _logFilePrefix, DateTime.UtcNow);
string logFile = Path.Combine(_logFileDirectory, fileName);
Stream logStream;
if (File.Exists(logFile))
{
logStream = new FileStream(logFile, FileMode.Append, FileAccess.Write, FileShare.Read, bufferSize: 4096);
}
else
{
logStream = new FileStream(logFile, FileMode.Create, FileAccess.ReadWrite, FileShare.Read, bufferSize: 4096);
}
return new StreamWriter(logStream);
}
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace GitHub.Runner.Common
{
public interface IExtension : IRunnerService
{
Type ExtensionType { get; }
}
}

View File

@@ -0,0 +1,296 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(JobNotification))]
public interface IJobNotification : IRunnerService, IDisposable
{
Task JobStarted(Guid jobId, string accessToken, Uri serverUrl);
Task JobCompleted(Guid jobId);
void StartClient(string pipeName, string monitorSocketAddress, CancellationToken cancellationToken);
void StartClient(string socketAddress, string monitorSocketAddress);
}
public sealed class JobNotification : RunnerService, IJobNotification
{
private NamedPipeClientStream _outClient;
private StreamWriter _writeStream;
private Socket _socket;
private Socket _monitorSocket;
private bool _configured = false;
private bool _useSockets = false;
private bool _isMonitorConfigured = false;
public async Task JobStarted(Guid jobId, string accessToken, Uri serverUrl)
{
Trace.Info("Entering JobStarted Notification");
StartMonitor(jobId, accessToken, serverUrl);
if (_configured)
{
String message = $"Starting job: {jobId.ToString()}";
if (_useSockets)
{
try
{
Trace.Info("Writing JobStarted to socket");
_socket.Send(Encoding.UTF8.GetBytes(message));
Trace.Info("Finished JobStarted writing to socket");
}
catch (SocketException e)
{
Trace.Error($"Failed sending message \"{message}\" on socket!");
Trace.Error(e);
}
}
else
{
Trace.Info("Writing JobStarted to pipe");
await _writeStream.WriteLineAsync(message);
await _writeStream.FlushAsync();
Trace.Info("Finished JobStarted writing to pipe");
}
}
}
public async Task JobCompleted(Guid jobId)
{
Trace.Info("Entering JobCompleted Notification");
await EndMonitor();
if (_configured)
{
String message = $"Finished job: {jobId.ToString()}";
if (_useSockets)
{
try
{
Trace.Info("Writing JobCompleted to socket");
_socket.Send(Encoding.UTF8.GetBytes(message));
Trace.Info("Finished JobCompleted writing to socket");
}
catch (SocketException e)
{
Trace.Error($"Failed sending message \"{message}\" on socket!");
Trace.Error(e);
}
}
else
{
Trace.Info("Writing JobCompleted to pipe");
await _writeStream.WriteLineAsync(message);
await _writeStream.FlushAsync();
Trace.Info("Finished JobCompleted writing to pipe");
}
}
}
public async void StartClient(string pipeName, string monitorSocketAddress, CancellationToken cancellationToken)
{
if (pipeName != null && !_configured)
{
Trace.Info("Connecting to named pipe {0}", pipeName);
_outClient = new NamedPipeClientStream(".", pipeName, PipeDirection.Out, PipeOptions.Asynchronous);
await _outClient.ConnectAsync(cancellationToken);
_writeStream = new StreamWriter(_outClient, Encoding.UTF8);
_configured = true;
Trace.Info("Connection successful to named pipe {0}", pipeName);
}
ConnectMonitor(monitorSocketAddress);
}
public void StartClient(string socketAddress, string monitorSocketAddress)
{
if (!_configured)
{
try
{
string[] splitAddress = socketAddress.Split(':');
if (splitAddress.Length != 2)
{
Trace.Error("Invalid socket address {0}. Job Notification will be disabled.", socketAddress);
return;
}
IPAddress address;
try
{
address = IPAddress.Parse(splitAddress[0]);
}
catch (FormatException e)
{
Trace.Error("Invalid socket ip address {0}. Job Notification will be disabled",splitAddress[0]);
Trace.Error(e);
return;
}
int port = -1;
Int32.TryParse(splitAddress[1], out port);
if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
{
Trace.Error("Invalid tcp socket port {0}. Job Notification will be disabled.", splitAddress[1]);
return;
}
_socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
_socket.Connect(address, port);
Trace.Info("Connection successful to socket {0}", socketAddress);
_useSockets = true;
_configured = true;
}
catch (SocketException e)
{
Trace.Error("Connection to socket {0} failed!", socketAddress);
Trace.Error(e);
}
}
ConnectMonitor(monitorSocketAddress);
}
private void StartMonitor(Guid jobId, string accessToken, Uri serverUri)
{
if(String.IsNullOrEmpty(accessToken))
{
Trace.Info("No access token could be retrieved to start the monitor.");
return;
}
try
{
Trace.Info("Entering StartMonitor");
if (_isMonitorConfigured)
{
String message = $"Start {jobId.ToString()} {accessToken} {serverUri.ToString()} {System.Diagnostics.Process.GetCurrentProcess().Id}";
Trace.Info("Writing StartMonitor to socket");
_monitorSocket.Send(Encoding.UTF8.GetBytes(message));
Trace.Info("Finished StartMonitor writing to socket");
}
}
catch (SocketException e)
{
Trace.Error($"Failed sending StartMonitor message on socket!");
Trace.Error(e);
}
catch (Exception e)
{
Trace.Error($"Unexpected error occurred while sending StartMonitor message on socket!");
Trace.Error(e);
}
}
private async Task EndMonitor()
{
try
{
Trace.Info("Entering EndMonitor");
if (_isMonitorConfigured)
{
String message = $"End {System.Diagnostics.Process.GetCurrentProcess().Id}";
Trace.Info("Writing EndMonitor to socket");
_monitorSocket.Send(Encoding.UTF8.GetBytes(message));
Trace.Info("Finished EndMonitor writing to socket");
await Task.Delay(TimeSpan.FromSeconds(2));
}
}
catch (SocketException e)
{
Trace.Error($"Failed sending end message on socket!");
Trace.Error(e);
}
catch (Exception e)
{
Trace.Error($"Unexpected error occurred while sending StartMonitor message on socket!");
Trace.Error(e);
}
}
private void ConnectMonitor(string monitorSocketAddress)
{
int port = -1;
if (!_isMonitorConfigured && !String.IsNullOrEmpty(monitorSocketAddress))
{
try
{
string[] splitAddress = monitorSocketAddress.Split(':');
if (splitAddress.Length != 2)
{
Trace.Error("Invalid socket address {0}. Unable to connect to monitor.", monitorSocketAddress);
return;
}
IPAddress address;
try
{
address = IPAddress.Parse(splitAddress[0]);
}
catch (FormatException e)
{
Trace.Error("Invalid socket IP address {0}. Unable to connect to monitor.", splitAddress[0]);
Trace.Error(e);
return;
}
Int32.TryParse(splitAddress[1], out port);
if (port < IPEndPoint.MinPort || port > IPEndPoint.MaxPort)
{
Trace.Error("Invalid TCP socket port {0}. Unable to connect to monitor.", splitAddress[1]);
return;
}
Trace.Verbose("Trying to connect to monitor at port {0}", port);
_monitorSocket = new Socket(SocketType.Stream, ProtocolType.Tcp);
_monitorSocket.Connect(address, port);
Trace.Info("Connection successful to local port {0}", port);
_isMonitorConfigured = true;
}
catch (Exception e)
{
Trace.Error("Connection to monitor port {0} failed!", port);
Trace.Error(e);
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
_outClient?.Dispose();
if (_socket != null)
{
_socket.Send(Encoding.UTF8.GetBytes("<EOF>"));
_socket.Shutdown(SocketShutdown.Both);
_socket = null;
}
if (_monitorSocket != null)
{
_monitorSocket.Send(Encoding.UTF8.GetBytes("<EOF>"));
_monitorSocket.Shutdown(SocketShutdown.Both);
_monitorSocket = null;
}
}
}
}
}

View File

@@ -0,0 +1,162 @@
using GitHub.DistributedTask.WebApi;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Services.WebApi;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(JobServer))]
public interface IJobServer : IRunnerService
{
Task ConnectAsync(VssConnection jobConnection);
// logging and console
Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken);
Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, CancellationToken cancellationToken);
Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, String type, String name, Stream uploadStream, CancellationToken cancellationToken);
Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken);
Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken);
Task RaisePlanEventAsync<T>(Guid scopeIdentifier, string hubName, Guid planId, T eventData, CancellationToken cancellationToken) where T : JobEvent;
Task<Timeline> GetTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken);
}
public sealed class JobServer : RunnerService, IJobServer
{
private bool _hasConnection;
private VssConnection _connection;
private TaskHttpClient _taskClient;
public async Task ConnectAsync(VssConnection jobConnection)
{
if (HostContext.RunMode == RunMode.Local)
{
return;
}
_connection = jobConnection;
int attemptCount = 5;
while (!_connection.HasAuthenticated && attemptCount-- > 0)
{
try
{
await _connection.ConnectAsync();
break;
}
catch (Exception ex) when (attemptCount > 0)
{
Trace.Info($"Catch exception during connect. {attemptCount} attemp left.");
Trace.Error(ex);
}
await Task.Delay(100);
}
_taskClient = _connection.GetClient<TaskHttpClient>();
_hasConnection = true;
}
private void CheckConnection()
{
if (!_hasConnection)
{
throw new InvalidOperationException("SetConnection");
}
}
//-----------------------------------------------------------------
// Feedback: WebConsole, TimelineRecords and Logs
//-----------------------------------------------------------------
public Task<TaskLog> AppendLogContentAsync(Guid scopeIdentifier, string hubName, Guid planId, int logId, Stream uploadStream, CancellationToken cancellationToken)
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult<TaskLog>(null);
}
CheckConnection();
return _taskClient.AppendLogContentAsync(scopeIdentifier, hubName, planId, logId, uploadStream, cancellationToken: cancellationToken);
}
public Task AppendTimelineRecordFeedAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, Guid stepId, IList<string> lines, CancellationToken cancellationToken)
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.CompletedTask;
}
CheckConnection();
return _taskClient.AppendTimelineRecordFeedAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, stepId, lines, cancellationToken: cancellationToken);
}
public Task<TaskAttachment> CreateAttachmentAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, Guid timelineRecordId, string type, string name, Stream uploadStream, CancellationToken cancellationToken)
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult<TaskAttachment>(null);
}
CheckConnection();
return _taskClient.CreateAttachmentAsync(scopeIdentifier, hubName, planId, timelineId, timelineRecordId, type, name, uploadStream, cancellationToken: cancellationToken);
}
public Task<TaskLog> CreateLogAsync(Guid scopeIdentifier, string hubName, Guid planId, TaskLog log, CancellationToken cancellationToken)
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult<TaskLog>(null);
}
CheckConnection();
return _taskClient.CreateLogAsync(scopeIdentifier, hubName, planId, log, cancellationToken: cancellationToken);
}
public Task<Timeline> CreateTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken)
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult<Timeline>(null);
}
CheckConnection();
return _taskClient.CreateTimelineAsync(scopeIdentifier, hubName, planId, new Timeline(timelineId), cancellationToken: cancellationToken);
}
public Task<List<TimelineRecord>> UpdateTimelineRecordsAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, IEnumerable<TimelineRecord> records, CancellationToken cancellationToken)
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult<List<TimelineRecord>>(null);
}
CheckConnection();
return _taskClient.UpdateTimelineRecordsAsync(scopeIdentifier, hubName, planId, timelineId, records, cancellationToken: cancellationToken);
}
public Task RaisePlanEventAsync<T>(Guid scopeIdentifier, string hubName, Guid planId, T eventData, CancellationToken cancellationToken) where T : JobEvent
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.CompletedTask;
}
CheckConnection();
return _taskClient.RaisePlanEventAsync(scopeIdentifier, hubName, planId, eventData, cancellationToken: cancellationToken);
}
public Task<Timeline> GetTimelineAsync(Guid scopeIdentifier, string hubName, Guid planId, Guid timelineId, CancellationToken cancellationToken)
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult<Timeline>(null);
}
CheckConnection();
return _taskClient.GetTimelineAsync(scopeIdentifier, hubName, planId, timelineId, includeRecords: true, cancellationToken: cancellationToken);
}
}
}

View File

@@ -0,0 +1,702 @@
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using System;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(JobServerQueue))]
public interface IJobServerQueue : IRunnerService, IThrottlingReporter
{
event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
Task ShutdownAsync();
void Start(Pipelines.AgentJobRequestMessage jobRequest);
void QueueWebConsoleLine(Guid stepRecordId, string line);
void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource);
void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord);
}
public sealed class JobServerQueue : RunnerService, IJobServerQueue
{
// Default delay for Dequeue process
private static readonly TimeSpan _aggressiveDelayForWebConsoleLineDequeue = TimeSpan.FromMilliseconds(250);
private static readonly TimeSpan _delayForWebConsoleLineDequeue = TimeSpan.FromMilliseconds(500);
private static readonly TimeSpan _delayForTimelineUpdateDequeue = TimeSpan.FromMilliseconds(500);
private static readonly TimeSpan _delayForFileUploadDequeue = TimeSpan.FromMilliseconds(1000);
// Job message information
private Guid _scopeIdentifier;
private string _hubName;
private Guid _planId;
private Guid _jobTimelineId;
private Guid _jobTimelineRecordId;
// queue for web console line
private readonly ConcurrentQueue<ConsoleLineInfo> _webConsoleLineQueue = new ConcurrentQueue<ConsoleLineInfo>();
// queue for file upload (log file or attachment)
private readonly ConcurrentQueue<UploadFileInfo> _fileUploadQueue = new ConcurrentQueue<UploadFileInfo>();
// queue for timeline or timeline record update (one queue per timeline)
private readonly ConcurrentDictionary<Guid, ConcurrentQueue<TimelineRecord>> _timelineUpdateQueue = new ConcurrentDictionary<Guid, ConcurrentQueue<TimelineRecord>>();
// indicate how many timelines we have, we will process _timelineUpdateQueue base on the order of timeline in this list
private readonly List<Guid> _allTimelines = new List<Guid>();
// bufferd timeline records that fail to update
private readonly Dictionary<Guid, List<TimelineRecord>> _bufferedRetryRecords = new Dictionary<Guid, List<TimelineRecord>>();
// Task for each queue's dequeue process
private Task _webConsoleLineDequeueTask;
private Task _fileUploadDequeueTask;
private Task _timelineUpdateDequeueTask;
// common
private IJobServer _jobServer;
private Task[] _allDequeueTasks;
private readonly TaskCompletionSource<int> _jobCompletionSource = new TaskCompletionSource<int>();
private bool _queueInProcess = false;
private ITerminal _term;
public event EventHandler<ThrottlingEventArgs> JobServerQueueThrottling;
// Web console dequeue will start with process queue every 250ms for the first 60*4 times (~60 seconds).
// Then the dequeue will happen every 500ms.
// In this way, customer still can get instance live console output on job start,
// at the same time we can cut the load to server after the build run for more than 60s
private int _webConsoleLineAggressiveDequeueCount = 0;
private const int _webConsoleLineAggressiveDequeueLimit = 4 * 60;
private bool _webConsoleLineAggressiveDequeue = true;
private bool _firstConsoleOutputs = true;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_jobServer = hostContext.GetService<IJobServer>();
}
public void Start(Pipelines.AgentJobRequestMessage jobRequest)
{
Trace.Entering();
if (HostContext.RunMode == RunMode.Local)
{
_term = HostContext.GetService<ITerminal>();
return;
}
if (_queueInProcess)
{
Trace.Info("No-opt, all queue process tasks are running.");
return;
}
ArgUtil.NotNull(jobRequest, nameof(jobRequest));
ArgUtil.NotNull(jobRequest.Plan, nameof(jobRequest.Plan));
ArgUtil.NotNull(jobRequest.Timeline, nameof(jobRequest.Timeline));
_scopeIdentifier = jobRequest.Plan.ScopeIdentifier;
_hubName = jobRequest.Plan.PlanType;
_planId = jobRequest.Plan.PlanId;
_jobTimelineId = jobRequest.Timeline.Id;
_jobTimelineRecordId = jobRequest.JobId;
// Server already create the job timeline
_timelineUpdateQueue[_jobTimelineId] = new ConcurrentQueue<TimelineRecord>();
_allTimelines.Add(_jobTimelineId);
// Start three dequeue task
Trace.Info("Start process web console line queue.");
_webConsoleLineDequeueTask = ProcessWebConsoleLinesQueueAsync();
Trace.Info("Start process file upload queue.");
_fileUploadDequeueTask = ProcessFilesUploadQueueAsync();
Trace.Info("Start process timeline update queue.");
_timelineUpdateDequeueTask = ProcessTimelinesUpdateQueueAsync();
_allDequeueTasks = new Task[] { _webConsoleLineDequeueTask, _fileUploadDequeueTask, _timelineUpdateDequeueTask };
_queueInProcess = true;
}
// WebConsoleLine queue and FileUpload queue are always best effort
// TimelineUpdate queue error will become critical when timeline records contain output variabls.
public async Task ShutdownAsync()
{
if (HostContext.RunMode == RunMode.Local)
{
return;
}
if (!_queueInProcess)
{
Trace.Info("No-op, all queue process tasks have been stopped.");
}
Trace.Info("Fire signal to shutdown all queues.");
_jobCompletionSource.TrySetResult(0);
await Task.WhenAll(_allDequeueTasks);
_queueInProcess = false;
Trace.Info("All queue process task stopped.");
// Drain the queue
// ProcessWebConsoleLinesQueueAsync() will never throw exception, live console update is always best effort.
Trace.Verbose("Draining web console line queue.");
await ProcessWebConsoleLinesQueueAsync(runOnce: true);
Trace.Info("Web console line queue drained.");
// ProcessFilesUploadQueueAsync() will never throw exception, log file upload is always best effort.
Trace.Verbose("Draining file upload queue.");
await ProcessFilesUploadQueueAsync(runOnce: true);
Trace.Info("File upload queue drained.");
// ProcessTimelinesUpdateQueueAsync() will throw exception during shutdown
// if there is any timeline records that failed to update contains output variabls.
Trace.Verbose("Draining timeline update queue.");
await ProcessTimelinesUpdateQueueAsync(runOnce: true);
Trace.Info("Timeline update queue drained.");
Trace.Info("All queue process tasks have been stopped, and all queues are drained.");
}
public void QueueWebConsoleLine(Guid stepRecordId, string line)
{
Trace.Verbose("Enqueue web console line queue: {0}", line);
if (HostContext.RunMode == RunMode.Local)
{
if ((line ?? string.Empty).StartsWith("##[section]"))
{
Console.WriteLine("******************************************************************************");
Console.WriteLine(line.Substring("##[section]".Length));
Console.WriteLine("******************************************************************************");
}
else
{
Console.WriteLine(line);
}
return;
}
_webConsoleLineQueue.Enqueue(new ConsoleLineInfo(stepRecordId, line));
}
public void QueueFileUpload(Guid timelineId, Guid timelineRecordId, string type, string name, string path, bool deleteSource)
{
if (HostContext.RunMode == RunMode.Local)
{
return;
}
ArgUtil.NotEmpty(timelineId, nameof(timelineId));
ArgUtil.NotEmpty(timelineRecordId, nameof(timelineRecordId));
// all parameter not null, file path exist.
var newFile = new UploadFileInfo()
{
TimelineId = timelineId,
TimelineRecordId = timelineRecordId,
Type = type,
Name = name,
Path = path,
DeleteSource = deleteSource
};
Trace.Verbose("Enqueue file upload queue: file '{0}' attach to record {1}", newFile.Path, timelineRecordId);
_fileUploadQueue.Enqueue(newFile);
}
public void QueueTimelineRecordUpdate(Guid timelineId, TimelineRecord timelineRecord)
{
if (HostContext.RunMode == RunMode.Local)
{
return;
}
ArgUtil.NotEmpty(timelineId, nameof(timelineId));
ArgUtil.NotNull(timelineRecord, nameof(timelineRecord));
ArgUtil.NotEmpty(timelineRecord.Id, nameof(timelineRecord.Id));
_timelineUpdateQueue.TryAdd(timelineId, new ConcurrentQueue<TimelineRecord>());
Trace.Verbose("Enqueue timeline {0} update queue: {1}", timelineId, timelineRecord.Id);
_timelineUpdateQueue[timelineId].Enqueue(timelineRecord.Clone());
}
public void ReportThrottling(TimeSpan delay, DateTime expiration)
{
Trace.Info($"Receive server throttling report, expect delay {delay} milliseconds till {expiration}");
var throttlingEvent = JobServerQueueThrottling;
if (throttlingEvent != null)
{
throttlingEvent(this, new ThrottlingEventArgs(delay, expiration));
}
}
private async Task ProcessWebConsoleLinesQueueAsync(bool runOnce = false)
{
while (!_jobCompletionSource.Task.IsCompleted || runOnce)
{
if (_webConsoleLineAggressiveDequeue && ++_webConsoleLineAggressiveDequeueCount > _webConsoleLineAggressiveDequeueLimit)
{
Trace.Info("Stop aggressive process web console line queue.");
_webConsoleLineAggressiveDequeue = false;
}
// Group consolelines by timeline record of each step
Dictionary<Guid, List<string>> stepsConsoleLines = new Dictionary<Guid, List<string>>();
List<Guid> stepRecordIds = new List<Guid>(); // We need to keep lines in order
int linesCounter = 0;
ConsoleLineInfo lineInfo;
while (_webConsoleLineQueue.TryDequeue(out lineInfo))
{
if (!stepsConsoleLines.ContainsKey(lineInfo.StepRecordId))
{
stepsConsoleLines[lineInfo.StepRecordId] = new List<string>();
stepRecordIds.Add(lineInfo.StepRecordId);
}
if (!string.IsNullOrEmpty(lineInfo.Line) && lineInfo.Line.Length > 1024)
{
Trace.Verbose("Web console line is more than 1024 chars, truncate to first 1024 chars");
lineInfo.Line = $"{lineInfo.Line.Substring(0, 1024)}...";
}
stepsConsoleLines[lineInfo.StepRecordId].Add(lineInfo.Line);
linesCounter++;
// process at most about 500 lines of web console line during regular timer dequeue task.
if (!runOnce && linesCounter > 500)
{
break;
}
}
// Batch post consolelines for each step timeline record
foreach (var stepRecordId in stepRecordIds)
{
// Split consolelines into batch, each batch will container at most 100 lines.
int batchCounter = 0;
List<List<string>> batchedLines = new List<List<string>>();
foreach (var line in stepsConsoleLines[stepRecordId])
{
var currentBatch = batchedLines.ElementAtOrDefault(batchCounter);
if (currentBatch == null)
{
batchedLines.Add(new List<string>());
currentBatch = batchedLines.ElementAt(batchCounter);
}
currentBatch.Add(line);
if (currentBatch.Count >= 100)
{
batchCounter++;
}
}
if (batchedLines.Count > 0)
{
// When job finish, web console lines becomes less interesting to customer
// We batch and produce 500 lines of web console output every 500ms
// If customer's task produce massive of outputs, then the last queue drain run might take forever.
// So we will only upload the last 200 lines of each step from all buffered web console lines.
if (runOnce && batchedLines.Count > 2)
{
Trace.Info($"Skip {batchedLines.Count - 2} batches web console lines for last run");
batchedLines = batchedLines.TakeLast(2).ToList();
batchedLines[0].Insert(0, "...");
}
int errorCount = 0;
foreach (var batch in batchedLines)
{
try
{
// we will not requeue failed batch, since the web console lines are time sensitive.
await _jobServer.AppendTimelineRecordFeedAsync(_scopeIdentifier, _hubName, _planId, _jobTimelineId, _jobTimelineRecordId, stepRecordId, batch, default(CancellationToken));
if (_firstConsoleOutputs)
{
HostContext.WritePerfCounter($"WorkerJobServerQueueAppendFirstConsoleOutput_{_planId.ToString()}");
_firstConsoleOutputs = false;
}
}
catch (Exception ex)
{
Trace.Info("Catch exception during append web console line, keep going since the process is best effort.");
Trace.Error(ex);
errorCount++;
}
}
Trace.Info("Try to append {0} batches web console lines for record '{2}', success rate: {1}/{0}.", batchedLines.Count, batchedLines.Count - errorCount, stepRecordId);
}
}
if (runOnce)
{
break;
}
else
{
await Task.Delay(_webConsoleLineAggressiveDequeue ? _aggressiveDelayForWebConsoleLineDequeue : _delayForWebConsoleLineDequeue);
}
}
}
private async Task ProcessFilesUploadQueueAsync(bool runOnce = false)
{
while (!_jobCompletionSource.Task.IsCompleted || runOnce)
{
List<UploadFileInfo> filesToUpload = new List<UploadFileInfo>();
UploadFileInfo dequeueFile;
while (_fileUploadQueue.TryDequeue(out dequeueFile))
{
filesToUpload.Add(dequeueFile);
// process at most 10 file upload.
if (!runOnce && filesToUpload.Count > 10)
{
break;
}
}
if (filesToUpload.Count > 0)
{
if (runOnce)
{
Trace.Info($"Uploading {filesToUpload.Count} files in one shot.");
}
// TODO: upload all file in parallel
int errorCount = 0;
foreach (var file in filesToUpload)
{
try
{
await UploadFile(file);
}
catch (Exception ex)
{
Trace.Info("Catch exception during log or attachment file upload, keep going since the process is best effort.");
Trace.Error(ex);
errorCount++;
// put the failed upload file back to queue.
// TODO: figure out how should we retry paging log upload.
//lock (_fileUploadQueueLock)
//{
// _fileUploadQueue.Enqueue(file);
//}
}
}
Trace.Info("Try to upload {0} log files or attachments, success rate: {1}/{0}.", filesToUpload.Count, filesToUpload.Count - errorCount);
}
if (runOnce)
{
break;
}
else
{
await Task.Delay(_delayForFileUploadDequeue);
}
}
}
private async Task ProcessTimelinesUpdateQueueAsync(bool runOnce = false)
{
while (!_jobCompletionSource.Task.IsCompleted || runOnce)
{
List<PendingTimelineRecord> pendingUpdates = new List<PendingTimelineRecord>();
foreach (var timeline in _allTimelines)
{
ConcurrentQueue<TimelineRecord> recordQueue;
if (_timelineUpdateQueue.TryGetValue(timeline, out recordQueue))
{
List<TimelineRecord> records = new List<TimelineRecord>();
TimelineRecord record;
while (recordQueue.TryDequeue(out record))
{
records.Add(record);
// process at most 25 timeline records update for each timeline.
if (!runOnce && records.Count > 25)
{
break;
}
}
if (records.Count > 0)
{
pendingUpdates.Add(new PendingTimelineRecord() { TimelineId = timeline, PendingRecords = records.ToList() });
}
}
}
// we need track whether we have new sub-timeline been created on the last run.
// if so, we need continue update timeline record even we on the last run.
bool pendingSubtimelineUpdate = false;
List<Exception> mainTimelineRecordsUpdateErrors = new List<Exception>();
if (pendingUpdates.Count > 0)
{
foreach (var update in pendingUpdates)
{
List<TimelineRecord> bufferedRecords;
if (_bufferedRetryRecords.TryGetValue(update.TimelineId, out bufferedRecords))
{
update.PendingRecords.InsertRange(0, bufferedRecords);
}
update.PendingRecords = MergeTimelineRecords(update.PendingRecords);
foreach (var detailTimeline in update.PendingRecords.Where(r => r.Details != null))
{
if (!_allTimelines.Contains(detailTimeline.Details.Id))
{
try
{
Timeline newTimeline = await _jobServer.CreateTimelineAsync(_scopeIdentifier, _hubName, _planId, detailTimeline.Details.Id, default(CancellationToken));
_allTimelines.Add(newTimeline.Id);
pendingSubtimelineUpdate = true;
}
catch (TimelineExistsException)
{
Trace.Info("Catch TimelineExistsException during timeline creation. Ignore the error since server already had this timeline.");
_allTimelines.Add(detailTimeline.Details.Id);
}
catch (Exception ex)
{
Trace.Error(ex);
}
}
}
try
{
await _jobServer.UpdateTimelineRecordsAsync(_scopeIdentifier, _hubName, _planId, update.TimelineId, update.PendingRecords, default(CancellationToken));
if (_bufferedRetryRecords.Remove(update.TimelineId))
{
Trace.Verbose("Cleanup buffered timeline record for timeline: {0}.", update.TimelineId);
}
}
catch (Exception ex)
{
Trace.Info("Catch exception during update timeline records, try to update these timeline records next time.");
Trace.Error(ex);
_bufferedRetryRecords[update.TimelineId] = update.PendingRecords.ToList();
if (update.TimelineId == _jobTimelineId)
{
mainTimelineRecordsUpdateErrors.Add(ex);
}
}
}
}
if (runOnce)
{
// continue process timeline records update,
// we might have more records need update,
// since we just create a new sub-timeline
if (pendingSubtimelineUpdate)
{
continue;
}
else
{
if (mainTimelineRecordsUpdateErrors.Count > 0 &&
_bufferedRetryRecords.ContainsKey(_jobTimelineId) &&
_bufferedRetryRecords[_jobTimelineId] != null &&
_bufferedRetryRecords[_jobTimelineId].Any(r => r.Variables.Count > 0))
{
Trace.Info("Fail to update timeline records with output variables. Throw exception to fail the job since output variables are critical to downstream jobs.");
throw new AggregateException("Failed to publish output variables.", mainTimelineRecordsUpdateErrors);
}
else
{
break;
}
}
}
else
{
await Task.Delay(_delayForTimelineUpdateDequeue);
}
}
}
private List<TimelineRecord> MergeTimelineRecords(List<TimelineRecord> timelineRecords)
{
if (timelineRecords == null || timelineRecords.Count <= 1)
{
return timelineRecords;
}
Dictionary<Guid, TimelineRecord> dict = new Dictionary<Guid, TimelineRecord>();
foreach (TimelineRecord rec in timelineRecords)
{
if (rec == null)
{
continue;
}
TimelineRecord timelineRecord;
if (dict.TryGetValue(rec.Id, out timelineRecord))
{
// Merge rec into timelineRecord
timelineRecord.CurrentOperation = rec.CurrentOperation ?? timelineRecord.CurrentOperation;
timelineRecord.Details = rec.Details ?? timelineRecord.Details;
timelineRecord.FinishTime = rec.FinishTime ?? timelineRecord.FinishTime;
timelineRecord.Log = rec.Log ?? timelineRecord.Log;
timelineRecord.Name = rec.Name ?? timelineRecord.Name;
timelineRecord.RefName = rec.RefName ?? timelineRecord.RefName;
timelineRecord.PercentComplete = rec.PercentComplete ?? timelineRecord.PercentComplete;
timelineRecord.RecordType = rec.RecordType ?? timelineRecord.RecordType;
timelineRecord.Result = rec.Result ?? timelineRecord.Result;
timelineRecord.ResultCode = rec.ResultCode ?? timelineRecord.ResultCode;
timelineRecord.StartTime = rec.StartTime ?? timelineRecord.StartTime;
timelineRecord.State = rec.State ?? timelineRecord.State;
timelineRecord.WorkerName = rec.WorkerName ?? timelineRecord.WorkerName;
if (rec.ErrorCount != null && rec.ErrorCount > 0)
{
timelineRecord.ErrorCount = rec.ErrorCount;
}
if (rec.WarningCount != null && rec.WarningCount > 0)
{
timelineRecord.WarningCount = rec.WarningCount;
}
if (rec.Issues.Count > 0)
{
timelineRecord.Issues.Clear();
timelineRecord.Issues.AddRange(rec.Issues.Select(i => i.Clone()));
}
if (rec.Variables.Count > 0)
{
foreach (var variable in rec.Variables)
{
timelineRecord.Variables[variable.Key] = variable.Value.Clone();
}
}
}
else
{
dict.Add(rec.Id, rec);
}
}
var mergedRecords = dict.Values.ToList();
Trace.Verbose("Merged Timeline records");
foreach (var record in mergedRecords)
{
Trace.Verbose($" Record: t={record.RecordType}, n={record.Name}, s={record.State}, st={record.StartTime}, {record.PercentComplete}%, ft={record.FinishTime}, r={record.Result}: {record.CurrentOperation}");
if (record.Issues != null && record.Issues.Count > 0)
{
foreach (var issue in record.Issues)
{
String source;
issue.Data.TryGetValue("sourcepath", out source);
Trace.Verbose($" Issue: c={issue.Category}, t={issue.Type}, s={source ?? string.Empty}, m={issue.Message}");
}
}
if (record.Variables != null && record.Variables.Count > 0)
{
foreach (var variable in record.Variables)
{
Trace.Verbose($" Variable: n={variable.Key}, secret={variable.Value.IsSecret}");
}
}
}
return mergedRecords;
}
private async Task UploadFile(UploadFileInfo file)
{
bool uploadSucceed = false;
try
{
if (String.Equals(file.Type, CoreAttachmentType.Log, StringComparison.OrdinalIgnoreCase))
{
// Create the log
var taskLog = await _jobServer.CreateLogAsync(_scopeIdentifier, _hubName, _planId, new TaskLog(String.Format(@"logs\{0:D}", file.TimelineRecordId)), default(CancellationToken));
// Upload the contents
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
var logUploaded = await _jobServer.AppendLogContentAsync(_scopeIdentifier, _hubName, _planId, taskLog.Id, fs, default(CancellationToken));
}
// Create a new record and only set the Log field
var attachmentUpdataRecord = new TimelineRecord() { Id = file.TimelineRecordId, Log = taskLog };
QueueTimelineRecordUpdate(file.TimelineId, attachmentUpdataRecord);
}
else
{
// Create attachment
using (FileStream fs = File.Open(file.Path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
var result = await _jobServer.CreateAttachmentAsync(_scopeIdentifier, _hubName, _planId, file.TimelineId, file.TimelineRecordId, file.Type, file.Name, fs, default(CancellationToken));
}
}
uploadSucceed = true;
}
finally
{
if (uploadSucceed && file.DeleteSource)
{
try
{
File.Delete(file.Path);
}
catch (Exception ex)
{
Trace.Info("Catch exception during delete success uploaded file.");
Trace.Error(ex);
}
}
}
}
}
internal class PendingTimelineRecord
{
public Guid TimelineId { get; set; }
public List<TimelineRecord> PendingRecords { get; set; }
}
internal class UploadFileInfo
{
public Guid TimelineId { get; set; }
public Guid TimelineRecordId { get; set; }
public string Type { get; set; }
public string Name { get; set; }
public string Path { get; set; }
public bool DeleteSource { get; set; }
}
internal class ConsoleLineInfo
{
public ConsoleLineInfo(Guid recordId, string line)
{
this.StepRecordId = recordId;
this.Line = line;
}
public Guid StepRecordId { get; set; }
public string Line { get; set; }
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Threading.Tasks;
using GitHub.Services.WebApi;
using GitHub.Services.Location.Client;
using GitHub.Services.Location;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(LocationServer))]
public interface ILocationServer : IRunnerService
{
Task ConnectAsync(VssConnection jobConnection);
Task<ConnectionData> GetConnectionDataAsync();
}
public sealed class LocationServer : RunnerService, ILocationServer
{
private bool _hasConnection;
private VssConnection _connection;
private LocationHttpClient _locationClient;
public async Task ConnectAsync(VssConnection jobConnection)
{
_connection = jobConnection;
int attemptCount = 5;
while (!_connection.HasAuthenticated && attemptCount-- > 0)
{
try
{
await _connection.ConnectAsync();
break;
}
catch (Exception ex) when (attemptCount > 0)
{
Trace.Info($"Catch exception during connect. {attemptCount} attempt left.");
Trace.Error(ex);
}
await Task.Delay(100);
}
_locationClient = _connection.GetClient<LocationHttpClient>();
_hasConnection = true;
}
private void CheckConnection()
{
if (!_hasConnection)
{
throw new InvalidOperationException("SetConnection");
}
}
public async Task<ConnectionData> GetConnectionDataAsync()
{
CheckConnection();
return await _locationClient.GetConnectionDataAsync(ConnectOptions.None, 0);
}
}
}

View File

@@ -0,0 +1,124 @@
using GitHub.Runner.Common.Util;
using System;
using System.IO;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(PagingLogger))]
public interface IPagingLogger : IRunnerService
{
long TotalLines { get; }
void Setup(Guid timelineId, Guid timelineRecordId);
void Write(string message);
void End();
}
public class PagingLogger : RunnerService, IPagingLogger
{
public static string PagingFolder = "pages";
// 8 MB
public const int PageSize = 8 * 1024 * 1024;
private Guid _timelineId;
private Guid _timelineRecordId;
private string _pageId;
private FileStream _pageData;
private StreamWriter _pageWriter;
private int _byteCount;
private int _pageCount;
private long _totalLines;
private string _dataFileName;
private string _pagesFolder;
private IJobServerQueue _jobServerQueue;
public long TotalLines => _totalLines;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_totalLines = 0;
_pageId = Guid.NewGuid().ToString();
_pagesFolder = Path.Combine(hostContext.GetDirectory(WellKnownDirectory.Diag), PagingFolder);
_jobServerQueue = HostContext.GetService<IJobServerQueue>();
Directory.CreateDirectory(_pagesFolder);
}
public void Setup(Guid timelineId, Guid timelineRecordId)
{
_timelineId = timelineId;
_timelineRecordId = timelineRecordId;
}
//
// Write a metadata file with id etc, point to pages on disk.
// Each page is a guid_#. As a page rolls over, it events it's done
// and the consumer queues it for upload
// Ensure this is lazy. Create a page on first write
//
public void Write(string message)
{
// lazy creation on write
if (_pageWriter == null)
{
Create();
}
string line = $"{DateTime.UtcNow.ToString("O")} {message}";
_pageWriter.WriteLine(line);
_totalLines++;
if (line.IndexOf('\n') != -1)
{
foreach (char c in line)
{
if (c == '\n')
{
_totalLines++;
}
}
}
_byteCount += System.Text.Encoding.UTF8.GetByteCount(line);
if (_byteCount >= PageSize)
{
NewPage();
}
}
public void End()
{
EndPage();
}
private void Create()
{
NewPage();
}
private void NewPage()
{
EndPage();
_byteCount = 0;
_dataFileName = Path.Combine(_pagesFolder, $"{_pageId}_{++_pageCount}.log");
_pageData = new FileStream(_dataFileName, FileMode.CreateNew);
_pageWriter = new StreamWriter(_pageData, System.Text.Encoding.UTF8);
}
private void EndPage()
{
if (_pageWriter != null)
{
_pageWriter.Flush();
_pageData.Flush();
//The StreamWriter object calls Dispose() on the provided Stream object when StreamWriter.Dispose is called.
_pageWriter.Dispose();
_pageWriter = null;
_pageData = null;
_jobServerQueue.QueueFileUpload(_timelineId, _timelineRecordId, "DistributedTask.Core.Log", "CustomToolLog", _dataFileName, true);
}
}
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.IO;
using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Runner.Common
{
public delegate void StartProcessDelegate(string pipeHandleOut, string pipeHandleIn);
public enum MessageType
{
NotInitialized = -1,
NewJobRequest = 1,
CancelRequest = 2,
RunnerShutdown = 3,
OperatingSystemShutdown = 4
}
public struct WorkerMessage
{
public MessageType MessageType;
public string Body;
public WorkerMessage(MessageType messageType, string body)
{
MessageType = messageType;
Body = body;
}
}
[ServiceLocator(Default = typeof(ProcessChannel))]
public interface IProcessChannel : IDisposable, IRunnerService
{
void StartServer(StartProcessDelegate startProcess);
void StartClient(string pipeNameInput, string pipeNameOutput);
Task SendAsync(MessageType messageType, string body, CancellationToken cancellationToken);
Task<WorkerMessage> ReceiveAsync(CancellationToken cancellationToken);
}
public sealed class ProcessChannel : RunnerService, IProcessChannel
{
private AnonymousPipeServerStream _inServer;
private AnonymousPipeServerStream _outServer;
private AnonymousPipeClientStream _inClient;
private AnonymousPipeClientStream _outClient;
private StreamString _writeStream;
private StreamString _readStream;
public void StartServer(StartProcessDelegate startProcess)
{
_outServer = new AnonymousPipeServerStream(PipeDirection.Out, HandleInheritability.Inheritable);
_inServer = new AnonymousPipeServerStream(PipeDirection.In, HandleInheritability.Inheritable);
_readStream = new StreamString(_inServer);
_writeStream = new StreamString(_outServer);
startProcess(_outServer.GetClientHandleAsString(), _inServer.GetClientHandleAsString());
_outServer.DisposeLocalCopyOfClientHandle();
_inServer.DisposeLocalCopyOfClientHandle();
}
public void StartClient(string pipeNameInput, string pipeNameOutput)
{
_inClient = new AnonymousPipeClientStream(PipeDirection.In, pipeNameInput);
_outClient = new AnonymousPipeClientStream(PipeDirection.Out, pipeNameOutput);
_readStream = new StreamString(_inClient);
_writeStream = new StreamString(_outClient);
}
public async Task SendAsync(MessageType messageType, string body, CancellationToken cancellationToken)
{
await _writeStream.WriteInt32Async((int)messageType, cancellationToken);
await _writeStream.WriteStringAsync(body, cancellationToken);
}
public async Task<WorkerMessage> ReceiveAsync(CancellationToken cancellationToken)
{
WorkerMessage result = new WorkerMessage(MessageType.NotInitialized, string.Empty);
result.MessageType = (MessageType)await _readStream.ReadInt32Async(cancellationToken);
result.Body = await _readStream.ReadStringAsync(cancellationToken);
return result;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
_inServer?.Dispose();
_outServer?.Dispose();
_inClient?.Dispose();
_outClient?.Dispose();
}
}
}
}

View File

@@ -0,0 +1,396 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Runner.Common
{
#if OS_WINDOWS
public static class WindowsProcessExtensions
{
// Reference: https://blogs.msdn.microsoft.com/matt_pietrek/2004/08/25/reading-another-processs-environment/
// Reference: http://blog.gapotchenko.com/eazfuscator.net/reading-environment-variables
public static string GetEnvironmentVariable(this Process process, IHostContext hostContext, string variable)
{
var trace = hostContext.GetTrace(nameof(WindowsProcessExtensions));
Dictionary<string, string> environmentVariables = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
IntPtr processHandle = process.SafeHandle.DangerousGetHandle();
IntPtr environmentBlockAddress;
if (Environment.Is64BitOperatingSystem)
{
PROCESS_BASIC_INFORMATION64 pbi = new PROCESS_BASIC_INFORMATION64();
int returnLength = 0;
int status = NtQueryInformationProcess64(processHandle, PROCESSINFOCLASS.ProcessBasicInformation, ref pbi, Marshal.SizeOf(pbi), ref returnLength);
if (status != 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
bool wow64;
if (!IsWow64Process(processHandle, out wow64))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (!wow64)
{
// 64 bits process on 64 bits OS
IntPtr UserProcessParameterAddress = ReadIntPtr64(processHandle, new IntPtr(pbi.PebBaseAddress) + 0x20);
environmentBlockAddress = ReadIntPtr64(processHandle, UserProcessParameterAddress + 0x80);
}
else
{
// 32 bits process on 64 bits OS
IntPtr UserProcessParameterAddress = ReadIntPtr32(processHandle, new IntPtr(pbi.PebBaseAddress) + 0x1010);
environmentBlockAddress = ReadIntPtr32(processHandle, UserProcessParameterAddress + 0x48);
}
}
else
{
PROCESS_BASIC_INFORMATION32 pbi = new PROCESS_BASIC_INFORMATION32();
int returnLength = 0;
int status = NtQueryInformationProcess32(processHandle, PROCESSINFOCLASS.ProcessBasicInformation, ref pbi, Marshal.SizeOf(pbi), ref returnLength);
if (status != 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
// 32 bits process on 32 bits OS
IntPtr UserProcessParameterAddress = ReadIntPtr32(processHandle, new IntPtr(pbi.PebBaseAddress) + 0x10);
environmentBlockAddress = ReadIntPtr32(processHandle, UserProcessParameterAddress + 0x48);
}
MEMORY_BASIC_INFORMATION memInfo = new MEMORY_BASIC_INFORMATION();
if (VirtualQueryEx(processHandle, environmentBlockAddress, ref memInfo, Marshal.SizeOf(memInfo)) == 0)
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
Int64 dataSize = memInfo.RegionSize.ToInt64() - (environmentBlockAddress.ToInt64() - memInfo.BaseAddress.ToInt64());
byte[] envData = new byte[dataSize];
IntPtr res_len = IntPtr.Zero;
if (!ReadProcessMemory(processHandle, environmentBlockAddress, envData, new IntPtr(dataSize), ref res_len))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (res_len.ToInt64() != dataSize)
{
throw new ArgumentOutOfRangeException(nameof(ReadProcessMemory));
}
string environmentVariableString;
Int64 environmentVariableBytesLength = 0;
// check env encoding
if (envData[0] != 0 && envData[1] == 0)
{
// Unicode
for (Int64 index = 0; index < dataSize; index++)
{
// Unicode encoded environment variables block ends up with '\0\0\0\0'.
if (environmentVariableBytesLength == 0 &&
envData[index] == 0 &&
index + 3 < dataSize &&
envData[index + 1] == 0 &&
envData[index + 2] == 0 &&
envData[index + 3] == 0)
{
environmentVariableBytesLength = index + 3;
}
else if (environmentVariableBytesLength != 0)
{
// set it '\0' so we can easily trim it, most array method doesn't take int64
envData[index] = 0;
}
}
if (environmentVariableBytesLength == 0)
{
throw new ArgumentException(nameof(environmentVariableBytesLength));
}
environmentVariableString = Encoding.Unicode.GetString(envData);
}
else if (envData[0] != 0 && envData[1] != 0)
{
// ANSI
for (Int64 index = 0; index < dataSize; index++)
{
// Unicode encoded environment variables block ends up with '\0\0'.
if (environmentVariableBytesLength == 0 &&
envData[index] == 0 &&
index + 1 < dataSize &&
envData[index + 1] == 0)
{
environmentVariableBytesLength = index + 1;
}
else if (environmentVariableBytesLength != 0)
{
// set it '\0' so we can easily trim it, most array method doesn't take int64
envData[index] = 0;
}
}
if (environmentVariableBytesLength == 0)
{
throw new ArgumentException(nameof(environmentVariableBytesLength));
}
environmentVariableString = Encoding.Default.GetString(envData);
}
else
{
throw new ArgumentException(nameof(envData));
}
foreach (var envString in environmentVariableString.Split("\0", StringSplitOptions.RemoveEmptyEntries))
{
string[] env = envString.Split("=", 2);
if (!string.IsNullOrEmpty(env[0]))
{
environmentVariables[env[0]] = env[1];
trace.Verbose($"PID:{process.Id} ({env[0]}={env[1]})");
}
}
if (environmentVariables.TryGetValue(variable, out string envVariable))
{
return envVariable;
}
else
{
return null;
}
}
private static IntPtr ReadIntPtr32(IntPtr hProcess, IntPtr ptr)
{
IntPtr readPtr = IntPtr.Zero;
IntPtr data = Marshal.AllocHGlobal(sizeof(Int32));
try
{
IntPtr res_len = IntPtr.Zero;
if (!ReadProcessMemory(hProcess, ptr, data, new IntPtr(sizeof(Int32)), ref res_len))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (res_len.ToInt32() != sizeof(Int32))
{
throw new ArgumentOutOfRangeException(nameof(ReadProcessMemory));
}
readPtr = new IntPtr(Marshal.ReadInt32(data));
}
finally
{
Marshal.FreeHGlobal(data);
}
return readPtr;
}
private static IntPtr ReadIntPtr64(IntPtr hProcess, IntPtr ptr)
{
IntPtr readPtr = IntPtr.Zero;
IntPtr data = Marshal.AllocHGlobal(IntPtr.Size);
try
{
IntPtr res_len = IntPtr.Zero;
if (!ReadProcessMemory(hProcess, ptr, data, new IntPtr(sizeof(Int64)), ref res_len))
{
throw new Win32Exception(Marshal.GetLastWin32Error());
}
if (res_len.ToInt32() != IntPtr.Size)
{
throw new ArgumentOutOfRangeException(nameof(ReadProcessMemory));
}
readPtr = Marshal.ReadIntPtr(data);
}
finally
{
Marshal.FreeHGlobal(data);
}
return readPtr;
}
private enum PROCESSINFOCLASS : int
{
ProcessBasicInformation = 0
};
[StructLayout(LayoutKind.Sequential)]
private struct MEMORY_BASIC_INFORMATION
{
public IntPtr BaseAddress;
public IntPtr AllocationBase;
public int AllocationProtect;
public IntPtr RegionSize;
public int State;
public int Protect;
public int Type;
}
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_BASIC_INFORMATION64
{
public long ExitStatus;
public long PebBaseAddress;
public long AffinityMask;
public long BasePriority;
public long UniqueProcessId;
public long InheritedFromUniqueProcessId;
};
[StructLayout(LayoutKind.Sequential)]
private struct PROCESS_BASIC_INFORMATION32
{
public int ExitStatus;
public int PebBaseAddress;
public int AffinityMask;
public int BasePriority;
public int UniqueProcessId;
public int InheritedFromUniqueProcessId;
};
[DllImport("ntdll.dll", SetLastError = true, EntryPoint = "NtQueryInformationProcess")]
private static extern int NtQueryInformationProcess64(IntPtr processHandle, PROCESSINFOCLASS processInformationClass, ref PROCESS_BASIC_INFORMATION64 processInformation, int processInformationLength, ref int returnLength);
[DllImport("ntdll.dll", SetLastError = true, EntryPoint = "NtQueryInformationProcess")]
private static extern int NtQueryInformationProcess32(IntPtr processHandle, PROCESSINFOCLASS processInformationClass, ref PROCESS_BASIC_INFORMATION32 processInformation, int processInformationLength, ref int returnLength);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool IsWow64Process(IntPtr processHandle, out bool wow64Process);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, IntPtr dwSize, ref IntPtr lpNumberOfBytesRead);
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, [Out] byte[] lpBuffer, IntPtr dwSize, ref IntPtr lpNumberOfBytesRead);
[DllImport("kernel32.dll")]
private static extern int VirtualQueryEx(IntPtr processHandle, IntPtr baseAddress, ref MEMORY_BASIC_INFORMATION memoryInformation, int memoryInformationLength);
}
#else
public static class LinuxProcessExtensions
{
public static string GetEnvironmentVariable(this Process process, IHostContext hostContext, string variable)
{
var trace = hostContext.GetTrace(nameof(LinuxProcessExtensions));
Dictionary<string, string> env = new Dictionary<string, string>();
if (Directory.Exists("/proc"))
{
string envFile = $"/proc/{process.Id}/environ";
trace.Info($"Read env from {envFile}");
string envContent = File.ReadAllText(envFile);
if (!string.IsNullOrEmpty(envContent))
{
// on linux, environment variables are seprated by '\0'
var envList = envContent.Split('\0', StringSplitOptions.RemoveEmptyEntries);
foreach (var envStr in envList)
{
// split on the first '='
var keyValuePair = envStr.Split('=', 2);
if (keyValuePair.Length == 2)
{
env[keyValuePair[0]] = keyValuePair[1];
trace.Verbose($"PID:{process.Id} ({keyValuePair[0]}={keyValuePair[1]})");
}
}
}
}
else
{
// On OSX, there is no /proc folder for us to read environment for given process,
// So we have call `ps e -p <pid> -o command` to print out env to STDOUT,
// However, the output env are not format in a parseable way, it's just a string that concatenate all envs with space,
// It doesn't escape '=' or ' ', so we can't parse the output into a dictionary of all envs.
// So we only look for the env you request, in the format of variable=value. (it won't work if you variable contains = or space)
trace.Info($"Read env from output of `ps e -p {process.Id} -o command`");
List<string> psOut = new List<string>();
object outputLock = new object();
using (var p = hostContext.CreateService<IProcessInvoker>())
{
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
psOut.Add(stdout.Data);
}
}
};
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
trace.Error(stderr.Data);
}
}
};
int exitCode = p.ExecuteAsync(workingDirectory: hostContext.GetDirectory(WellKnownDirectory.Root),
fileName: "ps",
arguments: $"e -p {process.Id} -o command",
environment: null,
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
if (exitCode == 0)
{
trace.Info($"Successfully dump environment variables for {process.Id}");
if (psOut.Count > 0)
{
string psOutputString = string.Join(" ", psOut);
trace.Verbose($"ps output: '{psOutputString}'");
int varStartIndex = psOutputString.IndexOf(variable, StringComparison.Ordinal);
if (varStartIndex >= 0)
{
string rightPart = psOutputString.Substring(varStartIndex + variable.Length + 1);
if (rightPart.IndexOf(' ') > 0)
{
string value = rightPart.Substring(0, rightPart.IndexOf(' '));
env[variable] = value;
}
else
{
env[variable] = rightPart;
}
trace.Verbose($"PID:{process.Id} ({variable}={env[variable]})");
}
}
}
}
}
if (env.TryGetValue(variable, out string envVariable))
{
return envVariable;
}
else
{
return null;
}
}
}
#endif
}

View File

@@ -0,0 +1,329 @@
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(ProcessInvokerWrapper))]
public interface IProcessInvoker : IDisposable, IRunnerService
{
event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
CancellationToken cancellationToken);
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
CancellationToken cancellationToken);
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
CancellationToken cancellationToken);
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
CancellationToken cancellationToken);
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
CancellationToken cancellationToken);
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
bool inheritConsoleHandler,
CancellationToken cancellationToken);
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
bool inheritConsoleHandler,
bool keepStandardInOpen,
CancellationToken cancellationToken);
Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
bool inheritConsoleHandler,
bool keepStandardInOpen,
bool highPriorityProcess,
CancellationToken cancellationToken);
}
// The implementation of the process invoker does not hook up DataReceivedEvent and ErrorReceivedEvent of Process,
// instead, we read both STDOUT and STDERR stream manually on seperate thread.
// The reason is we find a huge perf issue about process STDOUT/STDERR with those events.
//
// Missing functionalities:
// 1. Cancel/Kill process tree
// 2. Make sure STDOUT and STDERR not process out of order
public sealed class ProcessInvokerWrapper : RunnerService, IProcessInvoker
{
private ProcessInvoker _invoker;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_invoker = new ProcessInvoker(Trace);
}
public event EventHandler<ProcessDataReceivedEventArgs> OutputDataReceived;
public event EventHandler<ProcessDataReceivedEventArgs> ErrorDataReceived;
public Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
CancellationToken cancellationToken)
{
return ExecuteAsync(
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: false,
cancellationToken: cancellationToken);
}
public Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
CancellationToken cancellationToken)
{
return ExecuteAsync(
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: null,
cancellationToken: cancellationToken);
}
public Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
CancellationToken cancellationToken)
{
return ExecuteAsync(
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding,
killProcessOnCancel: false,
cancellationToken: cancellationToken);
}
public Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
CancellationToken cancellationToken)
{
return ExecuteAsync(
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding,
killProcessOnCancel: killProcessOnCancel,
redirectStandardIn: null,
cancellationToken: cancellationToken);
}
public Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
CancellationToken cancellationToken)
{
return ExecuteAsync(
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding,
killProcessOnCancel: killProcessOnCancel,
redirectStandardIn: redirectStandardIn,
inheritConsoleHandler: false,
cancellationToken: cancellationToken
);
}
public Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
bool inheritConsoleHandler,
CancellationToken cancellationToken)
{
return ExecuteAsync(
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding,
killProcessOnCancel: killProcessOnCancel,
redirectStandardIn: redirectStandardIn,
inheritConsoleHandler: inheritConsoleHandler,
keepStandardInOpen: false,
cancellationToken: cancellationToken
);
}
public Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
bool inheritConsoleHandler,
bool keepStandardInOpen,
CancellationToken cancellationToken)
{
return ExecuteAsync(
workingDirectory: workingDirectory,
fileName: fileName,
arguments: arguments,
environment: environment,
requireExitCodeZero: requireExitCodeZero,
outputEncoding: outputEncoding,
killProcessOnCancel: killProcessOnCancel,
redirectStandardIn: redirectStandardIn,
inheritConsoleHandler: inheritConsoleHandler,
keepStandardInOpen: keepStandardInOpen,
highPriorityProcess: false,
cancellationToken: cancellationToken
);
}
public async Task<int> ExecuteAsync(
string workingDirectory,
string fileName,
string arguments,
IDictionary<string, string> environment,
bool requireExitCodeZero,
Encoding outputEncoding,
bool killProcessOnCancel,
Channel<string> redirectStandardIn,
bool inheritConsoleHandler,
bool keepStandardInOpen,
bool highPriorityProcess,
CancellationToken cancellationToken)
{
_invoker.ErrorDataReceived += this.ErrorDataReceived;
_invoker.OutputDataReceived += this.OutputDataReceived;
return await _invoker.ExecuteAsync(
workingDirectory,
fileName,
arguments,
environment,
requireExitCodeZero,
outputEncoding,
killProcessOnCancel,
redirectStandardIn,
inheritConsoleHandler,
keepStandardInOpen,
highPriorityProcess,
cancellationToken);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
if (_invoker != null)
{
_invoker.Dispose();
_invoker = null;
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.2</TargetFramework>
<OutputType>Library</OutputType>
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm;rhel.6-x64;osx-x64</RuntimeIdentifiers>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AssetTargetFallback>portable-net45+win8</AssetTargetFallback>
<NoWarn>NU1701;NU1603</NoWarn>
<Version>$(Version)</Version>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Sdk\Sdk.csproj" />
<ProjectReference Include="..\Runner.Sdk\Runner.Sdk.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Win32.Registry" Version="4.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="4.4.0" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.4.0" />
<PackageReference Include="System.Threading.Channels" Version="4.4.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
<DebugType>portable</DebugType>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(PackageRuntime)' == 'win-x64'">
<DefineConstants>OS_WINDOWS;X64;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(PackageRuntime)' == 'win-x86'">
<DefineConstants>OS_WINDOWS;X86;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'win-x64'">
<DefineConstants>OS_WINDOWS;X64;DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'win-x86'">
<DefineConstants>OS_WINDOWS;X86;DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">
<DefineConstants>OS_OSX;X64;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true' AND '$(Configuration)' == 'Debug'">
<DefineConstants>OS_OSX;DEBUG;X64;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(PackageRuntime)' == 'linux-x64'">
<DefineConstants>OS_LINUX;X64;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(PackageRuntime)' == 'rhel.6-x64'">
<DefineConstants>OS_LINUX;OS_RHEL6;X64;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(PackageRuntime)' == 'linux-arm'">
<DefineConstants>OS_LINUX;ARM;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'linux-x64'">
<DefineConstants>OS_LINUX;X64;DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'rhel.6-x64'">
<DefineConstants>OS_LINUX;OS_RHEL6;X64;DEBUG;TRACE</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'linux-arm'">
<DefineConstants>OS_LINUX;ARM;DEBUG;TRACE</DefineConstants>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,231 @@
using System;
using GitHub.Runner.Common.Util;
using System.IO;
using System.Runtime.Serialization;
using GitHub.Services.Common;
using System.Security.Cryptography.X509Certificates;
using System.Net;
using System.Net.Security;
using System.Net.Http;
using GitHub.Services.WebApi;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(RunnerCertificateManager))]
public interface IRunnerCertificateManager : IRunnerService
{
bool SkipServerCertificateValidation { get; }
string CACertificateFile { get; }
string ClientCertificateFile { get; }
string ClientCertificatePrivateKeyFile { get; }
string ClientCertificateArchiveFile { get; }
string ClientCertificatePassword { get; }
IVssClientCertificateManager VssClientCertificateManager { get; }
}
public class RunnerCertificateManager : RunnerService, IRunnerCertificateManager
{
private RunnerClientCertificateManager _runnerClientCertificateManager = new RunnerClientCertificateManager();
public bool SkipServerCertificateValidation { private set; get; }
public string CACertificateFile { private set; get; }
public string ClientCertificateFile { private set; get; }
public string ClientCertificatePrivateKeyFile { private set; get; }
public string ClientCertificateArchiveFile { private set; get; }
public string ClientCertificatePassword { private set; get; }
public IVssClientCertificateManager VssClientCertificateManager => _runnerClientCertificateManager;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
LoadCertificateSettings();
}
// This should only be called from config
public void SetupCertificate(bool skipCertValidation, string caCert, string clientCert, string clientCertPrivateKey, string clientCertArchive, string clientCertPassword)
{
Trace.Info("Setup runner certificate setting base on configuration inputs.");
if (skipCertValidation)
{
Trace.Info("Ignore SSL server certificate validation error");
SkipServerCertificateValidation = true;
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
if (!string.IsNullOrEmpty(caCert))
{
ArgUtil.File(caCert, nameof(caCert));
Trace.Info($"Self-Signed CA '{caCert}'");
}
if (!string.IsNullOrEmpty(clientCert))
{
ArgUtil.File(clientCert, nameof(clientCert));
ArgUtil.File(clientCertPrivateKey, nameof(clientCertPrivateKey));
ArgUtil.File(clientCertArchive, nameof(clientCertArchive));
Trace.Info($"Client cert '{clientCert}'");
Trace.Info($"Client cert private key '{clientCertPrivateKey}'");
Trace.Info($"Client cert archive '{clientCertArchive}'");
}
CACertificateFile = caCert;
ClientCertificateFile = clientCert;
ClientCertificatePrivateKeyFile = clientCertPrivateKey;
ClientCertificateArchiveFile = clientCertArchive;
ClientCertificatePassword = clientCertPassword;
_runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword);
}
// This should only be called from config
public void SaveCertificateSetting()
{
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
IOUtil.DeleteFile(certSettingFile);
var setting = new RunnerCertificateSetting();
if (SkipServerCertificateValidation)
{
Trace.Info($"Store Skip ServerCertificateValidation setting to '{certSettingFile}'");
setting.SkipServerCertValidation = true;
}
if (!string.IsNullOrEmpty(CACertificateFile))
{
Trace.Info($"Store CA cert setting to '{certSettingFile}'");
setting.CACert = CACertificateFile;
}
if (!string.IsNullOrEmpty(ClientCertificateFile) &&
!string.IsNullOrEmpty(ClientCertificatePrivateKeyFile) &&
!string.IsNullOrEmpty(ClientCertificateArchiveFile))
{
Trace.Info($"Store client cert settings to '{certSettingFile}'");
setting.ClientCert = ClientCertificateFile;
setting.ClientCertPrivatekey = ClientCertificatePrivateKeyFile;
setting.ClientCertArchive = ClientCertificateArchiveFile;
if (!string.IsNullOrEmpty(ClientCertificatePassword))
{
string lookupKey = Guid.NewGuid().ToString("D").ToUpperInvariant();
Trace.Info($"Store client cert private key password with lookup key {lookupKey}");
var credStore = HostContext.GetService<IRunnerCredentialStore>();
credStore.Write($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{lookupKey}", "GitHub", ClientCertificatePassword);
setting.ClientCertPasswordLookupKey = lookupKey;
}
}
if (SkipServerCertificateValidation ||
!string.IsNullOrEmpty(CACertificateFile) ||
!string.IsNullOrEmpty(ClientCertificateFile))
{
IOUtil.SaveObject(setting, certSettingFile);
File.SetAttributes(certSettingFile, File.GetAttributes(certSettingFile) | FileAttributes.Hidden);
}
}
// This should only be called from unconfig
public void DeleteCertificateSetting()
{
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
if (File.Exists(certSettingFile))
{
Trace.Info($"Load runner certificate setting from '{certSettingFile}'");
var certSetting = IOUtil.LoadObject<RunnerCertificateSetting>(certSettingFile);
if (certSetting != null && !string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey))
{
Trace.Info("Delete client cert private key password from credential store.");
var credStore = HostContext.GetService<IRunnerCredentialStore>();
credStore.Delete($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}");
}
Trace.Info($"Delete cert setting file: {certSettingFile}");
IOUtil.DeleteFile(certSettingFile);
}
}
public void LoadCertificateSettings()
{
string certSettingFile = HostContext.GetConfigFile(WellKnownConfigFile.Certificates);
if (File.Exists(certSettingFile))
{
Trace.Info($"Load runner certificate setting from '{certSettingFile}'");
var certSetting = IOUtil.LoadObject<RunnerCertificateSetting>(certSettingFile);
ArgUtil.NotNull(certSetting, nameof(RunnerCertificateSetting));
if (certSetting.SkipServerCertValidation)
{
Trace.Info("Ignore SSL server certificate validation error");
SkipServerCertificateValidation = true;
VssClientHttpRequestSettings.Default.ServerCertificateValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator;
}
if (!string.IsNullOrEmpty(certSetting.CACert))
{
// make sure all settings file exist
ArgUtil.File(certSetting.CACert, nameof(certSetting.CACert));
Trace.Info($"CA '{certSetting.CACert}'");
CACertificateFile = certSetting.CACert;
}
if (!string.IsNullOrEmpty(certSetting.ClientCert))
{
// make sure all settings file exist
ArgUtil.File(certSetting.ClientCert, nameof(certSetting.ClientCert));
ArgUtil.File(certSetting.ClientCertPrivatekey, nameof(certSetting.ClientCertPrivatekey));
ArgUtil.File(certSetting.ClientCertArchive, nameof(certSetting.ClientCertArchive));
Trace.Info($"Client cert '{certSetting.ClientCert}'");
Trace.Info($"Client cert private key '{certSetting.ClientCertPrivatekey}'");
Trace.Info($"Client cert archive '{certSetting.ClientCertArchive}'");
ClientCertificateFile = certSetting.ClientCert;
ClientCertificatePrivateKeyFile = certSetting.ClientCertPrivatekey;
ClientCertificateArchiveFile = certSetting.ClientCertArchive;
if (!string.IsNullOrEmpty(certSetting.ClientCertPasswordLookupKey))
{
var cerdStore = HostContext.GetService<IRunnerCredentialStore>();
ClientCertificatePassword = cerdStore.Read($"GITHUB_ACTIONS_RUNNER_CLIENT_CERT_PASSWORD_{certSetting.ClientCertPasswordLookupKey}").Password;
HostContext.SecretMasker.AddValue(ClientCertificatePassword);
}
_runnerClientCertificateManager.AddClientCertificate(ClientCertificateArchiveFile, ClientCertificatePassword);
}
}
else
{
Trace.Info("No certificate setting found.");
}
}
}
[DataContract]
internal class RunnerCertificateSetting
{
[DataMember]
public bool SkipServerCertValidation { get; set; }
[DataMember]
public string CACert { get; set; }
[DataMember]
public string ClientCert { get; set; }
[DataMember]
public string ClientCertPrivatekey { get; set; }
[DataMember]
public string ClientCertArchive { get; set; }
[DataMember]
public string ClientCertPasswordLookupKey { get; set; }
}
}

View File

@@ -0,0 +1,948 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using GitHub.Runner.Common.Util;
using Newtonsoft.Json;
using System.IO;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
// The purpose of this class is to store user's credential during runner configuration and retrive the credential back at runtime.
#if OS_WINDOWS
[ServiceLocator(Default = typeof(WindowsRunnerCredentialStore))]
#elif OS_OSX
[ServiceLocator(Default = typeof(MacOSRunnerCredentialStore))]
#else
[ServiceLocator(Default = typeof(LinuxRunnerCredentialStore))]
#endif
public interface IRunnerCredentialStore : IRunnerService
{
NetworkCredential Write(string target, string username, string password);
// throw exception when target not found from cred store
NetworkCredential Read(string target);
// throw exception when target not found from cred store
void Delete(string target);
}
#if OS_WINDOWS
// Windows credential store is per user.
// This is a limitation for user configure the runner run as windows service, when user's current login account is different with the service run as account.
// Ex: I login the box as domain\admin, configure the runner as windows service and run as domian\buildserver
// domain\buildserver won't read the stored credential from domain\admin's windows credential store.
// To workaround this limitation.
// Anytime we try to save a credential:
// 1. store it into current user's windows credential store
// 2. use DP-API do a machine level encrypt and store the encrypted content on disk.
// At the first time we try to read the credential:
// 1. read from current user's windows credential store, delete the DP-API encrypted backup content on disk if the windows credential store read succeed.
// 2. if credential not found in current user's windows credential store, read from the DP-API encrypted backup content on disk,
// write the credential back the current user's windows credential store and delete the backup on disk.
public sealed class WindowsRunnerCredentialStore : RunnerService, IRunnerCredentialStore
{
private string _credStoreFile;
private Dictionary<string, string> _credStore;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
if (File.Exists(_credStoreFile))
{
_credStore = IOUtil.LoadObject<Dictionary<string, string>>(_credStoreFile);
}
else
{
_credStore = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
}
public NetworkCredential Write(string target, string username, string password)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
ArgUtil.NotNullOrEmpty(username, nameof(username));
ArgUtil.NotNullOrEmpty(password, nameof(password));
// save to .credential_store file first, then Windows credential store
string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username));
string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password));
// Base64Username:Base64Password -> DP-API machine level encrypt -> Base64Encoding
string encryptedUsernamePassword = Convert.ToBase64String(ProtectedData.Protect(Encoding.UTF8.GetBytes($"{usernameBase64}:{passwordBase64}"), null, DataProtectionScope.LocalMachine));
Trace.Info($"Credentials for '{target}' written to credential store file.");
_credStore[target] = encryptedUsernamePassword;
// save to .credential_store file
SyncCredentialStoreFile();
// save to Windows Credential Store
return WriteInternal(target, username, password);
}
public NetworkCredential Read(string target)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
IntPtr credPtr = IntPtr.Zero;
try
{
if (CredRead(target, CredentialType.Generic, 0, out credPtr))
{
Credential credStruct = (Credential)Marshal.PtrToStructure(credPtr, typeof(Credential));
int passwordLength = (int)credStruct.CredentialBlobSize;
string password = passwordLength > 0 ? Marshal.PtrToStringUni(credStruct.CredentialBlob, passwordLength / sizeof(char)) : String.Empty;
string username = Marshal.PtrToStringUni(credStruct.UserName);
Trace.Info($"Credentials for '{target}' read from windows credential store.");
// delete from .credential_store file since we are able to read it from windows credential store
if (_credStore.Remove(target))
{
Trace.Info($"Delete credentials for '{target}' from credential store file.");
SyncCredentialStoreFile();
}
return new NetworkCredential(username, password);
}
else
{
// Can't read from Windows Credential Store, fail back to .credential_store file
if (_credStore.ContainsKey(target) && !string.IsNullOrEmpty(_credStore[target]))
{
Trace.Info($"Credentials for '{target}' read from credential store file.");
// Base64Decode -> DP-API machine level decrypt -> Base64Username:Base64Password -> Base64Decode
string decryptedUsernamePassword = Encoding.UTF8.GetString(ProtectedData.Unprotect(Convert.FromBase64String(_credStore[target]), null, DataProtectionScope.LocalMachine));
string[] credential = decryptedUsernamePassword.Split(':');
if (credential.Length == 2 && !string.IsNullOrEmpty(credential[0]) && !string.IsNullOrEmpty(credential[1]))
{
string username = Encoding.UTF8.GetString(Convert.FromBase64String(credential[0]));
string password = Encoding.UTF8.GetString(Convert.FromBase64String(credential[1]));
// store back to windows credential store for current user
NetworkCredential creds = WriteInternal(target, username, password);
// delete from .credential_store file since we are able to write the credential to windows credential store for current user.
if (_credStore.Remove(target))
{
Trace.Info($"Delete credentials for '{target}' from credential store file.");
SyncCredentialStoreFile();
}
return creds;
}
else
{
throw new ArgumentOutOfRangeException(nameof(decryptedUsernamePassword));
}
}
throw new Win32Exception(Marshal.GetLastWin32Error(), $"CredRead throw an error for '{target}'");
}
}
finally
{
if (credPtr != IntPtr.Zero)
{
CredFree(credPtr);
}
}
}
public void Delete(string target)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
// remove from .credential_store file
if (_credStore.Remove(target))
{
Trace.Info($"Delete credentials for '{target}' from credential store file.");
SyncCredentialStoreFile();
}
// remove from windows credential store
if (!CredDelete(target, CredentialType.Generic, 0))
{
throw new Win32Exception(Marshal.GetLastWin32Error(), $"Failed to delete credentials for {target}");
}
else
{
Trace.Info($"Credentials for '{target}' deleted from windows credential store.");
}
}
private NetworkCredential WriteInternal(string target, string username, string password)
{
// save to Windows Credential Store
Credential credential = new Credential()
{
Type = CredentialType.Generic,
Persist = (UInt32)CredentialPersist.LocalMachine,
TargetName = Marshal.StringToCoTaskMemUni(target),
UserName = Marshal.StringToCoTaskMemUni(username),
CredentialBlob = Marshal.StringToCoTaskMemUni(password),
CredentialBlobSize = (UInt32)Encoding.Unicode.GetByteCount(password),
AttributeCount = 0,
Comment = IntPtr.Zero,
Attributes = IntPtr.Zero,
TargetAlias = IntPtr.Zero
};
try
{
if (CredWrite(ref credential, 0))
{
Trace.Info($"Credentials for '{target}' written to windows credential store.");
return new NetworkCredential(username, password);
}
else
{
int error = Marshal.GetLastWin32Error();
throw new Win32Exception(error, "Failed to write credentials");
}
}
finally
{
if (credential.CredentialBlob != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(credential.CredentialBlob);
}
if (credential.TargetName != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(credential.TargetName);
}
if (credential.UserName != IntPtr.Zero)
{
Marshal.FreeCoTaskMem(credential.UserName);
}
}
}
private void SyncCredentialStoreFile()
{
Trace.Info("Sync in-memory credential store with credential store file.");
// delete the cred store file first anyway, since it's a readonly file.
IOUtil.DeleteFile(_credStoreFile);
// delete cred store file when all creds gone
if (_credStore.Count == 0)
{
return;
}
else
{
IOUtil.SaveObject(_credStore, _credStoreFile);
File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden);
}
}
[DllImport("Advapi32.dll", EntryPoint = "CredDeleteW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CredDelete(string target, CredentialType type, int reservedFlag);
[DllImport("Advapi32.dll", EntryPoint = "CredReadW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CredRead(string target, CredentialType type, int reservedFlag, out IntPtr CredentialPtr);
[DllImport("Advapi32.dll", EntryPoint = "CredWriteW", CharSet = CharSet.Unicode, SetLastError = true)]
internal static extern bool CredWrite([In] ref Credential userCredential, [In] UInt32 flags);
[DllImport("Advapi32.dll", EntryPoint = "CredFree", SetLastError = true)]
internal static extern bool CredFree([In] IntPtr cred);
internal enum CredentialPersist : UInt32
{
Session = 0x01,
LocalMachine = 0x02
}
internal enum CredentialType : uint
{
Generic = 0x01,
DomainPassword = 0x02,
DomainCertificate = 0x03
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct Credential
{
public UInt32 Flags;
public CredentialType Type;
public IntPtr TargetName;
public IntPtr Comment;
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
public UInt32 CredentialBlobSize;
public IntPtr CredentialBlob;
public UInt32 Persist;
public UInt32 AttributeCount;
public IntPtr Attributes;
public IntPtr TargetAlias;
public IntPtr UserName;
}
}
#elif OS_OSX
public sealed class MacOSRunnerCredentialStore : RunnerService, IRunnerCredentialStore
{
private const string _osxRunnerCredStoreKeyChainName = "_GITHUB_ACTIONS_RUNNER_CREDSTORE_INTERNAL_";
// Keychain requires a password, but this is not intended to add security
private const string _osxRunnerCredStoreKeyChainPassword = "C46F23C36AF94B72B1EAEE32C68670A0";
private string _securityUtil;
private string _runnerCredStoreKeyChain;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_securityUtil = WhichUtil.Which("security", true, Trace);
_runnerCredStoreKeyChain = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
// Create osx key chain if it doesn't exists.
if (!File.Exists(_runnerCredStoreKeyChain))
{
List<string> securityOut = new List<string>();
List<string> securityError = new List<string>();
object outputLock = new object();
using (var p = HostContext.CreateService<IProcessInvoker>())
{
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
securityOut.Add(stdout.Data);
}
}
};
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
securityError.Add(stderr.Data);
}
}
};
// make sure the 'security' has access to the key so we won't get prompt at runtime.
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
fileName: _securityUtil,
arguments: $"create-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"",
environment: null,
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
if (exitCode == 0)
{
Trace.Info($"Successfully create-keychain for {_runnerCredStoreKeyChain}");
}
else
{
if (securityOut.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityOut));
}
if (securityError.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityError));
}
throw new InvalidOperationException($"'security create-keychain' failed with exit code {exitCode}.");
}
}
}
else
{
// Try unlock and lock the keychain, make sure it's still in good stage
UnlockKeyChain();
LockKeyChain();
}
}
public NetworkCredential Write(string target, string username, string password)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
ArgUtil.NotNullOrEmpty(username, nameof(username));
ArgUtil.NotNullOrEmpty(password, nameof(password));
try
{
UnlockKeyChain();
// base64encode username + ':' + base64encode password
// OSX keychain requires you provide -s target and -a username to retrieve password
// So, we will trade both username and password as 'secret' store into keychain
string usernameBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(username));
string passwordBase64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(password));
string secretForKeyChain = $"{usernameBase64}:{passwordBase64}";
List<string> securityOut = new List<string>();
List<string> securityError = new List<string>();
object outputLock = new object();
using (var p = HostContext.CreateService<IProcessInvoker>())
{
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
securityOut.Add(stdout.Data);
}
}
};
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
securityError.Add(stderr.Data);
}
}
};
// make sure the 'security' has access to the key so we won't get prompt at runtime.
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
fileName: _securityUtil,
arguments: $"add-generic-password -s {target} -a GITHUBACTIONSRUNNER -w {secretForKeyChain} -T \"{_securityUtil}\" \"{_runnerCredStoreKeyChain}\"",
environment: null,
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
if (exitCode == 0)
{
Trace.Info($"Successfully add-generic-password for {target} (GITHUBACTIONSRUNNER)");
}
else
{
if (securityOut.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityOut));
}
if (securityError.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityError));
}
throw new InvalidOperationException($"'security add-generic-password' failed with exit code {exitCode}.");
}
}
return new NetworkCredential(username, password);
}
finally
{
LockKeyChain();
}
}
public NetworkCredential Read(string target)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
try
{
UnlockKeyChain();
string username;
string password;
List<string> securityOut = new List<string>();
List<string> securityError = new List<string>();
object outputLock = new object();
using (var p = HostContext.CreateService<IProcessInvoker>())
{
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
securityOut.Add(stdout.Data);
}
}
};
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
securityError.Add(stderr.Data);
}
}
};
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
fileName: _securityUtil,
arguments: $"find-generic-password -s {target} -a GITHUBACTIONSRUNNER -w -g \"{_runnerCredStoreKeyChain}\"",
environment: null,
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
if (exitCode == 0)
{
string keyChainSecret = securityOut.First();
string[] secrets = keyChainSecret.Split(':');
if (secrets.Length == 2 && !string.IsNullOrEmpty(secrets[0]) && !string.IsNullOrEmpty(secrets[1]))
{
Trace.Info($"Successfully find-generic-password for {target} (GITHUBACTIONSRUNNER)");
username = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[0]));
password = Encoding.UTF8.GetString(Convert.FromBase64String(secrets[1]));
return new NetworkCredential(username, password);
}
else
{
throw new ArgumentOutOfRangeException(nameof(keyChainSecret));
}
}
else
{
if (securityOut.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityOut));
}
if (securityError.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityError));
}
throw new InvalidOperationException($"'security find-generic-password' failed with exit code {exitCode}.");
}
}
}
finally
{
LockKeyChain();
}
}
public void Delete(string target)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
try
{
UnlockKeyChain();
List<string> securityOut = new List<string>();
List<string> securityError = new List<string>();
object outputLock = new object();
using (var p = HostContext.CreateService<IProcessInvoker>())
{
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
securityOut.Add(stdout.Data);
}
}
};
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
securityError.Add(stderr.Data);
}
}
};
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
fileName: _securityUtil,
arguments: $"delete-generic-password -s {target} -a GITHUBACTIONSRUNNER \"{_runnerCredStoreKeyChain}\"",
environment: null,
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
if (exitCode == 0)
{
Trace.Info($"Successfully delete-generic-password for {target} (GITHUBACTIONSRUNNER)");
}
else
{
if (securityOut.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityOut));
}
if (securityError.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityError));
}
throw new InvalidOperationException($"'security delete-generic-password' failed with exit code {exitCode}.");
}
}
}
finally
{
LockKeyChain();
}
}
private void UnlockKeyChain()
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil));
ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain));
List<string> securityOut = new List<string>();
List<string> securityError = new List<string>();
object outputLock = new object();
using (var p = HostContext.CreateService<IProcessInvoker>())
{
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
securityOut.Add(stdout.Data);
}
}
};
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
securityError.Add(stderr.Data);
}
}
};
// make sure the 'security' has access to the key so we won't get prompt at runtime.
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
fileName: _securityUtil,
arguments: $"unlock-keychain -p {_osxRunnerCredStoreKeyChainPassword} \"{_runnerCredStoreKeyChain}\"",
environment: null,
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
if (exitCode == 0)
{
Trace.Info($"Successfully unlock-keychain for {_runnerCredStoreKeyChain}");
}
else
{
if (securityOut.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityOut));
}
if (securityError.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityError));
}
throw new InvalidOperationException($"'security unlock-keychain' failed with exit code {exitCode}.");
}
}
}
private void LockKeyChain()
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(_securityUtil, nameof(_securityUtil));
ArgUtil.NotNullOrEmpty(_runnerCredStoreKeyChain, nameof(_runnerCredStoreKeyChain));
List<string> securityOut = new List<string>();
List<string> securityError = new List<string>();
object outputLock = new object();
using (var p = HostContext.CreateService<IProcessInvoker>())
{
p.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stdout)
{
if (!string.IsNullOrEmpty(stdout.Data))
{
lock (outputLock)
{
securityOut.Add(stdout.Data);
}
}
};
p.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs stderr)
{
if (!string.IsNullOrEmpty(stderr.Data))
{
lock (outputLock)
{
securityError.Add(stderr.Data);
}
}
};
// make sure the 'security' has access to the key so we won't get prompt at runtime.
int exitCode = p.ExecuteAsync(workingDirectory: HostContext.GetDirectory(WellKnownDirectory.Root),
fileName: _securityUtil,
arguments: $"lock-keychain \"{_runnerCredStoreKeyChain}\"",
environment: null,
cancellationToken: CancellationToken.None).GetAwaiter().GetResult();
if (exitCode == 0)
{
Trace.Info($"Successfully lock-keychain for {_runnerCredStoreKeyChain}");
}
else
{
if (securityOut.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityOut));
}
if (securityError.Count > 0)
{
Trace.Error(string.Join(Environment.NewLine, securityError));
}
throw new InvalidOperationException($"'security lock-keychain' failed with exit code {exitCode}.");
}
}
}
}
#else
public sealed class LinuxRunnerCredentialStore : RunnerService, IRunnerCredentialStore
{
// 'ghrunner' 128 bits iv
private readonly byte[] iv = new byte[] { 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72, 0x67, 0x68, 0x72, 0x75, 0x6e, 0x6e, 0x65, 0x72 };
// 256 bits key
private byte[] _symmetricKey;
private string _credStoreFile;
private Dictionary<string, Credential> _credStore;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_credStoreFile = hostContext.GetConfigFile(WellKnownConfigFile.CredentialStore);
if (File.Exists(_credStoreFile))
{
_credStore = IOUtil.LoadObject<Dictionary<string, Credential>>(_credStoreFile);
}
else
{
_credStore = new Dictionary<string, Credential>(StringComparer.OrdinalIgnoreCase);
}
string machineId;
if (File.Exists("/etc/machine-id"))
{
// try use machine-id as encryption key
// this helps avoid accidental information disclosure, but isn't intended for true security
machineId = File.ReadAllLines("/etc/machine-id").FirstOrDefault();
Trace.Info($"machine-id length {machineId?.Length ?? 0}.");
// machine-id doesn't exist or machine-id is not 256 bits
if (string.IsNullOrEmpty(machineId) || machineId.Length != 32)
{
Trace.Warning("Can not get valid machine id from '/etc/machine-id'.");
machineId = "43e7fe5da07740cf914b90f1dac51c2a";
}
}
else
{
// /etc/machine-id not exist
Trace.Warning("/etc/machine-id doesn't exist.");
machineId = "43e7fe5da07740cf914b90f1dac51c2a";
}
List<byte> keyBuilder = new List<byte>();
foreach (var c in machineId)
{
keyBuilder.Add(Convert.ToByte(c));
}
_symmetricKey = keyBuilder.ToArray();
}
public NetworkCredential Write(string target, string username, string password)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
ArgUtil.NotNullOrEmpty(username, nameof(username));
ArgUtil.NotNullOrEmpty(password, nameof(password));
Trace.Info($"Store credential for '{target}' to cred store.");
Credential cred = new Credential(username, Encrypt(password));
_credStore[target] = cred;
SyncCredentialStoreFile();
return new NetworkCredential(username, password);
}
public NetworkCredential Read(string target)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
Trace.Info($"Read credential for '{target}' from cred store.");
if (_credStore.ContainsKey(target))
{
Credential cred = _credStore[target];
if (!string.IsNullOrEmpty(cred.UserName) && !string.IsNullOrEmpty(cred.Password))
{
Trace.Info($"Return credential for '{target}' from cred store.");
return new NetworkCredential(cred.UserName, Decrypt(cred.Password));
}
}
throw new KeyNotFoundException(target);
}
public void Delete(string target)
{
Trace.Entering();
ArgUtil.NotNullOrEmpty(target, nameof(target));
if (_credStore.ContainsKey(target))
{
Trace.Info($"Delete credential for '{target}' from cred store.");
_credStore.Remove(target);
SyncCredentialStoreFile();
}
else
{
throw new KeyNotFoundException(target);
}
}
private void SyncCredentialStoreFile()
{
Trace.Entering();
Trace.Info("Sync in-memory credential store with credential store file.");
// delete cred store file when all creds gone
if (_credStore.Count == 0)
{
IOUtil.DeleteFile(_credStoreFile);
return;
}
if (!File.Exists(_credStoreFile))
{
CreateCredentialStoreFile();
}
IOUtil.SaveObject(_credStore, _credStoreFile);
}
private string Encrypt(string secret)
{
using (Aes aes = Aes.Create())
{
aes.Key = _symmetricKey;
aes.IV = iv;
// Create a decrytor to perform the stream transform.
ICryptoTransform encryptor = aes.CreateEncryptor();
// Create the streams used for encryption.
using (MemoryStream msEncrypt = new MemoryStream())
{
using (CryptoStream csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write))
{
using (StreamWriter swEncrypt = new StreamWriter(csEncrypt))
{
swEncrypt.Write(secret);
}
return Convert.ToBase64String(msEncrypt.ToArray());
}
}
}
}
private string Decrypt(string encryptedText)
{
using (Aes aes = Aes.Create())
{
aes.Key = _symmetricKey;
aes.IV = iv;
// Create a decrytor to perform the stream transform.
ICryptoTransform decryptor = aes.CreateDecryptor();
// Create the streams used for decryption.
using (MemoryStream msDecrypt = new MemoryStream(Convert.FromBase64String(encryptedText)))
{
using (CryptoStream csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read))
{
using (StreamReader srDecrypt = new StreamReader(csDecrypt))
{
// Read the decrypted bytes from the decrypting stream and place them in a string.
return srDecrypt.ReadToEnd();
}
}
}
}
}
private void CreateCredentialStoreFile()
{
File.WriteAllText(_credStoreFile, "");
File.SetAttributes(_credStoreFile, File.GetAttributes(_credStoreFile) | FileAttributes.Hidden);
// Try to lock down the .credentials_store file to the owner/group
var chmodPath = WhichUtil.Which("chmod", trace: Trace);
if (!String.IsNullOrEmpty(chmodPath))
{
var arguments = $"600 {new FileInfo(_credStoreFile).FullName}";
using (var invoker = HostContext.CreateService<IProcessInvoker>())
{
var exitCode = invoker.ExecuteAsync(HostContext.GetDirectory(WellKnownDirectory.Root), chmodPath, arguments, null, default(CancellationToken)).GetAwaiter().GetResult();
if (exitCode == 0)
{
Trace.Info("Successfully set permissions for credentials store file {0}", _credStoreFile);
}
else
{
Trace.Warning("Unable to successfully set permissions for credentials store file {0}. Received exit code {1} from {2}", _credStoreFile, exitCode, chmodPath);
}
}
}
else
{
Trace.Warning("Unable to locate chmod to set permissions for credentials store file {0}.", _credStoreFile);
}
}
}
[DataContract]
internal class Credential
{
public Credential()
{ }
public Credential(string userName, string password)
{
UserName = userName;
Password = password;
}
[DataMember(IsRequired = true)]
public string UserName { get; set; }
[DataMember(IsRequired = true)]
public string Password { get; set; }
}
#endif
}

View File

@@ -0,0 +1,355 @@
using GitHub.DistributedTask.WebApi;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common.Util;
using GitHub.Services.WebApi;
using GitHub.Services.Common;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
public enum RunnerConnectionType
{
Generic,
MessageQueue,
JobRequest
}
[ServiceLocator(Default = typeof(RunnerServer))]
public interface IRunnerServer : IRunnerService
{
Task ConnectAsync(Uri serverUrl, VssCredentials credentials);
Task RefreshConnectionAsync(RunnerConnectionType connectionType, TimeSpan timeout);
void SetConnectionTimeout(RunnerConnectionType connectionType, TimeSpan timeout);
// Configuration
Task<TaskAgent> AddAgentAsync(Int32 agentPoolId, TaskAgent agent);
Task DeleteAgentAsync(int agentPoolId, int agentId);
Task<List<TaskAgentPool>> GetAgentPoolsAsync(string agentPoolName = null, TaskAgentPoolType poolType = TaskAgentPoolType.Automation);
Task<List<TaskAgent>> GetAgentsAsync(int agentPoolId, string agentName = null);
Task<TaskAgent> UpdateAgentAsync(int agentPoolId, TaskAgent agent);
// messagequeue
Task<TaskAgentSession> CreateAgentSessionAsync(Int32 poolId, TaskAgentSession session, CancellationToken cancellationToken);
Task DeleteAgentMessageAsync(Int32 poolId, Int64 messageId, Guid sessionId, CancellationToken cancellationToken);
Task DeleteAgentSessionAsync(Int32 poolId, Guid sessionId, CancellationToken cancellationToken);
Task<TaskAgentMessage> GetAgentMessageAsync(Int32 poolId, Guid sessionId, Int64? lastMessageId, CancellationToken cancellationToken);
// job request
Task<TaskAgentJobRequest> GetAgentRequestAsync(int poolId, long requestId, CancellationToken cancellationToken);
Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, CancellationToken cancellationToken);
Task<TaskAgentJobRequest> FinishAgentRequestAsync(int poolId, long requestId, Guid lockToken, DateTime finishTime, TaskResult result, CancellationToken cancellationToken);
// agent package
Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, CancellationToken cancellationToken);
Task<PackageMetadata> GetPackageAsync(string packageType, string platform, string version, CancellationToken cancellationToken);
// agent update
Task<TaskAgent> UpdateAgentUpdateStateAsync(int agentPoolId, int agentId, string currentState);
}
public sealed class RunnerServer : RunnerService, IRunnerServer
{
private bool _hasGenericConnection;
private bool _hasMessageConnection;
private bool _hasRequestConnection;
private VssConnection _genericConnection;
private VssConnection _messageConnection;
private VssConnection _requestConnection;
private TaskAgentHttpClient _genericTaskAgentClient;
private TaskAgentHttpClient _messageTaskAgentClient;
private TaskAgentHttpClient _requestTaskAgentClient;
public async Task ConnectAsync(Uri serverUrl, VssCredentials credentials)
{
if (HostContext.RunMode == RunMode.Local)
{
return;
}
var createGenericConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(100));
var createMessageConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60));
var createRequestConnection = EstablishVssConnection(serverUrl, credentials, TimeSpan.FromSeconds(60));
await Task.WhenAll(createGenericConnection, createMessageConnection, createRequestConnection);
_genericConnection = await createGenericConnection;
_messageConnection = await createMessageConnection;
_requestConnection = await createRequestConnection;
_genericTaskAgentClient = _genericConnection.GetClient<TaskAgentHttpClient>();
_messageTaskAgentClient = _messageConnection.GetClient<TaskAgentHttpClient>();
_requestTaskAgentClient = _requestConnection.GetClient<TaskAgentHttpClient>();
_hasGenericConnection = true;
_hasMessageConnection = true;
_hasRequestConnection = true;
}
// Refresh connection is best effort. it should never throw exception
public async Task RefreshConnectionAsync(RunnerConnectionType connectionType, TimeSpan timeout)
{
Trace.Info($"Refresh {connectionType} VssConnection to get on a different AFD node.");
VssConnection newConnection = null;
switch (connectionType)
{
case RunnerConnectionType.MessageQueue:
try
{
_hasMessageConnection = false;
newConnection = await EstablishVssConnection(_messageConnection.Uri, _messageConnection.Credentials, timeout);
var client = newConnection.GetClient<TaskAgentHttpClient>();
_messageConnection = newConnection;
_messageTaskAgentClient = client;
}
catch (Exception ex)
{
Trace.Error($"Catch exception during reset {connectionType} connection.");
Trace.Error(ex);
newConnection?.Dispose();
}
finally
{
_hasMessageConnection = true;
}
break;
case RunnerConnectionType.JobRequest:
try
{
_hasRequestConnection = false;
newConnection = await EstablishVssConnection(_requestConnection.Uri, _requestConnection.Credentials, timeout);
var client = newConnection.GetClient<TaskAgentHttpClient>();
_requestConnection = newConnection;
_requestTaskAgentClient = client;
}
catch (Exception ex)
{
Trace.Error($"Catch exception during reset {connectionType} connection.");
Trace.Error(ex);
newConnection?.Dispose();
}
finally
{
_hasRequestConnection = true;
}
break;
case RunnerConnectionType.Generic:
try
{
_hasGenericConnection = false;
newConnection = await EstablishVssConnection(_genericConnection.Uri, _genericConnection.Credentials, timeout);
var client = newConnection.GetClient<TaskAgentHttpClient>();
_genericConnection = newConnection;
_genericTaskAgentClient = client;
}
catch (Exception ex)
{
Trace.Error($"Catch exception during reset {connectionType} connection.");
Trace.Error(ex);
newConnection?.Dispose();
}
finally
{
_hasGenericConnection = true;
}
break;
default:
Trace.Error($"Unexpected connection type: {connectionType}.");
break;
}
}
public void SetConnectionTimeout(RunnerConnectionType connectionType, TimeSpan timeout)
{
Trace.Info($"Set {connectionType} VssConnection's timeout to {timeout.TotalSeconds} seconds.");
switch (connectionType)
{
case RunnerConnectionType.JobRequest:
_requestConnection.Settings.SendTimeout = timeout;
break;
case RunnerConnectionType.MessageQueue:
_messageConnection.Settings.SendTimeout = timeout;
break;
case RunnerConnectionType.Generic:
_genericConnection.Settings.SendTimeout = timeout;
break;
default:
Trace.Error($"Unexpected connection type: {connectionType}.");
break;
}
}
private async Task<VssConnection> EstablishVssConnection(Uri serverUrl, VssCredentials credentials, TimeSpan timeout)
{
Trace.Info($"Establish connection with {timeout.TotalSeconds} seconds timeout.");
int attemptCount = 5;
while (attemptCount-- > 0)
{
var connection = VssUtil.CreateConnection(serverUrl, credentials, timeout: timeout);
try
{
await connection.ConnectAsync();
return connection;
}
catch (Exception ex) when (attemptCount > 0)
{
Trace.Info($"Catch exception during connect. {attemptCount} attempt left.");
Trace.Error(ex);
await HostContext.Delay(TimeSpan.FromMilliseconds(100), CancellationToken.None);
}
}
// should never reach here.
throw new InvalidOperationException(nameof(EstablishVssConnection));
}
private void CheckConnection(RunnerConnectionType connectionType)
{
switch (connectionType)
{
case RunnerConnectionType.Generic:
if (!_hasGenericConnection)
{
throw new InvalidOperationException($"SetConnection {RunnerConnectionType.Generic}");
}
break;
case RunnerConnectionType.JobRequest:
if (!_hasRequestConnection)
{
throw new InvalidOperationException($"SetConnection {RunnerConnectionType.JobRequest}");
}
break;
case RunnerConnectionType.MessageQueue:
if (!_hasMessageConnection)
{
throw new InvalidOperationException($"SetConnection {RunnerConnectionType.MessageQueue}");
}
break;
default:
throw new NotSupportedException(connectionType.ToString());
}
}
//-----------------------------------------------------------------
// Configuration
//-----------------------------------------------------------------
public Task<List<TaskAgentPool>> GetAgentPoolsAsync(string agentPoolName = null, TaskAgentPoolType poolType = TaskAgentPoolType.Automation)
{
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.GetAgentPoolsAsync(agentPoolName, poolType: poolType);
}
public Task<TaskAgent> AddAgentAsync(Int32 agentPoolId, TaskAgent agent)
{
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.AddAgentAsync(agentPoolId, agent);
}
public Task<List<TaskAgent>> GetAgentsAsync(int agentPoolId, string agentName = null)
{
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.GetAgentsAsync(agentPoolId, agentName, false);
}
public Task<TaskAgent> UpdateAgentAsync(int agentPoolId, TaskAgent agent)
{
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.ReplaceAgentAsync(agentPoolId, agent);
}
public Task DeleteAgentAsync(int agentPoolId, int agentId)
{
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.DeleteAgentAsync(agentPoolId, agentId);
}
//-----------------------------------------------------------------
// MessageQueue
//-----------------------------------------------------------------
public Task<TaskAgentSession> CreateAgentSessionAsync(Int32 poolId, TaskAgentSession session, CancellationToken cancellationToken)
{
CheckConnection(RunnerConnectionType.MessageQueue);
return _messageTaskAgentClient.CreateAgentSessionAsync(poolId, session, cancellationToken: cancellationToken);
}
public Task DeleteAgentMessageAsync(Int32 poolId, Int64 messageId, Guid sessionId, CancellationToken cancellationToken)
{
CheckConnection(RunnerConnectionType.MessageQueue);
return _messageTaskAgentClient.DeleteMessageAsync(poolId, messageId, sessionId, cancellationToken: cancellationToken);
}
public Task DeleteAgentSessionAsync(Int32 poolId, Guid sessionId, CancellationToken cancellationToken)
{
CheckConnection(RunnerConnectionType.MessageQueue);
return _messageTaskAgentClient.DeleteAgentSessionAsync(poolId, sessionId, cancellationToken: cancellationToken);
}
public Task<TaskAgentMessage> GetAgentMessageAsync(Int32 poolId, Guid sessionId, Int64? lastMessageId, CancellationToken cancellationToken)
{
CheckConnection(RunnerConnectionType.MessageQueue);
return _messageTaskAgentClient.GetMessageAsync(poolId, sessionId, lastMessageId, cancellationToken: cancellationToken);
}
//-----------------------------------------------------------------
// JobRequest
//-----------------------------------------------------------------
public Task<TaskAgentJobRequest> RenewAgentRequestAsync(int poolId, long requestId, Guid lockToken, CancellationToken cancellationToken = default(CancellationToken))
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult(JsonUtility.FromString<TaskAgentJobRequest>("{ lockedUntil: \"" + DateTime.Now.Add(TimeSpan.FromMinutes(5)).ToString("u") + "\" }"));
}
CheckConnection(RunnerConnectionType.JobRequest);
return _requestTaskAgentClient.RenewAgentRequestAsync(poolId, requestId, lockToken, cancellationToken: cancellationToken);
}
public Task<TaskAgentJobRequest> FinishAgentRequestAsync(int poolId, long requestId, Guid lockToken, DateTime finishTime, TaskResult result, CancellationToken cancellationToken = default(CancellationToken))
{
if (HostContext.RunMode == RunMode.Local)
{
return Task.FromResult<TaskAgentJobRequest>(null);
}
CheckConnection(RunnerConnectionType.JobRequest);
return _requestTaskAgentClient.FinishAgentRequestAsync(poolId, requestId, lockToken, finishTime, result, cancellationToken: cancellationToken);
}
public Task<TaskAgentJobRequest> GetAgentRequestAsync(int poolId, long requestId, CancellationToken cancellationToken = default(CancellationToken))
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
CheckConnection(RunnerConnectionType.JobRequest);
return _requestTaskAgentClient.GetAgentRequestAsync(poolId, requestId, cancellationToken: cancellationToken);
}
//-----------------------------------------------------------------
// Agent Package
//-----------------------------------------------------------------
public Task<List<PackageMetadata>> GetPackagesAsync(string packageType, string platform, int top, CancellationToken cancellationToken)
{
ArgUtil.Equal(RunMode.Normal, HostContext.RunMode, nameof(HostContext.RunMode));
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.GetPackagesAsync(packageType, platform, top, cancellationToken: cancellationToken);
}
public Task<PackageMetadata> GetPackageAsync(string packageType, string platform, string version, CancellationToken cancellationToken)
{
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.GetPackageAsync(packageType, platform, version, cancellationToken: cancellationToken);
}
public Task<TaskAgent> UpdateAgentUpdateStateAsync(int agentPoolId, int agentId, string currentState)
{
CheckConnection(RunnerConnectionType.Generic);
return _genericTaskAgentClient.UpdateAgentUpdateStateAsync(agentPoolId, agentId, currentState);
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
namespace GitHub.Runner.Common
{
[AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = false)]
public sealed class ServiceLocatorAttribute : Attribute
{
public static readonly string DefaultPropertyName = "Default";
public Type Default { get; set; }
}
public interface IRunnerService
{
void Initialize(IHostContext context);
}
public abstract class RunnerService
{
protected IHostContext HostContext { get; private set; }
protected Tracing Trace { get; private set; }
public string TraceName
{
get
{
return GetType().Name;
}
}
public virtual void Initialize(IHostContext hostContext)
{
HostContext = hostContext;
Trace = HostContext.GetTrace(TraceName);
Trace.Entering();
}
}
}

View File

@@ -0,0 +1,196 @@
using GitHub.Runner.Common.Util;
using System;
using System.Linq;
using System.Net;
using System.IO;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
[ServiceLocator(Default = typeof(RunnerWebProxy))]
public interface IRunnerWebProxy : IRunnerService
{
string ProxyAddress { get; }
string ProxyUsername { get; }
string ProxyPassword { get; }
List<string> ProxyBypassList { get; }
IWebProxy WebProxy { get; }
}
public class RunnerWebProxy : RunnerService, IRunnerWebProxy
{
private readonly List<Regex> _regExBypassList = new List<Regex>();
private readonly List<string> _bypassList = new List<string>();
private RunnerWebProxyCore _runnerWebProxy = new RunnerWebProxyCore();
public string ProxyAddress { get; private set; }
public string ProxyUsername { get; private set; }
public string ProxyPassword { get; private set; }
public List<string> ProxyBypassList => _bypassList;
public IWebProxy WebProxy => _runnerWebProxy;
public override void Initialize(IHostContext context)
{
base.Initialize(context);
LoadProxySetting();
}
// This should only be called from config
public void SetupProxy(string proxyAddress, string proxyUsername, string proxyPassword)
{
ArgUtil.NotNullOrEmpty(proxyAddress, nameof(proxyAddress));
Trace.Info($"Update proxy setting from '{ProxyAddress ?? string.Empty}' to'{proxyAddress}'");
ProxyAddress = proxyAddress;
ProxyUsername = proxyUsername;
ProxyPassword = proxyPassword;
if (string.IsNullOrEmpty(ProxyUsername) || string.IsNullOrEmpty(ProxyPassword))
{
Trace.Info($"Config proxy use DefaultNetworkCredentials.");
}
else
{
Trace.Info($"Config authentication proxy as: {ProxyUsername}.");
}
_runnerWebProxy.Update(ProxyAddress, ProxyUsername, ProxyPassword, ProxyBypassList);
}
// This should only be called from config
public void SaveProxySetting()
{
if (!string.IsNullOrEmpty(ProxyAddress))
{
string proxyConfigFile = HostContext.GetConfigFile(WellKnownConfigFile.Proxy);
IOUtil.DeleteFile(proxyConfigFile);
Trace.Info($"Store proxy configuration to '{proxyConfigFile}' for proxy '{ProxyAddress}'");
File.WriteAllText(proxyConfigFile, ProxyAddress);
File.SetAttributes(proxyConfigFile, File.GetAttributes(proxyConfigFile) | FileAttributes.Hidden);
string proxyCredFile = HostContext.GetConfigFile(WellKnownConfigFile.ProxyCredentials);
IOUtil.DeleteFile(proxyCredFile);
if (!string.IsNullOrEmpty(ProxyUsername) && !string.IsNullOrEmpty(ProxyPassword))
{
string lookupKey = Guid.NewGuid().ToString("D").ToUpperInvariant();
Trace.Info($"Store proxy credential lookup key '{lookupKey}' to '{proxyCredFile}'");
File.WriteAllText(proxyCredFile, lookupKey);
File.SetAttributes(proxyCredFile, File.GetAttributes(proxyCredFile) | FileAttributes.Hidden);
var credStore = HostContext.GetService<IRunnerCredentialStore>();
credStore.Write($"GITHUB_ACTIONS_RUNNER_PROXY_{lookupKey}", ProxyUsername, ProxyPassword);
}
}
else
{
Trace.Info("No proxy configuration exist.");
}
}
// This should only be called from unconfig
public void DeleteProxySetting()
{
string proxyCredFile = HostContext.GetConfigFile(WellKnownConfigFile.ProxyCredentials);
if (File.Exists(proxyCredFile))
{
Trace.Info("Delete proxy credential from credential store.");
string lookupKey = File.ReadAllLines(proxyCredFile).FirstOrDefault();
if (!string.IsNullOrEmpty(lookupKey))
{
var credStore = HostContext.GetService<IRunnerCredentialStore>();
credStore.Delete($"GITHUB_ACTIONS_RUNNER_PROXY_{lookupKey}");
}
Trace.Info($"Delete .proxycredentials file: {proxyCredFile}");
IOUtil.DeleteFile(proxyCredFile);
}
string proxyBypassFile = HostContext.GetConfigFile(WellKnownConfigFile.ProxyBypass);
if (File.Exists(proxyBypassFile))
{
Trace.Info($"Delete .proxybypass file: {proxyBypassFile}");
IOUtil.DeleteFile(proxyBypassFile);
}
string proxyConfigFile = HostContext.GetConfigFile(WellKnownConfigFile.Proxy);
Trace.Info($"Delete .proxy file: {proxyConfigFile}");
IOUtil.DeleteFile(proxyConfigFile);
}
private void LoadProxySetting()
{
string proxyConfigFile = HostContext.GetConfigFile(WellKnownConfigFile.Proxy);
if (File.Exists(proxyConfigFile))
{
// we expect the first line of the file is the proxy url
Trace.Verbose($"Try read proxy setting from file: {proxyConfigFile}.");
ProxyAddress = File.ReadLines(proxyConfigFile).FirstOrDefault() ?? string.Empty;
ProxyAddress = ProxyAddress.Trim();
Trace.Verbose($"{ProxyAddress}");
}
if (!string.IsNullOrEmpty(ProxyAddress) && !Uri.IsWellFormedUriString(ProxyAddress, UriKind.Absolute))
{
Trace.Info($"The proxy url is not a well formed absolute uri string: {ProxyAddress}.");
ProxyAddress = string.Empty;
}
if (!string.IsNullOrEmpty(ProxyAddress))
{
Trace.Info($"Config proxy at: {ProxyAddress}.");
string proxyCredFile = HostContext.GetConfigFile(WellKnownConfigFile.ProxyCredentials);
if (File.Exists(proxyCredFile))
{
string lookupKey = File.ReadAllLines(proxyCredFile).FirstOrDefault();
if (!string.IsNullOrEmpty(lookupKey))
{
var credStore = HostContext.GetService<IRunnerCredentialStore>();
var proxyCred = credStore.Read($"GITHUB_ACTIONS_RUNNER_PROXY_{lookupKey}");
ProxyUsername = proxyCred.UserName;
ProxyPassword = proxyCred.Password;
}
}
if (!string.IsNullOrEmpty(ProxyPassword))
{
HostContext.SecretMasker.AddValue(ProxyPassword);
}
if (string.IsNullOrEmpty(ProxyUsername) || string.IsNullOrEmpty(ProxyPassword))
{
Trace.Info($"Config proxy use DefaultNetworkCredentials.");
}
else
{
Trace.Info($"Config authentication proxy as: {ProxyUsername}.");
}
string proxyBypassFile = HostContext.GetConfigFile(WellKnownConfigFile.ProxyBypass);
if (File.Exists(proxyBypassFile))
{
Trace.Verbose($"Try read proxy bypass list from file: {proxyBypassFile}.");
foreach (string bypass in File.ReadAllLines(proxyBypassFile))
{
if (string.IsNullOrWhiteSpace(bypass))
{
continue;
}
else
{
Trace.Info($"Bypass proxy for: {bypass}.");
ProxyBypassList.Add(bypass.Trim());
}
}
}
_runnerWebProxy.Update(ProxyAddress, ProxyUsername, ProxyPassword, ProxyBypassList);
}
else
{
Trace.Info($"No proxy setting found.");
}
}
}
}

View File

@@ -0,0 +1,96 @@
// Defines the data protocol for reading and writing strings on our stream
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace GitHub.Runner.Common
{
public class StreamString
{
private Stream _ioStream;
private UnicodeEncoding streamEncoding;
public StreamString(Stream ioStream)
{
_ioStream = ioStream;
streamEncoding = new UnicodeEncoding();
}
public async Task<Int32> ReadInt32Async(CancellationToken cancellationToken)
{
byte[] readBytes = new byte[sizeof(Int32)];
int dataread = 0;
while (sizeof(Int32) - dataread > 0 && (!cancellationToken.IsCancellationRequested))
{
Task<int> op = _ioStream.ReadAsync(readBytes, dataread, sizeof(Int32) - dataread, cancellationToken);
int newData = 0;
newData = await op.WithCancellation(cancellationToken);
dataread += newData;
if (0 == newData)
{
await Task.Delay(100, cancellationToken);
}
}
cancellationToken.ThrowIfCancellationRequested();
return BitConverter.ToInt32(readBytes, 0);
}
public async Task WriteInt32Async(Int32 value, CancellationToken cancellationToken)
{
byte[] int32Bytes = BitConverter.GetBytes(value);
Task op = _ioStream.WriteAsync(int32Bytes, 0, sizeof(Int32), cancellationToken);
await op.WithCancellation(cancellationToken);
}
const int MaxStringSize = 50 * 1000000;
public async Task<string> ReadStringAsync(CancellationToken cancellationToken)
{
Int32 len = await ReadInt32Async(cancellationToken);
if (len == 0)
{
return string.Empty;
}
if (len < 0 || len > MaxStringSize)
{
throw new InvalidDataException();
}
byte[] inBuffer = new byte[len];
int dataread = 0;
while (len - dataread > 0 && (!cancellationToken.IsCancellationRequested))
{
Task<int> op = _ioStream.ReadAsync(inBuffer, dataread, len - dataread, cancellationToken);
int newData = 0;
newData = await op.WithCancellation(cancellationToken);
dataread += newData;
if (0 == newData)
{
await Task.Delay(100, cancellationToken);
}
}
return streamEncoding.GetString(inBuffer);
}
public async Task WriteStringAsync(string outString, CancellationToken cancellationToken)
{
byte[] outBuffer = streamEncoding.GetBytes(outString);
Int32 len = outBuffer.Length;
if (len > MaxStringSize)
{
throw new ArgumentOutOfRangeException();
}
await WriteInt32Async(len, cancellationToken);
cancellationToken.ThrowIfCancellationRequested();
Task op = _ioStream.WriteAsync(outBuffer, 0, len, cancellationToken);
await op.WithCancellation(cancellationToken);
op = _ioStream.FlushAsync(cancellationToken);
await op.WithCancellation(cancellationToken);
}
}
}

View File

@@ -0,0 +1,198 @@
using System;
using System.Collections.Generic;
namespace GitHub.Runner.Common
{
//
// Abstracts away interactions with the terminal which allows:
// (1) Console writes also go to trace for better context in the trace
// (2) Reroute in tests
//
[ServiceLocator(Default = typeof(Terminal))]
public interface ITerminal : IRunnerService, IDisposable
{
event EventHandler CancelKeyPress;
bool Silent { get; set; }
string ReadLine();
string ReadSecret();
void Write(string message, ConsoleColor? colorCode = null);
void WriteLine();
void WriteLine(string line, ConsoleColor? colorCode = null);
void WriteError(Exception ex);
void WriteError(string line);
void WriteSection(string message);
void WriteSuccessMessage(string message);
}
public sealed class Terminal : RunnerService, ITerminal
{
public bool Silent { get; set; }
public event EventHandler CancelKeyPress;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
Console.CancelKeyPress += Console_CancelKeyPress;
}
private void Console_CancelKeyPress(object sender, ConsoleCancelEventArgs e)
{
e.Cancel = true;
CancelKeyPress?.Invoke(this, e);
}
public string ReadLine()
{
// Read and trace the value.
Trace.Info("READ LINE");
string value = Console.ReadLine();
Trace.Info($"Read value: '{value}'");
return value;
}
// TODO: Consider using SecureString.
public string ReadSecret()
{
Trace.Info("READ SECRET");
var chars = new List<char>();
while (true)
{
ConsoleKeyInfo key = Console.ReadKey(intercept: true);
if (key.Key == ConsoleKey.Enter)
{
Console.WriteLine();
break;
}
else if (key.Key == ConsoleKey.Backspace)
{
if (chars.Count > 0)
{
chars.RemoveAt(chars.Count - 1);
Console.Write("\b \b");
}
}
else if (key.KeyChar > 0)
{
chars.Add(key.KeyChar);
Console.Write("*");
}
}
// Trace whether a value was entered.
string val = new String(chars.ToArray());
if (!string.IsNullOrEmpty(val))
{
HostContext.SecretMasker.AddValue(val);
}
Trace.Info($"Read value: '{val}'");
return val;
}
public void Write(string message, ConsoleColor? colorCode = null)
{
Trace.Info($"WRITE: {message}");
if (!Silent)
{
if(colorCode != null)
{
Console.ForegroundColor = colorCode.Value;
Console.Write(message);
Console.ResetColor();
}
else {
Console.Write(message);
}
}
}
public void WriteLine()
{
WriteLine(string.Empty);
}
// Do not add a format string overload. Terminal messages are user facing and therefore
// should be localized. Use the Loc method in the StringUtil class.
public void WriteLine(string line, ConsoleColor? colorCode = null)
{
Trace.Info($"WRITE LINE: {line}");
if (!Silent)
{
if(colorCode != null)
{
Console.ForegroundColor = colorCode.Value;
Console.WriteLine(line);
Console.ResetColor();
}
else {
Console.WriteLine(line);
}
}
}
public void WriteError(Exception ex)
{
Trace.Error("WRITE ERROR (exception):");
Trace.Error(ex);
if (!Silent)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(ex.Message);
Console.ResetColor();
}
}
// Do not add a format string overload. Terminal messages are user facing and therefore
// should be localized. Use the Loc method in the StringUtil class.
public void WriteError(string line)
{
Trace.Error($"WRITE ERROR: {line}");
if (!Silent)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.Error.WriteLine(line);
Console.ResetColor();
}
}
public void WriteSection(string message)
{
if (!Silent)
{
Console.WriteLine();
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine($"# {message}");
Console.ResetColor();
Console.WriteLine();
}
}
public void WriteSuccessMessage(string message)
{
if (!Silent)
{
Console.ForegroundColor = ConsoleColor.Green;
Console.Write("√ ");
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine(message);
Console.ResetColor();
}
}
private void Dispose(bool disposing)
{
if (disposing)
{
Console.CancelKeyPress -= Console_CancelKeyPress;
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,65 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Services.Common.Internal;
namespace GitHub.Runner.Common
{
public class ThrottlingEventArgs : EventArgs
{
public ThrottlingEventArgs(TimeSpan delay, DateTime expiration)
{
Delay = delay;
Expiration = expiration;
}
public TimeSpan Delay { get; private set; }
public DateTime Expiration { get; private set; }
}
public interface IThrottlingReporter
{
void ReportThrottling(TimeSpan delay, DateTime expiration);
}
public class ThrottlingReportHandler : DelegatingHandler
{
private IThrottlingReporter _throttlingReporter;
public ThrottlingReportHandler(IThrottlingReporter throttlingReporter)
: base()
{
ArgUtil.NotNull(throttlingReporter, nameof(throttlingReporter));
_throttlingReporter = throttlingReporter;
}
protected async override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
// Call the inner handler.
var response = await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
// Inspect whether response has throttling information
IEnumerable<string> vssRequestDelayed = null;
IEnumerable<string> vssRequestQuotaReset = null;
if (response.Headers.TryGetValues(HttpHeaders.VssRateLimitDelay, out vssRequestDelayed) &&
response.Headers.TryGetValues(HttpHeaders.VssRateLimitReset, out vssRequestQuotaReset) &&
!string.IsNullOrEmpty(vssRequestDelayed.FirstOrDefault()) &&
!string.IsNullOrEmpty(vssRequestQuotaReset.FirstOrDefault()))
{
TimeSpan delay = TimeSpan.FromSeconds(double.Parse(vssRequestDelayed.First()));
int expirationEpoch = int.Parse(vssRequestQuotaReset.First());
DateTime expiration = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).AddSeconds(expirationEpoch);
_throttlingReporter.ReportThrottling(delay, expiration);
}
return response;
}
}
}

View File

@@ -0,0 +1,88 @@
using GitHub.Runner.Common.Util;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using GitHub.DistributedTask.Logging;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
public interface ITraceManager : IDisposable
{
SourceSwitch Switch { get; }
Tracing this[string name] { get; }
}
public sealed class TraceManager : ITraceManager
{
private readonly ConcurrentDictionary<string, Tracing> _sources = new ConcurrentDictionary<string, Tracing>(StringComparer.OrdinalIgnoreCase);
private readonly HostTraceListener _hostTraceListener;
private TraceSetting _traceSetting;
private ISecretMasker _secretMasker;
public TraceManager(HostTraceListener traceListener, ISecretMasker secretMasker)
: this(traceListener, new TraceSetting(), secretMasker)
{
}
public TraceManager(HostTraceListener traceListener, TraceSetting traceSetting, ISecretMasker secretMasker)
{
// Validate and store params.
ArgUtil.NotNull(traceListener, nameof(traceListener));
ArgUtil.NotNull(traceSetting, nameof(traceSetting));
ArgUtil.NotNull(secretMasker, nameof(secretMasker));
_hostTraceListener = traceListener;
_traceSetting = traceSetting;
_secretMasker = secretMasker;
Switch = new SourceSwitch("GitHubActionsRunnerSwitch")
{
Level = _traceSetting.DefaultTraceLevel.ToSourceLevels()
};
}
public SourceSwitch Switch { get; private set; }
public Tracing this[string name]
{
get
{
return _sources.GetOrAdd(name, key => CreateTraceSource(key));
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (disposing)
{
foreach (Tracing traceSource in _sources.Values)
{
traceSource.Dispose();
}
_sources.Clear();
}
}
private Tracing CreateTraceSource(string name)
{
SourceSwitch sourceSwitch = Switch;
TraceLevel sourceTraceLevel;
if (_traceSetting.DetailTraceSetting.TryGetValue(name, out sourceTraceLevel))
{
sourceSwitch = new SourceSwitch("GitHubActionsRunnerSubSwitch")
{
Level = sourceTraceLevel.ToSourceLevels()
};
}
return new Tracing(name, _secretMasker, sourceSwitch, _hostTraceListener);
}
}
}

View File

@@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.Serialization;
namespace GitHub.Runner.Common
{
[DataContract]
public class TraceSetting
{
public TraceSetting()
{
DefaultTraceLevel = TraceLevel.Info;
#if DEBUG
DefaultTraceLevel = TraceLevel.Verbose;
#endif
string actionsRunnerTrace = Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_TRACE");
if (!string.IsNullOrEmpty(actionsRunnerTrace))
{
DefaultTraceLevel = TraceLevel.Verbose;
}
}
[DataMember(EmitDefaultValue = false)]
public TraceLevel DefaultTraceLevel
{
get;
set;
}
public Dictionary<String, TraceLevel> DetailTraceSetting
{
get
{
if (m_detailTraceSetting == null)
{
m_detailTraceSetting = new Dictionary<String, TraceLevel>(StringComparer.OrdinalIgnoreCase);
}
return m_detailTraceSetting;
}
}
[DataMember(EmitDefaultValue = false, Name = "DetailTraceSetting")]
private Dictionary<String, TraceLevel> m_detailTraceSetting;
}
[DataContract]
public enum TraceLevel
{
[EnumMember]
Off = 0,
[EnumMember]
Critical = 1,
[EnumMember]
Error = 2,
[EnumMember]
Warning = 3,
[EnumMember]
Info = 4,
[EnumMember]
Verbose = 5,
}
public static class TraceLevelExtensions
{
public static SourceLevels ToSourceLevels(this TraceLevel traceLevel)
{
switch (traceLevel)
{
case TraceLevel.Off:
return SourceLevels.Off;
case TraceLevel.Critical:
return SourceLevels.Critical;
case TraceLevel.Error:
return SourceLevels.Error;
case TraceLevel.Warning:
return SourceLevels.Warning;
case TraceLevel.Info:
return SourceLevels.Information;
case TraceLevel.Verbose:
return SourceLevels.Verbose;
default:
return SourceLevels.Information;
}
}
}
}

View File

@@ -0,0 +1,128 @@
using GitHub.Runner.Common.Util;
using Newtonsoft.Json;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using GitHub.DistributedTask.Logging;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common
{
public sealed class Tracing : ITraceWriter, IDisposable
{
private ISecretMasker _secretMasker;
private TraceSource _traceSource;
public Tracing(string name, ISecretMasker secretMasker, SourceSwitch sourceSwitch, HostTraceListener traceListener)
{
ArgUtil.NotNull(secretMasker, nameof(secretMasker));
_secretMasker = secretMasker;
_traceSource = new TraceSource(name);
_traceSource.Switch = sourceSwitch;
// Remove the default trace listener.
if (_traceSource.Listeners.Count > 0 &&
_traceSource.Listeners[0] is DefaultTraceListener)
{
_traceSource.Listeners.RemoveAt(0);
}
_traceSource.Listeners.Add(traceListener);
}
public void Info(string message)
{
Trace(TraceEventType.Information, message);
}
public void Info(string format, params object[] args)
{
Trace(TraceEventType.Information, StringUtil.Format(format, args));
}
public void Info(object item)
{
string json = JsonConvert.SerializeObject(item, Formatting.Indented);
Trace(TraceEventType.Information, json);
}
public void Error(Exception exception)
{
Trace(TraceEventType.Error, exception.ToString());
}
// Do not remove the non-format overload.
public void Error(string message)
{
Trace(TraceEventType.Error, message);
}
public void Error(string format, params object[] args)
{
Trace(TraceEventType.Error, StringUtil.Format(format, args));
}
// Do not remove the non-format overload.
public void Warning(string message)
{
Trace(TraceEventType.Warning, message);
}
public void Warning(string format, params object[] args)
{
Trace(TraceEventType.Warning, StringUtil.Format(format, args));
}
// Do not remove the non-format overload.
public void Verbose(string message)
{
Trace(TraceEventType.Verbose, message);
}
public void Verbose(string format, params object[] args)
{
Trace(TraceEventType.Verbose, StringUtil.Format(format, args));
}
public void Verbose(object item)
{
string json = JsonConvert.SerializeObject(item, Formatting.Indented);
Trace(TraceEventType.Verbose, json);
}
public void Entering([CallerMemberName] string name = "")
{
Trace(TraceEventType.Verbose, $"Entering {name}");
}
public void Leaving([CallerMemberName] string name = "")
{
Trace(TraceEventType.Verbose, $"Leaving {name}");
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Trace(TraceEventType eventType, string message)
{
ArgUtil.NotNull(_traceSource, nameof(_traceSource));
_traceSource.TraceEvent(
eventType: eventType,
id: 0,
message: _secretMasker.MaskSecrets(message));
}
private void Dispose(bool disposing)
{
if (disposing)
{
_traceSource.Flush();
_traceSource.Close();
}
}
}
}

View File

@@ -0,0 +1,18 @@
namespace GitHub.Runner.Common.Util
{
using System;
public static class EnumUtil
{
public static T? TryParse<T>(string value) where T: struct
{
T val;
if (Enum.TryParse(value ?? string.Empty, ignoreCase: true, result: out val))
{
return val;
}
return null;
}
}
}

View File

@@ -0,0 +1,28 @@
using System;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common.Util
{
public static class PlanUtil
{
public static PlanFeatures GetFeatures(TaskOrchestrationPlanReference plan)
{
ArgUtil.NotNull(plan, nameof(plan));
PlanFeatures features = PlanFeatures.None;
if (plan.Version >= 8)
{
features |= PlanFeatures.JobCompletedPlanEvent;
}
return features;
}
}
[Flags]
public enum PlanFeatures
{
None = 0,
JobCompletedPlanEvent = 1,
}
}

View File

@@ -0,0 +1,79 @@
using GitHub.DistributedTask.WebApi;
using System;
namespace GitHub.Runner.Common.Util
{
public static class TaskResultUtil
{
private static readonly int _returnCodeOffset = 100;
public static bool IsValidReturnCode(int returnCode)
{
int resultInt = returnCode - _returnCodeOffset;
return Enum.IsDefined(typeof(TaskResult), resultInt);
}
public static int TranslateToReturnCode(TaskResult result)
{
return _returnCodeOffset + (int)result;
}
public static TaskResult TranslateFromReturnCode(int returnCode)
{
int resultInt = returnCode - _returnCodeOffset;
if (Enum.IsDefined(typeof(TaskResult), resultInt))
{
return (TaskResult)resultInt;
}
else
{
return TaskResult.Failed;
}
}
// Merge 2 TaskResults get the worst result.
// Succeeded -> Failed/Canceled/Skipped/Abandoned
// Failed -> Failed/Canceled
// Canceled -> Canceled
// Skipped -> Skipped
// Abandoned -> Abandoned
public static TaskResult MergeTaskResults(TaskResult? currentResult, TaskResult comingResult)
{
if (currentResult == null)
{
return comingResult;
}
// current result is Canceled/Skip/Abandoned
if (currentResult > TaskResult.Failed)
{
return currentResult.Value;
}
// comming result is bad than current result
if (comingResult >= currentResult)
{
return comingResult;
}
return currentResult.Value;
}
public static ActionResult ToActionResult(this TaskResult result)
{
switch (result)
{
case TaskResult.Succeeded:
return ActionResult.Success;
case TaskResult.Failed:
return ActionResult.Failure;
case TaskResult.Canceled:
return ActionResult.Cancelled;
case TaskResult.Skipped:
return ActionResult.Skipped;
default:
throw new NotSupportedException(result.ToString());
}
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common.Util
{
[ServiceLocator(Default = typeof(UnixUtil))]
public interface IUnixUtil : IRunnerService
{
Task ExecAsync(string workingDirectory, string toolName, string argLine);
Task ChmodAsync(string mode, string file);
Task ChownAsync(string owner, string group, string file);
}
public sealed class UnixUtil : RunnerService, IUnixUtil
{
private ITerminal _term;
public override void Initialize(IHostContext hostContext)
{
base.Initialize(hostContext);
_term = hostContext.GetService<ITerminal>();
}
public async Task ChmodAsync(string mode, string file)
{
Trace.Entering();
await ExecAsync(HostContext.GetDirectory(WellKnownDirectory.Root), "chmod", $"{mode} \"{file}\"");
}
public async Task ChownAsync(string owner, string group, string file)
{
Trace.Entering();
await ExecAsync(HostContext.GetDirectory(WellKnownDirectory.Root), "chown", $"{owner}:{group} \"{file}\"");
}
public async Task ExecAsync(string workingDirectory, string toolName, string argLine)
{
Trace.Entering();
string toolPath = WhichUtil.Which(toolName, trace: Trace);
Trace.Info($"Running {toolPath} {argLine}");
var processInvoker = HostContext.CreateService<IProcessInvoker>();
processInvoker.OutputDataReceived += OnOutputDataReceived;
processInvoker.ErrorDataReceived += OnErrorDataReceived;
try
{
using (var cs = new CancellationTokenSource(TimeSpan.FromSeconds(45)))
{
await processInvoker.ExecuteAsync(workingDirectory, toolPath, argLine, null, true, cs.Token);
}
}
finally
{
processInvoker.OutputDataReceived -= OnOutputDataReceived;
processInvoker.ErrorDataReceived -= OnErrorDataReceived;
}
}
private void OnOutputDataReceived(object sender, ProcessDataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data))
{
_term.WriteLine(e.Data);
}
}
private void OnErrorDataReceived(object sender, ProcessDataReceivedEventArgs e)
{
if (!string.IsNullOrEmpty(e.Data))
{
_term.WriteLine(e.Data);
}
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using GitHub.Runner.Sdk;
namespace GitHub.Runner.Common.Util
{
public static class VarUtil
{
public static StringComparer EnvironmentVariableKeyComparer
{
get
{
switch (Constants.Runner.Platform)
{
case Constants.OSPlatform.Linux:
case Constants.OSPlatform.OSX:
return StringComparer.Ordinal;
case Constants.OSPlatform.Windows:
return StringComparer.OrdinalIgnoreCase;
default:
throw new NotSupportedException(); // Should never reach here.
}
}
}
public static string OS
{
get
{
switch (Constants.Runner.Platform)
{
case Constants.OSPlatform.Linux:
return "Linux";
case Constants.OSPlatform.OSX:
return "macOS";
case Constants.OSPlatform.Windows:
return "Windows";
default:
throw new NotSupportedException(); // Should never reach here.
}
}
}
public static string OSArchitecture
{
get
{
switch (Constants.Runner.PlatformArchitecture)
{
case Constants.Architecture.X86:
return "X86";
case Constants.Architecture.X64:
return "X64";
case Constants.Architecture.Arm:
return "ARM";
default:
throw new NotSupportedException(); // Should never reach here.
}
}
}
}
}