Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
75 changes: 70 additions & 5 deletions Source/NETworkManager/ViewModels/PowerShellHostViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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"))
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The path check using .EndsWith() is case-sensitive and may not handle all variations of the path (e.g., lowercase drive letters, forward slashes). Consider using a more robust check:

if (!string.IsNullOrEmpty(applicationFilePath) && 
    applicationFilePath.Contains("\\AppData\\Local\\Microsoft\\WindowsApps\\", StringComparison.OrdinalIgnoreCase) &&
    applicationFilePath.EndsWith("pwsh.exe", StringComparison.OrdinalIgnoreCase))

This handles case variations and ensures we're matching the correct directory structure.

Suggested change
if (applicationFilePath.EndsWith("AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe"))
var normalizedPath = applicationFilePath?.Replace('/', '\\');
if (!string.IsNullOrEmpty(normalizedPath) &&
normalizedPath.EndsWith("AppData\\Local\\Microsoft\\WindowsApps\\pwsh.exe", StringComparison.OrdinalIgnoreCase))

Copilot uses AI. Check for mistakes.
{
Log.Info("Found pwsh.exe in AppData (Microsoft Store installation). Trying to resolve real path...");

var realPwshPath = FindRealPwshPath(applicationFilePath);

if (realPwshPath != null)
applicationFilePath = realPwshPath;
Comment on lines +577 to +578
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logic issue: If FindRealPwshPath returns null (line 575), the code keeps the WindowsApps stub path in applicationFilePath and proceeds to set it in the settings (line 585). This means the app will still try to use the stub path instead of falling back to Windows PowerShell.

Consider updating the logic:

if (realPwshPath != null)
    applicationFilePath = realPwshPath;
else
{
    Log.Warn("Failed to resolve real pwsh path. Falling back to Windows PowerShell.");
    applicationFilePath = ApplicationHelper.Find(PowerShell.WindowsPowerShellFileName);
}
Suggested change
if (realPwshPath != null)
applicationFilePath = realPwshPath;
if (realPwshPath != null)
{
applicationFilePath = realPwshPath;
}
else
{
Log.Warn("Failed to resolve real pwsh path. Falling back to Windows PowerShell.");
applicationFilePath = ApplicationHelper.Find(PowerShell.WindowsPowerShellFileName);
}

Copilot uses AI. Check for mistakes.
}
else if (string.IsNullOrEmpty(applicationFilePath))
{
applicationFilePath = ApplicationHelper.Find(PowerShell.WindowsPowerShellFileName);
}

SettingsManager.Current.PowerShell_ApplicationFilePath = applicationFilePath;

Expand All @@ -580,6 +590,61 @@ private void TryFindExecutable()
Log.Warn("Install PowerShell or configure the path in the settings.");
}

/// <summary>
/// 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>C:\Users\{USERNAME}\AppData\Local\Microsoft\WindowsApps\pwsh.exe</c>
///
/// This helper attempts to locate the corresponding real executable under the Program Files
/// WindowsApps package layout, e.g.:
/// <c>C:\Program Files\WindowsApps\Microsoft.PowerShell_7.*_8wekyb3d8bbwe\pwsh.exe</c>.
///
/// Workaround for: https://github.com/BornToBeRoot/NETworkManager/issues/3223
/// </summary>
/// <param name="path">Path to the pwsh proxy stub, typically located under the current user's <c>%LocalAppData%\Microsoft\WindowsApps\pwsh.exe</c>.</param>
/// <returns>Full path to the real pwsh executable under Program Files WindowsApps when found; otherwise null.</returns>
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();
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential resource leak: The Process object is disposed via using, but if process.WaitForExit() hangs or takes too long, this could block indefinitely. Consider adding a timeout:

if (!process.WaitForExit(5000)) // 5 second timeout
{
    process.Kill();
    return null;
}
Suggested change
process.WaitForExit();
// Wait for up to 5 seconds for the process to exit
if (!process.WaitForExit(5000))
{
process.Kill();
return null;
}

Copilot uses AI. Check for mistakes.

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();
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The string replacement logic appears redundant and potentially incorrect. Lines 632-637 perform multiple overlapping replacements for removing newlines and carriage returns:

  • Replace(@"\\", @"\") - converts double backslashes to single (this may corrupt valid Windows paths)
  • Replace(@"\r", string.Empty) and Replace(@"\n", string.Empty) - these won't match literal \r or \n in the string
  • Multiple redundant Replace("\r\n", ...), Replace("\n", ...), Replace("\r", ...) calls

Simplify to:

output = output.Trim();

The output from (Get-Command pwsh).Source should already be a clean path without embedded newlines. The .Trim() call will handle any leading/trailing whitespace.

Suggested change
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();
output = output.Trim();

Copilot uses AI. Check for mistakes.

return output;
}
catch
{
return null;
}
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The catch block silently swallows all exceptions without logging. This makes debugging difficult if FindRealPwshPath fails. Consider logging the exception:

catch (Exception ex)
{
    Log.Error($"Failed to resolve real pwsh path: {ex.Message}");
    return null;
}

Copilot uses AI. Check for mistakes.
}

private Task Connect(string host = null)
{
var childWindow = new PowerShellConnectChildWindow();
Expand Down
2 changes: 0 additions & 2 deletions Source/NETworkManager/Views/PowerShellHostView.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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}">
<UserControl.Resources>
Expand Down
5 changes: 2 additions & 3 deletions Source/NETworkManager/Views/PowerShellHostView.xaml.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;

Expand Down
9 changes: 8 additions & 1 deletion Website/docs/changelog/next-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading