mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-29 07:27:27 +01:00
Compare commits
2 Commits
main
...
issue/4483
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f37754cc61 | ||
|
|
9be06842c1 |
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user