From 404b5ac74122b81763a57c6b9b87554bf78d3200 Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Thu, 11 Jan 2018 16:13:17 -0800 Subject: [PATCH 01/10] Add -settingsFile command-line option Update PSConfiguration to support a custom powershell.config.json file Update various read/write/update methods to support custom json file override Refactor Read/Write/Update/Remove generic methods to accept a ConfigScope directly; removes redundant file logic in the various settings methods. --- .../host/msh/CommandLineParameterParser.cs | 20 ++ .../CommandLineParameterParserStrings.resx | 6 + .../resources/ManagedEntranceStrings.resx | 9 + .../engine/PSConfiguration.cs | 182 +++++++++++++----- 4 files changed, 169 insertions(+), 48 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index b297c248a1d..f154ff85acf 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -190,6 +190,7 @@ internal class CommandLineParameterParser "file", "executionpolicy", "command", + "settingsFile", "help" }; @@ -707,6 +708,25 @@ private void ParseHelper(string[] args) break; } } + + else if (MatchSwitch(switchKey, "settingsFile", "settings") ) + { + ++i; + if (i >= args.Length) + { + WriteCommandLineError( + CommandLineParameterParserStrings.MissingSettingsFileArgument); + break; + } + string configFile = args[i]; + if (!System.IO.File.Exists(configFile)) + { + WriteCommandLineError( + CommandLineParameterParserStrings.SettingsFileNotExists); + break; + } + PowerShellConfig.Instance.SystemConfigFilePath = configFile; + } #if STAMODE // explicit setting of the ApartmentState Not supported on NanoServer else if (MatchSwitch(switchKey, "sta", "s")) diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx index 7ad30993383..4949261d3d8 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx @@ -186,6 +186,12 @@ Cannot process the command because -Configuration requires an argument that is a remote endpoint configuration name. Specify this argument and try again. + + Cannot process the command because -SettingsFile requires a file path. Supply a path for the SettingsFile parameter and then try the command again. + + + Cannot process the command because -SettingsFile requires a file path to an existing SettingsFile file. Supply a valid file path for the SettingsFile parameter and then try the command again. + Invalid argument '{0}', did you mean: diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index 8bef56e8c24..b1cf531a082 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -239,6 +239,15 @@ All parameters are case-insensitive. -WindowStyle | -w Sets the window style to Normal, Minimized, Maximized or Hidden. + +-SettingsFile | -settings + Overrides the system-wide powershell.config.json settings file for the session. + By default, system-wide settings are read from the powershell.config.json + in the $PSHOME directory. + Note that per-user settings take precedence over system-wide settings. + + Example: pwsh -SettingsFile c:\myproject\powershell.config.json + diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs index ef01eba36c7..32dda81de16 100644 --- a/src/System.Management.Automation/engine/PSConfiguration.cs +++ b/src/System.Management.Automation/engine/PSConfiguration.cs @@ -28,6 +28,10 @@ internal sealed class PowerShellConfig internal static PowerShellConfig Instance => s_instance; private string psHomeConfigDirectory; + // When passed as a pwsh command-line option, + // overrides the system wide configuration file. + private string psCustomHomeConfigFilePath; + private string appDataConfigDirectory; private const string configFileName = "powershell.config.json"; @@ -50,6 +54,65 @@ private PowerShellConfig() appDataConfigDirectory = Utils.GetUserConfigurationDirectory(); } + private string GetConfigFilePath(ConfigScope scope) + { + string result = string.Empty; + + if (scope == ConfigScope.CurrentUser) + { + result = Path.Combine(appDataConfigDirectory, configFileName); + } + else + { + if (!string.IsNullOrEmpty(psCustomHomeConfigFilePath)) + { + // system wide configuration file overridden at the pwsh command-line. + result = psCustomHomeConfigFilePath; + } + else + { + result = Path.Combine(psHomeConfigDirectory, configFileName); + } + } + return result; + } + + /// + /// Gets or sets the system wide configuration file path + /// + /// A fully qualified path to the system wide configuration file. + internal string SystemConfigFilePath + { + get + { + fileLock.EnterReadLock(); + try + { + return GetConfigFilePath(ConfigScope.SystemWide); + } + finally + { + fileLock.ExitReadLock(); + } + } + set + { + if (!string.IsNullOrEmpty(value) && !File.Exists(value)) + { + throw new FileNotFoundException(value); + } + fileLock.EnterWriteLock(); + try + { + psCustomHomeConfigFilePath = value; + } + finally + { + fileLock.ExitWriteLock(); + } + } + } + /// /// Existing Key = HKLM:\System\CurrentControlSet\Control\Session Manager\Environment /// Proposed value = %ProgramFiles%\PowerShell\Modules by default @@ -59,10 +122,7 @@ private PowerShellConfig() /// Value if found, null otherwise. The behavior matches ModuleIntrinsics.GetExpandedEnvironmentVariable(). internal string GetModulePath(ConfigScope scope) { - string scopeDirectory = scope == ConfigScope.SystemWide ? psHomeConfigDirectory : appDataConfigDirectory; - string fileName = Path.Combine(scopeDirectory, configFileName); - - string modulePath = ReadValueFromFile(fileName, Constants.PSModulePathEnvVar); + string modulePath = ReadValueFromFile(scope, Constants.PSModulePathEnvVar); if (!string.IsNullOrEmpty(modulePath)) { modulePath = Environment.ExpandEnvironmentVariables(modulePath); @@ -87,17 +147,9 @@ internal string GetModulePath(ConfigScope scope) internal string GetExecutionPolicy(ConfigScope scope, string shellId) { string execPolicy = null; - string scopeDirectory = psHomeConfigDirectory; - - // Defaults to system wide. - if(ConfigScope.CurrentUser == scope) - { - scopeDirectory = appDataConfigDirectory; - } - string fileName = Path.Combine(scopeDirectory, configFileName); string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); - string rawExecPolicy = ReadValueFromFile(fileName, valueName); + string rawExecPolicy = ReadValueFromFile(scope, valueName); if (!String.IsNullOrEmpty(rawExecPolicy)) { @@ -108,23 +160,12 @@ internal string GetExecutionPolicy(ConfigScope scope, string shellId) internal void RemoveExecutionPolicy(ConfigScope scope, string shellId) { - string scopeDirectory = psHomeConfigDirectory; - - // Defaults to system wide. - if (ConfigScope.CurrentUser == scope) - { - scopeDirectory = appDataConfigDirectory; - } - - string fileName = Path.Combine(scopeDirectory, configFileName); string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); - RemoveValueFromFile(fileName, valueName); + RemoveValueFromFile(scope, valueName); } internal void SetExecutionPolicy(ConfigScope scope, string shellId, string executionPolicy) { - string scopeDirectory = psHomeConfigDirectory; - // Defaults to system wide. if (ConfigScope.CurrentUser == scope) { @@ -133,12 +174,10 @@ internal void SetExecutionPolicy(ConfigScope scope, string shellId, string execu // CreateDirectory will succeed if the directory already exists // so there is no reason to check Directory.Exists(). Directory.CreateDirectory(appDataConfigDirectory); - scopeDirectory = appDataConfigDirectory; } - string fileName = Path.Combine(scopeDirectory, configFileName); string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); - WriteValueToFile(fileName, valueName, executionPolicy); + WriteValueToFile(scope, valueName, executionPolicy); } /// @@ -153,14 +192,12 @@ internal void SetExecutionPolicy(ConfigScope scope, string shellId, string execu /// Whether console prompting should happen. If the value cannot be read it defaults to false. internal bool GetConsolePrompting() { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - return ReadValueFromFile(fileName, "ConsolePrompting"); + return ReadValueFromFile(ConfigScope.SystemWide, "ConsolePrompting"); } internal void SetConsolePrompting(bool shouldPrompt) { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - WriteValueToFile(fileName, "ConsolePrompting", shouldPrompt); + WriteValueToFile(ConfigScope.SystemWide, "ConsolePrompting", shouldPrompt); } /// @@ -175,14 +212,12 @@ internal void SetConsolePrompting(bool shouldPrompt) /// Boolean indicating whether Update-Help should prompt. If the value cannot be read, it defaults to false. internal bool GetDisablePromptToUpdateHelp() { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - return ReadValueFromFile(fileName, "DisablePromptToUpdateHelp"); + return ReadValueFromFile(ConfigScope.SystemWide, "DisablePromptToUpdateHelp"); } internal void SetDisablePromptToUpdateHelp(bool prompt) { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - WriteValueToFile(fileName, "DisablePromptToUpdateHelp", prompt); + WriteValueToFile(ConfigScope.SystemWide, "DisablePromptToUpdateHelp", prompt); } /// @@ -190,9 +225,7 @@ internal void SetDisablePromptToUpdateHelp(bool prompt) /// internal PowerShellPolicies GetPowerShellPolicies(ConfigScope scope) { - string scopeDirectory = (scope == ConfigScope.SystemWide) ? psHomeConfigDirectory : appDataConfigDirectory; - string fileName = Path.Combine(scopeDirectory, configFileName); - return ReadValueFromFile(fileName, nameof(PowerShellPolicies)); + return ReadValueFromFile(scope, nameof(PowerShellPolicies)); } #if UNIX @@ -204,8 +237,7 @@ internal PowerShellPolicies GetPowerShellPolicies(ConfigScope scope) /// internal string GetSysLogIdentity() { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - string identity = ReadValueFromFile(fileName, "LogIdentity"); + string identity = ReadValueFromFile(ConfigScope.SystemWide, "LogIdentity"); if (string.IsNullOrEmpty(identity) || identity.Equals(LogDefaultValue, StringComparison.OrdinalIgnoreCase)) @@ -223,8 +255,7 @@ internal string GetSysLogIdentity() /// internal PSLevel GetLogLevel() { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - string levelName = ReadValueFromFile(fileName, "LogLevel"); + string levelName = ReadValueFromFile(ConfigScope.SystemWide, "LogLevel"); PSLevel level; if (string.IsNullOrEmpty(levelName) || @@ -256,8 +287,7 @@ internal PSLevel GetLogLevel() /// internal PSChannel GetLogChannels() { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - string values = ReadValueFromFile(fileName, "LogChannels"); + string values = ReadValueFromFile(ConfigScope.SystemWide, "LogChannels"); PSChannel result = 0; if (!string.IsNullOrEmpty(values)) @@ -299,8 +329,7 @@ internal PSChannel GetLogChannels() /// internal PSKeyword GetLogKeywords() { - string fileName = Path.Combine(psHomeConfigDirectory, configFileName); - string values = ReadValueFromFile(fileName, "LogKeywords"); + string values = ReadValueFromFile(ConfigScope.SystemWide, "LogKeywords"); PSKeyword result = 0; if (!string.IsNullOrEmpty(values)) @@ -331,7 +360,23 @@ internal PSKeyword GetLogKeywords() return result; } #endif // UNIX - + + /// + /// Read a value from the configuration file. + /// + /// The type of the value + /// The ConfigScope of the configuration file to update. + /// The string key of the value. + /// The default value to return if the key is not present. + /// + private T ReadValueFromFile(ConfigScope scope, string key, T defaultValue = default(T), + Func readImpl = null) + { + + string fileName = GetConfigFilePath(scope); + return ReadValueFromFile(fileName, key, defaultValue, readImpl); + } + private T ReadValueFromFile(string fileName, string key, T defaultValue = default(T), Func readImpl = null) { @@ -362,6 +407,21 @@ internal PSKeyword GetLogKeywords() return defaultValue; } + /// + /// Update a value in the configuration file. + /// + /// The type of the value + /// The ConfigScope of the configuration file to update. + /// The string key of the value. + /// The value to set. + /// Whether the key-value pair should be added to or removed from the file + private void UpdateValueInFile(ConfigScope scope, string key, T value, bool addValue) + { + + string fileName = GetConfigFilePath(scope); + UpdateValueInFile(fileName, key, value, addValue); + } + /// /// TODO: Should this return success fail or throw? /// @@ -466,6 +526,20 @@ private void UpdateValueInFile(string fileName, string key, T value, bool add } } + + /// + /// TODO: Should this return success, fail, or throw? + /// + /// The type of value to write. + /// The ConfigScope of the file to update. + /// The string key of the value. + /// The value to write. + private void WriteValueToFile(ConfigScope scope, string key, T value) + { + string fileName = GetConfigFilePath(scope); + WriteValueToFile(fileName, key, value); + } + /// /// TODO: Should this return success, fail, or throw? /// @@ -478,6 +552,18 @@ private void WriteValueToFile(string fileName, string key, T value) UpdateValueInFile(fileName, key, value, true); } + /// + /// TODO: Should this return success, fail, or throw? + /// + /// The type of value to remove. + /// The ConfigScope of the file to update. + /// The string key of the value. + private void RemoveValueFromFile(ConfigScope scope, string key) + { + string fileName = GetConfigFilePath(scope); + RemoveValueFromFile(fileName, key); + } + /// /// TODO: Should this return success, fail, or throw? /// From 7e78cf221a08ecbdc1b347b94f7ae39c7431c537 Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Fri, 12 Jan 2018 09:43:00 -0800 Subject: [PATCH 02/10] Fix caseing of settingsfile command-line paraemeter --- .../host/msh/CommandLineParameterParser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index f154ff85acf..7b41e5525cc 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -709,7 +709,7 @@ private void ParseHelper(string[] args) } } - else if (MatchSwitch(switchKey, "settingsFile", "settings") ) + else if (MatchSwitch(switchKey, "settingsfile", "settings") ) { ++i; if (i >= args.Length) From e4be527d47229be6a8802fa93cb787a0436fa896 Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Fri, 12 Jan 2018 13:15:54 -0800 Subject: [PATCH 03/10] Initial tests confirming custom settings file is read and written --- test/powershell/Host/ConsoleHost.Tests.ps1 | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index f6b08ca4dbc..7660e0e958c 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -249,6 +249,33 @@ Describe "ConsoleHost unit tests" -tags "Feature" { } } + Context "-SettingsFile Commandline switch" { + BeforeAll { + $CustomSettingsFile = Join-Path -Path $TestDrive -ChildPath 'Powershell.test.json' + $DefaultExecutionPolicy = 'Unrestricted' + } + BeforeEach { + # reset the content of the settings file to a known state. + Set-Content -Path $CustomSettingsfile -Value "{`"Microsoft.PowerShell:ExecutionPolicy`":`"$DefaultExecutionPolicy`"}" -ErrorAction Stop + } + + It "PowerShell reads from the custom -settingsFile" { + $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy).ToString()} + $actualValue | Should Be $DefaultExecutionPolicy + } + + It "PowerShell writes to the custom -settingsFile" { + $expectedValue = 'AllSigned' + & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {Set-ExecutionPolicy AllSigned} + # ensure the setting was written to the settings file. + $content = (Get-Content -Path $CustomSettingsFile | ConvertFrom-Json) + $content.'Microsoft.PowerShell:ExecutionPolicy' | Should Be $expectedValue + # ensure the setting is applied on next run + $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy).ToString()} + $actualValue | Should Be $expectedValue + } + } + Context "Pipe to/from powershell" { $p = [PSCustomObject]@{X=10;Y=20} From 86f7135ce840b9c65ec9ac2096cc181c1999f76b Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Tue, 16 Jan 2018 12:29:27 -0800 Subject: [PATCH 04/10] [FEATURE] Cover read, update, and remove of a setting --- test/powershell/Host/ConsoleHost.Tests.ps1 | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1 index 7660e0e958c..70bbc0006a8 100644 --- a/test/powershell/Host/ConsoleHost.Tests.ps1 +++ b/test/powershell/Host/ConsoleHost.Tests.ps1 @@ -250,30 +250,48 @@ Describe "ConsoleHost unit tests" -tags "Feature" { } Context "-SettingsFile Commandline switch" { + BeforeAll { $CustomSettingsFile = Join-Path -Path $TestDrive -ChildPath 'Powershell.test.json' - $DefaultExecutionPolicy = 'Unrestricted' + $DefaultExecutionPolicy = 'RemoteSigned' } BeforeEach { # reset the content of the settings file to a known state. Set-Content -Path $CustomSettingsfile -Value "{`"Microsoft.PowerShell:ExecutionPolicy`":`"$DefaultExecutionPolicy`"}" -ErrorAction Stop } - It "PowerShell reads from the custom -settingsFile" { - $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy).ToString()} + # NOTE: The -settingsFile command-line option only reads settings for the local machine. As a result, the tests that use Set/Get-ExecutionPolicy + # must use an explicit scope of LocalMachine to ensure the setting is written to the expected file. + + It "Verifies PowerShell reads from the custom -settingsFile" { + $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy -Scope LocalMachine).ToString()} $actualValue | Should Be $DefaultExecutionPolicy } - It "PowerShell writes to the custom -settingsFile" { + It "Verifies PowerShell writes to the custom -settingsFile" { $expectedValue = 'AllSigned' - & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {Set-ExecutionPolicy AllSigned} + + # Update the execution policy; this should update the settings file. + & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {Set-ExecutionPolicy -ExecutionPolicy AllSigned -Scope LocalMachine } + # ensure the setting was written to the settings file. $content = (Get-Content -Path $CustomSettingsFile | ConvertFrom-Json) $content.'Microsoft.PowerShell:ExecutionPolicy' | Should Be $expectedValue + # ensure the setting is applied on next run - $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy).ToString()} + $actualValue = & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {(Get-ExecutionPolicy -Scope LocalMachine).ToString()} $actualValue | Should Be $expectedValue } + + It "Verify PowerShell removes a setting from the custom -settingsFile" { + # Remove the LocalMachine execution policy; this should update the settings file. + & $powershell -NoProfile -SettingsFile $CustomSettingsFile -Command {Set-ExecutionPolicy -ExecutionPolicy Undefined -Scope LocalMachine } + + # ensure the setting was removed from the settings file. + $content = (Get-Content -Path $CustomSettingsFile | ConvertFrom-Json) + $content.'Microsoft.PowerShell:ExecutionPolicy' | Should Be $null + } + } Context "Pipe to/from powershell" { From 761fd377ee8d57eb67218b2af597223ea200e69f Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Tue, 16 Jan 2018 12:29:27 -0800 Subject: [PATCH 05/10] [FEATURE} Cover read, update, and remove of a setting From 322478f555cf41689eee66f2114e0a3a3a5e8dc4 Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Fri, 19 Jan 2018 12:21:01 -0800 Subject: [PATCH 06/10] PR feedback. * Update error strings in CommandlineParameterParserStrings.resx * Merge Scope and File Read/Update/Remove overloads in PSConfiguration. Simplify handling of per-user and system-wide config files. * Normalize file path validation in CommandlineParameterParser.cs --- .../host/msh/CommandLineParameterParser.cs | 39 ++++-- .../CommandLineParameterParserStrings.resx | 5 +- .../resources/ManagedEntranceStrings.resx | 3 + .../engine/PSConfiguration.cs | 128 ++++-------------- 4 files changed, 65 insertions(+), 110 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 7b41e5525cc..0ea786750ae 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -190,7 +190,7 @@ internal class CommandLineParameterParser "file", "executionpolicy", "command", - "settingsFile", + "settingsfile", "help" }; @@ -718,14 +718,25 @@ private void ParseHelper(string[] args) CommandLineParameterParserStrings.MissingSettingsFileArgument); break; } - string configFile = args[i]; + string configFile = null; + try + { + configFile = NormalizeFilePath(args[i]); + } + catch (Exception ex) + { + string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.InvalidSettingsFileArgument, args[i], ex.Message); + WriteCommandLineError(error); + break; + } + if (!System.IO.File.Exists(configFile)) { - WriteCommandLineError( - CommandLineParameterParserStrings.SettingsFileNotExists); + string error = string.Format(CultureInfo.CurrentCulture, CommandLineParameterParserStrings.SettingsFileNotExists, configFile); + WriteCommandLineError(error); break; } - PowerShellConfig.Instance.SystemConfigFilePath = configFile; + PowerShellConfig.Instance.SetSystemConfigFilePath(configFile); } #if STAMODE // explicit setting of the ApartmentState Not supported on NanoServer @@ -867,6 +878,15 @@ private void ParseExecutionPolicy(string[] args, ref int i, ref string execution executionPolicy = args[i]; } + private static string NormalizeFilePath(string path) + { + // Normalize slashes + path = path.Replace(StringLiterals.AlternatePathSeparator, + StringLiterals.DefaultPathSeparator); + + return Path.GetFullPath(path); + } + private bool ParseFile(string[] args, ref int i, bool noexitSeen) { // Process file execution. We don't need to worry about checking -command @@ -924,10 +944,7 @@ bool TryGetBoolValue(string arg, out bool boolValue) string exceptionMessage = null; try { - // Normalize slashes - _file = args[i].Replace(StringLiterals.AlternatePathSeparator, - StringLiterals.DefaultPathSeparator); - _file = Path.GetFullPath(_file); + _file = NormalizeFilePath (args[i]); } catch (Exception e) { @@ -1004,11 +1021,11 @@ bool TryGetBoolValue(string arg, out bool boolValue) string argName = arg.Substring(0, offset); if (TryGetBoolValue(argValue, out bool boolValue)) { - _collectedArgs.Add(new CommandParameter(argName, boolValue)); + _collectedArgs.Add(new CommandParameter(argName, boolValue)); } else { - _collectedArgs.Add(new CommandParameter(argName, argValue)); + _collectedArgs.Add(new CommandParameter(argName, argValue)); } } } diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx index 4949261d3d8..cc46b3070db 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/CommandLineParameterParserStrings.resx @@ -189,8 +189,11 @@ Cannot process the command because -SettingsFile requires a file path. Supply a path for the SettingsFile parameter and then try the command again. + + Processing -SettingsFile '{0}' failed: {1}. Specify a valid path for the -SettingsFile parameter. + - Cannot process the command because -SettingsFile requires a file path to an existing SettingsFile file. Supply a valid file path for the SettingsFile parameter and then try the command again. + The argument '{0}' passed to the -SettingsFile does not exist. Provide the path to an existing json file as an argument to the -SettingsFile parameter. Invalid argument '{0}', did you mean: diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index b1cf531a082..ae3f247c2ec 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -244,7 +244,10 @@ All parameters are case-insensitive. Overrides the system-wide powershell.config.json settings file for the session. By default, system-wide settings are read from the powershell.config.json in the $PSHOME directory. + Note that per-user settings take precedence over system-wide settings. + Also note that these settings are not used by the endpoint specified + by the -ConfigurationName argument. Example: pwsh -SettingsFile c:\myproject\powershell.config.json diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs index 32dda81de16..97216f66ffb 100644 --- a/src/System.Management.Automation/engine/PSConfiguration.cs +++ b/src/System.Management.Automation/engine/PSConfiguration.cs @@ -27,12 +27,14 @@ internal sealed class PowerShellConfig private static readonly PowerShellConfig s_instance = new PowerShellConfig(); internal static PowerShellConfig Instance => s_instance; - private string psHomeConfigDirectory; + // The json file containing system-wide configuration settings. // When passed as a pwsh command-line option, // overrides the system wide configuration file. - private string psCustomHomeConfigFilePath; + private FileInfo systemWideConfigFile; + + // The json file containing the per-user configuration settings. + private FileInfo perUserConfigFile; - private string appDataConfigDirectory; private const string configFileName = "powershell.config.json"; /// @@ -44,72 +46,48 @@ internal sealed class PowerShellConfig private PowerShellConfig() { - // Sets the system-wide configuration directory - psHomeConfigDirectory = Utils.DefaultPowerShellAppBase; + // Sets the system-wide configuration file. + systemWideConfigFile = new FileInfo(Path.Combine(Utils.DefaultPowerShellAppBase, configFileName)); // Sets the per-user configuration directory // Note: This directory may or may not exist depending upon the // execution scenario. Writes will attempt to create the directory // if it does not already exist. - appDataConfigDirectory = Utils.GetUserConfigurationDirectory(); + perUserConfigFile = new FileInfo(Path.Combine(Utils.GetUserConfigurationDirectory(), configFileName)); } private string GetConfigFilePath(ConfigScope scope) { - string result = string.Empty; - - if (scope == ConfigScope.CurrentUser) + fileLock.EnterReadLock(); + try { - result = Path.Combine(appDataConfigDirectory, configFileName); + return (scope == ConfigScope.CurrentUser) ? perUserConfigFile.FullName : systemWideConfigFile.FullName; } - else + finally { - if (!string.IsNullOrEmpty(psCustomHomeConfigFilePath)) - { - // system wide configuration file overridden at the pwsh command-line. - result = psCustomHomeConfigFilePath; - } - else - { - result = Path.Combine(psHomeConfigDirectory, configFileName); - } + fileLock.ExitReadLock(); } - return result; } /// - /// Gets or sets the system wide configuration file path + /// Sets the system wide configuration file path /// - /// A fully qualified path to the system wide configuration file. - internal string SystemConfigFilePath + /// A fully qualified path to the system wide configuration file. + /// is a null reference or the associated file does not exist. + internal void SetSystemConfigFilePath(string value) { - get + if (!string.IsNullOrEmpty(value) && !File.Exists(value)) { - fileLock.EnterReadLock(); - try - { - return GetConfigFilePath(ConfigScope.SystemWide); - } - finally - { - fileLock.ExitReadLock(); - } + throw new FileNotFoundException(value); } - set + fileLock.EnterWriteLock(); + try { - if (!string.IsNullOrEmpty(value) && !File.Exists(value)) - { - throw new FileNotFoundException(value); - } - fileLock.EnterWriteLock(); - try - { - psCustomHomeConfigFilePath = value; - } - finally - { - fileLock.ExitWriteLock(); - } + systemWideConfigFile = new FileInfo(value); + } + finally + { + fileLock.ExitWriteLock(); } } @@ -173,7 +151,7 @@ internal void SetExecutionPolicy(ConfigScope scope, string shellId, string execu // host for display to the user. // CreateDirectory will succeed if the directory already exists // so there is no reason to check Directory.Exists(). - Directory.CreateDirectory(appDataConfigDirectory); + Directory.CreateDirectory(perUserConfigFile.Directory.FullName); } string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); @@ -372,14 +350,7 @@ internal PSKeyword GetLogKeywords() private T ReadValueFromFile(ConfigScope scope, string key, T defaultValue = default(T), Func readImpl = null) { - string fileName = GetConfigFilePath(scope); - return ReadValueFromFile(fileName, key, defaultValue, readImpl); - } - - private T ReadValueFromFile(string fileName, string key, T defaultValue = default(T), - Func readImpl = null) - { if (!File.Exists(fileName)) { return defaultValue; } // Open file for reading, but allow multiple readers @@ -417,21 +388,7 @@ internal PSKeyword GetLogKeywords() /// Whether the key-value pair should be added to or removed from the file private void UpdateValueInFile(ConfigScope scope, string key, T value, bool addValue) { - string fileName = GetConfigFilePath(scope); - UpdateValueInFile(fileName, key, value, addValue); - } - - /// - /// TODO: Should this return success fail or throw? - /// - /// - /// - /// - /// - /// Whether the key-value pair should be added to or removed from the file - private void UpdateValueInFile(string fileName, string key, T value, bool addValue) - { fileLock.EnterWriteLock(); try { @@ -526,8 +483,7 @@ private void UpdateValueInFile(string fileName, string key, T value, bool add } } - - /// + /// /// TODO: Should this return success, fail, or throw? /// /// The type of value to write. @@ -536,20 +492,7 @@ private void UpdateValueInFile(string fileName, string key, T value, bool add /// The value to write. private void WriteValueToFile(ConfigScope scope, string key, T value) { - string fileName = GetConfigFilePath(scope); - WriteValueToFile(fileName, key, value); - } - - /// - /// TODO: Should this return success, fail, or throw? - /// - /// - /// - /// - /// - private void WriteValueToFile(string fileName, string key, T value) - { - UpdateValueInFile(fileName, key, value, true); + UpdateValueInFile(scope, key, value, true); } /// @@ -561,21 +504,10 @@ private void WriteValueToFile(string fileName, string key, T value) private void RemoveValueFromFile(ConfigScope scope, string key) { string fileName = GetConfigFilePath(scope); - RemoveValueFromFile(fileName, key); - } - - /// - /// TODO: Should this return success, fail, or throw? - /// - /// - /// - /// - private void RemoveValueFromFile(string fileName, string key) - { // Optimization: If the file doesn't exist, there is nothing to remove if (File.Exists(fileName)) { - UpdateValueInFile(fileName, key, default(T), false); + UpdateValueInFile(scope, key, default(T), false); } } } From ca0df3ab0c7e2dae320bbbb4f23df4febfca8ff3 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 22 Jan 2018 10:50:38 -0800 Subject: [PATCH 07/10] Remove extra spaces in 'CommandLineParameterParser.cs' --- .../host/msh/CommandLineParameterParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs index 0ea786750ae..bb66d16ccd1 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/CommandLineParameterParser.cs @@ -882,7 +882,7 @@ private static string NormalizeFilePath(string path) { // Normalize slashes path = path.Replace(StringLiterals.AlternatePathSeparator, - StringLiterals.DefaultPathSeparator); + StringLiterals.DefaultPathSeparator); return Path.GetFullPath(path); } @@ -944,7 +944,7 @@ bool TryGetBoolValue(string arg, out bool boolValue) string exceptionMessage = null; try { - _file = NormalizeFilePath (args[i]); + _file = NormalizeFilePath(args[i]); } catch (Exception e) { From f35639567621c0abbcba48996c4aa85a7da0e2af Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Mon, 22 Jan 2018 12:23:13 -0800 Subject: [PATCH 08/10] PR feedback --- .../resources/ManagedEntranceStrings.resx | 3 +-- .../engine/PSConfiguration.cs | 26 ++++++++++++------- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx index ae3f247c2ec..67c74c04317 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx +++ b/src/Microsoft.PowerShell.ConsoleHost/resources/ManagedEntranceStrings.resx @@ -245,8 +245,7 @@ All parameters are case-insensitive. By default, system-wide settings are read from the powershell.config.json in the $PSHOME directory. - Note that per-user settings take precedence over system-wide settings. - Also note that these settings are not used by the endpoint specified + Note that these settings are not used by the endpoint specified by the -ConfigurationName argument. Example: pwsh -SettingsFile c:\myproject\powershell.config.json diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs index 97216f66ffb..aa3371b8761 100644 --- a/src/System.Management.Automation/engine/PSConfiguration.cs +++ b/src/System.Management.Automation/engine/PSConfiguration.cs @@ -30,10 +30,12 @@ internal sealed class PowerShellConfig // The json file containing system-wide configuration settings. // When passed as a pwsh command-line option, // overrides the system wide configuration file. - private FileInfo systemWideConfigFile; + private string systemWideConfigFile; + private string systemWideConfigDirectory; // The json file containing the per-user configuration settings. - private FileInfo perUserConfigFile; + private string perUserConfigFile; + private string perUserConfigDirectory; private const string configFileName = "powershell.config.json"; @@ -47,13 +49,15 @@ internal sealed class PowerShellConfig private PowerShellConfig() { // Sets the system-wide configuration file. - systemWideConfigFile = new FileInfo(Path.Combine(Utils.DefaultPowerShellAppBase, configFileName)); + systemWideConfigFile = Path.Combine(Utils.DefaultPowerShellAppBase, configFileName); + systemWideConfigDirectory = Utils.DefaultPowerShellAppBase; // Sets the per-user configuration directory // Note: This directory may or may not exist depending upon the // execution scenario. Writes will attempt to create the directory // if it does not already exist. - perUserConfigFile = new FileInfo(Path.Combine(Utils.GetUserConfigurationDirectory(), configFileName)); + perUserConfigDirectory = Utils.GetUserConfigurationDirectory(); + perUserConfigFile = Path.Combine(Utils.GetUserConfigurationDirectory(), configFileName); } private string GetConfigFilePath(ConfigScope scope) @@ -61,7 +65,7 @@ private string GetConfigFilePath(ConfigScope scope) fileLock.EnterReadLock(); try { - return (scope == ConfigScope.CurrentUser) ? perUserConfigFile.FullName : systemWideConfigFile.FullName; + return (scope == ConfigScope.CurrentUser) ? perUserConfigFile : systemWideConfigFile; } finally { @@ -70,10 +74,13 @@ private string GetConfigFilePath(ConfigScope scope) } /// - /// Sets the system wide configuration file path + /// Sets the system wide configuration file path. /// /// A fully qualified path to the system wide configuration file. /// is a null reference or the associated file does not exist. + /// + /// This method is for use when processing the -SettingsFile configuration setting and should not be used for any other purpose. + /// internal void SetSystemConfigFilePath(string value) { if (!string.IsNullOrEmpty(value) && !File.Exists(value)) @@ -83,7 +90,9 @@ internal void SetSystemConfigFilePath(string value) fileLock.EnterWriteLock(); try { - systemWideConfigFile = new FileInfo(value); + FileInfo info = new FileInfo(value); + systemWideConfigFile = info.FullName; + systemWideConfigDirectory = info.Directory.FullName; } finally { @@ -151,9 +160,8 @@ internal void SetExecutionPolicy(ConfigScope scope, string shellId, string execu // host for display to the user. // CreateDirectory will succeed if the directory already exists // so there is no reason to check Directory.Exists(). - Directory.CreateDirectory(perUserConfigFile.Directory.FullName); + Directory.CreateDirectory(perUserConfigDirectory); } - string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); WriteValueToFile(scope, valueName, executionPolicy); } From 4201bfb8b86e5437e448c9c76b920d9b89347ee1 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Mon, 22 Jan 2018 13:34:24 -0800 Subject: [PATCH 09/10] Minor update to PowerShellConfig constructor to avoid calling 'GetUserConfigurationDirectory' twice --- src/System.Management.Automation/engine/PSConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs index aa3371b8761..380a9e843cc 100644 --- a/src/System.Management.Automation/engine/PSConfiguration.cs +++ b/src/System.Management.Automation/engine/PSConfiguration.cs @@ -49,15 +49,15 @@ internal sealed class PowerShellConfig private PowerShellConfig() { // Sets the system-wide configuration file. - systemWideConfigFile = Path.Combine(Utils.DefaultPowerShellAppBase, configFileName); systemWideConfigDirectory = Utils.DefaultPowerShellAppBase; + systemWideConfigFile = Path.Combine(systemWideConfigDirectory, configFileName); // Sets the per-user configuration directory // Note: This directory may or may not exist depending upon the // execution scenario. Writes will attempt to create the directory // if it does not already exist. perUserConfigDirectory = Utils.GetUserConfigurationDirectory(); - perUserConfigFile = Path.Combine(Utils.GetUserConfigurationDirectory(), configFileName); + perUserConfigFile = Path.Combine(perUserConfigDirectory, configFileName); } private string GetConfigFilePath(ConfigScope scope) From 1b38501a1ebfe99a32bce8e7bc6525554430ce71 Mon Sep 17 00:00:00 2001 From: Dan Travison Date: Mon, 22 Jan 2018 14:25:22 -0800 Subject: [PATCH 10/10] [FEATURE] Remove lock from set and get methods --- .../engine/PSConfiguration.cs | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/System.Management.Automation/engine/PSConfiguration.cs b/src/System.Management.Automation/engine/PSConfiguration.cs index 380a9e843cc..8c60d6735d8 100644 --- a/src/System.Management.Automation/engine/PSConfiguration.cs +++ b/src/System.Management.Automation/engine/PSConfiguration.cs @@ -62,15 +62,7 @@ private PowerShellConfig() private string GetConfigFilePath(ConfigScope scope) { - fileLock.EnterReadLock(); - try - { - return (scope == ConfigScope.CurrentUser) ? perUserConfigFile : systemWideConfigFile; - } - finally - { - fileLock.ExitReadLock(); - } + return (scope == ConfigScope.CurrentUser) ? perUserConfigFile : systemWideConfigFile; } /// @@ -87,17 +79,9 @@ internal void SetSystemConfigFilePath(string value) { throw new FileNotFoundException(value); } - fileLock.EnterWriteLock(); - try - { - FileInfo info = new FileInfo(value); - systemWideConfigFile = info.FullName; - systemWideConfigDirectory = info.Directory.FullName; - } - finally - { - fileLock.ExitWriteLock(); - } + FileInfo info = new FileInfo(value); + systemWideConfigFile = info.FullName; + systemWideConfigDirectory = info.Directory.FullName; } ///