mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-15 03:07:56 +01:00
Allow Run to handle commandlines with spaces (#42016)
This better handles cases where commandlines might have embedded spaces. For something like ``` C:\Program Files\PowerShell\7\pwsh.exe -c write-host dawg ``` we'll now see that `C:\Program` isn't a file, and we'll try to look at `C:\Program Files\PowerShell\7\pwsh.exe` instead. This code is pilfered from https://github.com/microsoft/terminal/pull/12348 which fixed https://github.com/microsoft/terminal/issues/12345. Terminal has great code for normalizing a string into an executable and args, so why not just use it here. related to #41646 related to #41705 (but much more narrowly scoped) ---- I added some tests too. drive-by fix: as I was adding tests, I added a helper for "make a change to a page, and await the page's ItemsChanged". This removes a bunch of `await 1s` calls, and brings the shell page tests from like, 7s to 500ms
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
using Microsoft.CmdPal.Ext.UnitTestBase;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.UnitTests;
|
||||
|
||||
[TestClass]
|
||||
public class NormalizeCommandLineTests : CommandPaletteUnitTestBase
|
||||
{
|
||||
private void NormalizeTestCore(string input, string expectedExe, string expectedArgs = "")
|
||||
{
|
||||
ShellListPageHelpers.NormalizeCommandLineAndArgs(input, out var exe, out var args);
|
||||
|
||||
Assert.AreEqual(expectedExe, exe, ignoreCase: true, culture: System.Globalization.CultureInfo.InvariantCulture);
|
||||
Assert.AreEqual(expectedArgs, args);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("ping bing.com", "c:\\Windows\\system32\\ping.exe", "bing.com")]
|
||||
[DataRow("curl bing.com", "c:\\Windows\\system32\\curl.exe", "bing.com")]
|
||||
[DataRow("ipconfig /all", "c:\\Windows\\system32\\ipconfig.exe", "/all")]
|
||||
public void NormalizeCommandLineSimple(string input, string expectedExe, string expectedArgs = "")
|
||||
{
|
||||
NormalizeTestCore(input, expectedExe, expectedArgs);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("\"C:\\Program Files\\Windows Defender\\MsMpEng.exe\"", "C:\\Program Files\\Windows Defender\\MsMpEng.exe")]
|
||||
[DataRow("C:\\Program Files\\Windows Defender\\MsMpEng.exe", "C:\\Program Files\\Windows Defender\\MsMpEng.exe")]
|
||||
public void NormalizeCommandLineSpacesInExecutablePath(string input, string expectedExe, string expectedArgs = "")
|
||||
{
|
||||
NormalizeTestCore(input, expectedExe, expectedArgs);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("%SystemRoot%\\system32\\cmd.exe", "C:\\Windows\\System32\\cmd.exe")]
|
||||
public void NormalizeWithEnvVar(string input, string expectedExe, string expectedArgs = "")
|
||||
{
|
||||
NormalizeTestCore(input, expectedExe, expectedArgs);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("cmd --run --test", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
|
||||
[DataRow("cmd --run --test ", "C:\\Windows\\System32\\cmd.exe", "--run --test")]
|
||||
[DataRow("cmd \"--run --test\" --pass", "C:\\Windows\\System32\\cmd.exe", "--run --test --pass")]
|
||||
public void NormalizeArgsWithSpaces(string input, string expectedExe, string expectedArgs = "")
|
||||
{
|
||||
NormalizeTestCore(input, expectedExe, expectedArgs);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("ThereIsNoWayYouHaveAnExecutableNamedThisOnThePipeline", "ThereIsNoWayYouHaveAnExecutableNamedThisOnThePipeline", "")]
|
||||
[DataRow("C:\\ThisPathDoesNotExist\\NoExecutable.exe", "C:\\ThisPathDoesNotExist\\NoExecutable.exe", "")]
|
||||
public void NormalizeNonExistentExecutable(string input, string expectedExe, string expectedArgs = "")
|
||||
{
|
||||
NormalizeTestCore(input, expectedExe, expectedArgs);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[DataRow("C:\\Windows", "c:\\Windows", "")]
|
||||
[DataRow("C:\\Windows foo /bar", "c:\\Windows", "foo /bar")]
|
||||
public void NormalizeDirectoryAsExecutable(string input, string expectedExe, string expectedArgs = "")
|
||||
{
|
||||
NormalizeTestCore(input, expectedExe, expectedArgs);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
@@ -74,6 +75,8 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
[DataRow("ping bing.com", "ping.exe")]
|
||||
[DataRow("curl bing.com", "curl.exe")]
|
||||
[DataRow("ipconfig /all", "ipconfig.exe")]
|
||||
[DataRow("\"C:\\Program Files\\Windows Defender\\MsMpEng.exe\"", "MsMpEng.exe")]
|
||||
[DataRow("C:\\Program Files\\Windows Defender\\MsMpEng.exe", "MsMpEng.exe")]
|
||||
public async Task QueryWithoutHistoryCommand(string command, string exeName)
|
||||
{
|
||||
// Setup
|
||||
@@ -82,19 +85,24 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
|
||||
var pages = new ShellListPage(settings, mockHistory.Object);
|
||||
|
||||
pages.UpdateSearchText(string.Empty, command);
|
||||
|
||||
// wait for about 1s.
|
||||
await Task.Delay(1000);
|
||||
await UpdatePageAndWaitForItems(pages, () =>
|
||||
{
|
||||
// Test: Search for a command that exists in history
|
||||
pages.UpdateSearchText(string.Empty, command);
|
||||
});
|
||||
|
||||
var commandList = pages.GetItems();
|
||||
|
||||
Assert.AreEqual(1, commandList.Length);
|
||||
|
||||
var executeCommand = commandList.FirstOrDefault();
|
||||
Assert.IsNotNull(executeCommand);
|
||||
Assert.IsNotNull(executeCommand.Icon);
|
||||
Assert.IsTrue(executeCommand.Title.Contains(exeName), $"expect ${exeName} but got ${executeCommand.Title}");
|
||||
var listItem = commandList.FirstOrDefault();
|
||||
Assert.IsNotNull(listItem);
|
||||
|
||||
var runExeListItem = listItem as RunExeItem;
|
||||
Assert.IsNotNull(runExeListItem);
|
||||
Assert.AreEqual(exeName, runExeListItem.Exe);
|
||||
Assert.IsTrue(listItem.Title.Contains(exeName), $"expect ${exeName} but got ${listItem.Title}");
|
||||
Assert.IsNotNull(listItem.Icon);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
@@ -109,10 +117,11 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
|
||||
var pages = new ShellListPage(settings, mockHistoryService.Object);
|
||||
|
||||
// Test: Search for a command that exists in history
|
||||
pages.UpdateSearchText(string.Empty, command);
|
||||
|
||||
await Task.Delay(1000);
|
||||
await UpdatePageAndWaitForItems(pages, () =>
|
||||
{
|
||||
// Test: Search for a command that exists in history
|
||||
pages.UpdateSearchText(string.Empty, command);
|
||||
});
|
||||
|
||||
var commandList = pages.GetItems();
|
||||
|
||||
@@ -134,9 +143,11 @@ public class QueryTests : CommandPaletteUnitTestBase
|
||||
|
||||
var pages = new ShellListPage(settings, mockHistoryService.Object);
|
||||
|
||||
pages.UpdateSearchText("abcdefg", string.Empty);
|
||||
|
||||
await Task.Delay(1000);
|
||||
await UpdatePageAndWaitForItems(pages, () =>
|
||||
{
|
||||
// Test: Search for a command that exists in history
|
||||
pages.UpdateSearchText("abcdefg", string.Empty);
|
||||
});
|
||||
|
||||
var commandList = pages.GetItems();
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
@@ -16,10 +18,23 @@ public class CommandPaletteUnitTestBase
|
||||
|
||||
public IListItem[] Query(string query, IListItem[] candidates)
|
||||
{
|
||||
IListItem[] listItems = candidates
|
||||
var listItems = candidates
|
||||
.Where(item => MatchesFilter(query, item))
|
||||
.ToArray();
|
||||
|
||||
return listItems;
|
||||
}
|
||||
|
||||
public async Task UpdatePageAndWaitForItems(IDynamicListPage page, Action modification)
|
||||
{
|
||||
// Add an event handler for the ItemsChanged event,
|
||||
// Then call the modification action,
|
||||
// and wait for the event to be raised.
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
|
||||
page.ItemsChanged += (sender, args) => tcs.SetResult(null);
|
||||
|
||||
modification();
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos
|
||||
return;
|
||||
}
|
||||
|
||||
ShellHelpers.ParseExecutableAndArgs(searchText, out var exe, out var args);
|
||||
ShellListPageHelpers.NormalizeCommandLineAndArgs(searchText, out var exe, out var args);
|
||||
|
||||
// Check for cancellation before file system operations
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides command line normalization functionality compatible with .NET
|
||||
/// Native AOT. This is a C# port of the Profile::NormalizeCommandLine function
|
||||
/// from the Windows Terminal codebase.
|
||||
///
|
||||
/// It was ported from 7055b99ac on 2025-09-25
|
||||
/// </summary>
|
||||
public static class CommandLineNormalizer
|
||||
{
|
||||
#pragma warning disable SA1310 // Field names should not contain underscore
|
||||
private const int MAX_PATH = 260;
|
||||
private const uint INVALID_FILE_ATTRIBUTES = 0xFFFFFFFF;
|
||||
private const uint FILE_ATTRIBUTE_DIRECTORY = 0x10;
|
||||
#pragma warning restore SA1310 // Field names should not contain underscore
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern uint ExpandEnvironmentStringsW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpSrc,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpDst,
|
||||
uint nSize);
|
||||
|
||||
[DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern IntPtr CommandLineToArgvW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine,
|
||||
out int pNumArgs);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern uint SearchPathW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? lpPath,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string? lpExtension,
|
||||
uint nBufferLength,
|
||||
[MarshalAs(UnmanagedType.LPWStr)] StringBuilder lpBuffer,
|
||||
out IntPtr lpFilePart);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
private static extern uint GetFileAttributesW(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern IntPtr LocalFree(IntPtr hMem);
|
||||
|
||||
/// <summary>
|
||||
/// Normalizes a command line string by expanding environment variables, resolving executable paths,
|
||||
/// and standardizing the format for comparison purposes.
|
||||
/// </summary>
|
||||
/// <param name="commandLine">The command line string to normalize</param>
|
||||
/// <returns>A normalized command line string</returns>
|
||||
/// <remarks>
|
||||
/// This function performs the following operations:
|
||||
/// 1. Expands environment variables (e.g., %SystemRoot% -> C:\WINDOWS)
|
||||
/// 2. Parses the command line into arguments, stripping quotes
|
||||
/// 3. Resolves the executable path to an absolute, canonical path
|
||||
/// 4. Reconstructs the command line with null separators between arguments
|
||||
///
|
||||
/// Given a commandLine like:
|
||||
/// * "C:\WINDOWS\System32\cmd.exe"
|
||||
/// * "pwsh -WorkingDirectory ~"
|
||||
/// * "C:\Program Files\PowerShell\7\pwsh.exe"
|
||||
/// * "C:\Program Files\PowerShell\7\pwsh.exe -WorkingDirectory ~"
|
||||
///
|
||||
/// This function returns:
|
||||
/// * "C:\Windows\System32\cmd.exe"
|
||||
/// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
|
||||
/// * "C:\Program Files\PowerShell\7\pwsh.exe"
|
||||
/// * "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
|
||||
///
|
||||
/// The resulting strings are used for comparisons in profile matching.
|
||||
/// </remarks>
|
||||
public static string NormalizeCommandLine(string commandLine)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commandLine))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Turn "%SystemRoot%\System32\cmd.exe" into "C:\WINDOWS\System32\cmd.exe".
|
||||
// We do this early, as environment variables might occur anywhere in the commandLine.
|
||||
var normalized = ExpandEnvironmentVariables(commandLine);
|
||||
|
||||
// One of the most important things this function does is to strip quotes.
|
||||
// That way the commandLine "foo.exe -bar" and "\"foo.exe\" \"-bar\"" appear identical.
|
||||
// We'll use CommandLineToArgvW for that as it's close to what CreateProcessW uses.
|
||||
var argv = ParseCommandLineToArguments(normalized);
|
||||
|
||||
if (argv.Length == 0)
|
||||
{
|
||||
return normalized;
|
||||
}
|
||||
|
||||
// The index of the first argument in argv after our executable in argv[0].
|
||||
// Given {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"} this will be 1.
|
||||
var startOfArguments = 1;
|
||||
|
||||
// The given commandLine should start with an executable name or path.
|
||||
// This loop tries to resolve relative paths, as well as executable names in %PATH%
|
||||
// into absolute paths and normalizes them.
|
||||
var executablePath = ResolveExecutablePath(argv, ref startOfArguments);
|
||||
|
||||
// We've (hopefully) finished resolving the path to the executable.
|
||||
// We're now going to append all remaining arguments to the resulting string.
|
||||
// If argv is {"C:\Program Files\PowerShell\7\pwsh.exe", "-WorkingDirectory", "~"},
|
||||
// then we'll get "C:\Program Files\PowerShell\7\pwsh.exe\0-WorkingDirectory\0~"
|
||||
var result = new StringBuilder(executablePath);
|
||||
|
||||
for (var i = startOfArguments; i < argv.Length; i++)
|
||||
{
|
||||
result.Append('\0');
|
||||
result.Append(argv[i]);
|
||||
}
|
||||
|
||||
return result.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Expands environment variables in a string using Windows API.
|
||||
/// </summary>
|
||||
private static string ExpandEnvironmentVariables(string input)
|
||||
{
|
||||
const int initialBufferSize = 1024;
|
||||
var buffer = new StringBuilder(initialBufferSize);
|
||||
|
||||
var result = ExpandEnvironmentStringsW(input, buffer, (uint)buffer.Capacity);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
// Failed to expand, return original string
|
||||
return input;
|
||||
}
|
||||
|
||||
if (result > buffer.Capacity)
|
||||
{
|
||||
// Buffer was too small, resize and try again
|
||||
buffer.Capacity = (int)result;
|
||||
result = ExpandEnvironmentStringsW(input, buffer, (uint)buffer.Capacity);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
return input;
|
||||
}
|
||||
}
|
||||
|
||||
return buffer.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses a command line string into arguments using CommandLineToArgvW.
|
||||
/// </summary>
|
||||
private static string[] ParseCommandLineToArguments(string commandLine)
|
||||
{
|
||||
var argv = CommandLineToArgvW(commandLine, out var argc);
|
||||
|
||||
if (argv == IntPtr.Zero || argc == 0)
|
||||
{
|
||||
return Array.Empty<string>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var args = new string[argc];
|
||||
|
||||
for (var i = 0; i < argc; i++)
|
||||
{
|
||||
var argPtr = Marshal.ReadIntPtr(argv, i * IntPtr.Size);
|
||||
args[i] = Marshal.PtrToStringUni(argPtr) ?? string.Empty;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LocalFree(argv);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves the executable path from the command line arguments.
|
||||
/// Handles cases where the path contains spaces and was split during parsing.
|
||||
/// </summary>
|
||||
private static string ResolveExecutablePath(string[] argv, ref int startOfArguments)
|
||||
{
|
||||
if (argv.Length == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
// Try to resolve the executable path, handling cases where spaces in paths
|
||||
// might have caused the path to be split across multiple arguments
|
||||
for (var pathLength = 1; pathLength <= argv.Length; pathLength++)
|
||||
{
|
||||
// Build potential executable path by combining arguments
|
||||
var pathBuilder = new StringBuilder(argv[0]);
|
||||
for (var i = 1; i < pathLength; i++)
|
||||
{
|
||||
pathBuilder.Append(' ');
|
||||
pathBuilder.Append(argv[i]);
|
||||
}
|
||||
|
||||
var candidatePath = pathBuilder.ToString();
|
||||
var resolvedPath = TryResolveExecutable(candidatePath);
|
||||
|
||||
if (!string.IsNullOrEmpty(resolvedPath))
|
||||
{
|
||||
startOfArguments = pathLength;
|
||||
return GetCanonicalPath(resolvedPath);
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't resolve the path, return the first argument as-is
|
||||
startOfArguments = 1;
|
||||
return argv[0];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to resolve an executable path using SearchPathW.
|
||||
/// </summary>
|
||||
private static string TryResolveExecutable(string executableName)
|
||||
{
|
||||
var buffer = new StringBuilder(MAX_PATH);
|
||||
|
||||
var result = SearchPathW(
|
||||
null, // Use default search path
|
||||
executableName,
|
||||
".exe", // Default extension
|
||||
(uint)buffer.Capacity,
|
||||
buffer,
|
||||
out var _); // We don't need the file part
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (result > buffer.Capacity)
|
||||
{
|
||||
// Buffer was too small, resize and try again
|
||||
buffer.Capacity = (int)result;
|
||||
result = SearchPathW(null, executableName, ".exe", (uint)buffer.Capacity, buffer, out var _);
|
||||
|
||||
if (result == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
var resolvedPath = buffer.ToString();
|
||||
|
||||
// Verify the resolved path exists and is not a directory
|
||||
var attributes = GetFileAttributesW(resolvedPath);
|
||||
|
||||
return attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0 ? string.Empty : resolvedPath;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the canonical (absolute, normalized) path for a file.
|
||||
/// </summary>
|
||||
private static string GetCanonicalPath(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Path.GetFullPath(path);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If canonicalization fails, return the original path
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,4 +133,31 @@ public class ShellListPageHelpers
|
||||
|
||||
return li;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is a version of ParseExecutableAndArgs that handles whitespace in
|
||||
/// paths better. It will try to find the first matching executable in the
|
||||
/// input string.
|
||||
///
|
||||
/// If the input is quoted, it will treat everything inside the quotes as
|
||||
/// the executable. If the input is not quoted, it will try to find the
|
||||
/// first segment that matches
|
||||
/// </summary>
|
||||
public static void NormalizeCommandLineAndArgs(string input, out string executable, out string arguments)
|
||||
{
|
||||
var normalized = CommandLineNormalizer.NormalizeCommandLine(input);
|
||||
var segments = normalized.Split('\0', StringSplitOptions.RemoveEmptyEntries);
|
||||
executable = string.Empty;
|
||||
arguments = string.Empty;
|
||||
if (segments.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
executable = segments[0];
|
||||
if (segments.Length > 1)
|
||||
{
|
||||
arguments = string.Join(' ', segments[1..]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,14 +152,12 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
ShellHelpers.ParseExecutableAndArgs(expanded, out var exe, out var args);
|
||||
|
||||
// Check for cancellation before file system operations
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// Reset the path resolution flag
|
||||
var couldResolvePath = false;
|
||||
|
||||
var exe = string.Empty;
|
||||
var args = string.Empty;
|
||||
|
||||
var exeExists = false;
|
||||
var fullExePath = string.Empty;
|
||||
var pathIsDir = false;
|
||||
@@ -175,6 +173,8 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
|
||||
var pathResolutionTask = Task.Run(
|
||||
() =>
|
||||
{
|
||||
ShellListPageHelpers.NormalizeCommandLineAndArgs(expanded, out exe, out args);
|
||||
|
||||
// Don't check cancellation token here - let the Task timeout handle it
|
||||
exeExists = ShellListPageHelpers.FileExistInPath(exe, out fullExePath);
|
||||
pathIsDir = Directory.Exists(expanded);
|
||||
|
||||
Reference in New Issue
Block a user