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; }