mirror of
https://github.com/actions/runner.git
synced 2025-12-26 03:17:32 +08:00
GitHub Actions Runner
This commit is contained in:
253
src/Runner.Common/ActionCommand.cs
Normal file
253
src/Runner.Common/ActionCommand.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Runner.Common/ActionResult.cs
Normal file
15
src/Runner.Common/ActionResult.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
public enum ActionResult
|
||||
{
|
||||
Success = 0,
|
||||
|
||||
Failure = 1,
|
||||
|
||||
Cancelled = 2,
|
||||
|
||||
Skipped = 3
|
||||
}
|
||||
}
|
||||
33
src/Runner.Common/AsyncManualResetEvent.cs
Normal file
33
src/Runner.Common/AsyncManualResetEvent.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
73
src/Runner.Common/Capabilities/CapabilitiesManager.cs
Normal file
73
src/Runner.Common/Capabilities/CapabilitiesManager.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/Runner.Common/Capabilities/RunnerCapabilitiesProvider.cs
Normal file
86
src/Runner.Common/Capabilities/RunnerCapabilitiesProvider.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/Runner.Common/CommandLineParser.cs
Normal file
128
src/Runner.Common/CommandLineParser.cs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
252
src/Runner.Common/ConfigurationStore.cs
Normal file
252
src/Runner.Common/ConfigurationStore.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
343
src/Runner.Common/Constants.cs
Normal file
343
src/Runner.Common/Constants.cs
Normal 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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
src/Runner.Common/CredentialData.cs
Normal file
24
src/Runner.Common/CredentialData.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
19
src/Runner.Common/Exceptions.cs
Normal file
19
src/Runner.Common/Exceptions.cs
Normal 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)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
80
src/Runner.Common/ExtensionManager.cs
Normal file
80
src/Runner.Common/ExtensionManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
30
src/Runner.Common/Extensions.cs
Normal file
30
src/Runner.Common/Extensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
597
src/Runner.Common/HostContext.cs
Normal file
597
src/Runner.Common/HostContext.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
202
src/Runner.Common/HostTraceListener.cs
Normal file
202
src/Runner.Common/HostTraceListener.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
9
src/Runner.Common/IExtension.cs
Normal file
9
src/Runner.Common/IExtension.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace GitHub.Runner.Common
|
||||
{
|
||||
public interface IExtension : IRunnerService
|
||||
{
|
||||
Type ExtensionType { get; }
|
||||
}
|
||||
}
|
||||
296
src/Runner.Common/JobNotification.cs
Normal file
296
src/Runner.Common/JobNotification.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
162
src/Runner.Common/JobServer.cs
Normal file
162
src/Runner.Common/JobServer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
702
src/Runner.Common/JobServerQueue.cs
Normal file
702
src/Runner.Common/JobServerQueue.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
61
src/Runner.Common/LocationServer.cs
Normal file
61
src/Runner.Common/LocationServer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
124
src/Runner.Common/Logging.cs
Normal file
124
src/Runner.Common/Logging.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/Runner.Common/ProcessChannel.cs
Normal file
100
src/Runner.Common/ProcessChannel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
396
src/Runner.Common/ProcessExtensions.cs
Normal file
396
src/Runner.Common/ProcessExtensions.cs
Normal 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
|
||||
}
|
||||
329
src/Runner.Common/ProcessInvoker.cs
Normal file
329
src/Runner.Common/ProcessInvoker.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
68
src/Runner.Common/Runner.Common.csproj
Normal file
68
src/Runner.Common/Runner.Common.csproj
Normal 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>
|
||||
231
src/Runner.Common/RunnerCertificateManager.cs
Normal file
231
src/Runner.Common/RunnerCertificateManager.cs
Normal 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; }
|
||||
}
|
||||
}
|
||||
948
src/Runner.Common/RunnerCredentialStore.cs
Normal file
948
src/Runner.Common/RunnerCredentialStore.cs
Normal 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
|
||||
}
|
||||
355
src/Runner.Common/RunnerServer.cs
Normal file
355
src/Runner.Common/RunnerServer.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
39
src/Runner.Common/RunnerService.cs
Normal file
39
src/Runner.Common/RunnerService.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
196
src/Runner.Common/RunnerWebProxy.cs
Normal file
196
src/Runner.Common/RunnerWebProxy.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
96
src/Runner.Common/StreamString.cs
Normal file
96
src/Runner.Common/StreamString.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
src/Runner.Common/Terminal.cs
Normal file
198
src/Runner.Common/Terminal.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
65
src/Runner.Common/ThrottlingReportHandler.cs
Normal file
65
src/Runner.Common/ThrottlingReportHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
src/Runner.Common/TraceManager.cs
Normal file
88
src/Runner.Common/TraceManager.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
92
src/Runner.Common/TraceSetting.cs
Normal file
92
src/Runner.Common/TraceSetting.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/Runner.Common/Tracing.cs
Normal file
128
src/Runner.Common/Tracing.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
18
src/Runner.Common/Util/EnumUtil.cs
Normal file
18
src/Runner.Common/Util/EnumUtil.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/Runner.Common/Util/PlanUtil.cs
Normal file
28
src/Runner.Common/Util/PlanUtil.cs
Normal 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,
|
||||
}
|
||||
}
|
||||
79
src/Runner.Common/Util/TaskResultUtil.cs
Normal file
79
src/Runner.Common/Util/TaskResultUtil.cs
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/Runner.Common/Util/UnixUtil.cs
Normal file
79
src/Runner.Common/Util/UnixUtil.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/Runner.Common/Util/VarUtil.cs
Normal file
63
src/Runner.Common/Util/VarUtil.cs
Normal 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.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user