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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Plugins/Flow.Launcher.Plugin.Program/UACDialog.xaml.cs b/Plugins/Flow.Launcher.Plugin.Program/UACDialog.xaml.cs
new file mode 100644
index 00000000000..3ab087b489f
--- /dev/null
+++ b/Plugins/Flow.Launcher.Plugin.Program/UACDialog.xaml.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Threading.Tasks;
+using System.Windows;
+using System.Windows.Input;
+
+namespace Flow.Launcher.Plugin.Program
+{
+ public partial class UACDialog : Window
+ {
+ private static readonly string ClassName = nameof(UACDialog);
+
+ private static UACDialog msgBox;
+ private static MessageBoxResult _result = MessageBoxResult.None;
+
+ private UACDialog()
+ {
+ InitializeComponent();
+ }
+
+ public static MessageBoxResult Show(string iconPath, string appName, string fullPath)
+ {
+ if (!Application.Current.Dispatcher.CheckAccess())
+ {
+ return Application.Current.Dispatcher.Invoke(() => Show(iconPath, appName, fullPath));
+ }
+
+ try
+ {
+ msgBox = new UACDialog
+ {
+ Title = Main.Context.API.GetTranslation("flowlauncher_plugin_program_user_account_control_title")
+ };
+
+ // Set icon & app name & program location
+ _ = msgBox.SetImageAsync(iconPath);
+ msgBox.AppName.Text = appName;
+ msgBox.ProgramLocation.Text = string.Format(
+ Main.Context.API.GetTranslation("flowlauncher_plugin_program_user_account_control_program_location"),
+ fullPath);
+
+ // Focus No by default
+ msgBox.btnNo.Focus();
+ _result = MessageBoxResult.No;
+
+ msgBox.ShowDialog();
+ return _result;
+ }
+ catch (Exception e)
+ {
+ Main.Context.API.LogError(ClassName, $"An error occurred: {e.Message}");
+ msgBox = null;
+ return MessageBoxResult.None;
+ }
+ }
+
+ private async Task SetImageAsync(string imagePath)
+ {
+ var imageSource = await Main.Context.API.LoadImageAsync(imagePath);
+ Img.Source = imageSource;
+ }
+
+ private void KeyEsc_OnPress(object sender, ExecutedRoutedEventArgs e)
+ {
+ DialogResult = false;
+ Close();
+ }
+
+ private void Button_Click(object sender, RoutedEventArgs e)
+ {
+ if (sender == btnYes)
+ _result = MessageBoxResult.Yes;
+ else if (sender == btnNo)
+ _result = MessageBoxResult.No;
+ else
+ _result = MessageBoxResult.None;
+ msgBox.Close();
+ msgBox = null;
+ }
+
+ private void Button_Cancel(object sender, RoutedEventArgs e)
+ {
+ _result = MessageBoxResult.Cancel;
+ msgBox.Close();
+ msgBox = null;
+ }
+ }
+}