Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add Experimental Feature PSContentPath#26509

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
jshigetomi wants to merge19 commits intoPowerShell:master
base:master
Choose a base branch
Loading
fromjshigetomi:PSModulePathFix
Open
Show file tree
Hide file tree
Changes from1 commit
Commits
Show all changes
19 commits
Select commitHold shift + click to select a range
b73eea5
Testing
Mar 13, 2025
805fd0e
Update PSModulePath
Mar 17, 2025
6dc662d
Add Experimental feature for PSContent
jshigetomiAug 6, 2025
67a18aa
Merge branch 'master' into PSModulePathFix
jshigetomiAug 6, 2025
4a7bb3d
Removed unused variables
jshigetomiAug 6, 2025
00b6307
Switch to default PSContentPath LOCALAPPData, cmdlets added
jshigetomiAug 20, 2025
7e5802b
Add lazy migration
jshigetomiOct 2, 2025
830016e
Add null checks incase PSUserContentPath fails to get any value
jshigetomiOct 9, 2025
56acf2d
Able to use expanded environmental variables
jshigetomiOct 14, 2025
3f5709d
Remove help URI
jshigetomiOct 14, 2025
9e516fa
Reassign perUserConfigDirectory if experimental feature is enabled wh…
jshigetomiOct 14, 2025
d371979
Move migration to GetPSContentPath API, added safety fallbacks to def…
jshigetomiNov 21, 2025
c62f82a
Merge branch 'master' into PSModulePathFix
jshigetomiNov 21, 2025
b207b5c
Add tests
jshigetomiDec 1, 2025
8fc45c7
Merge branch 'PSModulePathFix' of https://github.com/jshigetomi/Power…
jshigetomiDec 1, 2025
fb2036c
Merge branch 'master' into PSModulePathFix
jshigetomiDec 1, 2025
eaaaedd
Add initial test cases for PSContentPath
jshigetomiDec 1, 2025
eed1cf3
Fix expanding env variable test
jshigetomiDec 12, 2025
ec18785
Separate commands for env var test
jshigetomiDec 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
PrevPrevious commit
NextNext commit
Add tests
  • Loading branch information
@jshigetomi
jshigetomi committedDec 1, 2025
commitb207b5ca3c5765597d5c67e3cc0a66c79c689bf3
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -165,7 +165,7 @@ public static bool IsStaSupported
// Gets the location for cache and config folders.
internal static readonly string CacheDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CACHE);
internal static readonly string ConfigDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.CONFIG);
internal static readonly string DefaultPSContentDirectory =Path.Combine(Platform.SelectProductNameForDirectory(Platform.XDG_Type.DATA), "PowerShell");
internal static readonly string DefaultPSContentDirectory = Platform.SelectProductNameForDirectory(Platform.XDG_Type.DATA);
#else
// Gets the location for cache and config folders.
internal static readonly string CacheDirectory = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + @"\Microsoft\PowerShell";
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -5468,7 +5468,6 @@ private static void InitializeCoreCmdletsAndProviders(
{ "Get-History", new SessionStateCmdletEntry("Get-History", typeof(GetHistoryCommand), helpFile) },
{ "Get-Job", new SessionStateCmdletEntry("Get-Job", typeof(GetJobCommand), helpFile) },
{ "Get-Module", new SessionStateCmdletEntry("Get-Module", typeof(GetModuleCommand), helpFile) },
{ "Get-PSContentPath", new SessionStateCmdletEntry("Get-PSContentPath", typeof(GetPSContentPathCommand), helpFile) },
{ "Get-PSHostProcessInfo", new SessionStateCmdletEntry("Get-PSHostProcessInfo", typeof(GetPSHostProcessInfoCommand), helpFile) },
{ "Get-PSSession", new SessionStateCmdletEntry("Get-PSSession", typeof(GetPSSessionCommand), helpFile) },
{ "Import-Module", new SessionStateCmdletEntry("Import-Module", typeof(ImportModuleCommand), helpFile) },
Expand All@@ -5490,7 +5489,6 @@ private static void InitializeCoreCmdletsAndProviders(
{ "Remove-Module", new SessionStateCmdletEntry("Remove-Module", typeof(RemoveModuleCommand), helpFile) },
{ "Remove-PSSession", new SessionStateCmdletEntry("Remove-PSSession", typeof(RemovePSSessionCommand), helpFile) },
{ "Save-Help", new SessionStateCmdletEntry("Save-Help", typeof(SaveHelpCommand), helpFile) },
{ "Set-PSContentPath", new SessionStateCmdletEntry("Set-PSContentPath", typeof(SetPSContentPathCommand), helpFile) },
{ "Set-PSDebug", new SessionStateCmdletEntry("Set-PSDebug", typeof(SetPSDebugCommand), helpFile) },
{ "Set-StrictMode", new SessionStateCmdletEntry("Set-StrictMode", typeof(SetStrictModeCommand), helpFile) },
{ "Start-Job", new SessionStateCmdletEntry("Start-Job", typeof(StartJobCommand), helpFile) },
Expand All@@ -5517,6 +5515,12 @@ private static void InitializeCoreCmdletsAndProviders(
cmdlets.Add("Get-PSSubsystem", new SessionStateCmdletEntry("Get-PSSubsystem", typeof(Subsystem.GetPSSubsystemCommand), helpFile));
}

if (ExperimentalFeature.IsEnabled(ExperimentalFeature.PSContentPath))
{
cmdlets.Add("Get-PSContentPath", new SessionStateCmdletEntry("Get-PSContentPath", typeof(GetPSContentPathCommand), helpFile));
cmdlets.Add("Set-PSContentPath", new SessionStateCmdletEntry("Set-PSContentPath", typeof(SetPSContentPathCommand), helpFile));
}

#if UNIX
cmdlets.Add("Switch-Process", new SessionStateCmdletEntry("Switch-Process", typeof(SwitchProcessCommand), helpFile));
#endif
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -965,18 +965,7 @@ internal static string GetModuleName(string path)
/// <returns>Personal module path.</returns>
internal static string GetPersonalModulePath()
{
string contentPath = Utils.GetPSContentPath();
// GetPSContentPath should never return null when experimental feature is enabled,
// but add defensive check for safety
if (string.IsNullOrEmpty(contentPath))
{
#if UNIX
contentPath = Platform.SelectProductNameForDirectory(Platform.XDG_Type.DATA);
#else
contentPath = Environment.GetFolderPath(Environment.SpecialFolder.Personal) + @"\PowerShell";
#endif
}
return Path.Combine(contentPath, "Modules");
return Path.Combine(Utils.GetPSContentPath, "Modules");
}

/// <summary>
Expand DownExpand Up@@ -1373,14 +1362,9 @@ private static string SetModulePath()
}
#endif
string allUsersModulePath = PowerShellConfig.Instance.GetModulePath(ConfigScope.AllUsers);
string personalModulePath =Utils.GetPSContentPath(true) ??string.Empty;
string personalModulePath =PowerShellConfig.Instance.GetModulePath(ConfigScope.CurrentUser) ??GetPersonalModulePath();
string newModulePathString = GetModulePath(currentModulePath, allUsersModulePath, personalModulePath);

if (!string.IsNullOrEmpty(personalModulePath))
{
Environment.SetEnvironmentVariable(Constants.PSUserContentPathEnvVar, personalModulePath);
}

if (!string.IsNullOrEmpty(newModulePathString))
{
Environment.SetEnvironmentVariable(Constants.PSModulePathEnvVar, newModulePathString);
Expand Down
196 changes: 179 additions & 17 deletionssrc/System.Management.Automation/engine/PSConfiguration.cs
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation.Internal;
using System.Text;
using System.Threading;
Expand DownExpand Up@@ -68,6 +69,9 @@ internal sealed class PowerShellConfig
// Flag to track if migration has been checked (lazy initialization to avoid circular dependency)
private int migrationChecked = 0;

// Track the legacy config file path after migration, so we can keep it in sync
private string legacyConfigFile = null;

// Note: JObject and JsonSerializer are thread safe.
// Root Json objects corresponding to the configuration file for 'AllUsers' and 'CurrentUser' respectively.
// They are used as a cache to avoid hitting the disk for every read operation.
Expand DownExpand Up@@ -225,17 +229,68 @@ private static string GetExecutionPolicySettingKey(string shellId)

/// <summary>
/// Get the names of experimental features enabled in the config file.
///
/// BOOTSTRAP PROBLEM SOLUTION:
/// This method reads from BOTH the current location (which may be LocalAppData or Documents)
/// AND the potential legacy location (Documents) to handle the bootstrap problem:
/// - We need to know if PSContentPath is enabled to know which config file to read
/// - But PSContentPath enabled state is stored IN the config file
///
/// By reading both locations and merging (union), we ensure correct behavior:
/// - If PSContentPath is disabled: only Documents config exists, we read it correctly
/// - If PSContentPath is enabled: both configs should be in sync (via write path), union gives same result
/// - During re-enable after disable: Documents has new state, LocalAppData may be stale, union captures intent
/// - Edge case (manual edit): if either location has a feature enabled, we honor it (permissive approach)
///
/// MIGRATION FLOW:
/// After this method determines the enabled features, if PSContentPath is enabled:
/// 1. CheckAndPerformPSContentPathMigration() switches to LocalAppData location
/// 2. SyncExperimentalFeaturesToNewLocation() updates LocalAppData config to match Documents
/// 3. Future writes keep both locations in sync via UpdateLegacyConfigFile()
///
/// This ensures both config files converge to the same state within one PowerShell session restart.
/// </summary>
internal string[] GetExperimentalFeatures()
{
string[] features = ReadValueFromFile(ConfigScope.CurrentUser, "ExperimentalFeatures", Array.Empty<string>());
// Read from current location (might be LocalAppData or Documents depending on migration state)
string[] currentFeatures = ReadValueFromFile(ConfigScope.CurrentUser, "ExperimentalFeatures", Array.Empty<string>());

// Also check the potential legacy location if it's different from current
string[] legacyFeatures = Array.Empty<string>();
if (!string.IsNullOrEmpty(legacyConfigFile) &&
!legacyConfigFile.Equals(perUserConfigFile, StringComparison.OrdinalIgnoreCase))
{
// Temporarily swap to read from legacy location
string originalFile = perUserConfigFile;
JObject originalCache = configRoots[(int)ConfigScope.CurrentUser];

try
{
perUserConfigFile = legacyConfigFile;
configRoots[(int)ConfigScope.CurrentUser] = null; // Force re-read
legacyFeatures = ReadValueFromFile(ConfigScope.CurrentUser, "ExperimentalFeatures", Array.Empty<string>());
}
finally
{
perUserConfigFile = originalFile;
configRoots[(int)ConfigScope.CurrentUser] = originalCache;
}
}

// Merge features from both locations (union) - if a feature is enabled in either, it's enabled
var mergedFeatures = new HashSet<string>(currentFeatures, StringComparer.OrdinalIgnoreCase);
foreach (string feature in legacyFeatures)
{
mergedFeatures.Add(feature);
}

if (features.Length == 0)
// If neither current nor legacy location has features, check AllUsers (system-wide) as fallback
if (mergedFeatures.Count == 0)
{
features = ReadValueFromFile(ConfigScope.AllUsers, "ExperimentalFeatures", Array.Empty<string>());
return ReadValueFromFile(ConfigScope.AllUsers, "ExperimentalFeatures", Array.Empty<string>());
}

returnfeatures;
returnmergedFeatures.ToArray();
}

/// <summary>
Expand DownExpand Up@@ -615,6 +670,15 @@ private void WriteValueToFile<T>(ConfigScope scope, string key, T value)
}

UpdateValueInFile<T>(scope, key, value, true);

// If we migrated from a legacy location, also update the legacy config to keep them in sync.
// This ensures that disabling features (like PSContentPath) updates both locations.
if (scope == ConfigScope.CurrentUser &&
!string.IsNullOrEmpty(legacyConfigFile) &&
File.Exists(legacyConfigFile))
{
UpdateLegacyConfigFile<T>(key, value, true);
}
}

/// <summary>
Expand DownExpand Up@@ -653,6 +717,90 @@ internal void MigrateUserConfig(string oldPath, string newPath)
}
}

/// <summary>
/// Syncs the ExperimentalFeatures array from the old config location to the new location.
/// This ensures that when PSContentPath is re-enabled after being disabled, the new location
/// gets updated with the current experimental features state.
/// </summary>
/// <param name="oldPath">Path to the old config file (Documents location)</param>
/// <param name="newPath">Path to the new config file (LocalAppData location)</param>
private void SyncExperimentalFeaturesToNewLocation(string oldPath, string newPath)
{
try
{
// Read experimental features from old location
string originalFile = perUserConfigFile;
JObject originalCache = configRoots[(int)ConfigScope.CurrentUser];

try
{
// Temporarily point to old location to read features
perUserConfigFile = oldPath;
configRoots[(int)ConfigScope.CurrentUser] = null;
string[] oldFeatures = ReadValueFromFile(ConfigScope.CurrentUser, "ExperimentalFeatures", Array.Empty<string>());

// Now point to new location to write features
perUserConfigFile = newPath;
configRoots[(int)ConfigScope.CurrentUser] = null;

// Write the features to new location (this will also invalidate the cache)
if (oldFeatures.Length > 0)
{
UpdateValueInFile<string[]>(ConfigScope.CurrentUser, "ExperimentalFeatures", oldFeatures, true);
}
}
finally
{
// Restore original state
perUserConfigFile = originalFile;
configRoots[(int)ConfigScope.CurrentUser] = originalCache;
}
}
catch
{
// Best-effort operation; don't fail PowerShell startup if sync fails
}
}

/// <summary>
/// Updates the legacy config file to keep it in sync with the new location.
/// This is a best-effort operation that silently fails if there are any issues.
/// Reuses UpdateValueInFile by temporarily treating the legacy file as the current user config.
/// </summary>
/// <typeparam name="T">The type of value</typeparam>
/// <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 UpdateLegacyConfigFile<T>(string key, T value, bool addValue)
{
try
{
// Save the current cache for the CurrentUser scope
JObject savedCache = configRoots[(int)ConfigScope.CurrentUser];

// Temporarily swap to the legacy config file path and clear cache
string originalPerUserConfigFile = perUserConfigFile;
perUserConfigFile = legacyConfigFile;
configRoots[(int)ConfigScope.CurrentUser] = null;

try
{
// Reuse the existing UpdateValueInFile logic
UpdateValueInFile<T>(ConfigScope.CurrentUser, key, value, addValue);
}
finally
{
// Restore the original path and cache
perUserConfigFile = originalPerUserConfigFile;
configRoots[(int)ConfigScope.CurrentUser] = savedCache;
}
}
catch
{
// Best-effort operation; don't fail if we can't update the legacy config
}
}

/// <summary>
/// Ensures migration is checked exactly once, using thread-safe lazy initialization.
/// This is called from GetPSContentPath() to avoid circular dependency with ExperimentalFeature.
Expand All@@ -667,6 +815,19 @@ private void CheckAndPerformPSContentPathMigrationOnce()

/// <summary>
/// Checks if PSContentPath migration is needed and performs it if the experimental feature is enabled.
///
/// MIGRATION SCENARIOS:
/// 1. First enable: Copies Documents config to LocalAppData, switches to LocalAppData
/// 2. Already migrated: Just switches to LocalAppData (both configs exist and should be in sync)
/// 3. Re-enable after disable: Syncs experimental features from Documents to LocalAppData, then switches
///
/// BIDIRECTIONAL SYNC:
/// After migration, legacyConfigFile points to Documents and perUserConfigFile points to LocalAppData.
/// All subsequent writes via WriteValueToFile() will update both locations via UpdateLegacyConfigFile().
/// This ensures that:
/// - Enabling/disabling features updates both configs
/// - Disabling PSContentPath and restarting uses Documents with current state
/// - Re-enabling PSContentPath and restarting switches back to LocalAppData with current state
/// </summary>
private void CheckAndPerformPSContentPathMigration()
{
Expand All@@ -677,6 +838,7 @@ private void CheckAndPerformPSContentPathMigration()
{
return;
}


string oldConfigFile = Path.Combine(Platform.ConfigDirectory, ConfigFileName);
string newConfigFile = Path.Combine(Platform.DefaultPSContentDirectory, ConfigFileName);
Expand All@@ -690,25 +852,25 @@ private void CheckAndPerformPSContentPathMigration()
// Always update to use the new location when experimental feature is enabled
string newConfigDir = Path.GetDirectoryName(newConfigFile);

// If migration was already completed (new config exists), just update paths
if (File.Exists(newConfigFile))
{
perUserConfigDirectory = newConfigDir;
perUserConfigFile = newConfigFile;
return;
}
// Update to use new location
perUserConfigDirectory = newConfigDir;
perUserConfigFile = newConfigFile;

// If old config exists and needs migration, perform the migration
if (File.Exists(oldConfigFile))
// If both configs exist, keep them in sync (legacyConfigFile already points to old location from constructor)
// If old config exists but new doesn't, perform migration
if (!File.Exists(newConfigFile) && File.Exists(oldConfigFile))
{
MigrateUserConfig(oldConfigFile, newConfigFile);
}
else
else if (File.Exists(oldConfigFile) && File.Exists(newConfigFile))
{
// No existing config, but still use new location going forward
perUserConfigDirectory = newConfigDir;
perUserConfigFile = newConfigFile;
// Both configs exist. Ensure they're in sync by copying the experimental features from old to new.
// This handles the case where PSContentPath was disabled, then re-enabled while running from Documents location.
// The Documents config now has PSContentPath enabled, but LocalAppData config still has it disabled (stale).
// We need to update LocalAppData to match so future operations see the correct state.
SyncExperimentalFeaturesToNewLocation(oldConfigFile, newConfigFile);
}
// legacyConfigFile was set to oldConfigFile in constructor and will be used for sync
}
catch
{
Expand Down
Loading

[8]ページ先頭

©2009-2025 Movatter.jp