-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Place System-Wide Config in the Machine Folder for MSIX Packages #27632
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jshigetomi
wants to merge
18
commits into
PowerShell:master
Choose a base branch
from
jshigetomi:perMachineDataStore
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
bfe8963
Add per-machine data store (appdata:MachineFolder) to MSIX manifest
63d9e69
Relocate AllUsers paths to per-machine data store when packaged
1dcbef3
Guard per-machine data store fields with #if !UNIX to fix CS0169 buil…
079e746
Remove redundant per-machine store Modules path from PSModulePath
6a1e674
Defer AllUsers updatable-help redirection to a separate PR
f054049
Rework MSIX system-wide config into a distinct machine-folder scope
1939cfb
Grant Built-in Users read on the MSIX machine-folder data store
2fd862f
Fix experimental-feature overrides with explicit enable/disable sets
a6108cc
Defer experimental features from MachineFolder; restore legacy scope …
76b45ed
Scope PR to config merging: drop profile relocation; union WinCompat …
d038853
Keep legacy execution-policy scope order; document policy vs preference
cdfe9b7
Clarify that config-based execution policy is a preference, not a policy
844ee34
Extract config merge helpers: MergePolicyList and MergePreferenceValue
a0088ce
Move config merge rationale into the merge helpers
401609d
Add xUnit tests for MachineFolder WinCompat config merge
f1f6c6c
Fix CodeFactor style issues in MachineFolder WinCompat tests
pwshBot bfd0cf9
Reorder test helpers so internal member precedes private members
pwshBot eb40468
Read PackageRepositoryRoot from 64-bit registry view
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,14 @@ public enum ConfigScope | |
| /// <summary> | ||
| /// CurrentUser configuration applies to the current user. | ||
| /// </summary> | ||
| CurrentUser = 1 | ||
| CurrentUser = 1, | ||
|
|
||
| /// <summary> | ||
| /// MachineFolder configuration applies to all users and is stored in the writable per-machine | ||
| /// data store of a packaged (MSIX) install. It is only present when running as such a package; | ||
| /// otherwise this scope has no backing file. | ||
| /// </summary> | ||
| MachineFolder = 2 | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -61,6 +68,11 @@ internal sealed class PowerShellConfig | |
| private string systemWideConfigFile; | ||
| private string systemWideConfigDirectory; | ||
|
|
||
| // When running as a packaged MSIX app with the per-machine data store provisioned, the path to the | ||
| // writable per-machine (admin) 'MachineFolder' config file. Null when not packaged, in which case | ||
| // the MachineFolder scope has no backing file and merge orders collapse to the legacy behavior. | ||
| private string machineFolderConfigFile; | ||
|
|
||
| // The json file containing the per-user configuration settings. | ||
| private readonly string perUserConfigFile; | ||
| private readonly string perUserConfigDirectory; | ||
|
|
@@ -85,6 +97,17 @@ private PowerShellConfig() | |
| systemWideConfigDirectory = Utils.DefaultPowerShellAppBase; | ||
| systemWideConfigFile = Path.Combine(systemWideConfigDirectory, ConfigFileName); | ||
|
|
||
| // When running as a packaged MSIX app that opts into the per-machine data store, expose that | ||
| // writable location as an additional 'MachineFolder' (admin) configuration scope. The shipped | ||
| // $PSHOME config remains the read-only product defaults; system-wide writes are redirected to | ||
| // the machine folder and only the changed keys are stored there (no full seed). Policy and | ||
| // preference settings merge these scopes with different precedence (see the Utils merge orders). | ||
| string machineStore = Utils.GetPackagedMachineDataStorePath(); | ||
| if (!string.IsNullOrEmpty(machineStore)) | ||
| { | ||
| machineFolderConfigFile = Path.Combine(machineStore, 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. | ||
|
|
@@ -95,15 +118,41 @@ private PowerShellConfig() | |
| } | ||
|
|
||
| emptyConfig = new JObject(); | ||
| configRoots = new JObject[2]; | ||
| configRoots = new JObject[3]; | ||
| serializer = JsonSerializer.Create(new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.None, MaxDepth = 10 }); | ||
|
|
||
| fileLock = new ReaderWriterLockSlim(); | ||
| } | ||
|
|
||
| private string GetConfigFilePath(ConfigScope scope) | ||
| { | ||
| return (scope == ConfigScope.CurrentUser) ? perUserConfigFile : systemWideConfigFile; | ||
| switch (scope) | ||
| { | ||
| case ConfigScope.CurrentUser: | ||
| return perUserConfigFile; | ||
| case ConfigScope.MachineFolder: | ||
| // Null when not running as a packaged app with the per-machine data store provisioned. | ||
| return machineFolderConfigFile; | ||
| default: | ||
| // AllUsers -> the read-only $PSHOME (product) config. | ||
| return systemWideConfigFile; | ||
| } | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Maps a logical write scope to the physical config scope that receives the write. System-wide | ||
| /// (AllUsers) writes are redirected to the writable per-machine data store (MachineFolder) when | ||
| /// running as a packaged app, so only the changed keys are stored there and the read-only $PSHOME | ||
| /// product config is left untouched. All other scopes write in place. | ||
| /// </summary> | ||
| private ConfigScope ResolveWriteScope(ConfigScope scope) | ||
| { | ||
| if (scope == ConfigScope.AllUsers && !string.IsNullOrEmpty(machineFolderConfigFile)) | ||
| { | ||
| return ConfigScope.MachineFolder; | ||
| } | ||
|
|
||
| return scope; | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -124,6 +173,9 @@ internal void SetSystemConfigFilePath(string value) | |
| FileInfo info = new FileInfo(value); | ||
| systemWideConfigFile = info.FullName; | ||
| systemWideConfigDirectory = info.Directory.FullName; | ||
|
|
||
| // An explicit settings file overrides the per-machine data store; disable the MachineFolder scope. | ||
| machineFolderConfigFile = null; | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -207,39 +259,43 @@ internal string[] GetExperimentalFeatures() | |
| /// <param name="setEnabled">If true, add to configuration; otherwise, remove from configuration.</param> | ||
| internal void SetExperimentalFeatures(ConfigScope scope, string featureName, bool setEnabled) | ||
| { | ||
| // Experimental features intentionally stay on the legacy scopes ($PSHOME for AllUsers) and are not | ||
| // redirected to the packaged per-machine data store; MachineFolder support for experimental features | ||
| // is deferred to a separate change (see https://github.com/PowerShell/PowerShell/issues/27702). | ||
| // This keeps behavior identical to non-packaged installs, so an AllUsers write under MSIX fails | ||
| // against the read-only $PSHOME just as it does today. | ||
| var features = new List<string>(GetExperimentalFeatures()); | ||
| bool containsFeature = features.Contains(featureName); | ||
| if (setEnabled && !containsFeature) | ||
| { | ||
| features.Add(featureName); | ||
| WriteValueToFile<string[]>(scope, "ExperimentalFeatures", features.ToArray()); | ||
| WriteValueToFile<string[]>(scope, "ExperimentalFeatures", features.ToArray(), allowMachineFolderRedirect: false); | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove experimental feature merging for another PR |
||
| } | ||
| else if (!setEnabled && containsFeature) | ||
| { | ||
| features.Remove(featureName); | ||
| WriteValueToFile<string[]>(scope, "ExperimentalFeatures", features.ToArray()); | ||
| WriteValueToFile<string[]>(scope, "ExperimentalFeatures", features.ToArray(), allowMachineFolderRedirect: false); | ||
| } | ||
| } | ||
|
|
||
| internal bool IsImplicitWinCompatEnabled() | ||
| { | ||
| bool settingValue = ReadValueFromFile<bool?>(ConfigScope.CurrentUser, DisableImplicitWinCompatKey) | ||
| ?? ReadValueFromFile<bool?>(ConfigScope.AllUsers, DisableImplicitWinCompatKey) | ||
| ?? false; | ||
| // DisableImplicitWinCompat is a preference: the highest-precedence scope that sets it wins. | ||
| bool settingValue = MergePreferenceValue<bool?>(DisableImplicitWinCompatKey) ?? false; | ||
|
|
||
| return !settingValue; | ||
| } | ||
|
|
||
| internal string[] GetWindowsPowerShellCompatibilityModuleDenyList() | ||
| { | ||
| return ReadValueFromFile<string[]>(ConfigScope.CurrentUser, WindowsPowerShellCompatibilityModuleDenyListKey) | ||
| ?? ReadValueFromFile<string[]>(ConfigScope.AllUsers, WindowsPowerShellCompatibilityModuleDenyListKey); | ||
| // The compatibility module deny list is a policy: its entries are unioned across every scope. | ||
| return MergePolicyList(WindowsPowerShellCompatibilityModuleDenyListKey); | ||
| } | ||
|
|
||
| internal string[] GetWindowsPowerShellCompatibilityNoClobberModuleList() | ||
| { | ||
| return ReadValueFromFile<string[]>(ConfigScope.CurrentUser, WindowsPowerShellCompatibilityNoClobberModuleListKey) | ||
| ?? ReadValueFromFile<string[]>(ConfigScope.AllUsers, WindowsPowerShellCompatibilityNoClobberModuleListKey); | ||
| // The no-clobber list is a preference: the highest-precedence scope that defines it wins. | ||
| return MergePreferenceValue<string[]>(WindowsPowerShellCompatibilityNoClobberModuleListKey); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -380,6 +436,78 @@ internal PSKeyword GetLogKeywords() | |
| } | ||
| #endif // UNIX | ||
|
|
||
| /// <summary> | ||
| /// The order in which configuration scopes are consulted when resolving a *preference* setting: | ||
| /// the current user's value wins, then the admin-writable MachineFolder override, then the | ||
| /// $PSHOME (AllUsers) product default. Policy lists resolved by <see cref="MergePolicyList"/> | ||
| /// union every scope, so the iteration order does not affect them. | ||
| /// </summary> | ||
| private static readonly ConfigScope[] s_configScopePreferenceOrder = new[] | ||
| { | ||
| ConfigScope.CurrentUser, | ||
| ConfigScope.MachineFolder, | ||
| ConfigScope.AllUsers | ||
| }; | ||
|
|
||
| /// <summary> | ||
| /// Resolves a *preference* setting: returns the value from the highest-precedence scope that | ||
| /// defines <paramref name="key"/> (see <see cref="s_configScopePreferenceOrder"/>), so that a | ||
| /// more specific scope overrides a broader one. Use this for settings that behave like a | ||
| /// preference - the current user's choice should take precedence over a system-wide value. | ||
| /// In contrast to <see cref="MergePolicyList"/> (which unions every scope), only the winning | ||
| /// scope's value is used; lower scopes are ignored once a higher one defines the key. Returns | ||
| /// <paramref name="defaultValue"/> when no scope defines the key. <typeparamref name="T"/> must | ||
| /// be a reference type or a nullable value type so that an unset scope is observable as null. | ||
| /// </summary> | ||
| /// <typeparam name="T">The type of the value.</typeparam> | ||
| /// <param name="key">The configuration key to resolve.</param> | ||
| /// <param name="defaultValue">The value to return when no scope defines the key.</param> | ||
| private T MergePreferenceValue<T>(string key, T defaultValue = default) | ||
| { | ||
| foreach (ConfigScope scope in s_configScopePreferenceOrder) | ||
| { | ||
| T value = ReadValueFromFile<T>(scope, key); | ||
| if (value is not null) | ||
| { | ||
| return value; | ||
| } | ||
| } | ||
|
|
||
| return defaultValue; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Resolves a *policy* list: returns the case-insensitive union of the string entries defined in | ||
| /// every configuration scope for <paramref name="key"/>. Use this for list settings that behave | ||
| /// like a policy and must accumulate across scopes, so that an entry contributed by one scope | ||
| /// (for example a module added to the list by a product update in $PSHOME) is never dropped | ||
| /// because a higher-precedence scope also defines the key. Every scope can only add entries; a | ||
| /// higher-precedence scope never removes an entry contributed by a lower scope. Returns null when | ||
| /// no scope contributes an entry. | ||
| /// </summary> | ||
| /// <param name="key">The configuration key to resolve.</param> | ||
| private string[] MergePolicyList(string key) | ||
| { | ||
| var merged = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
| foreach (ConfigScope scope in s_configScopePreferenceOrder) | ||
| { | ||
| string[] values = ReadValueFromFile<string[]>(scope, key); | ||
| if (values is not null) | ||
| { | ||
| merged.UnionWith(values); | ||
| } | ||
| } | ||
|
|
||
| if (merged.Count == 0) | ||
| { | ||
| return null; | ||
| } | ||
|
|
||
| var result = new string[merged.Count]; | ||
| merged.CopyTo(result); | ||
| return result; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Read a value from the configuration file. | ||
| /// </summary> | ||
|
|
@@ -472,10 +600,20 @@ private static FileStream OpenFileStreamWithRetry(string fullPath, FileMode mode | |
| /// <param name="key">The string key of the value.</param> | ||
| /// <param name="value">The value to set.</param> | ||
| /// <param name="addValue">Whether the key-value pair should be added to or removed from the file.</param> | ||
| private void UpdateValueInFile<T>(ConfigScope scope, string key, T value, bool addValue) | ||
| /// <param name="allowMachineFolderRedirect">When true (default), system-wide (AllUsers) writes are redirected to the writable per-machine data store on a packaged install; pass false to write to the literal scope.</param> | ||
| private void UpdateValueInFile<T>(ConfigScope scope, string key, T value, bool addValue, bool allowMachineFolderRedirect = true) | ||
| { | ||
| try | ||
| { | ||
| // Redirect system-wide writes to the writable per-machine data store when packaged, so only | ||
| // the changed keys are stored there and the read-only $PSHOME product config is untouched. | ||
| // Callers that must target the literal scope (e.g. experimental features, whose MachineFolder | ||
| // support is deferred) pass allowMachineFolderRedirect: false. | ||
| if (allowMachineFolderRedirect) | ||
| { | ||
| scope = ResolveWriteScope(scope); | ||
| } | ||
|
|
||
| string fileName = GetConfigFilePath(scope); | ||
| fileLock.EnterWriteLock(); | ||
|
|
||
|
|
@@ -573,14 +711,15 @@ private void UpdateValueInFile<T>(ConfigScope scope, string key, T value, bool a | |
| /// <param name="scope">The ConfigScope of the file to update.</param> | ||
| /// <param name="key">The string key of the value.</param> | ||
| /// <param name="value">The value to write.</param> | ||
| private void WriteValueToFile<T>(ConfigScope scope, string key, T value) | ||
| /// <param name="allowMachineFolderRedirect">When true (default), system-wide (AllUsers) writes are redirected to the writable per-machine data store on a packaged install; pass false to write to the literal scope.</param> | ||
| private void WriteValueToFile<T>(ConfigScope scope, string key, T value, bool allowMachineFolderRedirect = true) | ||
| { | ||
| if (scope == ConfigScope.CurrentUser && !Directory.Exists(perUserConfigDirectory)) | ||
| { | ||
| Directory.CreateDirectory(perUserConfigDirectory); | ||
| } | ||
|
|
||
| UpdateValueInFile<T>(scope, key, value, true); | ||
| UpdateValueInFile<T>(scope, key, value, true, allowMachineFolderRedirect); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
jshigetomi marked this conversation as resolved.
|
||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First set of sddl's grants built in admin to write to all files and directories
Second set of sddl's grants built in users to read all files and directories