diff --git a/tools/PI/DevHome.PI/Controls/AddToolControl.xaml b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml
index 4d59b92304..3ffd3bd233 100644
--- a/tools/PI/DevHome.PI/Controls/AddToolControl.xaml
+++ b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml
@@ -74,7 +74,7 @@
+ x:Name="ToolPathTextBox" Grid.Column="1" MinWidth="800" Margin="8,0,0,0" HorizontalAlignment="Stretch"/>
diff --git a/tools/PI/DevHome.PI/Controls/AddToolControl.xaml.cs b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml.cs
index 28c8a1abd6..16bb332707 100644
--- a/tools/PI/DevHome.PI/Controls/AddToolControl.xaml.cs
+++ b/tools/PI/DevHome.PI/Controls/AddToolControl.xaml.cs
@@ -5,9 +5,11 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
+using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Text;
using System.Threading.Tasks;
using DevHome.Common.Extensions;
using DevHome.PI.Helpers;
@@ -30,6 +32,14 @@ public sealed partial class AddToolControl : UserControl, INotifyPropertyChanged
private readonly string _invalidToolInfo = CommonHelper.GetLocalizedString("InvalidToolInfoMessage");
private readonly string _messageCloseText = CommonHelper.GetLocalizedString("MessageCloseText");
+ private readonly string _executablesFilter = CommonHelper.GetLocalizedString("FileDialogFilterExecutables");
+ private readonly string _batchFilesFilter = CommonHelper.GetLocalizedString("FileDialogFilterBatchFiles");
+ private readonly string _commandFilesFilter = CommonHelper.GetLocalizedString("FileDialogFilterCommandFiles");
+ private readonly string _mmcFilesFilter = CommonHelper.GetLocalizedString("FileDialogFilterMmcFiles");
+ private readonly string _powershellFilter = CommonHelper.GetLocalizedString("FileDialogFilterPowershell");
+ private readonly string _allFilesFilter = CommonHelper.GetLocalizedString("FileDialogFilterAllFiles");
+
+ private readonly char[] _fileDialogFilter;
// We have 3 sets of operations, and we arbitrarily divide the progress timing into 3 equal segments.
private const int ShortcutProcessingEndIndex = 33;
@@ -71,10 +81,23 @@ public int ProgressPercentage
public AddToolControl()
{
+ _fileDialogFilter = CreateFileDialogFilter().ToCharArray();
InitializeComponent();
LoadingProgressTextRing.DataContext = this;
}
+ private string CreateFileDialogFilter()
+ {
+ var sb = new StringBuilder();
+ sb.Append(CultureInfo.InvariantCulture, $"{_executablesFilter} (*.exe)\0*.exe\0");
+ sb.Append(CultureInfo.InvariantCulture, $"{_batchFilesFilter} (*.bat)\0*.bat\0");
+ sb.Append(CultureInfo.InvariantCulture, $"{_commandFilesFilter} (*.cmd)\0*.cmd\0");
+ sb.Append(CultureInfo.InvariantCulture, $"{_mmcFilesFilter} (*.msc)\0*.msc\0");
+ sb.Append(CultureInfo.InvariantCulture, $"{_powershellFilter} (*.ps1)\0*.ps1\0");
+ sb.Append(CultureInfo.InvariantCulture, $"{_allFilesFilter} (*.*)\0*.*\0\0");
+ return sb.ToString();
+ }
+
private void ToolBrowseButton_Click(object sender, RoutedEventArgs e)
{
HandleBrowseButton();
@@ -84,13 +107,11 @@ private void HandleBrowseButton()
{
// WinUI3's OpenFileDialog does not work if we're running elevated. So we have to use the old Win32 API.
var fileName = string.Empty;
- var filter = "Executables (*.exe)\0*.exe\0Batch Files (*.bat)\0*.bat\0\0";
- var filterarray = filter.ToCharArray();
var barWindow = Application.Current.GetService().DBarWindow;
unsafe
{
- fixed (char* file = new char[255], pFilter = filterarray)
+ fixed (char* file = new char[255], pFilter = _fileDialogFilter)
{
var openfile = new OPENFILENAMEW
{
diff --git a/tools/PI/DevHome.PI/Helpers/ExternalTool.cs b/tools/PI/DevHome.PI/Helpers/ExternalTool.cs
index a9c7ebd008..1da10f1bc4 100644
--- a/tools/PI/DevHome.PI/Helpers/ExternalTool.cs
+++ b/tools/PI/DevHome.PI/Helpers/ExternalTool.cs
@@ -5,6 +5,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
+using System.IO;
+using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -13,8 +15,10 @@
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Serilog;
-using Windows.System;
-using Windows.Win32.Foundation;
+using Windows.System;
+using Windows.Win32;
+using Windows.Win32.Foundation;
+using Windows.Win32.System.Com;
using static DevHome.PI.Helpers.WindowHelper;
namespace DevHome.PI.Helpers;
@@ -26,11 +30,12 @@ public enum ToolActivationType
Launch,
}
-// ExternalTool represents an imported tool
public partial class ExternalTool : ObservableObject
{
private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExternalTool));
+ private readonly string _errorMessageText = CommonHelper.GetLocalizedString("ToolLaunchErrorMessage");
+
public string ID { get; private set; }
public string Name { get; private set; }
@@ -45,7 +50,7 @@ public partial class ExternalTool : ObservableObject
public string AppUserModelId { get; private set; }
public string IconFilePath { get; private set; }
-
+
[ObservableProperty]
private bool _isPinned;
@@ -120,59 +125,121 @@ private async void GetIcons()
}
}
- internal async Task Invoke(int? pid, HWND? hwnd)
+ internal async Task Invoke(int? pid, HWND? hwnd)
{
- var result = false;
+ var process = default(Process);
- try
+ var parsedArguments = string.Empty;
+ if (!string.IsNullOrEmpty(Arguments))
{
- var parsedArguments = string.Empty;
- if (!string.IsNullOrEmpty(Arguments))
+ var argumentVariables = new Dictionary();
+ if (pid.HasValue)
{
- var argumentVariables = new Dictionary();
- if (pid.HasValue)
- {
- argumentVariables.Add("pid", pid.Value);
- }
-
- if (hwnd.HasValue)
- {
- argumentVariables.Add("hwnd", (int)hwnd.Value);
- }
+ argumentVariables.Add("pid", pid.Value);
+ }
- parsedArguments = ReplaceKnownVariables(Arguments, argumentVariables);
+ if (hwnd.HasValue)
+ {
+ argumentVariables.Add("hwnd", (int)hwnd.Value);
}
+ parsedArguments = ReplaceKnownVariables(Arguments, argumentVariables);
+ }
+
+ try
+ {
if (ActivationType == ToolActivationType.Protocol)
{
- result = await Launcher.LaunchUriAsync(new Uri($"{parsedArguments}"));
+ // Docs say this returns true if the default app for the URI scheme was launched;
+ // false otherwise. However, if there's no registered app for the protocol, it shows
+ // the "get an app from the store" dialog, and returns true. So we can't rely on the
+ // return value to know if the tool was actually launched.
+ var result = await Launcher.LaunchUriAsync(new Uri(parsedArguments));
+ if (result != true)
+ {
+ // We get here if the user supplied a valid registered protocol, but the app failed to launch.
+ var errorMessage = string.Format(
+ CultureInfo.InvariantCulture, _errorMessageText, parsedArguments);
+ throw new InvalidOperationException(errorMessage);
+ }
}
else
{
if (ActivationType == ToolActivationType.Msix)
{
- var process = Process.Start("explorer.exe", $"shell:AppsFolder\\{AppUserModelId}");
- result = process is not null;
+ process = LaunchPackagedTool(AppUserModelId);
}
else
{
- var startInfo = new ProcessStartInfo(Executable)
+ var finalExecutable = string.Empty;
+ var finalArguments = string.Empty;
+
+ if (Path.GetExtension(Executable).Equals(".msc", StringComparison.OrdinalIgnoreCase))
+ {
+ // Note: running most msc files requires elevation.
+ finalExecutable = "mmc.exe";
+ finalArguments = $"{Executable} {parsedArguments}";
+ }
+ else if (Path.GetExtension(Executable).Equals(".ps1", StringComparison.OrdinalIgnoreCase))
+ {
+ // Note: running powershell scripts might require setting the execution policy.
+ finalExecutable = "powershell.exe";
+ finalArguments = $"{Executable} {parsedArguments}";
+ }
+ else
{
- Arguments = parsedArguments,
+ finalExecutable = Executable;
+ finalArguments = parsedArguments;
+ }
+
+ var startInfo = new ProcessStartInfo()
+ {
+ FileName = finalExecutable,
+ Arguments = finalArguments,
UseShellExecute = false,
- RedirectStandardOutput = true,
};
- var process = Process.Start(startInfo);
- result = process is not null;
+ process = Process.Start(startInfo);
}
}
- }
- catch (Exception ex)
- {
- _log.Error(ex, "Tool launched failed");
+ }
+ catch (Exception ex)
+ {
+ // We compose a custom exception because an exception from executing some tools
+ // (powershell, mmc) will have lost the target tool information.
+ var errorMessage = string.Format(CultureInfo.InvariantCulture, _errorMessageText, Executable);
+ throw new InvalidOperationException(errorMessage, ex);
+ }
+
+ return process;
+ }
+
+ public static Process? LaunchPackagedTool(string appUserModelId)
+ {
+ var process = default(Process);
+ var clsid = CLSID.ApplicationActivationManager;
+ var iid = typeof(IApplicationActivationManager).GUID;
+ object obj;
+
+ int hr = PInvoke.CoCreateInstance(
+ in clsid, null, CLSCTX.CLSCTX_LOCAL_SERVER, in iid, out obj);
+
+ if (HResult.Succeeded(hr))
+ {
+ var appActiveManager = (IApplicationActivationManager)obj;
+ uint processId;
+ hr = appActiveManager.ActivateApplication(
+ appUserModelId, string.Empty, ACTIVATEOPTIONS.None, out processId);
+ if (HResult.Succeeded(hr))
+ {
+ process = Process.GetProcessById((int)processId);
+ }
+ }
+ else
+ {
+ Marshal.ThrowExceptionForHR(hr);
}
- return result;
+ return process;
}
private string ReplaceKnownVariables(string input, Dictionary argumentValues)
diff --git a/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs b/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs
index 44928a466c..83351a6cc5 100644
--- a/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs
+++ b/tools/PI/DevHome.PI/Helpers/ExternalToolsHelper.cs
@@ -18,37 +18,37 @@ namespace DevHome.PI.Helpers;
internal sealed class ExternalToolsHelper
{
- private readonly JsonSerializerOptions serializerOptions = new() { WriteIndented = true };
- private readonly string toolInfoFileName;
+ private readonly JsonSerializerOptions _serializerOptions = new() { WriteIndented = true };
+ private readonly string _toolInfoFileName;
public static readonly ExternalToolsHelper Instance = new();
private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(ExternalToolsHelper));
- private readonly ObservableCollection filteredExternalTools = [];
+ private readonly ObservableCollection _filteredExternalTools = [];
- private ObservableCollection allExternalTools = [];
+ private ObservableCollection _allExternalTools = [];
// The ExternalTools menu shows all registered tools.
public ObservableCollection AllExternalTools
{
- get => allExternalTools;
+ get => _allExternalTools;
set
{
// We're assigning the collection once, and this also covers the case where we reassign it again:
// we need to unsubscribe from the old collection's events, subscribe to the new collection's events,
// and initialize the filtered collection.
- if (allExternalTools != value)
+ if (_allExternalTools != value)
{
- if (allExternalTools != null)
+ if (_allExternalTools != null)
{
- allExternalTools.CollectionChanged -= AllExternalTools_CollectionChanged;
+ _allExternalTools.CollectionChanged -= AllExternalTools_CollectionChanged;
}
- allExternalTools = value;
- if (allExternalTools != null)
+ _allExternalTools = value;
+ if (_allExternalTools != null)
{
- allExternalTools.CollectionChanged += AllExternalTools_CollectionChanged;
+ _allExternalTools.CollectionChanged += AllExternalTools_CollectionChanged;
}
// Synchronize the filtered collection with this unfiltered one.
@@ -76,30 +76,30 @@ private ExternalToolsHelper()
// The file should be in this location:
// %LocalAppData%\Packages\Microsoft.Windows.DevHome_8wekyb3d8bbwe\LocalState\externaltools.json
- toolInfoFileName = Path.Combine(localFolder, "externaltools.json");
- AllExternalTools = new(allExternalTools);
- FilteredExternalTools = new(filteredExternalTools);
+ _toolInfoFileName = Path.Combine(localFolder, "externaltools.json");
+ AllExternalTools = new(_allExternalTools);
+ FilteredExternalTools = new(_filteredExternalTools);
}
internal void Init()
{
- allExternalTools.Clear();
- if (File.Exists(toolInfoFileName))
+ _allExternalTools.Clear();
+ if (File.Exists(_toolInfoFileName))
{
- var jsonData = File.ReadAllText(toolInfoFileName);
+ var jsonData = File.ReadAllText(_toolInfoFileName);
try
{
var toolCollection = JsonSerializer.Deserialize(jsonData);
var existingData = toolCollection?.ExternalTools ?? [];
foreach (var toolItem in existingData)
{
- allExternalTools.Add(toolItem);
+ _allExternalTools.Add(toolItem);
toolItem.PropertyChanged += ToolItem_PropertyChanged;
}
}
catch (Exception ex)
{
- _log.Error(ex, $"Failed to parse {toolInfoFileName}, attempting migration");
+ _log.Error(ex, $"Failed to parse {_toolInfoFileName}, attempting migration");
MigrateOldTools(jsonData);
}
}
@@ -135,7 +135,7 @@ private void MigrateOldTools(string jsonData)
string.Empty,
oldTool.IsPinned);
- allExternalTools.Add(newTool);
+ _allExternalTools.Add(newTool);
newTool.PropertyChanged += ToolItem_PropertyChanged;
}
@@ -155,14 +155,14 @@ private void ToolItem_PropertyChanged(object? sender, PropertyChangedEventArgs e
{
if (tool.IsPinned)
{
- if (!filteredExternalTools.Contains(tool))
+ if (!_filteredExternalTools.Contains(tool))
{
- filteredExternalTools.Add(tool);
+ _filteredExternalTools.Add(tool);
}
}
else
{
- filteredExternalTools.Remove(tool);
+ _filteredExternalTools.Remove(tool);
}
}
@@ -192,14 +192,14 @@ private bool IsJsonIgnoreProperty(string? propertyName)
public ExternalTool AddExternalTool(ExternalTool tool)
{
- allExternalTools.Add(tool);
+ _allExternalTools.Add(tool);
WriteToolsJsonFile();
return tool;
}
public void RemoveExternalTool(ExternalTool tool)
{
- if (allExternalTools.Remove(tool))
+ if (_allExternalTools.Remove(tool))
{
WriteToolsJsonFile();
}
@@ -207,12 +207,12 @@ public void RemoveExternalTool(ExternalTool tool)
private void WriteToolsJsonFile()
{
- var toolCollection = new ExternalToolCollection(ToolsCollectionVersion, allExternalTools);
- var updatedJson = JsonSerializer.Serialize(toolCollection, serializerOptions);
+ var toolCollection = new ExternalToolCollection(ToolsCollectionVersion, _allExternalTools);
+ var updatedJson = JsonSerializer.Serialize(toolCollection, _serializerOptions);
try
{
- File.WriteAllText(toolInfoFileName, updatedJson);
+ File.WriteAllText(_toolInfoFileName, updatedJson);
}
catch (Exception ex)
{
@@ -232,7 +232,7 @@ private void AllExternalTools_CollectionChanged(object? sender, NotifyCollection
{
if (newItem.IsPinned)
{
- filteredExternalTools.Add(newItem);
+ _filteredExternalTools.Add(newItem);
}
newItem.PropertyChanged += ToolItem_PropertyChanged;
@@ -247,7 +247,7 @@ private void AllExternalTools_CollectionChanged(object? sender, NotifyCollection
foreach (ExternalTool oldItem in e.OldItems)
{
oldItem.PropertyChanged -= ToolItem_PropertyChanged;
- filteredExternalTools.Remove(oldItem);
+ _filteredExternalTools.Remove(oldItem);
}
}
@@ -259,7 +259,7 @@ private void AllExternalTools_CollectionChanged(object? sender, NotifyCollection
foreach (ExternalTool oldItem in e.OldItems)
{
oldItem.PropertyChanged -= ToolItem_PropertyChanged;
- filteredExternalTools.Remove(oldItem);
+ _filteredExternalTools.Remove(oldItem);
}
}
@@ -269,7 +269,7 @@ private void AllExternalTools_CollectionChanged(object? sender, NotifyCollection
{
if (newItem.IsPinned)
{
- filteredExternalTools.Add(newItem);
+ _filteredExternalTools.Add(newItem);
}
newItem.PropertyChanged += ToolItem_PropertyChanged;
@@ -286,12 +286,12 @@ private void AllExternalTools_CollectionChanged(object? sender, NotifyCollection
private void SynchronizeAllFilteredItems()
{
- filteredExternalTools.Clear();
+ _filteredExternalTools.Clear();
foreach (var item in AllExternalTools)
{
if (item.IsPinned)
{
- filteredExternalTools.Add(item);
+ _filteredExternalTools.Add(item);
}
}
}
diff --git a/tools/PI/DevHome.PI/Helpers/IApplicationActivationManager.cs b/tools/PI/DevHome.PI/Helpers/IApplicationActivationManager.cs
new file mode 100644
index 0000000000..e4a6c77bc4
--- /dev/null
+++ b/tools/PI/DevHome.PI/Helpers/IApplicationActivationManager.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Runtime.InteropServices;
+
+namespace DevHome.PI.Helpers;
+
+[ComImport]
+[Guid("2E941141-7F97-4756-BA1D-9DECDE894A3D")]
+[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
+public interface IApplicationActivationManager
+{
+ int ActivateApplication(
+ [In] string appUserModelId,
+ [In] string arguments,
+ [In] ACTIVATEOPTIONS options,
+ [Out] out uint processId);
+}
+
+public enum ACTIVATEOPTIONS
+{
+ None = 0x00000000,
+ DesignMode = 0x00000001,
+ NoErrorUI = 0x00000002,
+ NoSplashScreen = 0x00000004,
+}
+
+public static class CLSID
+{
+ public static readonly Guid ApplicationActivationManager = new("45BA127D-10A8-46EA-8AB7-56EA9078943C");
+}
+
+public static class HResult
+{
+ public static bool Succeeded(int hr) => hr >= 0;
+
+ public static bool Failed(int hr) => hr < 0;
+}
diff --git a/tools/PI/DevHome.PI/Strings/en-us/Resources.resw b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw
index 92a8465f40..a307cbb5fe 100644
--- a/tools/PI/DevHome.PI/Strings/en-us/Resources.resw
+++ b/tools/PI/DevHome.PI/Strings/en-us/Resources.resw
@@ -1045,6 +1045,30 @@
Manage Tools
Label for a button to allow users to manage external tools
+
+ All Files
+ Filter string to specify that the file dialog should include all files
+
+
+ Batch Files
+ Filter string to specify that the file dialog should include batch files
+
+
+ Command Files
+ Filter string to specify that the file dialog should include command files
+
+
+ Executables
+ Filter string to specify that the file dialog should include executable files
+
+
+ Management Console Files
+ Filter string to specify that the file dialog should include Management Console files
+
+
+ PowerShell Scripts
+ Filter string to specify that the file dialog should include PowerShell files
+
Local WER collection for app
Label for Windows Error reporting toggle switch
diff --git a/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs b/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs
index 75c1a9a8a2..5182fc73c9 100644
--- a/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs
+++ b/tools/PI/DevHome.PI/ViewModels/BarWindowViewModel.cs
@@ -1,11 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
+using System;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
+using System.Text;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using DevHome.Common.Extensions;
@@ -14,19 +16,23 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
+using Serilog;
using Windows.Graphics;
using Windows.System;
+using Windows.Win32;
using Windows.Win32.Foundation;
+using Windows.Win32.UI.WindowsAndMessaging;
namespace DevHome.PI.ViewModels;
public partial class BarWindowViewModel : ObservableObject
{
+ private static readonly ILogger _log = Log.ForContext("SourceContext", nameof(BarWindowViewModel));
+
private const string UnsnapButtonText = "\ue89f";
private const string SnapButtonText = "\ue8a0";
private readonly string _errorTitleText = CommonHelper.GetLocalizedString("ToolLaunchErrorTitle");
- private readonly string _errorMessageText = CommonHelper.GetLocalizedString("ToolLaunchErrorMessage");
private readonly string _unsnapToolTip = CommonHelper.GetLocalizedString("UnsnapToolTip");
private readonly string _snapToolTip = CommonHelper.GetLocalizedString("SnapToolTip");
private readonly string _expandToolTip = CommonHelper.GetLocalizedString("SwitchToLargeLayoutToolTip");
@@ -114,7 +120,7 @@ public BarWindowViewModel()
var process = TargetAppData.Instance.TargetProcess;
- // Show either the process chooser, or the app bar. Not both
+ // Show either the result chooser, or the app bar. Not both
IsProcessChooserVisible = process is null;
IsAppBarVisible = !IsProcessChooserVisible;
if (IsAppBarVisible)
@@ -290,7 +296,7 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs
_dispatcher.TryEnqueue(() =>
{
- // The App status bar is only visible if we're attached to a process
+ // The App status bar is only visible if we're attached to a result
IsAppBarVisible = process is not null;
if (process is not null)
@@ -299,7 +305,7 @@ private void TargetApp_PropertyChanged(object? sender, PropertyChangedEventArgs
ApplicationName = process.ProcessName;
}
- // Conversely, the process chooser is only visible if we're not attached to a process
+ // Conversely, the result chooser is only visible if we're not attached to a result
IsProcessChooserVisible = process is null;
});
}
@@ -367,18 +373,25 @@ public void ManageExternalToolsButton_ExternalToolLaunchRequest(object sender, E
InvokeTool(tool, TargetAppData.Instance.TargetProcess?.Id, TargetAppData.Instance.HWnd);
}
- private void InvokeTool(ExternalTool tool, int? pid, HWND hWnd)
+ private async void InvokeTool(ExternalTool tool, int? pid, HWND hWnd)
{
- var process = tool.Invoke(pid, hWnd);
- if (process is null)
+ try
+ {
+ var process = await tool.Invoke(pid, hWnd);
+ }
+ catch (Exception ex)
{
- // A ContentDialog only renders in the space its parent occupies. Since the parent is a narrow
- // bar, the dialog doesn't have enough space to render. So, we'll use MessageBox to display errors.
- Windows.Win32.PInvoke.MessageBox(
- HWND.Null, // ThisHwnd,
- string.Format(CultureInfo.CurrentCulture, _errorMessageText, tool.Executable),
- _errorTitleText,
- Windows.Win32.UI.WindowsAndMessaging.MESSAGEBOX_STYLE.MB_ICONERROR);
+ _log.Error(ex, "Tool launched failed");
+
+ var builder = new StringBuilder();
+ builder.AppendLine(ex.Message);
+ if (ex.InnerException is not null)
+ {
+ builder.AppendLine(ex.InnerException.Message);
+ }
+
+ var errorMessage = string.Format(CultureInfo.CurrentCulture, builder.ToString(), tool.Executable);
+ PInvoke.MessageBox(HWND.Null, errorMessage, _errorTitleText, MESSAGEBOX_STYLE.MB_ICONERROR);
}
}