From 320202bb1388582f68f10c00cc365e796c1b923f Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:01:42 +0100 Subject: [PATCH 1/3] Fix: Resolve correct pwsh path if installed via Windows Store --- .../ViewModels/PowerShellHostViewModel.cs | 75 +++++++++++++++++-- .../Views/PowerShellHostView.xaml | 2 - .../Views/PowerShellHostView.xaml.cs | 5 +- 3 files changed, 72 insertions(+), 10 deletions(-) diff --git a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs index a81f96da32..5a29d8c6d8 100644 --- a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs @@ -32,7 +32,6 @@ public class PowerShellHostViewModel : ViewModelBase, IProfileManager #region Variables private static readonly ILog Log = LogManager.GetLogger(typeof(PowerShellHostViewModel)); - private readonly IDialogCoordinator _dialogCoordinator; private readonly DispatcherTimer _searchDispatcherTimer = new(); public IInterTabClient InterTabClient { get; } @@ -307,16 +306,15 @@ public bool ProfileContextMenuIsOpen #region Constructor, load settings - public PowerShellHostViewModel(IDialogCoordinator instance) + public PowerShellHostViewModel() { _isLoading = true; - _dialogCoordinator = instance; - // Check if PowerShell executable is configured CheckExecutable(); // Try to find PowerShell executable + if (!IsExecutableConfigured) TryFindExecutable(); @@ -569,8 +567,20 @@ private void TryFindExecutable() var applicationFilePath = ApplicationHelper.Find(PowerShell.PwshFileName); - if (string.IsNullOrEmpty(applicationFilePath)) + // Workaround for: https://github.com/BornToBeRoot/NETworkManager/issues/3223 + if (applicationFilePath.EndsWith("AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe")) + { + Log.Info("Found pwsh.exe in AppData (Microsoft Store installation). Trying to resolve real path..."); + + var realPwshPath = FindRealPwshPath(applicationFilePath); + + if (realPwshPath != null) + applicationFilePath = realPwshPath; + } + else if (string.IsNullOrEmpty(applicationFilePath)) + { applicationFilePath = ApplicationHelper.Find(PowerShell.WindowsPowerShellFileName); + } SettingsManager.Current.PowerShell_ApplicationFilePath = applicationFilePath; @@ -580,6 +590,61 @@ private void TryFindExecutable() Log.Warn("Install PowerShell or configure the path in the settings."); } + /// + /// Resolves the actual installation path of a PowerShell executable that was installed via the + /// Microsoft Store / WindowsApps and therefore appears as a proxy stub in the user's AppData. + /// + /// Typical input is a path like: + /// C:\Users\{USERNAME}\AppData\Local\Microsoft\WindowsApps\pwsh.exe + /// + /// This helper attempts to locate the corresponding real executable under the Program Files + /// WindowsApps package layout, e.g.: + /// C:\Program Files\WindowsApps\Microsoft.PowerShell_7.*_8wekyb3d8bbwe\pwsh.exe. + /// + /// Workaround for: https://github.com/BornToBeRoot/NETworkManager/issues/3223 + /// + /// Path to the pwsh proxy stub, typically located under the current user's %LocalAppData%\Microsoft\WindowsApps\pwsh.exe. + /// Full path to the real pwsh executable under Program Files WindowsApps when found; otherwise null. + private string FindRealPwshPath(string path) + { + try + { + var command = "(Get-Command pwsh).Source"; + + ProcessStartInfo psi = new() + { + FileName = path, + Arguments = $"-NoProfile -ExecutionPolicy Bypass -Command \"{command}\"", + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using Process process = Process.Start(psi); + + string output = process.StandardOutput.ReadToEnd(); + + process.WaitForExit(); + + if (string.IsNullOrEmpty(output)) + return null; + + output = output.Replace(@"\\", @"\") + .Replace(@"\r", string.Empty) + .Replace(@"\n", string.Empty) + .Replace("\r\n", string.Empty) + .Replace("\n", string.Empty) + .Replace("\r", string.Empty) + .Trim(); + + return output; + } + catch + { + return null; + } + } + private Task Connect(string host = null) { var childWindow = new PowerShellConnectChildWindow(); diff --git a/Source/NETworkManager/Views/PowerShellHostView.xaml b/Source/NETworkManager/Views/PowerShellHostView.xaml index 1491d03108..64358b2656 100644 --- a/Source/NETworkManager/Views/PowerShellHostView.xaml +++ b/Source/NETworkManager/Views/PowerShellHostView.xaml @@ -8,7 +8,6 @@ xmlns:iconPacks="http://metro.mahapps.com/winfx/xaml/iconpacks" xmlns:converters="clr-namespace:NETworkManager.Converters;assembly=NETworkManager.Converters" xmlns:controls="clr-namespace:NETworkManager.Controls;assembly=NETworkManager.Controls" - xmlns:dialogs="clr-namespace:MahApps.Metro.Controls.Dialogs;assembly=MahApps.Metro" xmlns:viewModels="clr-namespace:NETworkManager.ViewModels" xmlns:localization="clr-namespace:NETworkManager.Localization.Resources;assembly=NETworkManager.Localization" xmlns:settings="clr-namespace:NETworkManager.Settings;assembly=NETworkManager.Settings" @@ -18,7 +17,6 @@ xmlns:profiles="clr-namespace:NETworkManager.Profiles;assembly=NETworkManager.Profiles" xmlns:wpfHelpers="clr-namespace:NETworkManager.Utilities.WPF;assembly=NETworkManager.Utilities.WPF" xmlns:networkManager="clr-namespace:NETworkManager" - dialogs:DialogParticipation.Register="{Binding}" Loaded="UserControl_Loaded" mc:Ignorable="d" d:DataContext="{d:DesignInstance viewModels:PowerShellHostViewModel}"> diff --git a/Source/NETworkManager/Views/PowerShellHostView.xaml.cs b/Source/NETworkManager/Views/PowerShellHostView.xaml.cs index b89a38879c..1edb19d556 100644 --- a/Source/NETworkManager/Views/PowerShellHostView.xaml.cs +++ b/Source/NETworkManager/Views/PowerShellHostView.xaml.cs @@ -1,5 +1,4 @@ -using MahApps.Metro.Controls.Dialogs; -using NETworkManager.ViewModels; +using NETworkManager.ViewModels; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; @@ -9,7 +8,7 @@ namespace NETworkManager.Views; public partial class PowerShellHostView { - private readonly PowerShellHostViewModel _viewModel = new(DialogCoordinator.Instance); + private readonly PowerShellHostViewModel _viewModel = new(); private bool _loaded; From 520c651995bb11eb258b07cf0f00739a0b6ff5c3 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:15:43 +0100 Subject: [PATCH 2/3] Docs: #3246 --- Website/docs/changelog/next-release.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Website/docs/changelog/next-release.md b/Website/docs/changelog/next-release.md index 776bd0329d..ea7d42180d 100644 --- a/Website/docs/changelog/next-release.md +++ b/Website/docs/changelog/next-release.md @@ -63,7 +63,14 @@ Release date: **xx.xx.2025** ## Bug Fixes -- The new profile filter popup introduced in version `2025.10.18.0` was instantly closed when a `PuTTY`, `PowerShell` or `AWS Session Manager` session was opened and the respective application / view was selected. [#3219](https://github.com/BornToBeRoot/NETworkManager/pull/3219) +**PowerShell** + +- Resolve the actual path to `pwsh.exe` under `C:\Program Files\WindowsApps\` instead of relying on the stub located at `%LocalAppData%\Microsoft\WindowsApps\`. The stub simply redirects to the real executable, and settings such as themes are applied only to the real binary via the registry. [#3246](https://github.com/BornToBeRoot/NETworkManager/pull/3246) +- The new profile filter popup introduced in version `2025.10.18.0` was instantly closed when a `PowerShell` session was opened and the respective application / view was selected. [#3219](https://github.com/BornToBeRoot/NETworkManager/pull/3219) + +**PuTTY** + +- The new profile filter popup introduced in version `2025.10.18.0` was instantly closed when a `PuTTY` session was opened and the respective application / view was selected. [#3219](https://github.com/BornToBeRoot/NETworkManager/pull/3219) ## Dependencies, Refactoring & Documentation From e3790f607c1da99a38c93d9dab6deec81fa2f129 Mon Sep 17 00:00:00 2001 From: BornToBeRoot <16019165+BornToBeRoot@users.noreply.github.com> Date: Sun, 23 Nov 2025 00:27:45 +0100 Subject: [PATCH 3/3] Fix: Apply suggestions from copilot --- .../ViewModels/PowerShellHostViewModel.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs index 5a29d8c6d8..e35ec44932 100644 --- a/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs +++ b/Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs @@ -577,8 +577,12 @@ private void TryFindExecutable() if (realPwshPath != null) applicationFilePath = realPwshPath; } - else if (string.IsNullOrEmpty(applicationFilePath)) + + // Fallback to Windows PowerShell + if (string.IsNullOrEmpty(applicationFilePath)) { + Log.Warn("Failed to resolve pwsh.exe path. Falling back to Windows PowerShell."); + applicationFilePath = ApplicationHelper.Find(PowerShell.WindowsPowerShellFileName); } @@ -624,7 +628,13 @@ private string FindRealPwshPath(string path) string output = process.StandardOutput.ReadToEnd(); - process.WaitForExit(); + if(!process.WaitForExit(10000)) + { + process.Kill(); + Log.Warn("Timeout while trying to resolve real pwsh path."); + + return null; + } if (string.IsNullOrEmpty(output)) return null; @@ -634,13 +644,14 @@ private string FindRealPwshPath(string path) .Replace(@"\n", string.Empty) .Replace("\r\n", string.Empty) .Replace("\n", string.Empty) - .Replace("\r", string.Empty) - .Trim(); + .Replace("\r", string.Empty); - return output; + return output.Trim(); } - catch + catch (Exception ex) { + Log.Error($"Failed to resolve real pwsh path: {ex.Message}"); + return null; } }