Compare commits

...

2 Commits

Author SHA1 Message Date
Gordon Lam (SH)
f37754cc61 fix: improve comment wording for spell check 2026-01-27 19:20:57 -08:00
Gordon Lam (SH)
9be06842c1 feat(cmdpal): implement ActionRunner for elevated process execution 2026-01-27 19:10:21 -08:00
5 changed files with 179 additions and 4 deletions

View File

@@ -104,6 +104,66 @@ int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
}
}
}
else if (action == RUN_AS_USER || action == RUN_AS_ADMIN)
{
// Handle "Run as different user" and "Run as administrator" actions.
// This is used by Command Palette to work around WinUI3/MSIX packaging limitations
// where ShellExecute with "runas user"/"runas" verbs doesn't work properly from packaged apps.
int nextArg = 2;
std::wstring_view target;
std::wstring_view workingDir;
while (nextArg < nArgs)
{
if (std::wstring_view(args[nextArg]) == L"-target" && nextArg + 1 < nArgs)
{
target = args[nextArg + 1];
nextArg += 2;
}
else if (std::wstring_view(args[nextArg]) == L"-workingDir" && nextArg + 1 < nArgs)
{
workingDir = args[nextArg + 1];
nextArg += 2;
}
else
{
nextArg++;
}
}
if (target.empty())
{
Logger::error(L"ActionRunner: {} called without -target argument", action);
return 1;
}
Logger::trace(L"ActionRunner: {} target='{}' workingDir='{}'", action, target, workingDir);
SHELLEXECUTEINFOW sei = { sizeof(sei) };
sei.fMask = SEE_MASK_FLAG_NO_UI;
sei.lpFile = target.data();
sei.lpDirectory = workingDir.empty() ? nullptr : workingDir.data();
sei.lpVerb = (action == RUN_AS_ADMIN) ? L"runas" : L"runasuser";
sei.nShow = SW_SHOWNORMAL;
if (!ShellExecuteExW(&sei))
{
DWORD error = GetLastError();
if (error == ERROR_CANCELLED)
{
// User cancelled the UAC/credential dialog - this is expected behavior
Logger::trace(L"ActionRunner: User cancelled {} dialog for '{}'", action, target);
}
else
{
Logger::error(L"ActionRunner: ShellExecuteEx failed for {} '{}': error {}", action, target, error);
}
return static_cast<int>(error);
}
Logger::trace(L"ActionRunner: Successfully launched '{}' with {}", target, action);
}
return 0;
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
@@ -33,6 +34,7 @@ internal sealed partial class RunAsAdminCommand : InvokableCommand
{
if (packaged)
{
// For UWP/packaged apps, use shell:AppsFolder which works from packaged context
var command = "shell:AppsFolder\\" + target;
command = Environment.ExpandEnvironmentVariables(command.Trim());
@@ -43,9 +45,37 @@ internal sealed partial class RunAsAdminCommand : InvokableCommand
}
else
{
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.Administrator);
// For Win32 apps, use ActionRunner helper process to work around WinUI3/MSIX packaging limitation.
// When running from a packaged app, ShellExecute with "runas" verb may fail for certain apps
// (e.g., apps launched via .lnk shortcuts). ActionRunner runs outside the MSIX container,
// so it can properly invoke the UAC dialog.
var actionRunnerPath = ActionRunnerHelper.GetActionRunnerPath();
Process.Start(info);
if (string.IsNullOrEmpty(actionRunnerPath))
{
// Fallback to direct Process.Start if ActionRunner is not found
ExtensionHost.LogMessage($"ActionRunner not found, falling back to direct Process.Start for '{target}'");
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.Administrator);
Process.Start(info);
return;
}
var args = $"-run-as-admin -target \"{target}\"";
if (!string.IsNullOrEmpty(parentDir))
{
args += $" -workingDir \"{parentDir}\"";
}
var processInfo = new ProcessStartInfo
{
FileName = actionRunnerPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
};
ExtensionHost.LogMessage($"Launching '{target}' as administrator via ActionRunner");
Process.Start(processInfo);
}
});
}

View File

@@ -4,6 +4,7 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CmdPal.Ext.Apps.Utils;
@@ -29,9 +30,38 @@ internal sealed partial class RunAsUserCommand : InvokableCommand
{
await Task.Run(() =>
{
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.OtherUser);
// Use ActionRunner helper process to work around WinUI3/MSIX packaging limitation.
// When running from a packaged app, ShellExecute with the "runas user" verb causes
// CredentialUIBroker.exe to spawn infinitely without showing the credential dialog.
// ActionRunner runs outside the MSIX container, so it can properly invoke the credential UI.
var actionRunnerPath = ActionRunnerHelper.GetActionRunnerPath();
Process.Start(info);
if (string.IsNullOrEmpty(actionRunnerPath))
{
// Fallback to direct Process.Start if ActionRunner is not found
// This may not work in packaged context, but provides a fallback for development
ExtensionHost.LogMessage($"ActionRunner not found, falling back to direct Process.Start for '{target}'");
var info = ShellCommand.GetProcessStartInfo(target, parentDir, string.Empty, ShellCommand.RunAsType.OtherUser);
Process.Start(info);
return;
}
var args = $"-run-as-user -target \"{target}\"";
if (!string.IsNullOrEmpty(parentDir))
{
args += $" -workingDir \"{parentDir}\"";
}
var processInfo = new ProcessStartInfo
{
FileName = actionRunnerPath,
Arguments = args,
UseShellExecute = false,
CreateNoWindow = true,
};
ExtensionHost.LogMessage($"Launching '{target}' as different user via ActionRunner");
Process.Start(processInfo);
});
}

View File

@@ -0,0 +1,53 @@
// 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.IO;
using ManagedCommon;
namespace Microsoft.CmdPal.Ext.Apps.Utils;
/// <summary>
/// Helper class to locate and invoke the PowerToys ActionRunner executable.
/// ActionRunner is used to work around WinUI3/MSIX packaging limitations where
/// certain shell operations (like "Run as different user" or "Run as administrator")
/// don't work properly from within a packaged app context.
/// </summary>
internal static class ActionRunnerHelper
{
private const string ActionRunnerExeName = "PowerToys.ActionRunner.exe";
private static string? _cachedPath;
/// <summary>
/// Gets the path to the ActionRunner executable.
/// </summary>
/// <returns>The full path to ActionRunner.exe, or null if not found.</returns>
public static string? GetActionRunnerPath()
{
if (_cachedPath != null)
{
return _cachedPath;
}
_cachedPath = FindActionRunnerPath();
return _cachedPath;
}
private static string? FindActionRunnerPath()
{
// Use the standard PowerToys path resolver to find the installation directory.
// This handles registry lookups for installed versions and debug builds correctly.
var installPath = PowerToysPathResolver.GetPowerToysInstallPath();
if (!string.IsNullOrEmpty(installPath))
{
var actionRunnerPath = Path.Combine(installPath, ActionRunnerExeName);
if (File.Exists(actionRunnerPath))
{
return actionRunnerPath;
}
}
return null;
}
}

View File

@@ -6,4 +6,6 @@
namespace cmdArg
{
const inline wchar_t* RUN_NONELEVATED = L"-run-non-elevated";
const inline wchar_t* RUN_AS_USER = L"-run-as-user";
const inline wchar_t* RUN_AS_ADMIN = L"-run-as-admin";
}