using GitHub.Runner.Listener.Configuration; using GitHub.Runner.Common.Util; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using GitHub.DistributedTask.Logging; using GitHub.Runner.Common; using GitHub.Runner.Sdk; namespace GitHub.Runner.Listener { public sealed class CommandSettings { private readonly Dictionary _envArgs = new Dictionary(StringComparer.OrdinalIgnoreCase); private readonly CommandLineParser _parser; private readonly IPromptManager _promptManager; private readonly Tracing _trace; private readonly string[] validCommands = { Constants.Runner.CommandLine.Commands.Configure, Constants.Runner.CommandLine.Commands.Remove, Constants.Runner.CommandLine.Commands.Run, Constants.Runner.CommandLine.Commands.Warmup, }; private readonly string[] validFlags = { Constants.Runner.CommandLine.Flags.Commit, #if OS_WINDOWS Constants.Runner.CommandLine.Flags.GitUseSChannel, #endif Constants.Runner.CommandLine.Flags.Help, Constants.Runner.CommandLine.Flags.Replace, Constants.Runner.CommandLine.Flags.RunAsService, Constants.Runner.CommandLine.Flags.Once, Constants.Runner.CommandLine.Flags.SslSkipCertValidation, Constants.Runner.CommandLine.Flags.Unattended, Constants.Runner.CommandLine.Flags.Version }; private readonly string[] validArgs = { Constants.Runner.CommandLine.Args.Agent, Constants.Runner.CommandLine.Args.Auth, Constants.Runner.CommandLine.Args.MonitorSocketAddress, Constants.Runner.CommandLine.Args.NotificationPipeName, Constants.Runner.CommandLine.Args.Password, Constants.Runner.CommandLine.Args.Pool, Constants.Runner.CommandLine.Args.ProxyPassword, Constants.Runner.CommandLine.Args.ProxyUrl, Constants.Runner.CommandLine.Args.ProxyUserName, Constants.Runner.CommandLine.Args.SslCACert, Constants.Runner.CommandLine.Args.SslClientCert, Constants.Runner.CommandLine.Args.SslClientCertKey, Constants.Runner.CommandLine.Args.SslClientCertArchive, Constants.Runner.CommandLine.Args.SslClientCertPassword, Constants.Runner.CommandLine.Args.StartupType, Constants.Runner.CommandLine.Args.Token, Constants.Runner.CommandLine.Args.Url, Constants.Runner.CommandLine.Args.UserName, Constants.Runner.CommandLine.Args.WindowsLogonAccount, Constants.Runner.CommandLine.Args.WindowsLogonPassword, Constants.Runner.CommandLine.Args.Work }; // Commands. public bool Configure => TestCommand(Constants.Runner.CommandLine.Commands.Configure); public bool Remove => TestCommand(Constants.Runner.CommandLine.Commands.Remove); public bool Run => TestCommand(Constants.Runner.CommandLine.Commands.Run); public bool Warmup => TestCommand(Constants.Runner.CommandLine.Commands.Warmup); // Flags. public bool Commit => TestFlag(Constants.Runner.CommandLine.Flags.Commit); public bool Help => TestFlag(Constants.Runner.CommandLine.Flags.Help); public bool Unattended => TestFlag(Constants.Runner.CommandLine.Flags.Unattended); public bool Version => TestFlag(Constants.Runner.CommandLine.Flags.Version); #if OS_WINDOWS public bool GitUseSChannel => TestFlag(Constants.Runner.CommandLine.Flags.GitUseSChannel); #endif public bool RunOnce => TestFlag(Constants.Runner.CommandLine.Flags.Once); // Constructor. public CommandSettings(IHostContext context, string[] args) { ArgUtil.NotNull(context, nameof(context)); _promptManager = context.GetService(); _trace = context.GetTrace(nameof(CommandSettings)); // Parse the command line args. _parser = new CommandLineParser( hostContext: context, secretArgNames: Constants.Runner.CommandLine.Args.Secrets); _parser.Parse(args); // Store and remove any args passed via environment variables. IDictionary environment = Environment.GetEnvironmentVariables(); string envPrefix = "ACTIONS_RUNNER_INPUT_"; foreach (DictionaryEntry entry in environment) { // Test if starts with ACTIONS_RUNNER_INPUT_. string fullKey = entry.Key as string ?? string.Empty; if (fullKey.StartsWith(envPrefix, StringComparison.OrdinalIgnoreCase)) { string val = (entry.Value as string ?? string.Empty).Trim(); if (!string.IsNullOrEmpty(val)) { // Extract the name. string name = fullKey.Substring(envPrefix.Length); // Mask secrets. bool secret = Constants.Runner.CommandLine.Args.Secrets.Any(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)); if (secret) { context.SecretMasker.AddValue(val); } // Store the value. _envArgs[name] = val; } // Remove from the environment block. _trace.Info($"Removing env var: '{fullKey}'"); Environment.SetEnvironmentVariable(fullKey, null); } } } // Validate commandline parser result public List Validate() { List unknowns = new List(); // detect unknown commands unknowns.AddRange(_parser.Commands.Where(x => !validCommands.Contains(x, StringComparer.OrdinalIgnoreCase))); // detect unknown flags unknowns.AddRange(_parser.Flags.Where(x => !validFlags.Contains(x, StringComparer.OrdinalIgnoreCase))); // detect unknown args unknowns.AddRange(_parser.Args.Keys.Where(x => !validArgs.Contains(x, StringComparer.OrdinalIgnoreCase))); return unknowns; } // // Interactive flags. // public bool GetReplace() { return TestFlagOrPrompt( name: Constants.Runner.CommandLine.Flags.Replace, description: "Would you like to replace the existing runner? (Y/N)", defaultValue: false); } public bool GetRunAsService() { return TestFlagOrPrompt( name: Constants.Runner.CommandLine.Flags.RunAsService, description: "Would you like to run the runner as service? (Y/N)", defaultValue: false); } public bool GetAutoLaunchBrowser() { return TestFlagOrPrompt( name: Constants.Runner.CommandLine.Flags.LaunchBrowser, description: "Would you like to launch your browser for AAD Device Code Flow? (Y/N)", defaultValue: true); } // // Args. // public string GetAgentName() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Agent, description: "Enter the name of runner:", defaultValue: Environment.MachineName ?? "myagent", validator: Validators.NonEmptyValidator); } public string GetAuth(string defaultValue) { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Auth, description: "How would you like to authenticate?", defaultValue: defaultValue, validator: Validators.AuthSchemeValidator); } public string GetPassword() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Password, description: "What is your GitHub password?", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } public string GetPool() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Pool, description: "Enter the name of your runner pool:", defaultValue: "default", validator: Validators.NonEmptyValidator); } public string GetToken() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Token, description: "Enter your personal access token:", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } public string GetRunnerRegisterToken() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Token, description: "Enter runner register token:", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } public string GetUrl(bool suppressPromptIfEmpty = false) { // Note, GetArg does not consume the arg (like GetArgOrPrompt does). if (suppressPromptIfEmpty && string.IsNullOrEmpty(GetArg(Constants.Runner.CommandLine.Args.Url))) { return string.Empty; } return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Url, description: "What is the URL of your repository?", defaultValue: string.Empty, validator: Validators.ServerUrlValidator); } public string GetUserName() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.UserName, description: "What is your GitHub username?", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } public string GetWindowsLogonAccount(string defaultValue, string descriptionMsg) { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.WindowsLogonAccount, description: descriptionMsg, defaultValue: defaultValue, validator: Validators.NTAccountValidator); } public string GetWindowsLogonPassword(string accountName) { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.WindowsLogonPassword, description: $"Password for the account {accountName}", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } public string GetWork() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Work, description: "Enter name of work folder:", defaultValue: Constants.Path.WorkDirectory, validator: Validators.NonEmptyValidator); } public string GetMonitorSocketAddress() { return GetArg(Constants.Runner.CommandLine.Args.MonitorSocketAddress); } public string GetNotificationPipeName() { return GetArg(Constants.Runner.CommandLine.Args.NotificationPipeName); } public string GetNotificationSocketAddress() { return GetArg(Constants.Runner.CommandLine.Args.NotificationSocketAddress); } // This is used to find out the source from where the Runner.Listener.exe was launched at the time of run public string GetStartupType() { return GetArg(Constants.Runner.CommandLine.Args.StartupType); } public string GetProxyUrl() { return GetArg(Constants.Runner.CommandLine.Args.ProxyUrl); } public string GetProxyUserName() { return GetArg(Constants.Runner.CommandLine.Args.ProxyUserName); } public string GetProxyPassword() { return GetArg(Constants.Runner.CommandLine.Args.ProxyPassword); } public bool GetSkipCertificateValidation() { return TestFlag(Constants.Runner.CommandLine.Flags.SslSkipCertValidation); } public string GetCACertificate() { return GetArg(Constants.Runner.CommandLine.Args.SslCACert); } public string GetClientCertificate() { return GetArg(Constants.Runner.CommandLine.Args.SslClientCert); } public string GetClientCertificatePrivateKey() { return GetArg(Constants.Runner.CommandLine.Args.SslClientCertKey); } public string GetClientCertificateArchrive() { return GetArg(Constants.Runner.CommandLine.Args.SslClientCertArchive); } public string GetClientCertificatePassword() { return GetArg(Constants.Runner.CommandLine.Args.SslClientCertPassword); } // // Private helpers. // private string GetArg(string name) { string result; if (!_parser.Args.TryGetValue(name, out result)) { result = GetEnvArg(name); } return result; } private void RemoveArg(string name) { if (_parser.Args.ContainsKey(name)) { _parser.Args.Remove(name); } if (_envArgs.ContainsKey(name)) { _envArgs.Remove(name); } } private string GetArgOrPrompt( string name, string description, string defaultValue, Func validator) { // Check for the arg in the command line parser. ArgUtil.NotNull(validator, nameof(validator)); string result = GetArg(name); // Return the arg if it is not empty and is valid. _trace.Info($"Arg '{name}': '{result}'"); if (!string.IsNullOrEmpty(result)) { // After read the arg from input commandline args, remove it from Arg dictionary, // This will help if bad arg value passed through CommandLine arg, when ConfigurationManager ask CommandSetting the second time, // It will prompt for input instead of continue use the bad input. _trace.Info($"Remove {name} from Arg dictionary."); RemoveArg(name); if (validator(result)) { return result; } _trace.Info("Arg is invalid."); } // Otherwise prompt for the arg. return _promptManager.ReadValue( argName: name, description: description, secret: Constants.Runner.CommandLine.Args.Secrets.Any(x => string.Equals(x, name, StringComparison.OrdinalIgnoreCase)), defaultValue: defaultValue, validator: validator, unattended: Unattended); } private string GetEnvArg(string name) { string val; if (_envArgs.TryGetValue(name, out val) && !string.IsNullOrEmpty(val)) { _trace.Info($"Env arg '{name}': '{val}'"); return val; } return null; } private bool TestCommand(string name) { bool result = _parser.IsCommand(name); _trace.Info($"Command '{name}': '{result}'"); return result; } private bool TestFlag(string name) { bool result = _parser.Flags.Contains(name); if (!result) { string envStr = GetEnvArg(name); if (!bool.TryParse(envStr, out result)) { result = false; } } _trace.Info($"Flag '{name}': '{result}'"); return result; } private bool TestFlagOrPrompt( string name, string description, bool defaultValue) { bool result = TestFlag(name); if (!result) { result = _promptManager.ReadBool( argName: name, description: description, defaultValue: defaultValue, unattended: Unattended); } return result; } } }