using GitHub.Runner.Listener.Configuration; using GitHub.Runner.Common.Util; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using GitHub.Runner.Common; using GitHub.Runner.Sdk; namespace GitHub.Runner.Listener { public sealed class CommandSettings { private readonly Dictionary _envArgs = new(StringComparer.OrdinalIgnoreCase); private readonly CommandLineParser _parser; private readonly IPromptManager _promptManager; private readonly Tracing _trace; // Valid flags for all commands private readonly string[] genericOptions = { Constants.Runner.CommandLine.Flags.Help, Constants.Runner.CommandLine.Flags.Version, Constants.Runner.CommandLine.Flags.Commit, Constants.Runner.CommandLine.Flags.Check }; // Valid flags and args for specific command - key: command, value: array of valid flags and args private readonly Dictionary validOptions = new() { // Valid configure flags and args [Constants.Runner.CommandLine.Commands.Configure] = new string[] { Constants.Runner.CommandLine.Flags.DisableUpdate, Constants.Runner.CommandLine.Flags.Ephemeral, Constants.Runner.CommandLine.Flags.GenerateServiceConfig, Constants.Runner.CommandLine.Flags.Replace, Constants.Runner.CommandLine.Flags.RunAsService, Constants.Runner.CommandLine.Flags.Unattended, Constants.Runner.CommandLine.Args.Auth, Constants.Runner.CommandLine.Args.Labels, Constants.Runner.CommandLine.Args.MonitorSocketAddress, Constants.Runner.CommandLine.Args.Name, Constants.Runner.CommandLine.Args.PAT, Constants.Runner.CommandLine.Args.RunnerGroup, 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 }, // Valid remove flags and args [Constants.Runner.CommandLine.Commands.Remove] = new string[] { Constants.Runner.CommandLine.Args.Token, Constants.Runner.CommandLine.Args.PAT }, // Valid run flags and args [Constants.Runner.CommandLine.Commands.Run] = new string[] { Constants.Runner.CommandLine.Flags.Once, Constants.Runner.CommandLine.Args.JitConfig, Constants.Runner.CommandLine.Args.StartupType }, // valid warmup flags and args [Constants.Runner.CommandLine.Commands.Warmup] = new string[] { } }; // 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 Check => TestFlag(Constants.Runner.CommandLine.Flags.Check); public bool Commit => TestFlag(Constants.Runner.CommandLine.Flags.Commit); public bool DisableUpdate => TestFlag(Constants.Runner.CommandLine.Flags.DisableUpdate); public bool Ephemeral => TestFlag(Constants.Runner.CommandLine.Flags.Ephemeral); public bool GenerateServiceConfig => TestFlag(Constants.Runner.CommandLine.Flags.GenerateServiceConfig); 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); // Keep this around since customers still relies on it 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(); // detect unknown commands unknowns.AddRange(_parser.Commands.Where(x => !validOptions.Keys.Contains(x, StringComparer.OrdinalIgnoreCase))); if (unknowns.Count == 0) { // detect unknown flags and args for valid commands foreach (var command in _parser.Commands) { if (validOptions.TryGetValue(command, out string[] options)) { unknowns.AddRange(_parser.Flags.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase) && !genericOptions.Contains(x, StringComparer.OrdinalIgnoreCase))); unknowns.AddRange(_parser.Args.Keys.Where(x => !options.Contains(x, StringComparer.OrdinalIgnoreCase))); } } } return unknowns; } public string GetCommandName() { string command = string.Empty; if (Configure) { command = Constants.Runner.CommandLine.Commands.Configure; } else if (Remove) { command = Constants.Runner.CommandLine.Commands.Remove; } else if (Run) { command = Constants.Runner.CommandLine.Commands.Run; } else if (Warmup) { command = Constants.Runner.CommandLine.Commands.Warmup; } return command; } // // 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); } // // Args. // 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 GetJitConfig() { return GetArg( name: Constants.Runner.CommandLine.Args.JitConfig); } public string GetRunnerName() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Name, description: "Enter the name of runner:", defaultValue: Environment.MachineName ?? "myrunner", validator: Validators.NonEmptyValidator); } public string GetRunnerGroupName(string defaultPoolName = null) { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.RunnerGroup, description: "Enter the name of the runner group to add this runner to:", defaultValue: defaultPoolName ?? "default", validator: Validators.NonEmptyValidator); } public string GetToken() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Token, description: "What is your pool admin oauth access token?", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } public string GetGitHubPersonalAccessToken(bool required = false) { if (required) { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.PAT, description: "What is your GitHub personal access token?", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } else { return GetArg(name: Constants.Runner.CommandLine.Args.PAT); } } public string GetRunnerRegisterToken() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Token, description: "What is your runner register token?", defaultValue: string.Empty, validator: Validators.NonEmptyValidator); } public string GetRunnerDeletionToken() { return GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Token, description: "Enter runner remove 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); } #if OS_WINDOWS 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); } #endif 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); } // 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 ISet GetLabels() { var labelSet = new HashSet(StringComparer.OrdinalIgnoreCase); string labels = GetArgOrPrompt( name: Constants.Runner.CommandLine.Args.Labels, description: $"This runner will have the following labels: 'self-hosted', '{VarUtil.OS}', '{VarUtil.OSArchitecture}' \nEnter any additional labels (ex. label-1,label-2):", defaultValue: string.Empty, validator: Validators.LabelsValidator, isOptional: true); if (!string.IsNullOrEmpty(labels)) { labelSet = labels.Split(',').Where(x => !string.IsNullOrEmpty(x)).ToHashSet(StringComparer.OrdinalIgnoreCase); } return labelSet; } // // 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, bool isOptional = false) { // 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, isOptional: isOptional); } 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; } } }