Skip to content

Support Always Run As Administrator #3573

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 45 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
7549bba
Add administrator mode check
Jack251970 May 22, 2025
369ed86
Add administrator text in tray icon
Jack251970 May 22, 2025
10d379f
Support running application under non-admin mode & Show UAC dialogs w…
Jack251970 May 22, 2025
484f91c
Improve UAC dialog
Jack251970 May 22, 2025
7611b00
Add new restart api
Jack251970 May 22, 2025
d29a5d4
Add setting model
Jack251970 May 22, 2025
40cf413
Add admin support for auto startup
Jack251970 May 22, 2025
fe70a72
Add admin mode configuration in general page
Jack251970 May 22, 2025
0edd626
Remove useless semicolon
Jack251970 May 22, 2025
91f7a4f
Remove useless semicolon
Jack251970 May 22, 2025
902c8f4
Check value changed
Jack251970 May 22, 2025
c5c24ac
Check run level
Jack251970 May 22, 2025
1073061
Remove old before creating new one
Jack251970 May 22, 2025
e9c40e4
Revert "Add new restart api"
Jack251970 May 23, 2025
365ba6a
Code quality
Jack251970 May 23, 2025
eae4a95
Use readonly bool variable
Jack251970 May 23, 2025
52c36ff
Only restart from non-admin to admin & Fix restart as administrator i…
Jack251970 May 23, 2025
35f4fd5
Fix restart as administrator issue
Jack251970 May 23, 2025
eacccf9
Add code comments & Use local function
Jack251970 May 23, 2025
d6462f4
Move restart function to app class
Jack251970 May 23, 2025
479b49d
Throw UnauthorizedAccessException when encountering admin issue
Jack251970 May 23, 2025
c3ec002
Show message box to ask users to restart as administrator when encoun…
Jack251970 May 23, 2025
80c0288
Throw exception when editing highest run level task
Jack251970 May 23, 2025
1b80290
Do not show noticification when run level is correct
Jack251970 May 23, 2025
ca6f077
Merge branch 'dev' into administrator_mode
Jack251970 Jun 3, 2025
ba1ada7
Continue to restart app as administrator if app is run as administrat…
Jack251970 Jun 3, 2025
b2edea4
Improve code quality
Jack251970 Jun 3, 2025
de26abe
Use api functions instead
Jack251970 Jun 3, 2025
23f2489
Force admin restart and fix build issue
Jack251970 Jun 3, 2025
2c1f161
Merge branch 'dev' into administrator_mode
Jack251970 Jun 10, 2025
1a8334f
Add RunAsDesktopUser helper method
Jack251970 Jun 11, 2025
e391c98
Code quality
Jack251970 Jun 11, 2025
100c21a
Add new api to run process as desktop user
Jack251970 Jun 11, 2025
fc53efd
Use new api function to open application
Jack251970 Jun 11, 2025
d77993e
Use new api function to start shell process
Jack251970 Jun 11, 2025
4cf6636
Fix handle close issue
Jack251970 Jun 11, 2025
f1f7309
Add working directory check & Add try-catch
Jack251970 Jun 11, 2025
e8c03c6
Update documents
Jack251970 Jun 11, 2025
ea9ede2
Fix deelevate process running issue
Jack251970 Jun 11, 2025
d13901b
Update work directory correctly
Jack251970 Jun 11, 2025
f9b2be1
Let work directory can be empty
Jack251970 Jun 11, 2025
30deb6d
Fix argument sequence issue
Jack251970 Jun 11, 2025
fb4735f
Fix program deelevate open issue
Jack251970 Jun 11, 2025
b15cece
Fix absolute exe file path issue
Jack251970 Jun 11, 2025
255ca5a
Improve code quality
Jack251970 Jun 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Flow.Launcher.Core/Configuration/Portable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
API.ShowMsgBox("Flow Launcher needs to restart to finish disabling portable mode, " +
"after the restart your portable data profile will be deleted and roaming data profile kept");

UpdateManager.RestartApp(Constant.ApplicationFileName);
API.RestartApp();
}
catch (Exception e)
{
Expand All @@ -62,17 +62,17 @@
{
MoveUserDataFolder(DataLocation.RoamingDataPath, DataLocation.PortableDataPath);
#if !DEBUG
// Remove shortcuts and uninstaller are not required in debug mode,

Check warning on line 65 in Flow.Launcher.Core/Configuration/Portable.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`uninstaller` is not a recognized word. (unrecognized-spelling)
// otherwise will delete the actual installed production version
RemoveShortcuts();
RemoveUninstallerEntry();

Check warning on line 68 in Flow.Launcher.Core/Configuration/Portable.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Uninstaller` is not a recognized word. (unrecognized-spelling)
#endif
IndicateDeletion(DataLocation.RoamingDataPath);

API.ShowMsgBox("Flow Launcher needs to restart to finish enabling portable mode, " +
"after the restart your roaming data profile will be deleted and portable data profile kept");

UpdateManager.RestartApp(Constant.ApplicationFileName);
API.RestartApp();
}
catch (Exception e)
{
Expand All @@ -88,10 +88,10 @@
portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup);
}

public void RemoveUninstallerEntry()

Check warning on line 91 in Flow.Launcher.Core/Configuration/Portable.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Uninstaller` is not a recognized word. (unrecognized-spelling)
{
using var portabilityUpdater = NewUpdateManager();
portabilityUpdater.RemoveUninstallerRegistryEntry();

Check warning on line 94 in Flow.Launcher.Core/Configuration/Portable.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Uninstaller` is not a recognized word. (unrecognized-spelling)
}

public void MoveUserDataFolder(string fromLocation, string toLocation)
Expand All @@ -113,7 +113,7 @@
portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false);
}

public void CreateUninstallerEntry()

Check warning on line 116 in Flow.Launcher.Core/Configuration/Portable.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Uninstaller` is not a recognized word. (unrecognized-spelling)
{
var uninstallRegSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall";

Expand All @@ -130,7 +130,7 @@

private static void IndicateDeletion(string filePathTodelete)
{
var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile);

Check warning on line 133 in Flow.Launcher.Core/Configuration/Portable.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Todelete` is not a recognized word. (unrecognized-spelling)
using var _ = File.CreateText(deleteFilePath);
}

Expand Down
6 changes: 3 additions & 3 deletions Flow.Launcher.Core/Updater.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -89,7 +89,7 @@

if (_api.ShowMsgBox(newVersionTips, _api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
UpdateManager.RestartApp(Constant.ApplicationFileName);
_api.RestartApp();
}
}
catch (Exception e)
Expand Down Expand Up @@ -138,7 +138,7 @@
var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First();
var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/");

var client = new WebClient

Check warning on line 141 in Flow.Launcher.Core/Updater.cs

View workflow job for this annotation

GitHub Actions / build

'WebClient.WebClient()' is obsolete: 'WebRequest, HttpWebRequest, ServicePoint, and WebClient are obsolete. Use HttpClient instead.' (https://aka.ms/dotnet-warnings/SYSLIB0014)
{
Proxy = Http.WebProxy
};
Expand Down
15 changes: 15 additions & 0 deletions Flow.Launcher.Infrastructure/NativeMethods.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,18 @@
SHParseDisplayName
SHOpenFolderAndSelectItems
CoTaskMemFree

OpenProcessToken
GetCurrentProcess
LookupPrivilegeValue
SE_INCREASE_QUOTA_NAME
CloseHandle
TOKEN_PRIVILEGES
AdjustTokenPrivileges
GetShellWindow
GetWindowThreadProcessId
OpenProcess
GetProcessId
DuplicateTokenEx
CreateProcessWithTokenW
STARTUPINFO

Check warning on line 83 in Flow.Launcher.Infrastructure/NativeMethods.txt

View workflow job for this annotation

GitHub Actions / build

Method, type or constant "STARTUPINFO" not found. Did you mean , "STARTUPINFOW_FLAGS", "STARTUPINFOA" or "STARTUPINFOW" or "STARTUPINFOEXA"?
2 changes: 2 additions & 0 deletions Flow.Launcher.Infrastructure/UserSettings/Settings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,8 @@ public bool HideNotifyIcon
public bool LeaveCmdOpen { get; set; }
public bool HideWhenDeactivated { get; set; } = true;

public bool AlwaysRunAsAdministrator { get; set; } = false;

private bool _showAtTopmost = true;
public bool ShowAtTopmost
{
Expand Down
153 changes: 153 additions & 0 deletions Flow.Launcher.Infrastructure/Win32Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,6 +18,8 @@
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;
using Windows.Win32.UI.WindowsAndMessaging;
Expand Down Expand Up @@ -791,5 +794,155 @@ public static unsafe void OpenFolderAndSelectFile(string filePath)
}

#endregion

#region Administrator Mode

public static bool IsAdministrator()
{
using var identity = WindowsIdentity.GetCurrent();
var principal = new WindowsPrincipal(identity);
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}

/// <summary>
/// Inspired by <see href="https://github.com/jay/RunAsDesktopUser">
/// Document: <see href="https://learn.microsoft.com/en-us/archive/blogs/aaron_margosis/faq-how-do-i-start-a-program-as-the-desktop-user-from-an-elevated-app">
/// </summary>
public static unsafe bool RunAsDesktopUser(string app, string currentDir, string cmdLine, 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 == IntPtr.Zero)
{
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)
fixed (char* cmdLinePtr = cmdLine)
fixed (char* currentDirPtr = currentDir)
{
if (!PInvoke.CreateProcessWithToken(hPrimaryToken,
0,
appPtr,
cmdLinePtr,
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
}
}
12 changes: 12 additions & 0 deletions Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
Original file line number Diff line number Diff line change
Expand Up @@ -580,5 +580,17 @@ public interface IPublicAPI
/// </summary>
/// <returns>The time taken to execute the method in milliseconds</returns>
public Task<long> StopwatchLogInfoAsync(string className, string message, Func<Task> action, [CallerMemberName] string methodName = "");

/// <summary>
/// Start a process with the given file path and arguments
/// </summary>
/// <remarks>
/// It can help to start a deelevated process when Flow is running as administrator.
/// </remarks>
/// <param name="filePath">Absolute file path. It can be an executable file or a script file</param>
/// <param name="workingDirectory">Working directory. If not specified, the current directory will be used</param>
/// <param name="arguments">Optional arguments to pass to the process. If not specified, no arguments will be passed</param>
/// <param name="runAsAdmin">Whether to run the process as administrator</param>
public void StartProcess(string filePath, string workingDirectory = "", string arguments = "", bool runAsAdmin = false);
}
}
77 changes: 74 additions & 3 deletions Flow.Launcher/App.xaml.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -24,6 +26,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.VisualStudio.Threading;
using Squirrel;

namespace Flow.Launcher
{
Expand Down Expand Up @@ -238,12 +241,23 @@ private void AutoStartup()
{
try
{
Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup);
Helper.AutoStartup.CheckIsEnabled(_settings.UseLogonTaskForStartup, _settings.AlwaysRunAsAdministrator);
}
catch (UnauthorizedAccessException)
{
// If it fails for permission, we need to ask the user to restart as administrator
if (API.ShowMsgBox(
API.GetTranslation("runAsAdministratorChangeAndRestart"),
API.GetTranslation("runAsAdministratorChange"),
MessageBoxButton.YesNo) == MessageBoxResult.Yes)
{
RestartAppAsAdministrator();
}
}
catch (Exception e)
{
// but if it fails (permissions, etc) then don't keep retrying
// this also gives the user a visual indication in the Settings widget
// But if it fails for other reasons then do not keep retrying,
// set startup to false to give users a visual indication in the general page
_settings.StartFlowLauncherOnSystemStartup = false;
API.ShowMsg(API.GetTranslation("setAutoStartFailed"), e.Message);
}
Expand Down Expand Up @@ -321,6 +335,63 @@ private static void RegisterTaskSchedulerUnhandledException()

#endregion

#region Restart

/// <summary>
/// Restart the application without changing the user privileges.
/// </summary>
public static void RestartApp(bool forceAdmin = false)
{
if (Win32Helper.IsAdministrator() || forceAdmin)
{
RestartAppAsAdministrator();
}
else
{
// Restart requires Squirrel's Update.exe to be present in the parent folder,
// it is only published from the project's release pipeline. When debugging without it,
// the project may not restart or just terminates. This is expected.
UpdateManager.RestartApp(Constant.ApplicationFileName);
}
}

// Since Squirrel does not provide a way to restart the app as administrator,
// we need to do it manually by starting the update.exe with the runas verb
private static void RestartAppAsAdministrator()
{
var startInfo = new ProcessStartInfo
{
FileName = getUpdateExe(),
Arguments = $"--processStartAndWait {Constant.ExecutablePath}",
UseShellExecute = true,
Verb = "runas",
};
Process.Start(startInfo);
Thread.Sleep(500);
Environment.Exit(0);

// Local function
static string getUpdateExe()
{
Assembly entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly != null && Path.GetFileName(entryAssembly.Location).Equals("update.exe", StringComparison.OrdinalIgnoreCase) && entryAssembly.Location.IndexOf("app-", StringComparison.OrdinalIgnoreCase) == -1 && entryAssembly.Location.IndexOf("SquirrelTemp", StringComparison.OrdinalIgnoreCase) == -1)
{
return Path.GetFullPath(entryAssembly.Location);
}

entryAssembly = Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly();
FileInfo fileInfo = new FileInfo(Path.Combine(Path.GetDirectoryName(entryAssembly.Location), "..\\Update.exe"));
if (!fileInfo.Exists)
{
throw new Exception("Update.exe not found, not a Squirrel-installed app?");
}

return fileInfo.FullName;
}
}

#endregion

#region IDisposable

protected virtual void Dispose(bool disposing)
Expand Down
Loading
Loading