diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs index 5da419cd40..c5f0b4ea3e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs @@ -2,8 +2,6 @@ // 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.Text.Json; using System.Text.Json.Nodes; using Microsoft.CmdPal.Ext.Bookmarks.Properties; @@ -75,31 +73,9 @@ internal sealed partial class AddBookmarkForm : FormContent var formBookmark = formInput["bookmark"] ?? string.Empty; var hasPlaceholder = formBookmark.ToString().Contains('{') && formBookmark.ToString().Contains('}'); - // Determine the type of the bookmark - string bookmarkType; - - if (formBookmark.ToString().StartsWith("http://", StringComparison.OrdinalIgnoreCase) || formBookmark.ToString().StartsWith("https://", StringComparison.OrdinalIgnoreCase)) - { - bookmarkType = "web"; - } - else if (File.Exists(formBookmark.ToString())) - { - bookmarkType = "file"; - } - else if (Directory.Exists(formBookmark.ToString())) - { - bookmarkType = "folder"; - } - else - { - // Default to web if we can't determine the type - bookmarkType = "web"; - } - var updated = _bookmark ?? new BookmarkData(); updated.Name = formName.ToString(); updated.Bookmark = formBookmark.ToString(); - updated.Type = bookmarkType; AddedCommand?.Invoke(this, updated); return CommandResult.GoHome(); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs index af6f1ef245..bf92a4413b 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkData.cs @@ -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.Text.Json.Serialization; +using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.Bookmarks; @@ -12,8 +14,38 @@ public class BookmarkData public string Bookmark { get; set; } = string.Empty; - public string Type { get; set; } = string.Empty; - + // public string Type { get; set; } = string.Empty; [JsonIgnore] public bool IsPlaceholder => Bookmark.Contains('{') && Bookmark.Contains('}'); + + internal void GetExeAndArgs(out string exe, out string args) + { + ShellHelpers.ParseExecutableAndArgs(Bookmark, out exe, out args); + } + + internal bool IsWebUrl() + { + GetExeAndArgs(out var exe, out var args); + if (string.IsNullOrEmpty(exe)) + { + return false; + } + + if (Uri.TryCreate(exe, UriKind.Absolute, out var uri)) + { + if (uri.Scheme == Uri.UriSchemeFile) + { + return false; + } + + // return true if the scheme is http or https, or if there's no scheme (e.g., "www.example.com") but there is a dot in the host + return + uri.Scheme == Uri.UriSchemeHttp || + uri.Scheme == Uri.UriSchemeHttps || + (string.IsNullOrEmpty(uri.Scheme) && uri.Host.Contains('.')); + } + + // If we can't parse it as a URI, we assume it's not a web URL + return false; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs index fedeb61467..4aac3e600e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderForm.cs @@ -2,17 +2,14 @@ // 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.Globalization; using System.Linq; using System.Text; using System.Text.Json.Nodes; using System.Text.RegularExpressions; -using ManagedCommon; using Microsoft.CmdPal.Ext.Bookmarks.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; -using Windows.System; namespace Microsoft.CmdPal.Ext.Bookmarks; @@ -25,7 +22,7 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent private readonly string _bookmark = string.Empty; // TODO pass in an array of placeholders - public BookmarkPlaceholderForm(string name, string url, string type) + public BookmarkPlaceholderForm(string name, string url) { _bookmark = url; var r = new Regex(Regex.Escape("{") + "(.*?)" + Regex.Escape("}")); @@ -88,23 +85,8 @@ internal sealed partial class BookmarkPlaceholderForm : FormContent target = target.Replace(placeholderString, placeholderData); } - try - { - var uri = UrlCommand.GetUri(target); - if (uri != null) - { - _ = Launcher.LaunchUriAsync(uri); - } - else - { - // throw new UriFormatException("The provided URL is not valid."); - } - } - catch (Exception ex) - { - Logger.LogError(ex.Message); - } + var success = UrlCommand.LaunchCommand(target); - return CommandResult.GoHome(); + return success ? CommandResult.Dismiss() : CommandResult.KeepOpen(); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs index d30f72bd95..7cea160954 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkPlaceholderPage.cs @@ -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 Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -9,19 +10,30 @@ namespace Microsoft.CmdPal.Ext.Bookmarks; internal sealed partial class BookmarkPlaceholderPage : ContentPage { + private readonly Lazy _icon; private readonly FormContent _bookmarkPlaceholder; public override IContent[] GetContent() => [_bookmarkPlaceholder]; + public override IconInfo Icon { get => _icon.Value; set => base.Icon = value; } + public BookmarkPlaceholderPage(BookmarkData data) - : this(data.Name, data.Bookmark, data.Type) + : this(data.Name, data.Bookmark) { } - public BookmarkPlaceholderPage(string name, string url, string type) + public BookmarkPlaceholderPage(string name, string url) { - Name = name; - Icon = new IconInfo(UrlCommand.IconFromUrl(url, type)); - _bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url, type); + Name = Properties.Resources.bookmarks_command_name_open; + + _bookmarkPlaceholder = new BookmarkPlaceholderForm(name, url); + + _icon = new Lazy(() => + { + ShellHelpers.ParseExecutableAndArgs(url, out var exe, out var args); + var t = UrlCommand.GetIconForPath(exe); + t.Wait(); + return t.Result; + }); } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs index 55cd7c93a1..91d9f902cb 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarksCommandProvider.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using ManagedCommon; @@ -35,10 +34,7 @@ public partial class BookmarksCommandProvider : CommandProvider private void AddNewCommand_AddedCommand(object sender, BookmarkData args) { ExtensionHost.LogMessage($"Adding bookmark ({args.Name},{args.Bookmark})"); - if (_bookmarks != null) - { - _bookmarks.Data.Add(args); - } + _bookmarks?.Data.Add(args); SaveAndUpdateCommands(); } @@ -116,7 +112,7 @@ public partial class BookmarksCommandProvider : CommandProvider // Add commands for folder types if (command is UrlCommand urlCommand) { - if (urlCommand.Type == "folder") + if (!bookmark.IsWebUrl()) { contextMenu.Add( new CommandContextItem(new DirectoryPage(urlCommand.Url))); @@ -124,10 +120,11 @@ public partial class BookmarksCommandProvider : CommandProvider contextMenu.Add( new CommandContextItem(new OpenInTerminalCommand(urlCommand.Url))); } - - listItem.Subtitle = urlCommand.Url; } + listItem.Title = bookmark.Name; + listItem.Subtitle = bookmark.Bookmark; + var edit = new AddBookmarkPage(bookmark) { Icon = Icons.EditIcon }; edit.AddedCommand += Edit_AddedCommand; contextMenu.Add(new CommandContextItem(edit)); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs index 6dddb9c32b..9cdf20805d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.Designer.cs @@ -78,6 +78,15 @@ namespace Microsoft.CmdPal.Ext.Bookmarks.Properties { } } + /// + /// Looks up a localized string similar to Open. + /// + public static string bookmarks_command_name_open { + get { + return ResourceManager.GetString("bookmarks_command_name_open", resourceCulture); + } + } + /// /// Looks up a localized string similar to Delete. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx index 5fe1e74e62..1038055b2d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Properties/Resources.resx @@ -148,6 +148,9 @@ Open + + Open + Name is required diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs index c641006730..d94f4619f1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/UrlCommand.cs @@ -3,52 +3,89 @@ // See the LICENSE file in the project root for more information. using System; +using System.Threading; +using System.Threading.Tasks; using ManagedCommon; using Microsoft.CommandPalette.Extensions.Toolkit; +using Windows.Storage.Streams; using Windows.System; namespace Microsoft.CmdPal.Ext.Bookmarks; public partial class UrlCommand : InvokableCommand { - public string Type { get; } + private readonly Lazy _icon; public string Url { get; } + public override IconInfo Icon { get => _icon.Value; set => base.Icon = value; } + public UrlCommand(BookmarkData data) - : this(data.Name, data.Bookmark, data.Type) + : this(data.Name, data.Bookmark) { } - public UrlCommand(string name, string url, string type) + public UrlCommand(string name, string url) { - Name = name; - Type = type; + Name = Properties.Resources.bookmarks_command_name_open; + Url = url; - Icon = new IconInfo(IconFromUrl(Url, type)); + + _icon = new Lazy(() => + { + ShellHelpers.ParseExecutableAndArgs(Url, out var exe, out var args); + var t = GetIconForPath(exe); + t.Wait(); + return t.Result; + }); } public override CommandResult Invoke() { - var target = Url; - try + var success = LaunchCommand(Url); + + return success ? CommandResult.Dismiss() : CommandResult.KeepOpen(); + } + + internal static bool LaunchCommand(string target) + { + ShellHelpers.ParseExecutableAndArgs(target, out var exe, out var args); + return LaunchCommand(exe, args); + } + + internal static bool LaunchCommand(string exe, string args) + { + if (string.IsNullOrEmpty(exe)) { - var uri = GetUri(target); + var message = "No executable found in the command."; + Logger.LogError(message); + + return false; + } + + if (ShellHelpers.OpenInShell(exe, args)) + { + return true; + } + + // If we reach here, it means the command could not be executed + // If there aren't args, then try again as a https: uri + if (string.IsNullOrEmpty(args)) + { + var uri = GetUri(exe); if (uri != null) { _ = Launcher.LaunchUriAsync(uri); } else { - // throw new UriFormatException("The provided URL is not valid."); + Logger.LogError("The provided URL is not valid."); } - } - catch (Exception ex) - { - Logger.LogError(ex.Message); + + return true; } - return CommandResult.Dismiss(); + return false; } internal static Uri? GetUri(string url) @@ -65,35 +102,90 @@ public partial class UrlCommand : InvokableCommand return uri; } - internal static string IconFromUrl(string url, string type) + public static async Task GetIconForPath(string target) { - switch (type) - { - case "file": - return "📄"; - case "folder": - return "📁"; - case "web": - default: - // Get the base url up to the first placeholder - var placeholderIndex = url.IndexOf('{'); - var baseString = placeholderIndex > 0 ? url.Substring(0, placeholderIndex) : url; - try - { - var uri = GetUri(baseString); - if (uri != null) - { - var hostname = uri.Host; - var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico"; - return faviconUrl; - } - } - catch (UriFormatException ex) - { - Logger.LogError(ex.Message); - } + IconInfo? icon = null; - return "🔗"; + // First, try to get the icon from the thumbnail helper + // This works for local files and folders + icon = await MaybeGetIconForPath(target); + if (icon != null) + { + return icon; } + + // Okay, that failed. Try to resolve the full path of the executable + var exeExists = false; + var fullExePath = string.Empty; + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(200)); + + // Use Task.Run with timeout - this will actually timeout even if the sync operations don't respond to cancellation + var pathResolutionTask = Task.Run( + () => + { + // Don't check cancellation token here - let the Task timeout handle it + exeExists = ShellHelpers.FileExistInPath(target, out fullExePath); + }, + CancellationToken.None); + + // Wait for either completion or timeout + pathResolutionTask.Wait(cts.Token); + } + catch (OperationCanceledException) + { + // Debug.WriteLine("Operation was canceled."); + } + + if (exeExists) + { + // If the executable exists, try to get the icon from the file + icon = await MaybeGetIconForPath(fullExePath); + if (icon != null) + { + return icon; + } + } + + // Get the base url up to the first placeholder + var placeholderIndex = target.IndexOf('{'); + var baseString = placeholderIndex > 0 ? target.Substring(0, placeholderIndex) : target; + try + { + var uri = GetUri(baseString); + if (uri != null) + { + var hostname = uri.Host; + var faviconUrl = $"{uri.Scheme}://{hostname}/favicon.ico"; + icon = new IconInfo(faviconUrl); + } + } + catch (UriFormatException) + { + } + + // If we still don't have an icon, use the target as the icon + icon = icon ?? new IconInfo(target); + + return icon; + } + + private static async Task MaybeGetIconForPath(string target) + { + try + { + var stream = await ThumbnailHelper.GetThumbnail(target); + if (stream != null) + { + var data = new IconData(RandomAccessStreamReference.CreateFromStream(stream)); + return new IconInfo(data, data); + } + } + catch + { + } + + return null; } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs index 6f5efb45fd..167956c166 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/FallbackExecuteItem.cs @@ -89,7 +89,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos return; } - ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args); + ShellHelpers.ParseExecutableAndArgs(searchText, out var exe, out var args); // Check for cancellation before file system operations cancellationToken.ThrowIfCancellationRequested(); @@ -191,7 +191,7 @@ internal sealed partial class FallbackExecuteItem : FallbackCommandItem, IDispos return false; } - ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args); + ShellHelpers.ParseExecutableAndArgs(searchText, out var exe, out var args); var exeExists = ShellListPageHelpers.FileExistInPath(exe, out var fullExePath); var pathIsDir = Directory.Exists(exe); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs index 7665c9b5f4..acf739cdbf 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Helpers/ShellListPageHelpers.cs @@ -54,47 +54,8 @@ public class ShellListPageHelpers internal static bool FileExistInPath(string filename, out string fullPath, CancellationToken? token = null) { - fullPath = string.Empty; - - if (File.Exists(filename)) - { - token?.ThrowIfCancellationRequested(); - fullPath = Path.GetFullPath(filename); - return true; - } - else - { - var values = Environment.GetEnvironmentVariable("PATH"); - if (values != null) - { - foreach (var path in values.Split(';')) - { - var path1 = Path.Combine(path, filename); - if (File.Exists(path1)) - { - fullPath = Path.GetFullPath(path1); - return true; - } - - token?.ThrowIfCancellationRequested(); - - var path2 = Path.Combine(path, filename + ".exe"); - if (File.Exists(path2)) - { - fullPath = Path.GetFullPath(path2); - return true; - } - - token?.ThrowIfCancellationRequested(); - } - - return false; - } - else - { - return false; - } - } + // TODO! remove this method and just use ShellHelpers.FileExistInPath directly + return ShellHelpers.FileExistInPath(filename, out fullPath, token ?? CancellationToken.None); } internal static ListItem? ListItemForCommandString(string query, Action? addToHistory) @@ -109,7 +70,7 @@ public class ShellListPageHelpers return null; } - ShellListPage.ParseExecutableAndArgs(searchText, out var exe, out var args); + ShellHelpers.ParseExecutableAndArgs(searchText, out var exe, out var args); var exeExists = false; var pathIsDir = false; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs index fdff707b3a..c3b5a66bf2 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Pages/ShellListPage.cs @@ -152,7 +152,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable return; } - ParseExecutableAndArgs(expanded, out var exe, out var args); + ShellHelpers.ParseExecutableAndArgs(expanded, out var exe, out var args); // Check for cancellation before file system operations cancellationToken.ThrowIfCancellationRequested(); @@ -439,46 +439,6 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable } } - internal static void ParseExecutableAndArgs(string input, out string executable, out string arguments) - { - input = input.Trim(); - executable = string.Empty; - arguments = string.Empty; - - if (string.IsNullOrEmpty(input)) - { - return; - } - - if (input.StartsWith("\"", System.StringComparison.InvariantCultureIgnoreCase)) - { - // Find the closing quote - var closingQuoteIndex = input.IndexOf('\"', 1); - if (closingQuoteIndex > 0) - { - executable = input.Substring(1, closingQuoteIndex - 1); - if (closingQuoteIndex + 1 < input.Length) - { - arguments = input.Substring(closingQuoteIndex + 1).TrimStart(); - } - } - } - else - { - // Executable ends at first space - var firstSpaceIndex = input.IndexOf(' '); - if (firstSpaceIndex > 0) - { - executable = input.Substring(0, firstSpaceIndex); - arguments = input[(firstSpaceIndex + 1)..].TrimStart(); - } - else - { - executable = input; - } - } - } - internal void CreateUriItems(string searchText) { if (!System.Uri.TryCreate(searchText, UriKind.Absolute, out var uri)) diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs index c75c59ba68..4ab7cfb02f 100644 --- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs +++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ShellHelpers.cs @@ -59,4 +59,101 @@ public static class ShellHelpers Administrator, OtherUser, } + + /// + /// Parses the input string to extract the executable and its arguments. + /// + public static void ParseExecutableAndArgs(string input, out string executable, out string arguments) + { + input = input.Trim(); + executable = string.Empty; + arguments = string.Empty; + + if (string.IsNullOrEmpty(input)) + { + return; + } + + if (input.StartsWith("\"", System.StringComparison.InvariantCultureIgnoreCase)) + { + // Find the closing quote + var closingQuoteIndex = input.IndexOf('\"', 1); + if (closingQuoteIndex > 0) + { + executable = input.Substring(1, closingQuoteIndex - 1); + if (closingQuoteIndex + 1 < input.Length) + { + arguments = input.Substring(closingQuoteIndex + 1).TrimStart(); + } + } + } + else + { + // Executable ends at first space + var firstSpaceIndex = input.IndexOf(' '); + if (firstSpaceIndex > 0) + { + executable = input.Substring(0, firstSpaceIndex); + arguments = input[(firstSpaceIndex + 1)..].TrimStart(); + } + else + { + executable = input; + } + } + } + + /// + /// Checks if a file exists somewhere in the PATH. + /// If it exists, returns the full path to the file in the out parameter. + /// If it does not exist, returns false and the out parameter is set to an empty string. + /// The name of the file to check. + /// The full path to the file if it exists; otherwise an empty string. + /// An optional cancellation token to cancel the operation. + /// True if the file exists in the PATH; otherwise false. + /// + public static bool FileExistInPath(string filename, out string fullPath, CancellationToken? token = null) + { + fullPath = string.Empty; + + if (File.Exists(filename)) + { + token?.ThrowIfCancellationRequested(); + fullPath = Path.GetFullPath(filename); + return true; + } + else + { + var values = Environment.GetEnvironmentVariable("PATH"); + if (values != null) + { + foreach (var path in values.Split(';')) + { + var path1 = Path.Combine(path, filename); + if (File.Exists(path1)) + { + fullPath = Path.GetFullPath(path1); + return true; + } + + token?.ThrowIfCancellationRequested(); + + var path2 = Path.Combine(path, filename + ".exe"); + if (File.Exists(path2)) + { + fullPath = Path.GetFullPath(path2); + return true; + } + + token?.ThrowIfCancellationRequested(); + } + + return false; + } + else + { + return false; + } + } + } }