diff --git a/Flow.Launcher.Command/Flow.Launcher.Command.csproj b/Flow.Launcher.Command/Flow.Launcher.Command.csproj
new file mode 100644
index 00000000000..4b68121f71b
--- /dev/null
+++ b/Flow.Launcher.Command/Flow.Launcher.Command.csproj
@@ -0,0 +1,47 @@
+
+
+
+ Exe
+ net7.0
+ enable
+ enable
+ Flow.Launcher.Command
+ false
+ false
+ false
+ app.ico
+
+
+
+ AnyCPU
+ true
+ portable
+ false
+ ..\Output\Debug\Command\
+ DEBUG;TRACE
+ prompt
+ 4
+ true
+ false
+
+
+
+ AnyCPU
+ pdbonly
+ true
+ ..\Output\Release\Command\
+ TRACE;RELEASE
+ prompt
+ 4
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Flow.Launcher.Command/Program.cs b/Flow.Launcher.Command/Program.cs
new file mode 100644
index 00000000000..e8a28cf5b99
--- /dev/null
+++ b/Flow.Launcher.Command/Program.cs
@@ -0,0 +1,132 @@
+using System.Diagnostics;
+
+namespace Flow.Launcher.Command;
+
+internal static class Program
+{
+ [STAThread]
+ private static int Main(string[] args)
+ {
+ if (args.Length == 0) return -1;
+
+ // Start process with arguments
+ // Usage: Flow.Launcher.Command -StartProcess -FileName -WorkingDirectory -Arguments -UseShellExecute -Verb -CreateNoWindow
+ if (args[0] == @"-StartProcess")
+ {
+ var fileName = string.Empty;
+ var workingDirectory = Environment.CurrentDirectory;
+ var argumentList = new List();
+ var useShellExecute = true;
+ var verb = string.Empty;
+ var createNoWindow = false;
+ var isArguments = false;
+
+ for (int i = 1; i < args.Length; i++)
+ {
+ switch (args[i])
+ {
+ case "-FileName":
+ if (i + 1 < args.Length)
+ fileName = args[++i];
+ isArguments = false;
+ break;
+
+ case "-WorkingDirectory":
+ if (i + 1 < args.Length)
+ workingDirectory = args[++i];
+ isArguments = false;
+ break;
+
+ case "-Arguments":
+ if (i + 1 < args.Length)
+ argumentList.Add(args[++i]);
+ isArguments = true;
+ break;
+
+ case "-UseShellExecute":
+ if (i + 1 < args.Length && bool.TryParse(args[++i], out bool useShell))
+ useShellExecute = useShell;
+ isArguments = false;
+ break;
+
+ case "-Verb":
+ if (i + 1 < args.Length)
+ verb = args[++i];
+ isArguments = false;
+ break;
+
+ case "-CreateNoWindow":
+ if (i + 1 < args.Length && bool.TryParse(args[++i], out bool createNoWin))
+ createNoWindow = createNoWin;
+ break;
+
+ default:
+ if (isArguments)
+ argumentList.Add(args[i]);
+ else
+ Console.WriteLine($"Unknown parameter: {args[i]}");
+ break;
+ }
+ }
+
+ if (string.IsNullOrEmpty(fileName))
+ {
+ Console.WriteLine("Error: -FileName is required.");
+ return -2;
+ }
+
+ try
+ {
+ ProcessStartInfo info;
+ if (argumentList.Count == 0)
+ {
+ info = new ProcessStartInfo
+ {
+ FileName = fileName,
+ WorkingDirectory = workingDirectory,
+ UseShellExecute = useShellExecute,
+ Verb = verb,
+ CreateNoWindow = createNoWindow
+ };
+ }
+ else if (argumentList.Count == 1)
+ {
+ info = new ProcessStartInfo
+ {
+ FileName = fileName,
+ WorkingDirectory = workingDirectory,
+ Arguments = argumentList[0],
+ UseShellExecute = useShellExecute,
+ Verb = verb,
+ CreateNoWindow = createNoWindow
+ };
+ }
+ else
+ {
+ info = new ProcessStartInfo
+ {
+ FileName = fileName,
+ WorkingDirectory = workingDirectory,
+ UseShellExecute = useShellExecute,
+ Verb = verb,
+ CreateNoWindow = createNoWindow
+ };
+ foreach (var arg in argumentList)
+ {
+ info.ArgumentList.Add(arg);
+ }
+ }
+ Process.Start(info)?.Dispose();
+ Console.WriteLine("Success.");
+ return 0;
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"Error: {ex.Message}");
+ return -3;
+ }
+ }
+
+ return -4;
+ }
+}
diff --git a/Flow.Launcher.Command/Properties/PublishProfiles/Net7.0-SelfContained.pubxml b/Flow.Launcher.Command/Properties/PublishProfiles/Net7.0-SelfContained.pubxml
new file mode 100644
index 00000000000..0e5cf4489b2
--- /dev/null
+++ b/Flow.Launcher.Command/Properties/PublishProfiles/Net7.0-SelfContained.pubxml
@@ -0,0 +1,18 @@
+
+
+
+
+ FileSystem
+ Release
+ Any CPU
+ net7.0-windows10.0.19041.0
+ ..\Output\Release\
+ win-x64
+ true
+ False
+ False
+ False
+
+
diff --git a/Flow.Launcher.Command/app.ico b/Flow.Launcher.Command/app.ico
new file mode 100644
index 00000000000..36b1d22d0b3
Binary files /dev/null and b/Flow.Launcher.Command/app.ico differ
diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs
index 721e14dcaac..029d78a608b 100644
--- a/Flow.Launcher.Core/Configuration/Portable.cs
+++ b/Flow.Launcher.Core/Configuration/Portable.cs
@@ -47,7 +47,7 @@ public void DisablePortableMode()
API.ShowMsgBox(API.GetTranslation("restartToDisablePortableMode"));
- UpdateManager.RestartApp(Constant.ApplicationFileName);
+ API.RestartApp();
}
catch (Exception e)
{
@@ -70,7 +70,7 @@ public void EnablePortableMode()
API.ShowMsgBox(API.GetTranslation("restartToEnablePortableMode"));
- UpdateManager.RestartApp(Constant.ApplicationFileName);
+ API.RestartApp();
}
catch (Exception e)
{
diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs
index 45275696c1d..7aab5f4ed47 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;
@@ -94,7 +94,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/Constant.cs b/Flow.Launcher.Infrastructure/Constant.cs
index 13da9f79f3b..fae742d9908 100644
--- a/Flow.Launcher.Infrastructure/Constant.cs
+++ b/Flow.Launcher.Infrastructure/Constant.cs
@@ -16,6 +16,7 @@ public static class Constant
private static readonly Assembly Assembly = Assembly.GetExecutingAssembly();
public static readonly string ProgramDirectory = Directory.GetParent(Assembly.Location.NonNull()).ToString();
public static readonly string ExecutablePath = Path.Combine(ProgramDirectory, FlowLauncher + ".exe");
+ public static readonly string CommandExecutablePath = Path.Combine(ProgramDirectory, "Command", "Flow.Launcher.Command.exe");
public static readonly string ApplicationDirectory = Directory.GetParent(ProgramDirectory).ToString();
public static readonly string RootDirectory = Directory.GetParent(ApplicationDirectory).ToString();
diff --git a/Flow.Launcher.Infrastructure/Http/Http.cs b/Flow.Launcher.Infrastructure/Http/Http.cs
index 8afab419bbc..7e2c292fa30 100644
--- a/Flow.Launcher.Infrastructure/Http/Http.cs
+++ b/Flow.Launcher.Infrastructure/Http/Http.cs
@@ -232,7 +232,7 @@ public static async Task GetStringAsync(string url, CancellationToken to
Log.Debug(ClassName, $"Url <{url}>");
return await client.GetStringAsync(url, token);
}
- catch (System.Exception e)
+ catch (System.Exception)
{
return string.Empty;
}
diff --git a/Flow.Launcher.Infrastructure/NativeMethods.txt b/Flow.Launcher.Infrastructure/NativeMethods.txt
index eb844dd7ca0..07cdcf8f555 100644
--- a/Flow.Launcher.Infrastructure/NativeMethods.txt
+++ b/Flow.Launcher.Infrastructure/NativeMethods.txt
@@ -86,4 +86,18 @@ EVENT_OBJECT_HIDE
EVENT_SYSTEM_DIALOGEND
WM_POWERBROADCAST
-PBT_APMRESUMEAUTOMATIC
\ No newline at end of file
+PBT_APMRESUMEAUTOMATIC
+
+OpenProcessToken
+GetCurrentProcess
+LookupPrivilegeValue
+SE_INCREASE_QUOTA_NAME
+CloseHandle
+TOKEN_PRIVILEGES
+AdjustTokenPrivileges
+GetShellWindow
+GetWindowThreadProcessId
+OpenProcess
+GetProcessId
+DuplicateTokenEx
+CreateProcessWithTokenW
\ No newline at end of file
diff --git a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
index 23f9047fef7..e6944871aa9 100644
--- a/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
+++ b/Flow.Launcher.Infrastructure/UserSettings/Settings.cs
@@ -482,6 +482,8 @@ public bool HideNotifyIcon
public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactivated { get; set; } = true;
+ public bool AlwaysRunAsAdministrator { get; set; } = false;
+
private bool _showAtTopmost = false;
public bool ShowAtTopmost
{
diff --git a/Flow.Launcher.Infrastructure/Win32Helper.cs b/Flow.Launcher.Infrastructure/Win32Helper.cs
index 7cc644eaa32..38c57565e9d 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;
@@ -19,6 +20,7 @@
using Windows.Win32;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
+using Windows.Win32.Security;
using Windows.Win32.System.Threading;
using Windows.Win32.UI.Input.KeyboardAndMouse;
using Windows.Win32.UI.Shell.Common;
@@ -692,6 +694,7 @@ public static void OpenImeSettings()
{
try
{
+ // No need to de-elevate since we are opening windows settings which cannot bring security risks
Process.Start(new ProcessStartInfo("ms-settings:regionlanguage") { UseShellExecute = true });
}
catch (System.Exception)
@@ -904,5 +907,160 @@ public static void EnableWin32DarkMode(string colorScheme)
}
#endregion
+
+ #region Administrator Mode
+
+ public static bool IsAdministrator()
+ {
+ using var identity = WindowsIdentity.GetCurrent();
+ var principal = new WindowsPrincipal(identity);
+ return principal.IsInRole(WindowsBuiltInRole.Administrator);
+ }
+
+ ///
+ /// Inspired by
+ /// Document:
+ ///
+ public static unsafe bool RunAsDesktopUser(string app, string currentDir, string cmdLine, bool loadProfile, bool createNoWindow, out string errorInfo)
+ {
+ STARTUPINFOW si = new();
+ PROCESS_INFORMATION pi = new();
+ errorInfo = string.Empty;
+ HANDLE hShellProcess = HANDLE.Null, hShellProcessToken = HANDLE.Null, hPrimaryToken = HANDLE.Null;
+ HWND hwnd;
+ uint dwPID;
+
+ // 1. Enable the SeIncreaseQuotaPrivilege in your current token
+ if (!PInvoke.OpenProcessToken(PInvoke.GetCurrentProcess_SafeHandle(), TOKEN_ACCESS_MASK.TOKEN_ADJUST_PRIVILEGES, out var hProcessToken))
+ {
+ errorInfo = $"OpenProcessToken failed: {Marshal.GetLastWin32Error()}";
+ return false;
+ }
+
+ if (!PInvoke.LookupPrivilegeValue(null, PInvoke.SE_INCREASE_QUOTA_NAME, out var luid))
+ {
+ errorInfo = $"LookupPrivilegeValue failed: {Marshal.GetLastWin32Error()}";
+ hProcessToken.Dispose();
+ return false;
+ }
+
+ var tp = new TOKEN_PRIVILEGES
+ {
+ PrivilegeCount = 1,
+ Privileges = new()
+ {
+ e0 = new LUID_AND_ATTRIBUTES
+ {
+ Luid = luid,
+ Attributes = TOKEN_PRIVILEGES_ATTRIBUTES.SE_PRIVILEGE_ENABLED
+ }
+ }
+ };
+
+ PInvoke.AdjustTokenPrivileges(hProcessToken, false, &tp, 0, null, null);
+ var lastError = Marshal.GetLastWin32Error();
+ hProcessToken.Dispose();
+
+ if (lastError != 0)
+ {
+ errorInfo = $"AdjustTokenPrivileges failed: {lastError}";
+ return false;
+ }
+
+retry:
+ // 2. Get an HWND representing the desktop shell
+ hwnd = PInvoke.GetShellWindow();
+ if (hwnd == HWND.Null)
+ {
+ errorInfo = "No desktop shell is present.";
+ return false;
+ }
+
+ // 3. Get the Process ID (PID) of the process associated with that window
+ _ = PInvoke.GetWindowThreadProcessId(hwnd, &dwPID);
+ if (dwPID == 0)
+ {
+ errorInfo = "Unable to get PID of desktop shell.";
+ return false;
+ }
+
+ // 4. Open that process
+ hShellProcess = PInvoke.OpenProcess(PROCESS_ACCESS_RIGHTS.PROCESS_QUERY_INFORMATION, false, dwPID);
+ if (hShellProcess == HANDLE.Null)
+ {
+ errorInfo = $"Can't open desktop shell process: {Marshal.GetLastWin32Error()}";
+ return false;
+ }
+
+ if (hwnd != PInvoke.GetShellWindow())
+ {
+ PInvoke.CloseHandle(hShellProcess);
+ goto retry;
+ }
+
+ _ = PInvoke.GetWindowThreadProcessId(hwnd, &dwPID);
+ if (dwPID != PInvoke.GetProcessId(hShellProcess))
+ {
+ PInvoke.CloseHandle(hShellProcess);
+ goto retry;
+ }
+
+ // 5. Get the access token from that process
+ if (!PInvoke.OpenProcessToken(hShellProcess, TOKEN_ACCESS_MASK.TOKEN_DUPLICATE, &hShellProcessToken))
+ {
+ errorInfo = $"Can't get process token of desktop shell: {Marshal.GetLastWin32Error()}";
+ goto cleanup;
+ }
+
+ // 6. Make a primary token with that token
+ var tokenRights = TOKEN_ACCESS_MASK.TOKEN_QUERY | TOKEN_ACCESS_MASK.TOKEN_ASSIGN_PRIMARY |
+ TOKEN_ACCESS_MASK.TOKEN_DUPLICATE | TOKEN_ACCESS_MASK.TOKEN_ADJUST_DEFAULT |
+ TOKEN_ACCESS_MASK.TOKEN_ADJUST_SESSIONID;
+ if (!PInvoke.DuplicateTokenEx(hShellProcessToken, tokenRights, null, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, &hPrimaryToken))
+ {
+ errorInfo = $"Can't get primary token: {Marshal.GetLastWin32Error()}";
+ goto cleanup;
+ }
+
+ // 7. Start the new process with that primary token
+ fixed (char* appPtr = app)
+ // Because argv[0] is the module name, C programmers generally repeat the module name as the first token in the command line
+ // So we add one more dash before the command line to make command line work correctly
+ fixed (char* cmdLinePtr = $"- {cmdLine}")
+ fixed (char* currentDirPtr = currentDir)
+ {
+ if (!PInvoke.CreateProcessWithToken(
+ hPrimaryToken,
+ // If you need to access content in HKEY_CURRENT_USER, please set loadProfile to true
+ loadProfile ? CREATE_PROCESS_LOGON_FLAGS.LOGON_WITH_PROFILE : 0,
+ appPtr,
+ cmdLinePtr,
+ // If you do not want to create a window for console app, please set createNoWindow to true
+ createNoWindow ? PROCESS_CREATION_FLAGS.CREATE_NO_WINDOW : 0,
+ null,
+ currentDirPtr,
+ &si,
+ &pi))
+ {
+ errorInfo = $"CreateProcessWithTokenW failed: {Marshal.GetLastWin32Error()}";
+ goto cleanup;
+ }
+ }
+
+ if (pi.hProcess != HANDLE.Null) PInvoke.CloseHandle(pi.hProcess);
+ if (pi.hThread != HANDLE.Null) PInvoke.CloseHandle(pi.hThread);
+ if (hShellProcessToken != HANDLE.Null) PInvoke.CloseHandle(hShellProcessToken);
+ if (hPrimaryToken != HANDLE.Null) PInvoke.CloseHandle(hPrimaryToken);
+ if (hShellProcess != HANDLE.Null) PInvoke.CloseHandle(hShellProcess);
+ return true;
+
+cleanup:
+ if (hShellProcessToken != HANDLE.Null) PInvoke.CloseHandle(hShellProcessToken);
+ if (hPrimaryToken != HANDLE.Null) PInvoke.CloseHandle(hPrimaryToken);
+ if (hShellProcess != HANDLE.Null) PInvoke.CloseHandle(hShellProcess);
+ return false;
+ }
+
+ #endregion
}
}
diff --git a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
index dcccaebebed..43dc44c69fa 100644
--- a/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
+++ b/Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Runtime.CompilerServices;
@@ -29,13 +30,21 @@ public interface IPublicAPI
void ChangeQuery(string query, bool requery = false);
///
- /// Restart Flow Launcher
+ /// Restart Flow Launcher without changing the user privileges.
///
void RestartApp();
+ ///
+ /// Restart Flow Launcher as administrator.
+ ///
+ void RestartAppAsAdmin();
+
///
/// Run a shell command
///
+ ///
+ /// It can help to start a de-elevated process and show user account control dialog when Flow is running as administrator.
+ ///
/// The command or program to run
/// the shell type to run, e.g. powershell.exe
/// Thrown when unable to find the file specified in the command
@@ -613,7 +622,7 @@ public interface IPublicAPI
/// Invoked when the actual theme of the application has changed. Currently, the plugin will continue to be subscribed even if it is turned off.
///
event ActualApplicationThemeChangedEventHandler ActualApplicationThemeChanged;
-
+
///
/// Get the user data directory of Flow Launcher.
///
@@ -625,5 +634,35 @@ public interface IPublicAPI
///
///
string GetLogDirectory();
+
+ ///
+ /// Start a process with support for handling administrative privileges
+ ///
+ ///
+ /// It can help to start a de-elevated process and show user account control dialog when Flow is running as administrator.
+ ///
+ /// File name
+ /// Working directory. If not specified, the current directory will be used
+ /// Optional arguments to pass to the process. If not specified, no arguments will be passed
+ /// Whether to use shell to execute the process
+ /// Verb to use when starting the process, e.g. "runas" for elevated permissions. If not specified, no verb will be used.
+ /// Whether to create console window
+ /// Whether process is started successfully
+ public bool StartProcess(string fileName, string workingDirectory = "", string arguments = "", bool useShellExecute = false, string verb = "", bool createNoWindow = false);
+
+ ///
+ /// Start a process with support for handling administrative privileges
+ ///
+ ///
+ /// It can help to start a de-elevated process and show user account control dialog when Flow is running as administrator.
+ ///
+ /// File name
+ /// Working directory. If not specified, the current directory will be used
+ /// Optional argument list to pass to the process. If not specified, no arguments will be passed
+ /// Whether to use shell to execute the process
+ /// Verb to use when starting the process, e.g. "runas" for elevated permissions. If not specified, no verb will be used.
+ /// Whether to create console window
+ /// Whether process is started successfully
+ public bool StartProcess(string fileName, string workingDirectory = "", Collection argumentList = null, bool useShellExecute = false, string verb = "", bool createNoWindow = false);
}
}
diff --git a/Flow.Launcher.sln b/Flow.Launcher.sln
index e44b23232fb..eb607689320 100644
--- a/Flow.Launcher.sln
+++ b/Flow.Launcher.sln
@@ -71,6 +71,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.Plugin
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Flow.Launcher.Plugin.WindowsSettings", "Plugins\Flow.Launcher.Plugin.WindowsSettings\Flow.Launcher.Plugin.WindowsSettings.csproj", "{5043CECE-E6A7-4867-9CBE-02D27D83747A}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Flow.Launcher.Command", "Flow.Launcher.Command\Flow.Launcher.Command.csproj", "{A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -82,7 +84,7 @@ Global
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.ActiveCfg = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x64.Build.0 = Debug|Any CPU
{FF742965-9A80-41A5-B042-D6C7D3A21708}.Debug|x86.ActiveCfg = Debug|Any CPU
@@ -286,6 +288,18 @@ Global
{5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x64.Build.0 = Release|Any CPU
{5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.ActiveCfg = Release|Any CPU
{5043CECE-E6A7-4867-9CBE-02D27D83747A}.Release|x86.Build.0 = Release|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Debug|x64.Build.0 = Debug|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Debug|x86.Build.0 = Debug|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Release|x64.ActiveCfg = Release|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Release|x64.Build.0 = Release|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Release|x86.ActiveCfg = Release|Any CPU
+ {A9976C5C-B73A-4D29-B654-EF1C0C4C9C8C}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs
index 6e053db29c8..c7dd340f4bf 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;
@@ -55,6 +57,13 @@ public partial class App : IDisposable, ISingleInstanceApp
public App()
{
+ // Check if the application is running as administrator
+ if (_settings.AlwaysRunAsAdministrator && !Win32Helper.IsAdministrator())
+ {
+ RestartApp(true);
+ return;
+ }
+
// Initialize settings
_settings.WMPInstalled = WindowsMediaPlayerHelper.IsWindowsMediaPlayerInstalled();
@@ -267,12 +276,23 @@ private static 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)
+ {
+ RestartApp(true);
+ }
}
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.ShowMsgError(API.GetTranslation("setAutoStartFailed"), e.Message);
}
@@ -381,6 +401,59 @@ private static void RegisterTaskSchedulerUnhandledException()
#endregion
+ #region Restart
+
+ ///
+ /// Restart the application without changing the user privileges.
+ ///
+ ///
+ /// 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
+ ///
+ ///
+ /// If true, the application will be restarted as administrator.
+ /// If false, it will be restarted with the same privileges as the current user.
+ ///
+ /// Thrown when the Update.exe is not found in the expected location
+ public static void RestartApp(bool forceAdmin = false)
+ {
+ // 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.
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = getUpdateExe(),
+ Arguments = $"--processStartAndWait \"{Constant.ExecutablePath}\"",
+ UseShellExecute = true,
+ Verb = Win32Helper.IsAdministrator() || forceAdmin ? "runas" : ""
+ };
+ // No need to de-elevate since we are restarting Flow Launcher which cannot bring security risks
+ 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/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj
index 12c726e9548..a4cf567c063 100644
--- a/Flow.Launcher/Flow.Launcher.csproj
+++ b/Flow.Launcher/Flow.Launcher.csproj
@@ -142,4 +142,16 @@
+
+
+
+
diff --git a/Flow.Launcher/Helper/AutoStartup.cs b/Flow.Launcher/Helper/AutoStartup.cs
index 34700c61015..a33c551350a 100644
--- a/Flow.Launcher/Helper/AutoStartup.cs
+++ b/Flow.Launcher/Helper/AutoStartup.cs
@@ -17,18 +17,18 @@ public class AutoStartup
private const string LogonTaskName = $"{Constant.FlowLauncher} Startup";
private const string LogonTaskDesc = $"{Constant.FlowLauncher} Auto Startup";
- public static void CheckIsEnabled(bool useLogonTaskForStartup)
+ 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 +41,7 @@ public static void CheckIsEnabled(bool useLogonTaskForStartup)
// Enable registry
if (!registryEnabled)
{
- Enable(false);
+ Enable(false, alwaysRunAsAdministrator);
}
// Disable logon task
if (logonTaskEnabled)
@@ -51,7 +51,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 +59,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 (Win32Helper.IsAdministrator())
+ {
+ // If path or run level is not correct, we need to unschedule and reschedule the task
+ if (!pathCorrect || !runLevelCorrect)
+ {
+ UnscheduleLogonTask();
+ ScheduleLogonTask(alwaysRunAsAdministrator);
+ }
+ }
+ else
{
- UnscheduleLogonTask();
- ScheduleLogonTask();
+ // 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 +109,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 +148,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 +183,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 +203,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 (Win32Helper.IsAdministrator() && alwaysRunAsAdministrator)
{
td.Principal.RunLevel = TaskRunLevel.Highest;
}
@@ -212,13 +247,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 bf6cb674e95..0a06400c116 100644
--- a/Flow.Launcher/Languages/en.xaml
+++ b/Flow.Launcher/Languages/en.xaml
@@ -62,6 +62,7 @@
Position Reset
Reset search window position
Type here to search
+ (Admin)
Settings
@@ -171,6 +172,10 @@
Show warning when installing plugins from unknown sources
Auto update plugins
Automatically check plugin updates and notify if there are any updates available
+ Always run as administrator
+ Run Flow Launcher as administrator on 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 8eb41e032fa..21d12116c68 100644
--- a/Flow.Launcher/MainWindow.xaml.cs
+++ b/Flow.Launcher/MainWindow.xaml.cs
@@ -723,9 +723,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 e0ed105cff9..ac729f33b09 100644
--- a/Flow.Launcher/PublicAPIInstance.cs
+++ b/Flow.Launcher/PublicAPIInstance.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics;
@@ -32,7 +33,6 @@
using Flow.Launcher.ViewModel;
using JetBrains.Annotations;
using ModernWpf;
-using Squirrel;
using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch;
namespace Flow.Launcher
@@ -73,8 +73,12 @@ public void ChangeQuery(string query, bool requery = false)
_mainVM.ChangeQueryText(query, requery);
}
+ public void RestartApp() => RestartApp(false);
+
+ public void RestartAppAsAdmin() => RestartApp(true);
+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")]
- public async void RestartApp()
+ private async void RestartApp(bool runAsAdmin)
{
_mainVM.Hide();
@@ -89,7 +93,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(runAsAdmin);
}
public void ShowMainWindow() => _mainVM.Show();
@@ -155,8 +159,7 @@ public void ShellRun(string cmd, string filename = "cmd.exe")
{
var args = filename == "cmd.exe" ? $"/C {cmd}" : $"{cmd}";
- var startInfo = ShellCommand.SetProcessStartInfo(filename, arguments: args, createNoWindow: true);
- ShellCommand.Execute(startInfo);
+ StartProcess(filename, arguments: args, createNoWindow: true);
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD100:Avoid async void methods", Justification = "")]
@@ -434,13 +437,7 @@ private void OpenUri(Uri uri, bool? inPrivate = null, bool forceBrowser = false)
}
else
{
- Process.Start(new ProcessStartInfo()
- {
- FileName = uri.AbsoluteUri,
- UseShellExecute = true
- })?.Dispose();
-
- return;
+ StartProcess(uri.AbsoluteUri, arguments: string.Empty, useShellExecute: true);
}
}
@@ -603,6 +600,85 @@ public event ActualApplicationThemeChangedEventHandler ActualApplicationThemeCha
public string GetLogDirectory() => DataLocation.VersionLogDirectory;
+ public bool StartProcess(string fileName, string workingDirectory = "", string arguments = "", bool useShellExecute = false, string verb = "", bool createNoWindow = false)
+ {
+ try
+ {
+ workingDirectory = string.IsNullOrEmpty(workingDirectory) ? Environment.CurrentDirectory : workingDirectory;
+
+ // Use command executer to run the process as desktop user if running as admin
+ if (Win32Helper.IsAdministrator())
+ {
+ var result = Win32Helper.RunAsDesktopUser(
+ Constant.CommandExecutablePath,
+ Environment.CurrentDirectory,
+ $"-StartProcess " +
+ $"-FileName {AddDoubleQuotes(fileName)} " +
+ $"-WorkingDirectory {AddDoubleQuotes(workingDirectory)} " +
+ $"-Arguments {AddDoubleQuotes(arguments)} " +
+ $"-UseShellExecute {useShellExecute} " +
+ $"-Verb {AddDoubleQuotes(verb)} " +
+ $"-CreateNoWindow {createNoWindow}",
+ false,
+ true, // Do not show the command window
+ out var errorInfo);
+ if (!string.IsNullOrEmpty(errorInfo))
+ {
+ LogError(ClassName, $"Failed to start process {fileName} with arguments {arguments} under {workingDirectory}: {errorInfo}");
+ }
+
+ return result;
+ }
+
+ var info = new ProcessStartInfo
+ {
+ FileName = fileName,
+ WorkingDirectory = workingDirectory,
+ Arguments = arguments,
+ UseShellExecute = useShellExecute,
+ Verb = verb,
+ CreateNoWindow = createNoWindow
+ };
+ Process.Start(info)?.Dispose();
+ return true;
+ }
+ catch (Exception e)
+ {
+ LogException(ClassName, $"Failed to start process {fileName} with arguments {arguments} under {workingDirectory}", e);
+ return false;
+ }
+ }
+
+ public bool StartProcess(string fileName, string workingDirectory = "", Collection argumentList = null, bool useShellExecute = false, string verb = "", bool createNoWindow = false) =>
+ StartProcess(fileName, workingDirectory, JoinArgumentList(argumentList), useShellExecute, verb, createNoWindow);
+
+ private static string AddDoubleQuotes(string arg)
+ {
+ if (string.IsNullOrEmpty(arg))
+ return "\"\"";
+
+ // If already wrapped in double quotes, return as is
+ if (arg.Length >= 2 && arg[0] == '"' && arg[^1] == '"')
+ return arg;
+
+ return $"\"{arg}\"";
+ }
+
+ private static string JoinArgumentList(Collection args)
+ {
+ if (args == null || args.Count == 0)
+ return string.Empty;
+
+ return string.Join(" ", args.Select(arg =>
+ {
+ if (string.IsNullOrEmpty(arg))
+ return "\"\"";
+
+ // Add double quotes
+ return AddDoubleQuotes(arg);
+ }));
+ }
+
#endregion
#region Private Methods
diff --git a/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs b/Flow.Launcher/Resources/Pages/WelcomePage5.xaml.cs
index 10cd18821e1..bc877ae318a 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 5057ebb4443..39d08518303 100644
--- a/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
+++ b/Flow.Launcher/SettingPages/ViewModels/SettingsPaneGeneralViewModel.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
+using System.Windows;
using System.Windows.Forms;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core;
@@ -43,6 +44,8 @@ public bool StartFlowLauncherOnSystemStartup
get => Settings.StartFlowLauncherOnSystemStartup;
set
{
+ if (Settings.StartFlowLauncherOnSystemStartup == value) return;
+
Settings.StartFlowLauncherOnSystemStartup = value;
try
@@ -51,7 +54,7 @@ public bool StartFlowLauncherOnSystemStartup
{
if (UseLogonTaskForStartup)
{
- AutoStartup.ChangeToViaLogonTask();
+ AutoStartup.ChangeToViaLogonTask(AlwaysRunAsAdministrator);
}
else
{
@@ -67,6 +70,13 @@ public bool StartFlowLauncherOnSystemStartup
{
App.API.ShowMsgError(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)
+ {
+ CheckAdminChangeAndAskForRestart();
+ }
}
}
@@ -75,6 +85,8 @@ public bool UseLogonTaskForStartup
get => Settings.UseLogonTaskForStartup;
set
{
+ if (UseLogonTaskForStartup == value) return;
+
Settings.UseLogonTaskForStartup = value;
if (StartFlowLauncherOnSystemStartup)
@@ -83,7 +95,7 @@ public bool UseLogonTaskForStartup
{
if (value)
{
- AutoStartup.ChangeToViaLogonTask();
+ AutoStartup.ChangeToViaLogonTask(AlwaysRunAsAdministrator);
}
else
{
@@ -94,10 +106,61 @@ public bool UseLogonTaskForStartup
{
App.API.ShowMsgError(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)
+ {
+ CheckAdminChangeAndAskForRestart();
+ }
+ }
+ }
+
+ 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
+ CheckAdminChangeAndAskForRestart();
+ }
}
}
+ private void CheckAdminChangeAndAskForRestart()
+ {
+ // 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 && !Win32Helper.IsAdministrator())
+ {
+ if (App.API.ShowMsgBox(
+ App.API.GetTranslation("runAsAdministratorChangeAndRestart"),
+ App.API.GetTranslation("runAsAdministratorChange"),
+ MessageBoxButton.YesNo) == MessageBoxResult.Yes)
+ {
+ // Restart the app as administrator
+ App.API.RestartAppAsAdmin();
+ }
+ }
+ }
+
public List SearchWindowScreens { get; } =
DropdownDataGeneric.GetValues("SearchWindowScreen");
@@ -123,7 +186,7 @@ public List ScreenNumbers
}
// This is only required to set at startup. When portable mode enabled/disabled a restart is always required
- private static bool _portableMode = DataLocation.PortableDataLocationInUse();
+ private static readonly bool _portableMode = DataLocation.PortableDataLocationInUse();
public bool PortableMode
{
diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
index 81e15df6950..e612004f2a0 100644
--- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
+++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml
@@ -45,8 +45,9 @@
+ Type="Inside">
+
+
+
-
-
+ IsEqualToBool=True}">
{
+ // No need to de-elevate since we are opening windows settings which cannot bring security risks
Process.Start("rundll32.exe", $"{Path.Combine(Environment.SystemDirectory, "shell32.dll")},OpenAs_RunDLL {record.FullPath}");
return true;
},
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs
index ce71c94ba34..4e92b7f10d1 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/Search/Everything/EverythingSearchManager.cs
@@ -57,7 +57,7 @@ private async ValueTask ClickToInstallEverythingAsync(ActionContext _)
}
Settings.EverythingInstalledPath = installedPath;
- Process.Start(installedPath, "-startup");
+ Main.Context.API.StartProcess(installedPath, arguments: "-startup");
return true;
}
diff --git a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
index 7292697ce33..9a2d30cb096 100644
--- a/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
+++ b/Plugins/Flow.Launcher.Plugin.Explorer/ViewModels/SettingsViewModel.cs
@@ -502,6 +502,7 @@ internal static void OpenWindowsIndexingOptions()
Arguments = Constants.WindowsIndexingOptions
};
+ // No need to de-elevate since we are opening windows settings which cannot bring security risks
Process.Start(psi);
}
diff --git a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs
index 9a8326e9aa6..f5e4008acbc 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/UWPPackage.cs
@@ -1,20 +1,19 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
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
{
@@ -455,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 +498,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 +512,14 @@ 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" : "" };
-
- Main.StartProcess(Process.Start, info);
+ _ = Task.Run(() => Main.Context.API.StartProcess(
+ command,
+ arguments: string.Empty,
+ useShellExecute: true,
+ verb: elevated ? "runas" : ""));
}
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 7aca8f3b6a7..274c024045a 100644
--- a/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
+++ b/Plugins/Flow.Launcher.Plugin.Program/Programs/Win32.cs
@@ -1,21 +1,21 @@
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.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 +196,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 +205,15 @@ public Result Result(string query, IPublicAPI api)
return result;
}
+ private void Launch(bool runAsAdmin = false)
+ {
+ _ = Task.Run(() => Main.Context.API.StartProcess(
+ FullPath,
+ workingDirectory: ParentDirectory,
+ arguments: string.Empty,
+ useShellExecute: true,
+ verb: runAsAdmin ? "runas" : ""));
+ }
public List ContextMenus(IPublicAPI api)
{
@@ -240,15 +241,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.Shell/Main.cs b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
index 8880099763b..bea761a26b5 100644
--- a/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Shell/Main.cs
@@ -5,9 +5,9 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
+using Flow.Launcher.Plugin.SharedCommands;
using WindowsInput;
using WindowsInput.Native;
-using Flow.Launcher.Plugin.SharedCommands;
using Control = System.Windows.Controls.Control;
using Keys = System.Windows.Forms.Keys;
@@ -17,7 +17,7 @@ public class Main : IPlugin, ISettingProvider, IPluginI18n, IContextMenu, IDispo
{
private static readonly string ClassName = nameof(Main);
- internal PluginInitContext Context { get; private set; }
+ internal static PluginInitContext Context { get; private set; }
private const string Image = "Images/shell.png";
private bool _winRStroked;
@@ -80,7 +80,7 @@ public List Query(Query query)
!c.SpecialKeyState.AltPressed &&
!c.SpecialKeyState.WinPressed;
- Execute(Process.Start, PrepareProcessStartInfo(m, runAsAdministrator));
+ Execute(StartProcess, PrepareProcessStartInfo(m, runAsAdministrator));
return true;
},
CopyText = m
@@ -120,7 +120,7 @@ private List GetHistoryCmds(string cmd, Result result)
!c.SpecialKeyState.AltPressed &&
!c.SpecialKeyState.WinPressed;
- Execute(Process.Start, PrepareProcessStartInfo(m.Key, runAsAdministrator));
+ Execute(StartProcess, PrepareProcessStartInfo(m.Key, runAsAdministrator));
return true;
},
CopyText = m.Key
@@ -150,7 +150,7 @@ private Result GetCurrentCmd(string cmd)
!c.SpecialKeyState.AltPressed &&
!c.SpecialKeyState.WinPressed;
- Execute(Process.Start, PrepareProcessStartInfo(cmd, runAsAdministrator));
+ Execute(StartProcess, PrepareProcessStartInfo(cmd, runAsAdministrator));
return true;
},
CopyText = cmd
@@ -175,7 +175,7 @@ private List ResultsFromHistory()
!c.SpecialKeyState.AltPressed &&
!c.SpecialKeyState.WinPressed;
- Execute(Process.Start, PrepareProcessStartInfo(m.Key, runAsAdministrator));
+ Execute(StartProcess, PrepareProcessStartInfo(m.Key, runAsAdministrator));
return true;
},
CopyText = m.Key
@@ -327,6 +327,17 @@ private ProcessStartInfo PrepareProcessStartInfo(string command, bool runAsAdmin
return info;
}
+ private static Process StartProcess(ProcessStartInfo info)
+ {
+ Context.API.StartProcess(
+ info.FileName,
+ workingDirectory: info.WorkingDirectory,
+ argumentList: info.ArgumentList,
+ useShellExecute: info.UseShellExecute,
+ verb: info.Verb);
+ return null;
+ }
+
private void Execute(Func startProcess, ProcessStartInfo info)
{
try
@@ -405,7 +416,7 @@ bool API_GlobalKeyboardEvent(int keyevent, int vkcode, SpecialKeyState state)
return true;
}
- private void OnWinRPressed()
+ private static void OnWinRPressed()
{
Context.API.ShowMainWindow();
// show the main window and set focus to the query box
@@ -456,7 +467,7 @@ public List LoadContextMenus(Result selectedResult)
Title = Context.API.GetTranslation("flowlauncher_plugin_cmd_run_as_administrator"),
Action = c =>
{
- Execute(Process.Start, PrepareProcessStartInfo(selectedResult.Title, true));
+ Execute(StartProcess, PrepareProcessStartInfo(selectedResult.Title, true));
return true;
},
IcoPath = "Images/admin.png",
diff --git a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
index 77278a0545c..2892c28d973 100644
--- a/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
+++ b/Plugins/Flow.Launcher.Plugin.Sys/Main.cs
@@ -216,6 +216,7 @@ private List Commands(Query query)
if (EnableShutdownPrivilege())
PInvoke.ExitWindowsEx(EXIT_WINDOWS_FLAGS.EWX_SHUTDOWN | EXIT_WINDOWS_FLAGS.EWX_POWEROFF, REASON);
else
+ // No need to de-elevate since we already have message box asking for confirmation
Process.Start("shutdown", "/s /t 0");
return true;
@@ -237,6 +238,7 @@ private List Commands(Query query)
if (EnableShutdownPrivilege())
PInvoke.ExitWindowsEx(EXIT_WINDOWS_FLAGS.EWX_REBOOT, REASON);
else
+ // No need to de-elevate since we already have message box asking for confirmation
Process.Start("shutdown", "/r /t 0");
return true;
@@ -258,6 +260,7 @@ private List Commands(Query query)
if (EnableShutdownPrivilege())
PInvoke.ExitWindowsEx(EXIT_WINDOWS_FLAGS.EWX_REBOOT | EXIT_WINDOWS_FLAGS.EWX_BOOTOPTIONS, REASON);
else
+ // No need to de-elevate since we already have message box asking for confirmation
Process.Start("shutdown", "/r /o /t 0");
return true;
@@ -321,6 +324,7 @@ private List Commands(Query query)
Glyph = new GlyphInfo (FontFamily:"/Resources/#Segoe Fluent Icons", Glyph:"\xe773"),
Action = c =>
{
+ // No need to de-elevate since we are opening windows settings which cannot bring security risks
Process.Start("control.exe", "srchadmin.dll");
return true;
}
@@ -355,6 +359,7 @@ private List Commands(Query query)
CopyText = recycleBinFolder,
Action = c =>
{
+ // No need to de-elevate since we are opening windows settings which cannot bring security risks
Process.Start("explorer", recycleBinFolder);
return true;
}
diff --git a/Plugins/Flow.Launcher.Plugin.WindowsSettings/Helper/ResultHelper.cs b/Plugins/Flow.Launcher.Plugin.WindowsSettings/Helper/ResultHelper.cs
index 9e85a8580c0..7b8df532cd0 100644
--- a/Plugins/Flow.Launcher.Plugin.WindowsSettings/Helper/ResultHelper.cs
+++ b/Plugins/Flow.Launcher.Plugin.WindowsSettings/Helper/ResultHelper.cs
@@ -198,6 +198,7 @@ private static bool DoOpenSettingsAction(WindowsSetting entry)
try
{
+ // No need to de-elevate since we are opening windows settings which cannot bring security risks
Process.Start(processStartInfo);
return true;
}
@@ -207,6 +208,7 @@ private static bool DoOpenSettingsAction(WindowsSetting entry)
{
processStartInfo.UseShellExecute = true;
processStartInfo.Verb = "runas";
+ // No need to de-elevate since we are opening windows settings which cannot bring security risks
Process.Start(processStartInfo);
return true;
}