diff --git a/appveyor.yml b/appveyor.yml index f53c786d2..6caea8428 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -40,7 +40,7 @@ test_script: $env:CoreOutput = Split-Path -Parent (Get-PSOutput -Options (New-PSOptions -Publish)) Write-Host -Foreground Green 'Run CoreCLR tests' $testResultsFile = "$pwd\TestsResults.xml" - & ("$env:CoreOutput\powershell.exe") -noprofile -noninteractive -c "Invoke-Pester test/powershell -Tag 'CI' -ExcludeTag 'Slow' -OutputFormat NUnitXml -OutputFile $testResultsFile" + & ("$env:CoreOutput\powershell.exe") -noprofile -noninteractive -c "Set-ExecutionPolicy -Scope Process Unrestricted; Invoke-Pester test/powershell -Tag 'CI' -ExcludeTag 'Slow' -OutputFormat NUnitXml -OutputFile $testResultsFile" (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFile)) # @@ -48,7 +48,7 @@ test_script: $env:FullOutput = Split-Path -Parent (Get-PSOutput -Options (New-PSOptions -FullCLR)) Write-Host -Foreground Green 'Run FullCLR tests' $testResultsFileFullCLR = "$pwd\TestsResults.FullCLR.xml" - Start-DevPowerShell -FullCLR -NoNewWindow -ArgumentList '-noprofile', '-noninteractive' -Command "Invoke-Pester test/fullCLR -ExcludeTag 'Slow' -OutputFormat NUnitXml -OutputFile $testResultsFileFullCLR" + Start-DevPowerShell -FullCLR -NoNewWindow -ArgumentList '-noprofile', '-noninteractive' -Command "Set-ExecutionPolicy -Scope Process Unrestricted; Invoke-Pester test/fullCLR -ExcludeTag 'Slow' -OutputFormat NUnitXml -OutputFile $testResultsFileFullCLR" (New-Object 'System.Net.WebClient').UploadFile("https://ci.appveyor.com/api/testresults/nunit/$($env:APPVEYOR_JOB_ID)", (Resolve-Path $testResultsFileFullCLR)) # # Fail the build, if tests failed diff --git a/build.psm1 b/build.psm1 index ad44e0e92..dd850d1c8 100644 --- a/build.psm1 +++ b/build.psm1 @@ -267,11 +267,12 @@ function Start-PSBuild { Push-Location "$PSScriptRoot\src\powershell-native" # Compile native resources + $currentLocation = Get-Location @("nativemsh/pwrshplugin") | % { $nativeResourcesFolder = $_ Get-ChildItem $nativeResourcesFolder -Filter "*.mc" | % { $command = @" -cmd.exe /C cd /d "(Get-Location)" "&" "$($vcVarsPath)\vcvarsall.bat" "$NativeHostArch" "&" "$mcexe" -o -d -c -U $($_.FullName) -h "$nativeResourcesFolder" -r "$nativeResourcesFolder" +cmd.exe /C cd /d "$currentLocation" "&" "$($vcVarsPath)\vcvarsall.bat" "$NativeHostArch" "&" mc.exe -o -d -c -U $($_.FullName) -h "$nativeResourcesFolder" -r "$nativeResourcesFolder" "@ log " Executing mc.exe Command: $command" Start-NativeExecution { Invoke-Expression -Command:$command } @@ -526,6 +527,7 @@ function Start-PSPester { [string[]]$Tag = "CI", [string]$Path = "$PSScriptRoot/test/powershell" ) + $tagString = "-outputFormat ${OutputFormat} -outputFile ${outputFile} " if ( ! $DisableExit ) { $tagString += " -EnableExit" } if ( $ExcludeTag -and ($ExcludeTag -ne "")) { $tagString += " -ExcludeTag @('" + (${ExcludeTag} -join "','") + "')" } @@ -537,7 +539,7 @@ function Start-PSPester { Write-Verbose "Import-Module '$moduleDir'; Invoke-Pester $tagString $Path" $powershellexe = get-psoutput - & $powershell -noprofile -c "Import-Module '$moduleDir'; Invoke-Pester $tagString $Path" + & $powershell -noprofile -c "Set-ExecutionPolicy -Scope Process Unrestricted; Import-Module '$moduleDir'; Invoke-Pester $tagString $Path" if ($LASTEXITCODE -ne 0) { throw "$LASTEXITCODE Pester tests failed" } diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterfaceSecurity.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterfaceSecurity.cs index bff625594..1bf437fd4 100644 --- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterfaceSecurity.cs +++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHostUserInterfaceSecurity.cs @@ -170,58 +170,33 @@ namespace Microsoft.PowerShell // APIs private bool PromptUsingConsole() { -#if CORECLR // on Nano there is no other way to prompt except by using console +#if CORECLR + // on Nano there is no other way to prompt except by using console return true; #else - string PolicyKeyName = Utils.GetRegistryConfigurationPrefix(); - const string PromptValueName = "ConsolePrompting"; bool promptUsingConsole = false; - RegistryKey key; - - // Open the registry key that holds the configuration setting - try - { - key = Registry.LocalMachine.OpenSubKey(PolicyKeyName); - } - catch (System.Security.SecurityException) - { - s_tracer.TraceError("User doesn't have access to read CredUI registry key."); - return promptUsingConsole; - } - - if (key == null) - { - return promptUsingConsole; - } - // Get the configuration setting try { - object consolePromptingKey = key.GetValue(PromptValueName); - if (consolePromptingKey != null) { promptUsingConsole = Convert.ToBoolean(consolePromptingKey.ToString(), CultureInfo.InvariantCulture); } + promptUsingConsole = ConfigPropertyAccessor.Instance.GetConsolePrompting(); } catch (System.Security.SecurityException e) { s_tracer.TraceError("Could not read CredUI registry key: " + e.Message); - if (key != null) { key.Dispose(); } // No RegistryKey.Close() on CoreCLR return promptUsingConsole; } catch (InvalidCastException e) { s_tracer.TraceError("Could not parse CredUI registry key: " + e.Message); - if (key != null) { key.Dispose(); } // No RegistryKey.Close() on CoreCLR return promptUsingConsole; } catch (FormatException e) { s_tracer.TraceError("Could not parse CredUI registry key: " + e.Message); - if (key != null) { key.Dispose(); } // No RegistryKey.Close() on CoreCLR return promptUsingConsole; } s_tracer.WriteLine("DetermineCredUIPolicy: policy == {0}", promptUsingConsole); - - if (key != null) { key.Dispose(); } // No RegistryKey.Close() on CoreCLR return promptUsingConsole; #endif } diff --git a/src/System.Management.Automation/System.Management.Automation.csproj b/src/System.Management.Automation/System.Management.Automation.csproj index bd8f4e67c..51ab6aaf7 100644 --- a/src/System.Management.Automation/System.Management.Automation.csproj +++ b/src/System.Management.Automation/System.Management.Automation.csproj @@ -390,6 +390,7 @@ + diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs index a1a353766..5f205bce6 100644 --- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs +++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs @@ -641,7 +641,7 @@ namespace System.Management.Automation return null; } - private static string GetExpandedEnvironmentVariable(string name, EnvironmentVariableTarget target) + internal static string GetExpandedEnvironmentVariable(string name, EnvironmentVariableTarget target) { string result = Environment.GetEnvironmentVariable(name, target); if (!string.IsNullOrEmpty(result)) @@ -886,8 +886,8 @@ namespace System.Management.Automation internal static string SetModulePath() { string currentModulePath = GetExpandedEnvironmentVariable("PSMODULEPATH", EnvironmentVariableTarget.Process); - string systemWideModulePath = GetExpandedEnvironmentVariable("PSMODULEPATH", EnvironmentVariableTarget.Machine); - string personalModulePath = GetExpandedEnvironmentVariable("PSMODULEPATH", EnvironmentVariableTarget.User); + string systemWideModulePath = ConfigPropertyAccessor.Instance.GetModulePath(ConfigPropertyAccessor.PropertyScope.SystemWide); + string personalModulePath = ConfigPropertyAccessor.Instance.GetModulePath(ConfigPropertyAccessor.PropertyScope.CurrentUser); string newModulePathString = GetModulePath(currentModulePath, systemWideModulePath, personalModulePath); @@ -929,7 +929,7 @@ namespace System.Management.Automation if (processedPath != null) yield return processedPath; } - } + } if (includeSystemModulePath) { diff --git a/src/System.Management.Automation/engine/PropertyAccessor.cs b/src/System.Management.Automation/engine/PropertyAccessor.cs new file mode 100644 index 000000000..6a63302ad --- /dev/null +++ b/src/System.Management.Automation/engine/PropertyAccessor.cs @@ -0,0 +1,797 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Xml; +using System.IO; +using System.Text; +using System.Reflection; +using System.Globalization; +using System.Threading; + +using System.Management.Automation; +using Microsoft.Win32; + +#if CORECLR +// Some APIs are missing from System.Environment. We use System.Management.Automation.Environment as a proxy type: +// - for missing APIs, System.Management.Automation.Environment has extension implementation. +// - for existing APIs, System.Management.Automation.Environment redirect the call to System.Environment. +using Environment = System.Management.Automation.Environment; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +#endif + +namespace System.Management.Automation +{ + + /// + /// Leverages the strategy pattern to abstract away the details of gathering properties from outside sources. + /// Note: This is a class so that it can be internal. + /// + internal abstract class ConfigPropertyAccessor + { + #region Statics + /// + /// Static constructor to instantiate an instance + /// + static ConfigPropertyAccessor() + { +#if CORECLR + // TODO: Update this for #1184 + //if (Platform.IsInbox()) + //{ + // Instance = new RegistryAccessor(); + //} + //else + //{ + // Instance = new JsonConfigFileAccessor(); + //} + // Remove this following line: + Instance = new JsonConfigFileAccessor(); +#else + Instance = new RegistryAccessor(); +#endif + } + /// + /// The instance of the ConfigPropertyAccessor to use to iteract with properties. + /// Derived classes should not be directly instantiated. + /// + internal static readonly ConfigPropertyAccessor Instance; + + #endregion // Statics + + #region Enums + + /// + /// Describes the scope of the property query. + /// SystemWide properties apply to all users. + /// CurrentUser properties apply to the current user that is impersonated. + /// + internal enum PropertyScope + { + SystemWide = 0, + CurrentUser = 1 + } + + #endregion // Enums + + #region Interface Methods + + /// + /// Existing Key = HKLM:\System\CurrentControlSet\Control\Session Manager\Environment + /// Proposed value = %ProgramFiles%\PowerShell\Modules by default + /// + /// Note: There is no setter because this value is immutable. + /// + /// Module path values from the config file. + internal abstract string GetModulePath(PropertyScope scope); + + /// + /// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell + /// Proposed value = Existing default execution policy if not already specified + /// + /// Where it should check for the value. + /// The shell associated with this policy. Typically, it is "Microsoft.PowerShell" + /// + internal abstract string GetExecutionPolicy(PropertyScope scope, string shellId); + internal abstract void RemoveExecutionPolicy(PropertyScope scope, string shellId); + internal abstract void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy); + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds + /// Proposed value = existing default. Probably "1" + /// + /// Whether console prompting should happen. + internal abstract bool GetConsolePrompting(); + internal abstract void SetConsolePrompting(bool shouldPrompt); + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell + /// Proposed value = Existing default. Probably "0" + /// + /// Boolean indicating whether Update-Help should prompt + internal abstract bool GetDisablePromptToUpdateHelp(); + internal abstract void SetDisablePromptToUpdateHelp(bool prompt); + + /// + /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp + /// Proposed value = blank.This should be supported though + /// + /// + internal abstract string GetDefaultSourcePath(); + internal abstract void SetDefaultSourcePath(string defaultPath); + + #endregion // Interface Methods + } + +#if CORECLR + /// + /// JSON configuration file accessor + /// + /// Reads from and writes to configuration files. The values stored were + /// originally stored in the Windows registry. + /// + internal class JsonConfigFileAccessor : ConfigPropertyAccessor + { + private string psHomeConfigDirectory; + private string appDataConfigDirectory; + private const string configDirectoryName = "Configuration"; + private const string configFileName = "PowerShellProperties.json"; + + /// + /// Lock used to enable multiple concurrent readers and singular write locks within a + /// single process. + /// TODO: This solution only works for IO from a single process. A more robust solution is needed to enable ReaderWriterLockSlim behavior between proceses. + /// + private ReaderWriterLockSlim fileLock = new ReaderWriterLockSlim(); + + internal JsonConfigFileAccessor() + { + // + // Initialize (and create if necessary) the system-wide configuration directory + // + Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly; + psHomeConfigDirectory = Path.Combine(Path.GetDirectoryName(assembly.Location), configDirectoryName); + + if (!Directory.Exists(psHomeConfigDirectory)) + { + Directory.CreateDirectory(psHomeConfigDirectory); + } + + // + // Initialize (and create if necessary) the per-user configuration directory + // + appDataConfigDirectory = Path.Combine(Utils.GetUserSettingsDirectory(), configDirectoryName); + + if (!Directory.Exists(appDataConfigDirectory)) + { + Directory.CreateDirectory(appDataConfigDirectory); + } + } + + /// + /// This value is not writable via the API and must be set using a text editor. + /// + /// + /// Value if found, null otherwise. The behavior matches ModuleIntrinsics.GetExpandedEnvironmentVariable(). + internal override string GetModulePath(PropertyScope scope) + { + string scopeDirectory = psHomeConfigDirectory; + + // Defaults to system wide. + if (PropertyScope.CurrentUser == scope) + { + scopeDirectory = appDataConfigDirectory; + } + + string fileName = Path.Combine(scopeDirectory, configFileName); + + string modulePath = ReadValueFromFile(fileName, "PsModulePath"); + if (!string.IsNullOrEmpty(modulePath)) + { + modulePath = Environment.ExpandEnvironmentVariables(modulePath); + } + return modulePath; + } + + /// + /// Existing Key = HKCU and HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell + /// Proposed value = Existing default execution policy if not already specified + /// + /// Schema: + /// { + /// "shell ID string","ExecutionPolicy" : "execution policy string" + /// } + /// + /// TODO: In a single config file, it might be better to nest this. It is unnecessary complexity until a need arises for more nested values. + /// + /// Whether this is a system-wide or per-user setting. + /// The shell associated with this policy. Typically, it is "Microsoft.PowerShell" + /// The execution policy if found. Null otherwise. + internal override string GetExecutionPolicy(PropertyScope scope, string shellId) + { + string execPolicy = null; + string scopeDirectory = psHomeConfigDirectory; + + // Defaults to system wide. + if(PropertyScope.CurrentUser == scope) + { + scopeDirectory = appDataConfigDirectory; + } + + string fileName = Path.Combine(scopeDirectory, configFileName); + string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); + string rawExecPolicy = ReadValueFromFile(fileName, valueName); + + if (!String.IsNullOrEmpty(rawExecPolicy)) + { + execPolicy = rawExecPolicy; + } + return execPolicy; + } + + internal override void RemoveExecutionPolicy(PropertyScope scope, string shellId) + { + string scopeDirectory = psHomeConfigDirectory; + + // Defaults to system wide. + if (PropertyScope.CurrentUser == scope) + { + scopeDirectory = appDataConfigDirectory; + } + + string fileName = Path.Combine(scopeDirectory, configFileName); + string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); + RemoveValueFromFile(fileName, valueName); + } + + internal override void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy) + { + string scopeDirectory = psHomeConfigDirectory; + + // Defaults to system wide. + if (PropertyScope.CurrentUser == scope) + { + scopeDirectory = appDataConfigDirectory; + } + + string fileName = Path.Combine(scopeDirectory, configFileName); + string valueName = string.Concat(shellId, ":", "ExecutionPolicy"); + WriteValueToFile(fileName, valueName, executionPolicy); + } + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds + /// Proposed value = existing default. Probably "1" + /// + /// Schema: + /// { + /// "ConsolePrompting" : bool + /// } + /// + /// Whether console prompting should happen. If the value cannot be read it defaults to false. + internal override bool GetConsolePrompting() + { + string fileName = Path.Combine(psHomeConfigDirectory, configFileName); + return ReadValueFromFile(fileName, "ConsolePrompting"); + } + + internal override void SetConsolePrompting(bool shouldPrompt) + { + string fileName = Path.Combine(psHomeConfigDirectory, configFileName); + WriteValueToFile(fileName, "ConsolePrompting", shouldPrompt); + } + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell + /// Proposed value = Existing default. Probably "0" + /// + /// Schema: + /// { + /// "DisablePromptToUpdateHelp" : bool + /// } + /// + /// Boolean indicating whether Update-Help should prompt. If the value cannot be read, it defaults to false. + internal override bool GetDisablePromptToUpdateHelp() + { + string fileName = Path.Combine(psHomeConfigDirectory, configFileName); + return ReadValueFromFile(fileName, "DisablePromptToUpdateHelp"); + } + + internal override void SetDisablePromptToUpdateHelp(bool prompt) + { + string fileName = Path.Combine(psHomeConfigDirectory, configFileName); + WriteValueToFile(fileName, "DisablePromptToUpdateHelp", prompt); + } + + /// + /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp + /// Proposed value = blank.This should be supported though + /// + /// Schema: + /// { + /// "DefaultSourcePath" : "path to local updatable help location" + /// } + /// + /// The source path if found, null otherwise. + internal override string GetDefaultSourcePath() + { + string fileName = Path.Combine(psHomeConfigDirectory, configFileName); + + string rawExecPolicy = ReadValueFromFile(fileName, "DefaultSourcePath"); + + if (!String.IsNullOrEmpty(rawExecPolicy)) + { + return rawExecPolicy; + } + return null; + } + + internal override void SetDefaultSourcePath(string defaultPath) + { + string fileName = Path.Combine(psHomeConfigDirectory, configFileName); + + WriteValueToFile(fileName, "DefaultSourcePath", defaultPath); + } + + private T ReadValueFromFile(string fileName, string key) + { + fileLock.EnterReadLock(); + try + { + // Open file for reading, but allow multiple readers + using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.Read)) + using (StreamReader streamRdr = new StreamReader(fs)) + using (JsonTextReader jsonReader = new JsonTextReader(streamRdr)) + { + JObject jsonObject = (JObject) JToken.ReadFrom(jsonReader); + JToken value = jsonObject.GetValue(key); + if (null != value) + { + return value.ToObject(); + } + } + } + catch (FileNotFoundException) + { + // The file doesn't exist. Treat this the same way as if the + // key was not present in the file. + } + finally + { + fileLock.ExitReadLock(); + } + + return default(T); + } + + /// + /// 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 + { + // Since multiple properties can be in a single file, replacement + // is required instead of overwrite if a file already exists. + // Handling the read and write operations within a single FileStream + // prevents other processes from reading or writing the file while + // the update is in progress. It also locks out readers during write + // operations. + using (FileStream fs = new FileStream(fileName, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) + { + JObject jsonObject = null; + + // UTF8, BOM detection, and bufferSize are the same as the basic stream constructor. + // The most important parameter here is the last one, which keeps the StreamReader + // (and FileStream) open during Dispose so that it can be reused for the write + // operation. + using (StreamReader streamRdr = new StreamReader(fs, Encoding.UTF8, true, 1024, true)) + using (JsonTextReader jsonReader = new JsonTextReader(streamRdr)) + { + // Safely determines whether there is content to read from the file + bool isReadSuccess = jsonReader.Read(); + if (isReadSuccess) + { + // Read the stream into a root JObject for manipulation + jsonObject = (JObject) JToken.ReadFrom(jsonReader); + JProperty propertyToModify = jsonObject.Property(key); + + if (null == propertyToModify) + { + // The property doesn't exist, so add it + if (addValue) + { + jsonObject.Add(new JProperty(key, value)); + } + // else the property doesn't exist so there is nothing to remove + } + // The property exists + else + { + if (addValue) + { + propertyToModify.Replace(new JProperty(key, value)); + } + else + { + propertyToModify.Remove(); + } + } + } + else + { + // The file doesn't already exist and we want to write to it + // or it exists with no content. + // A new file will be created that contains only this value. + // If the file doesn't exist and a we don't want to write to it, no + // action is necessary. + if (addValue) + { + jsonObject = new JObject(new JProperty(key, value)); + } + else + { + return; + } + } + } + + // Reset the stream position to the beginning so that the + // changes to the file can be written to disk + fs.Seek(0, SeekOrigin.Begin); + + // Update the file with new content + using (StreamWriter streamWriter = new StreamWriter(fs)) + using (JsonTextWriter jsonWriter = new JsonTextWriter(streamWriter)) + { + // The entire document exists within the root JObject. + // I just need to write that object to produce the document. + jsonObject.WriteTo(jsonWriter); + + // This trims the file if the file shrank. If the file grew, + // it is a no-op. The purpose is to trim extraneous characters + // from the file stream when the resultant JObject is smaller + // than the input JObject. + fs.SetLength(fs.Position); + } + } + } + finally + { + fileLock.ExitWriteLock(); + } + } + + /// + /// TODO: Should this return success, fail, or throw? + /// + /// + /// + /// + /// + private void WriteValueToFile(string fileName, string key, T value) + { + UpdateValueInFile(fileName, key, value, true); + } + + /// + /// TODO: Should this return success, fail, or throw? + /// + /// + /// + /// + private void RemoveValueFromFile(string fileName, string key) + { + UpdateValueInFile(fileName, key, default(T), false); + } + } + +#endif // CORECLR + + internal class RegistryAccessor : ConfigPropertyAccessor + { + private const string DisablePromptToUpdateHelpRegPath = "Software\\Microsoft\\PowerShell"; + private const string DisablePromptToUpdateHelpRegPath32 = "Software\\Wow6432Node\\Microsoft\\PowerShell"; + private const string DisablePromptToUpdateHelpRegKey = "DisablePromptToUpdateHelp"; + private const string DefaultSourcePathRegPath = "Software\\Policies\\Microsoft\\Windows\\PowerShell\\UpdatableHelp"; + private const string DefaultSourcePathRegKey = "DefaultSourcePath"; + + internal RegistryAccessor() + { + } + + /// + /// Gets the specified module path from the appropriate Environment entry in the registry. + /// + /// + /// The specified module path. Null if not present. + internal override string GetModulePath(PropertyScope scope) + { + if (PropertyScope.CurrentUser == scope) + { + return ModuleIntrinsics.GetExpandedEnvironmentVariable("PSMODULEPATH", EnvironmentVariableTarget.User); + } + else + { + return ModuleIntrinsics.GetExpandedEnvironmentVariable("PSMODULEPATH", EnvironmentVariableTarget.Machine); + } + } + + /// + /// + /// + /// + /// + /// The execution policy string if found, otherwise null. + internal override string GetExecutionPolicy(PropertyScope scope, string shellId) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + RegistryKey scopedKey = Registry.LocalMachine; + + // Override if set to another value; + if (PropertyScope.CurrentUser == scope) + { + scopedKey = Registry.CurrentUser; + } + + return GetRegistryString(scopedKey, regKeyName, "ExecutionPolicy"); + } + + internal override void SetExecutionPolicy(PropertyScope scope, string shellId, string executionPolicy) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + RegistryKey scopedKey = Registry.LocalMachine; + + // Override if set to another value; + if (PropertyScope.CurrentUser == scope) + { + scopedKey = Registry.CurrentUser; + } + + using (RegistryKey key = scopedKey.CreateSubKey(regKeyName)) + { + if (null != key) + { + key.SetValue("ExecutionPolicy", executionPolicy, RegistryValueKind.String); + } + } + } + + internal override void RemoveExecutionPolicy(PropertyScope scope, string shellId) + { + string regKeyName = Utils.GetRegistryConfigurationPath(shellId); + RegistryKey scopedKey = Registry.LocalMachine; + + // Override if set to another value; + if (PropertyScope.CurrentUser == scope) + { + scopedKey = Registry.CurrentUser; + } + + using (RegistryKey key = scopedKey.OpenSubKey(regKeyName, true)) + { + if (key != null) + { + if (key.GetValue("ExecutionPolicy") != null) + key.DeleteValue("ExecutionPolicy"); + } + } + } + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell\1\ShellIds + /// Proposed value = existing default. Probably "1" + /// + /// Whether console prompting should happen. + internal override bool GetConsolePrompting() + { + string policyKeyName = Utils.GetRegistryConfigurationPrefix(); + string tempPrompt = GetRegistryString(Registry.LocalMachine, policyKeyName, "ConsolePrompting"); + + if (null != tempPrompt) + { + return Convert.ToBoolean(tempPrompt, CultureInfo.InvariantCulture); + } + else + { + return false; + } + } + + internal override void SetConsolePrompting(bool shouldPrompt) + { + string policyKeyName = Utils.GetRegistryConfigurationPrefix(); + SetRegistryString(Registry.LocalMachine, policyKeyName, "ConsolePrompting", shouldPrompt.ToString()); + } + + /// + /// Existing Key = HKLM\SOFTWARE\Microsoft\PowerShell + /// Proposed value = Existing default. Probably "0" + /// + /// Boolean indicating whether Update-Help should prompt + internal override bool GetDisablePromptToUpdateHelp() + { + using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath)) + { + if (hklm != null) + { + object disablePromptToUpdateHelp = hklm.GetValue(DisablePromptToUpdateHelpRegKey, null, RegistryValueOptions.None); + + if (disablePromptToUpdateHelp == null) + { + return true; + } + else + { + int result; + + if (LanguagePrimitives.TryConvertTo(disablePromptToUpdateHelp, out result)) + { + return (result != 1); + } + + return true; + } + } + else + { + return true; + } + } + } + + internal override void SetDisablePromptToUpdateHelp(bool prompt) + { + int valueToSet = prompt ? 1 : 0; + using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath, true)) + { + if (hklm != null) + { + hklm.SetValue(DisablePromptToUpdateHelpRegKey, valueToSet, RegistryValueKind.DWord); + } + } + + using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath32, true)) + { + if (hklm != null) + { + hklm.SetValue(DisablePromptToUpdateHelpRegKey, valueToSet, RegistryValueKind.DWord); + } + } + } + + /// + /// Existing Key = HKCU and HKLM\Software\Policies\Microsoft\Windows\PowerShell\UpdatableHelp + /// Proposed value = blank.This should be supported though + /// + /// + internal override string GetDefaultSourcePath() + { + return GetRegistryString(Registry.LocalMachine, DefaultSourcePathRegPath, DefaultSourcePathRegKey); + } + + internal override void SetDefaultSourcePath(string defaultPath) + { + SetRegistryString(Registry.LocalMachine, DefaultSourcePathRegPath, DefaultSourcePathRegKey, defaultPath); + } + + /// + /// Reads a DWORD from the Registry. Excpetions are intentionally allowed to pass through to + /// the caller because different classes and methods within the code base handle Registry + /// exceptions differently. Some suppress exceptions and others pass them to the user. + /// + /// + /// + /// + /// + private int? GetRegistryDword(RegistryKey rootKey, string pathToKey, string valueName) + { + using (RegistryKey regKey = rootKey.OpenSubKey(pathToKey)) + { + if (null == regKey) + { + // Key not found + return null; + } + + // verify the value kind as a string + RegistryValueKind kind = regKey.GetValueKind(valueName); + + if (kind == RegistryValueKind.DWord) + { + return regKey.GetValue(valueName) as int?; + } + else + { + // The function expected a DWORD, but got another type. This is a coding error or a registry key typing error. + return null; + } + } + } + + /// + /// Excpetions are intentionally allowed to pass through to + /// the caller because different classes and methods within the code base handle Registry + /// exceptions differently. Some suppress exceptions and others pass them to the user. + /// + /// + /// + /// + /// + private void SetRegistryDword(RegistryKey rootKey, string pathToKey, string valueName, int value) + { + using (RegistryKey regKey = rootKey.OpenSubKey(pathToKey)) + { + if (null != regKey) + { + regKey.SetValue(valueName, value, RegistryValueKind.DWord); + } + } + } + + /// + /// Excpetions are intentionally allowed to pass through to + /// the caller because different classes and methods within the code base handle Registry + /// exceptions differently. Some suppress exceptions and others pass them to the user. + /// + /// + /// + /// + /// + private string GetRegistryString(RegistryKey rootKey, string pathToKey, string valueName) + { + using (RegistryKey regKey = rootKey.OpenSubKey(pathToKey)) + { + if (null == regKey) + { + // Key not found + return null; + } + + object regValue = regKey.GetValue(valueName); + if (null != regValue) + { + // verify the value kind as a string + RegistryValueKind kind = regKey.GetValueKind(valueName); + + if (kind == RegistryValueKind.ExpandString || + kind == RegistryValueKind.String) + { + return regValue as string; + } + } + + // The function expected a string, but got another type or the value doesn't exist. + return null; + } + } + + /// + /// Excpetions are intentionally allowed to pass through to + /// the caller because different classes and methods within the code base handle Registry + /// exceptions differently. Some suppress exceptions and others pass them to the user. + /// + /// + /// + /// + /// + /// + private void SetRegistryString(RegistryKey rootKey, string pathToKey, string valueName, string value) + { + using (RegistryKey key = rootKey.CreateSubKey(pathToKey)) + { + if (null != key) + { + key.SetValue(valueName, value, RegistryValueKind.String); + } + } + } + } +} // Namespace System.Management.Automation + diff --git a/src/System.Management.Automation/engine/Utils.cs b/src/System.Management.Automation/engine/Utils.cs index 8fb2d8b04..3ba32c7bf 100644 --- a/src/System.Management.Automation/engine/Utils.cs +++ b/src/System.Management.Automation/engine/Utils.cs @@ -237,24 +237,22 @@ namespace System.Management.Automation /// internal static string GetApplicationBase(string shellId) { - // TODO: #1184 will resolve this work-around - // The application base cannot be resolved from the registry for side-by-side versions - // of PowerShell. -#if !CORECLR +#if CORECLR + // Use the location of SMA.dll as the application base + // Assembly.GetEntryAssembly and GAC are not in CoreCLR. + Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly; + return Path.GetDirectoryName(assembly.Location); +#else + // This code path applies to Windows FullCLR inbox deployments. All CoreCLR + // implementations should use the location of SMA.dll since it must reside in PSHOME. + // // try to get the path from the registry first string result = GetApplicationBaseFromRegistry(shellId); if (result != null) { return result; } -#endif - - -#if CORECLR // Use the location of SMA.dll as the application base - // Assembly.GetEntryAssembly is not in CoreCLR. GAC is not in CoreCLR. - Assembly assembly = typeof(PSObject).GetTypeInfo().Assembly; - return Path.GetDirectoryName(assembly.Location); -#else + // The default keys aren't installed, so try and use the entry assembly to // get the application base. This works for managed apps like minishells... Assembly assem = Assembly.GetEntryAssembly(); @@ -281,6 +279,20 @@ namespace System.Management.Automation private static string[] s_productFolderDirectories; + /// + /// Specifies the per-user configuration settings directory in a platform agnostic manner. + /// Windows Ex: + /// %LOCALAPPDATA%\PowerShell + /// Non-Windows Ex: + /// ~/.config/PowerShell + /// + /// The current user's configuration settings directory + internal static string GetUserSettingsDirectory() + { + string appDataPath = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + return Path.Combine(appDataPath, "PowerShell"); + } + private static string[] GetProductFolderDirectories() { if (s_productFolderDirectories == null) diff --git a/src/System.Management.Automation/help/UpdatableHelpSystem.cs b/src/System.Management.Automation/help/UpdatableHelpSystem.cs index fc7111cbc..ad7d4c866 100644 --- a/src/System.Management.Automation/help/UpdatableHelpSystem.cs +++ b/src/System.Management.Automation/help/UpdatableHelpSystem.cs @@ -238,12 +238,6 @@ namespace System.Management.Automation.Help /// internal class UpdatableHelpSystem : IDisposable { - internal const string DisablePromptToUpdateHelpRegPath = "Software\\Microsoft\\PowerShell"; - internal const string DisablePromptToUpdateHelpRegPath32 = "Software\\Wow6432Node\\Microsoft\\PowerShell"; - internal const string DisablePromptToUpdateHelpRegKey = "DisablePromptToUpdateHelp"; - internal const string DefaultSourcePathRegPath = "Software\\Policies\\Microsoft\\Windows\\PowerShell\\UpdatableHelp"; - internal const string DefaultSourcePathRegKey = "DefaultSourcePath"; - #if CORECLR private TimeSpan _defaultTimeout; #else @@ -1601,25 +1595,12 @@ namespace System.Management.Automation.Help { try { - using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DefaultSourcePathRegPath)) - { - if (hklm != null) - { - object defaultSourcePath = hklm.GetValue(DefaultSourcePathRegKey, null, RegistryValueOptions.None); - - if (defaultSourcePath != null) - { - return defaultSourcePath as string; - } - } - } + return ConfigPropertyAccessor.Instance.GetDefaultSourcePath(); } catch (SecurityException) { return null; } - - return null; } /// @@ -1629,21 +1610,7 @@ namespace System.Management.Automation.Help { try { - using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath, true)) - { - if (hklm != null) - { - hklm.SetValue(DisablePromptToUpdateHelpRegKey, 1, RegistryValueKind.DWord); - } - } - - using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath32, true)) - { - if (hklm != null) - { - hklm.SetValue(DisablePromptToUpdateHelpRegKey, 1, RegistryValueKind.DWord); - } - } + ConfigPropertyAccessor.Instance.SetDisablePromptToUpdateHelp(true); } catch (UnauthorizedAccessException) { @@ -1668,33 +1635,7 @@ namespace System.Management.Automation.Help return false; } - using (RegistryKey hklm = Registry.LocalMachine.OpenSubKey(DisablePromptToUpdateHelpRegPath)) - { - if (hklm != null) - { - object disablePromptToUpdateHelp = hklm.GetValue(DisablePromptToUpdateHelpRegKey, null, RegistryValueOptions.None); - - if (disablePromptToUpdateHelp == null) - { - return true; - } - else - { - int result; - - if (LanguagePrimitives.TryConvertTo(disablePromptToUpdateHelp, out result)) - { - return (result != 1); - } - - return true; - } - } - else - { - return true; - } - } + return ConfigPropertyAccessor.Instance.GetDisablePromptToUpdateHelp(); } catch (SecurityException) { diff --git a/src/System.Management.Automation/project.json b/src/System.Management.Automation/project.json index 0d6ee12b6..f60feb16c 100644 --- a/src/System.Management.Automation/project.json +++ b/src/System.Management.Automation/project.json @@ -144,6 +144,7 @@ "Microsoft.CSharp": "4.0.1", "Microsoft.Win32.Registry.AccessControl": "4.0.0", + "Newtonsoft.Json": "7.0.1", "System.Collections.Specialized": "4.0.1", "System.Collections.NonGeneric": "4.0.1", "System.ComponentModel.EventBasedAsync": "4.0.11", diff --git a/src/System.Management.Automation/security/SecuritySupport.cs b/src/System.Management.Automation/security/SecuritySupport.cs index 9da5da10e..f2ec5051a 100644 --- a/src/System.Management.Automation/security/SecuritySupport.cs +++ b/src/System.Management.Automation/security/SecuritySupport.cs @@ -146,7 +146,6 @@ namespace System.Management.Automation.Internal #else string executionPolicy = "Restricted"; string preferenceKey = Utils.GetRegistryConfigurationPath(shellId); - const string PolicyKeyValueName = "ExecutionPolicy"; switch (policy) { @@ -166,67 +165,43 @@ namespace System.Management.Automation.Internal switch (scope) { case ExecutionPolicyScope.Process: - { - if (policy == ExecutionPolicy.Undefined) - executionPolicy = null; + { + if (policy == ExecutionPolicy.Undefined) + executionPolicy = null; - Environment.SetEnvironmentVariable("PSExecutionPolicyPreference", executionPolicy); - break; - } + Environment.SetEnvironmentVariable("PSExecutionPolicyPreference", executionPolicy); + break; + } case ExecutionPolicyScope.CurrentUser: + { + // They want to remove it + if (policy == ExecutionPolicy.Undefined) { - // They want to remove it - if (policy == ExecutionPolicy.Undefined) - { - using (RegistryKey key = Registry.CurrentUser.OpenSubKey(preferenceKey, true)) - { - if (key != null) - { - if (key.GetValue(PolicyKeyValueName) != null) - key.DeleteValue(PolicyKeyValueName); - } - } - - CleanKeyParents(Registry.CurrentUser, preferenceKey); - } - else - { - using (RegistryKey key = Registry.CurrentUser.CreateSubKey(preferenceKey)) - { - key.SetValue(PolicyKeyValueName, executionPolicy, RegistryValueKind.String); - } - } - - break; + ConfigPropertyAccessor.Instance.RemoveExecutionPolicy(ConfigPropertyAccessor.PropertyScope.CurrentUser, shellId); + CleanKeyParents(Registry.CurrentUser, preferenceKey); } + else + { + ConfigPropertyAccessor.Instance.SetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.CurrentUser, shellId, executionPolicy); + } + break; + } case ExecutionPolicyScope.LocalMachine: + { + // They want to remove it + if (policy == ExecutionPolicy.Undefined) { - // They want to remove it - if (policy == ExecutionPolicy.Undefined) - { - using (RegistryKey key = Registry.LocalMachine.OpenSubKey(preferenceKey, true)) - { - if (key != null) - { - if (key.GetValue(PolicyKeyValueName) != null) - key.DeleteValue(PolicyKeyValueName); - } - } - - CleanKeyParents(Registry.LocalMachine, preferenceKey); - } - else - { - using (RegistryKey key = Registry.LocalMachine.CreateSubKey(preferenceKey)) - { - key.SetValue(PolicyKeyValueName, executionPolicy, RegistryValueKind.String); - } - } - - break; + ConfigPropertyAccessor.Instance.RemoveExecutionPolicy(ConfigPropertyAccessor.PropertyScope.SystemWide, shellId); + CleanKeyParents(Registry.LocalMachine, preferenceKey); } + else + { + ConfigPropertyAccessor.Instance.SetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.SystemWide, shellId, executionPolicy); + } + break; + } } #endif } @@ -235,6 +210,10 @@ namespace System.Management.Automation.Internal // contain at most a single child private static void CleanKeyParents(RegistryKey baseKey, string keyPath) { +#if CORECLR + // if ( ! Platform.Inbox) // Modify this to support registry checks for inbox CoreCLR + return; +#else // Windows && FullCLR using (RegistryKey key = baseKey.OpenSubKey(keyPath, true)) { // Verify the child key has no children @@ -269,6 +248,7 @@ namespace System.Management.Automation.Internal return; } } +#endif } internal static ExecutionPolicy GetExecutionPolicy(string shellId) @@ -311,6 +291,8 @@ namespace System.Management.Automation.Internal return ExecutionPolicy.Undefined; } + // TODO: Group Policy is only supported on Full systems, but !LINUX && CORECLR + // will run there as well, so I don't think we should remove it. case ExecutionPolicyScope.UserPolicy: case ExecutionPolicyScope.MachinePolicy: { @@ -604,48 +586,21 @@ namespace System.Management.Automation.Internal /// NULL if it is not defined at this level private static string GetLocalPreferenceValue(string shellId, ExecutionPolicyScope scope) { - const string PolicyValueName = "ExecutionPolicy"; - string LocalPreferenceKey = Utils.GetRegistryConfigurationPath(shellId); - switch (scope) { // 1: Look up the current-user preference case ExecutionPolicyScope.CurrentUser: - { - using (RegistryKey key = Registry.CurrentUser.OpenSubKey(LocalPreferenceKey)) - { - if (key != null) - { - object temp = key.GetValue(PolicyValueName); - string policy = temp as string; - key.Dispose(); + return ConfigPropertyAccessor.Instance.GetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.CurrentUser, shellId); - return policy; - } - } - }; break; - - // 1: Look up the system-wide preference + // 2: Look up the system-wide preference case ExecutionPolicyScope.LocalMachine: - { - using (RegistryKey key = Registry.LocalMachine.OpenSubKey(LocalPreferenceKey)) - { - if (key != null) - { - object temp = key.GetValue(PolicyValueName); - string policy = temp as string; - key.Dispose(); - - return policy; - } - } - }; break; + return ConfigPropertyAccessor.Instance.GetExecutionPolicy(ConfigPropertyAccessor.PropertyScope.SystemWide, shellId); } return null; } - #endregion execution policy +#endregion execution policy /// /// throw if file does not exist diff --git a/src/powershell-native/CMakeLists.txt b/src/powershell-native/CMakeLists.txt index 7a7d72a53..dd039341f 100644 --- a/src/powershell-native/CMakeLists.txt +++ b/src/powershell-native/CMakeLists.txt @@ -40,7 +40,7 @@ endif (BUILD_ONECORE) foreach(OUTPUTCONFIG ${CMAKE_CONFIGURATION_TYPES}) string(TOUPPER ${OUTPUTCONFIG} OUTPUTCONFIG) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG} "${PROJECT_SOURCE_DIR}/Bin/${OUTPUTCONFIG}/${PWRSH_NATIVE_OUTPUT_DIRECTORY}") - message(" Setting output directory for ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG}}") + # message(" Setting output directory for ${CMAKE_RUNTIME_OUTPUT_DIRECTORY_${OUTPUTCONFIG}}") endforeach( OUTPUTCONFIG CMAKE_CONFIGURATION_TYPES ) # diff --git a/src/powershell-native/nativemsh/pwrshcommon/CMakeLists.txt b/src/powershell-native/nativemsh/pwrshcommon/CMakeLists.txt index 24fd4afe4..d8170fa41 100644 --- a/src/powershell-native/nativemsh/pwrshcommon/CMakeLists.txt +++ b/src/powershell-native/nativemsh/pwrshcommon/CMakeLists.txt @@ -11,14 +11,12 @@ add_library(pwrshcommon target_include_directories(pwrshcommon PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) if (BUILD_ONECORE) - message("Building OneCore version of pwrshcommon.lib") # Libraries to use when creating this binary for Windows on OneCore-based SKUs set(PWRSHCOMMON_WINDOWS_LIBS # onecore.lib ) set_target_properties(pwrshcommon PROPERTIES COMPILE_DEFINITIONS "CORECLR") else () # NOT BUILD_ONECORE - message("Building Full version of pwrshcommon.lib") # Libraries to use when creating this binary for Windows on full SKUs # Note: The appropriate libs get added automatically by VS set(PWRSHCOMMON_WINDOWS_LIBS