diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index 7f02cef0990..00662d1e5da 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -48,7 +48,7 @@ public void DisablePortableMode() API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " + "after the restart your portable data profile will be deleted and roaming data profile kept"); - UpdateManager.RestartApp(Constant.ApplicationFileName); + API.RestartApp(); } catch (Exception e) { @@ -72,7 +72,7 @@ public void EnablePortableMode() API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " + "after the restart your roaming data profile will be deleted and portable data profile kept"); - UpdateManager.RestartApp(Constant.ApplicationFileName); + API.RestartApp(); } catch (Exception e) { diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index bc3655f69e7..f0a573f9550 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -1,19 +1,19 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net; using System.Net.Http; using System.Net.Sockets; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using System.Windows; -using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Http; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedCommands; using JetBrains.Annotations; using Squirrel; @@ -89,7 +89,7 @@ public async Task UpdateAppAsync(bool silentUpdate = true) if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) { - UpdateManager.RestartApp(Constant.ApplicationFileName); + _api.RestartApp(); } } catch (Exception e) diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs index 975609e0f08..e7513593220 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs @@ -383,6 +383,8 @@ public bool HideNotifyIcon public bool LeaveCmdOpen { get; set; } public bool HideWhenDeactivated { get; set; } = true; + public bool AlwaysRunAsAdministrator { get; set; } = false; + private bool _showAtTopmost = true; public bool ShowAtTopmost { diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs index 86e7b7c971c..e6b59dc6199 100644 --- a/Flow.Launcher.Infrastructure/Win32Helper.cs +++ b/Flow.Launcher.Infrastructure/Win32Helper.cs @@ -6,6 +6,7 @@ using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using System.Windows; @@ -791,5 +792,16 @@ public static unsafe void OpenFolderAndSelectFile(string filePath) } #endregion + + #region Administrator Mode + + public static bool IsAdministrator() + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } + + #endregion } } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 59e8cac202c..631b98cf1ae 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -1,5 +1,7 @@ using System; using System.Diagnostics; +using System.IO; +using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -24,6 +26,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.VisualStudio.Threading; +using Squirrel; namespace Flow.Launcher { @@ -238,12 +241,23 @@ private void AutoStartup() { try { - Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup); + Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup, _settings.AlwaysRunAsAdministrator); + } + catch (UnauthorizedAccessException) + { + // If it fails for permission, we need to ask the user to restart as administrator + if (API.ShowMsgBox( + API.GetTranslation("runAsAdministratorChangeAndRestart"), + API.GetTranslation("runAsAdministratorChange"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + RestartAppAsAdministrator(); + } } catch (Exception e) { - // but if it fails (permissions, etc) then don't keep retrying - // this also gives the user a visual indication in the Settings widget + // But if it fails for other reasons then do not keep retrying, + // set startup to false to give users a visual indication in the general page _settings.StartFlowLauncherOnSystemStartup = false; API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message); } @@ -321,6 +335,63 @@ private static void RegisterTaskSchedulerUnhandledException() #endregion + #region Restart + + /// + /// Restart the application without changing the user privileges. + /// + public static void RestartApp(bool forceAdmin = false) + { + if (Win32Helper.IsAdministrator() || forceAdmin) + { + RestartAppAsAdministrator(); + } + else + { + // Restart requires Squirrel's Update.exe to be present in the parent folder, + // it is only published from the project's release pipeline. When debugging without it, + // the project may not restart or just terminates. This is expected. + UpdateManager.RestartApp(Constant.ApplicationFileName); + } + } + + // Since Squirrel does not provide a way to restart the app as administrator, + // we need to do it manually by starting the update.exe with the runas verb + private static void RestartAppAsAdministrator() + { + var startInfo = new ProcessStartInfo + { + FileName = getUpdateExe(), + Arguments = $"--processStartAndWait {Constant.ExecutablePath}", + UseShellExecute = true, + Verb = "runas", + }; + Process.Start(startInfo); + Thread.Sleep(500); + Environment.Exit(0); + + // Local function + static string getUpdateExe() + { + Assembly entryAssembly = Assembly.GetEntryAssembly(); + if (entryAssembly != null && Path.GetFileName(entryAssembly.Location).Equals("update.exe", StringComparison.OrdinalIgnoreCase) && entryAssembly.Location.IndexOf("app-", StringComparison.OrdinalIgnoreCase) == -1 && entryAssembly.Location.IndexOf("SquirrelTemp", StringComparison.OrdinalIgnoreCase) == -1) + { + return Path.GetFullPath(entryAssembly.Location); + } + + entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly(); + FileInfo fileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(entryAssembly.Location), "..\\Update.exe")); + if (!fileInfo.Exists) + { + throw new Exception("Update.exe not found, not a Squirrel-installed app?"); + } + + return fileInfo.FullName; + } + } + + #endregion + #region IDisposable protected virtual void Dispose(bool disposing) diff --git a/Flow.Launcher/Helper/AutoStartup.cs b/Flow.Launcher/Helper/AutoStartup.cs index 34700c61015..2601645dbba 100644 --- a/Flow.Launcher/Helper/AutoStartup.cs +++ b/Flow.Launcher/Helper/AutoStartup.cs @@ -17,18 +17,20 @@ public class AutoStartup private const string LogonTaskName = $"{Constant.FlowLauncher} Startup"; private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup"; - public static void CheckIsEnabled(bool useLogonTaskForStartup) + private static readonly bool _isAdministrator = Win32Helper.IsAdministrator(); + + public static void CheckIsEnabled(bool useLogonTaskForStartup, bool alwaysRunAsAdministrator) { // We need to check both because if both of them are enabled, // Hide Flow Launcher on startup will not work since the later one will trigger main window show event - var logonTaskEnabled = CheckLogonTask(); + var logonTaskEnabled = CheckLogonTask(alwaysRunAsAdministrator); var registryEnabled = CheckRegistry(); if (useLogonTaskForStartup) { // Enable logon task if (!logonTaskEnabled) { - Enable(true); + Enable(true, alwaysRunAsAdministrator); } // Disable registry if (registryEnabled) @@ -41,7 +43,7 @@ public static void CheckIsEnabled(bool useLogonTaskForStartup) // Enable registry if (!registryEnabled) { - Enable(false); + Enable(false, alwaysRunAsAdministrator); } // Disable logon task if (logonTaskEnabled) @@ -51,7 +53,7 @@ public static void CheckIsEnabled(bool useLogonTaskForStartup) } } - private static bool CheckLogonTask() + private static bool CheckLogonTask(bool alwaysRunAsAdministrator) { using var taskService = new TaskService(); var task = taskService.RootFolder.AllTasks.FirstOrDefault(t => t.Name == LogonTaskName); @@ -59,20 +61,46 @@ private static bool CheckLogonTask() { try { - // Check if the action is the same as the current executable path - // If not, we need to unschedule and reschedule the task if (task.Definition.Actions.FirstOrDefault() is Microsoft.Win32.TaskScheduler.Action taskAction) { var action = taskAction.ToString().Trim(); - if (!action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase)) + var pathCorrect = action.Equals(Constant.ExecutablePath, StringComparison.OrdinalIgnoreCase); + var runLevelCorrect = CheckRunLevel(task.Definition.Principal.RunLevel, alwaysRunAsAdministrator); + + if (_isAdministrator) { - UnscheduleLogonTask(); - ScheduleLogonTask(); + // If path or run level is not correct, we need to unschedule and reschedule the task + if (!pathCorrect || !runLevelCorrect) + { + UnscheduleLogonTask(); + ScheduleLogonTask(alwaysRunAsAdministrator); + } + } + else + { + // If run level is not correct, we cannot edit it because we are not administrator + // So we just throw an exception to let the user know + if (!runLevelCorrect) + { + throw new UnauthorizedAccessException("Cannot edit task run level because the app is not running as administrator."); + } + + // If run level is correct and path is not correct, we need to unschedule and reschedule the task + if (!pathCorrect) + { + UnscheduleLogonTask(); + ScheduleLogonTask(alwaysRunAsAdministrator); + } } } return true; } + catch (UnauthorizedAccessException e) + { + App.API.LogError(ClassName, $"Failed to check logon task: {e}"); + throw; // Throw exception so that App.AutoStartup can show error message + } catch (Exception e) { App.API.LogError(ClassName, $"Failed to check logon task: {e}"); @@ -83,6 +111,11 @@ private static bool CheckLogonTask() return false; } + private static bool CheckRunLevel(TaskRunLevel rl, bool alwaysRunAsAdministrator) + { + return alwaysRunAsAdministrator ? rl == TaskRunLevel.Highest : rl != TaskRunLevel.Highest; + } + private static bool CheckRegistry() { try @@ -117,16 +150,19 @@ public static void DisableViaLogonTaskAndRegistry() Disable(false); } - public static void ChangeToViaLogonTask() + public static void ChangeToViaLogonTask(bool alwaysRunAsAdministrator) { Disable(false); - Enable(true); + Disable(true); // Remove old logon task so that we can create a new one + Enable(true, alwaysRunAsAdministrator); } public static void ChangeToViaRegistry() { Disable(true); - Enable(false); + Disable(false); // Remove old registry so that we can create a new one + // We do not need to use alwaysRunAsAdministrator for registry, so we just set false here + Enable(false, false); } private static void Disable(bool logonTask) @@ -149,13 +185,13 @@ private static void Disable(bool logonTask) } } - private static void Enable(bool logonTask) + private static void Enable(bool logonTask, bool alwaysRunAsAdministrator) { try { if (logonTask) { - ScheduleLogonTask(); + ScheduleLogonTask(alwaysRunAsAdministrator); } else { @@ -169,14 +205,15 @@ private static void Enable(bool logonTask) } } - private static bool ScheduleLogonTask() + private static bool ScheduleLogonTask(bool alwaysRunAsAdministrator) { using var td = TaskService.Instance.NewTask(); td.RegistrationInfo.Description = LogonTaskDesc; td.Triggers.Add(new LogonTrigger { UserId = WindowsIdentity.GetCurrent().Name, Delay = TimeSpan.FromSeconds(2) }); td.Actions.Add(Constant.ExecutablePath); - if (IsCurrentUserIsAdmin()) + // Only if the app is running as administrator, we can set the run level to highest + if (_isAdministrator && alwaysRunAsAdministrator) { td.Principal.RunLevel = TaskRunLevel.Highest; } @@ -212,13 +249,6 @@ private static bool UnscheduleLogonTask() } } - private static bool IsCurrentUserIsAdmin() - { - var identity = WindowsIdentity.GetCurrent(); - var principal = new WindowsPrincipal(identity); - return principal.IsInRole(WindowsBuiltInRole.Administrator); - } - private static bool UnscheduleRegistry() { using var key = Registry.CurrentUser.OpenSubKey(StartupPath, true); diff --git a/Flow.Launcher/Languages/en.xaml b/Flow.Launcher/Languages/en.xaml index f7f8dfd4272..97f294b14cb 100644 --- a/Flow.Launcher/Languages/en.xaml +++ b/Flow.Launcher/Languages/en.xaml @@ -46,6 +46,7 @@ Position Reset Reset search window position Type here to search + (Admin) Settings @@ -133,6 +134,10 @@ This can only be edited if plugin supports Home feature and Home Page is enabled. Show Search Window at Topmost Show search window above other windows + Always run as administrator + Run Flow Launcher as administrator on system startup + Administrator Mode Change + Do you want to restart as administrator to apply this change? Or you need to run as administrator during next start manually. Search Plugin diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index e527ab57b9b..3e03d51c69b 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -690,9 +690,13 @@ private void SoundPlay() private void InitializeNotifyIcon() { + var text = Win32Helper.IsAdministrator() ? + Constant.FlowLauncherFullName + " " + App.API.GetTranslation("admin") : + Constant.FlowLauncherFullName; + _notifyIcon = new NotifyIcon { - Text = Constant.FlowLauncherFullName, + Text = text, Icon = Constant.Version == "1.0.0" ? Properties.Resources.dev : Properties.Resources.app, Visible = !_settings.HideNotifyIcon }; diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index c125ea00f11..694b13cdd76 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -31,7 +31,6 @@ using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.ViewModel; using JetBrains.Annotations; -using Squirrel; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher @@ -88,7 +87,7 @@ public async void RestartApp() // Restart requires Squirrel's Update.exe to be present in the parent folder, // it is only published from the project's release pipeline. When debugging without it, // the project may not restart or just terminates. This is expected. - UpdateManager.RestartApp(Constant.ApplicationFileName); + App.RestartApp(); } public void ShowMainWindow() => _mainVM.Show(); diff --git a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs index 8db0a9f7e08..dc88f74fd58 100644 --- a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs +++ b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs @@ -45,7 +45,7 @@ private void ChangeAutoStartup(bool value) { if (Settings.UseLogonTaskForStartup) { - AutoStartup.ChangeToViaLogonTask(); + AutoStartup.ChangeToViaLogonTask(Settings.AlwaysRunAsAdministrator); } else { diff --git a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs index bec59a2b187..477db011768 100644 --- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs +++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using System.Windows; using System.Windows.Forms; using CommunityToolkit.Mvvm.Input; using Flow.Launcher.Core; @@ -8,6 +10,7 @@ using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; using Flow.Launcher.Infrastructure; +using Flow.Launcher.Infrastructure.Image; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; @@ -22,6 +25,8 @@ public partial class SettingsPaneGeneralViewModel : BaseModel private readonly Portable _portable; private readonly Internationalization _translater; + private static readonly bool _isAdministrator = Win32Helper.IsAdministrator(); + public SettingsPaneGeneralViewModel(Settings settings, Updater updater, Portable portable, Internationalization translater) { Settings = settings; @@ -41,6 +46,8 @@ public bool StartFlowLauncherOnSystemStartup get => Settings.StartFlowLauncherOnSystemStartup; set { + if (Settings.StartFlowLauncherOnSystemStartup == value) return; + Settings.StartFlowLauncherOnSystemStartup = value; try @@ -49,7 +56,7 @@ public bool StartFlowLauncherOnSystemStartup { if (UseLogonTaskForStartup) { - AutoStartup.ChangeToViaLogonTask(); + AutoStartup.ChangeToViaLogonTask(AlwaysRunAsAdministrator); } else { @@ -65,6 +72,13 @@ public bool StartFlowLauncherOnSystemStartup { App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message); } + + // If we have enabled logon task startup, we need to check if we need to restart the app + // even if we encounter an error while setting the startup method + if (value && UseLogonTaskForStartup) + { + _ = CheckAdminChangeAndAskForRestartAsync(); + } } } @@ -73,6 +87,8 @@ public bool UseLogonTaskForStartup get => Settings.UseLogonTaskForStartup; set { + if (UseLogonTaskForStartup == value) return; + Settings.UseLogonTaskForStartup = value; if (StartFlowLauncherOnSystemStartup) @@ -81,7 +97,7 @@ public bool UseLogonTaskForStartup { if (value) { - AutoStartup.ChangeToViaLogonTask(); + AutoStartup.ChangeToViaLogonTask(AlwaysRunAsAdministrator); } else { @@ -92,10 +108,70 @@ public bool UseLogonTaskForStartup { App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message); } - } + } + + // If we have enabled logon task startup, we need to check if we need to restart the app + // even if we encounter an error while setting the startup method + if (StartFlowLauncherOnSystemStartup && value) + { + _ = CheckAdminChangeAndAskForRestartAsync(); + } } } + public bool AlwaysRunAsAdministrator + { + get => Settings.AlwaysRunAsAdministrator; + set + { + if (AlwaysRunAsAdministrator == value) return; + + Settings.AlwaysRunAsAdministrator = value; + + if (StartFlowLauncherOnSystemStartup && UseLogonTaskForStartup) + { + try + { + AutoStartup.ChangeToViaLogonTask(value); + } + catch (Exception e) + { + App.API.ShowMsg(App.API.GetTranslation("setAutoStartFailed"), e.Message); + } + + // If we have enabled logon task startup, we need to check if we need to restart the app + // even if we encounter an error while setting the startup method + _ = CheckAdminChangeAndAskForRestartAsync(); + } + } + } + + private async Task CheckAdminChangeAndAskForRestartAsync() + { + // When we change from non-admin to admin, we need to restart the app as administrator to apply the changes + // Under non-administrator, we cannot delete or set the logon task which is run as administrator + if (AlwaysRunAsAdministrator && !_isAdministrator) + { + if (App.API.ShowMsgBox( + App.API.GetTranslation("runAsAdministratorChangeAndRestart"), + App.API.GetTranslation("runAsAdministratorChange"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + App.API.HideMainWindow(); + + // We must manually save because of Environment.Exit(0) + // which will cause ungraceful exit + App.API.SaveAppAllSettings(); + + // Wait for all image caches to be saved before restarting + await ImageLoader.WaitSaveAsync(); + + // Restart the app as administrator + App.RestartApp(true); + } + } + } + public List SearchWindowScreens { get; } = DropdownDataGeneric.GetValues("SearchWindowScreen"); diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index e8ae0dc3c3a..8f9fc14b59d 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -43,15 +43,27 @@ OffContent="{DynamicResource disable}" OnContent="{DynamicResource enable}" /> - - - + + + + + + + + + - Successfully disabled this program from displaying in your query This app is not intended to be run as administrator Unable to run {0} + User Account Control + Do you want to allow this app to make changes to your device? + Program location: {0} diff --git a/Plugins/Flow.Launcher.Plugin.Program/Main.cs b/Plugins/Flow.Launcher.Plugin.Program/Main.cs index d2884599467..f220190e253 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Main.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.IO; using System.Linq; +using System.Security.Principal; using System.Threading; using System.Threading.Tasks; using System.Windows.Controls; @@ -32,6 +33,8 @@ public class Main : ISettingProvider, IAsyncPlugin, IPluginI18n, IContextMenu, I internal static PluginInitContext Context { get; private set; } + internal static bool IsAdmin = IsAdministrator(); + private static readonly List emptyResults = new(); private static readonly MemoryCacheOptions cacheOptions = new() { SizeLimit = 1560 }; @@ -459,5 +462,12 @@ public void Dispose() { Win32.Dispose(); } + + private static bool IsAdministrator() + { + using var identity = WindowsIdentity.GetCurrent(); + var principal = new WindowsPrincipal(identity); + return principal.IsInRole(WindowsBuiltInRole.Administrator); + } } } diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs index cb33250e15e..182f3fed537 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs @@ -4,17 +4,17 @@ using System.IO; using System.Linq; using System.Security.Principal; +using System.Threading.Channels; using System.Threading.Tasks; +using System.Windows.Input; using System.Windows.Media.Imaging; -using Windows.ApplicationModel; -using Windows.Management.Deployment; +using System.Xml; using Flow.Launcher.Plugin.Program.Logger; using Flow.Launcher.Plugin.SharedModels; -using System.Threading.Channels; -using System.Xml; -using Windows.ApplicationModel.Core; -using System.Windows.Input; using MemoryPack; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Core; +using Windows.Management.Deployment; namespace Flow.Launcher.Plugin.Program.Programs { @@ -454,7 +454,9 @@ public Result Result(string query, IPublicAPI api) bool elevated = e.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); bool shouldRunElevated = elevated && CanRunElevated; - _ = Task.Run(() => Launch(shouldRunElevated)).ConfigureAwait(false); + + Launch(shouldRunElevated); + if (elevated && !shouldRunElevated) { var title = api.GetTranslation("flowlauncher_plugin_program_disable_dlgtitle_error"); @@ -497,7 +499,8 @@ public List ContextMenus(IPublicAPI api) Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Action = c => { - _ = Task.Run(() => Launch(true)).ConfigureAwait(false); + Launch(true); + return true; }, IcoPath = "Images/cmd.png", @@ -510,12 +513,17 @@ public List ContextMenus(IPublicAPI api) private void Launch(bool elevated = false) { - string command = "shell:AppsFolder\\" + UserModelId; + var command = "shell:AppsFolder\\" + UserModelId; command = Environment.ExpandEnvironmentVariables(command.Trim()); - var info = new ProcessStartInfo(command) { UseShellExecute = true, Verb = elevated ? "runas" : "" }; + var info = new ProcessStartInfo() + { + FileName = command, + UseShellExecute = true, + Verb = elevated ? "runas" : "" + }; - Main.StartProcess(Process.Start, info); + _ = Task.Run(() => Main.StartProcess(Process.Start, info)).ConfigureAwait(false); } internal static bool IfAppCanRunElevated(XmlNode appNode) diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs index a87b002d414..4e1d99454be 100644 --- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs +++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs @@ -1,21 +1,22 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Security; using System.Text; +using System.Threading.Channels; using System.Threading.Tasks; -using Microsoft.Win32; +using System.Windows; +using System.Windows.Input; using Flow.Launcher.Plugin.Program.Logger; +using Flow.Launcher.Plugin.Program.Views.Models; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Plugin.SharedModels; -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Channels; -using Flow.Launcher.Plugin.Program.Views.Models; using IniParser; -using System.Windows.Input; using MemoryPack; +using Microsoft.Win32; namespace Flow.Launcher.Plugin.Program.Programs { @@ -196,15 +197,7 @@ public Result Result(string query, IPublicAPI api) // Ctrl + Shift + Enter to run as admin bool runAsAdmin = c.SpecialKeyState.ToModifierKeys() == (ModifierKeys.Control | ModifierKeys.Shift); - var info = new ProcessStartInfo - { - FileName = FullPath, - WorkingDirectory = ParentDirectory, - UseShellExecute = true, - Verb = runAsAdmin ? "runas" : "", - }; - - _ = Task.Run(() => Main.StartProcess(Process.Start, info)); + Launch(runAsAdmin); return true; } @@ -213,6 +206,41 @@ public Result Result(string query, IPublicAPI api) return result; } + private void Launch(bool elevated = false) + { + var info = new ProcessStartInfo + { + FileName = FullPath, + WorkingDirectory = ParentDirectory, + UseShellExecute = true, + Verb = elevated ? "runas" : "", + }; + + if (Main.IsAdmin) + { + if (elevated) + { + // Since we are already elevated, we need to create UAC dialog manually + if (UACDialog.Show(IcoPath, Name, FullPath) != MessageBoxResult.Yes) + { + return; + } + } + else + { + // Use explorer.exe as workaround to start process as standard user + info = new ProcessStartInfo + { + FileName = "explorer.exe", + Arguments = $"\"{FullPath}\"", + WorkingDirectory = ParentDirectory, + UseShellExecute = true, + }; + } + } + + _ = Task.Run(() => Main.StartProcess(Process.Start, info)).ConfigureAwait(false); + } public List ContextMenus(IPublicAPI api) { @@ -228,7 +256,7 @@ public List ContextMenus(IPublicAPI api) FileName = FullPath, WorkingDirectory = ParentDirectory, UseShellExecute = true }; - _ = Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)); + _ = Task.Run(() => Main.StartProcess(ShellCommand.RunAsDifferentUser, info)).ConfigureAwait(false); return true; }, @@ -240,15 +268,7 @@ public List ContextMenus(IPublicAPI api) Title = api.GetTranslation("flowlauncher_plugin_program_run_as_administrator"), Action = c => { - var info = new ProcessStartInfo - { - FileName = FullPath, - WorkingDirectory = ParentDirectory, - Verb = "runas", - UseShellExecute = true - }; - - _ = Task.Run(() => Main.StartProcess(Process.Start, info)); + Launch(true); return true; }, diff --git a/Plugins/Flow.Launcher.Plugin.Program/UACDialog.xaml b/Plugins/Flow.Launcher.Plugin.Program/UACDialog.xaml new file mode 100644 index 00000000000..d619f47654d --- /dev/null +++ b/Plugins/Flow.Launcher.Plugin.Program/UACDialog.xaml @@ -0,0 +1,154 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +