Skip to content
This repository was archived by the owner on Jun 5, 2025. It is now read-only.

Expand the list of file types for external tools #3406

Merged
merged 3 commits into from
Jul 12, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion tools/PI/DevHome.PI/Controls/AddToolControl.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@
<Button
x:Name="ToolBrowseButton" x:Uid="ToolBrowseButton" HorizontalAlignment="Left" MinWidth="100" Click="ToolBrowseButton_Click"/>
<TextBox
x:Name="ToolPathTextBox" Grid.Column="1" MinWidth="800" Margin="8,0,0,0" IsReadOnly="True" HorizontalAlignment="Stretch"/>
x:Name="ToolPathTextBox" Grid.Column="1" MinWidth="800" Margin="8,0,0,0" HorizontalAlignment="Stretch"/>
</Grid>

<Grid Grid.Row="2" Margin="0,6,0,0">
Expand Down
7 changes: 6 additions & 1 deletion tools/PI/DevHome.PI/Controls/AddToolControl.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,12 @@ 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 filter = "Executables (*.exe)\0*.exe\0" +
"Batch Files (*.bat)\0*.bat\0" +
"Command Files (*.cmd)\0*.cmd\0" +
"Management Console Files (*.msc)\0*.msc\0" +
"PowerShell Scripts (*.ps1)\0*.ps1\0" +
"All Files (*.*)\0*.*\0\0";
var filterarray = filter.ToCharArray();
var barWindow = Application.Current.GetService<PrimaryWindow>().DBarWindow;

Expand Down
132 changes: 99 additions & 33 deletions tools/PI/DevHome.PI/Helpers/ExternalTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.Json.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
Expand All @@ -13,8 +14,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;
Expand All @@ -26,11 +29,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; }
Expand All @@ -45,7 +49,7 @@ public partial class ExternalTool : ObservableObject
public string AppUserModelId { get; private set; }

public string IconFilePath { get; private set; }

[ObservableProperty]
private bool _isPinned;

Expand Down Expand Up @@ -120,59 +124,121 @@ private async void GetIcons()
}
}

internal async Task<bool> Invoke(int? pid, HWND? hwnd)
internal async Task<Process?> 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<string, int>();
if (pid.HasValue)
{
var argumentVariables = new Dictionary<string, int>();
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)
string finalExecutable;
string finalArguments;

if (Executable.EndsWith(".msc", StringComparison.OrdinalIgnoreCase))
{
// Note: running most msc files requires elevation.
finalExecutable = "mmc.exe";
finalArguments = $"{Executable} {parsedArguments}";
}
else if (Executable.EndsWith(".ps1", StringComparison.OrdinalIgnoreCase))
{
// Note: running powershell scripts might need 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<string, int> argumentValues)
Expand Down
Loading
Loading