From 097a5275e20f3bc3792c69fb9da43c04509aba35 Mon Sep 17 00:00:00 2001 From: SpoinkyNL Date: Sun, 24 Jan 2021 23:16:13 +0100 Subject: [PATCH 1/2] Auto-run - Moved to task-based approach (WIP) --- .../Services/PluginManagementService.cs | 13 +- src/Artemis.Core/Utilities/Utilities.cs | 17 +- src/Artemis.UI/Artemis.UI.csproj | 11 +- src/Artemis.UI/Bootstrapper.cs | 35 +-- .../Properties/Resources.Designer.cs | 24 ++ src/Artemis.UI/Properties/Resources.resx | 9 +- .../Properties/Settings.Designer.cs | 2 +- src/Artemis.UI/Properties/launchSettings.json | 3 +- .../Resources/Artemis 2 - Autorun.xml | Bin 0 -> 3410 bytes .../Screens/Settings/Debug/DebugViewModel.cs | 6 +- .../General/GeneralSettingsTabViewModel.cs | 102 +++++++- src/Artemis.UI/Utilities/ProcessUtilities.cs | 245 ++++++++++++++++++ 12 files changed, 424 insertions(+), 43 deletions(-) create mode 100644 src/Artemis.UI/Resources/Artemis 2 - Autorun.xml create mode 100644 src/Artemis.UI/Utilities/ProcessUtilities.cs diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index af53c79a5..a6ca023be 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -198,9 +198,9 @@ namespace Artemis.Core.Services // ReSharper disable InconsistentlySynchronizedField - It's read-only, idc _logger.Debug("Loaded {count} plugin(s)", _plugins.Count); - - bool mustElevate = !isElevated && _plugins.Any(p => p.Entity.IsEnabled && p.Info.RequiresAdmin); - if (mustElevate) + + bool adminRequired = _plugins.Any(p => p.Entity.IsEnabled && p.Info.RequiresAdmin); + if (!isElevated && adminRequired) { _logger.Information("Restarting because one or more plugins requires elevation"); // No need for a delay this early on, nothing that needs graceful shutdown is happening yet @@ -208,6 +208,13 @@ namespace Artemis.Core.Services return; } + if (isElevated && !adminRequired) + { + // No need for a delay this early on, nothing that needs graceful shutdown is happening yet + Utilities.Restart(false, TimeSpan.Zero); + return; + } + foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) EnablePlugin(plugin, false, ignorePluginLock); diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index 47861c970..fa03f516d 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -1,7 +1,6 @@ using System; using System.Diagnostics; using System.IO; -using System.Runtime.InteropServices; using System.Security.AccessControl; using System.Security.Principal; @@ -35,7 +34,7 @@ namespace Artemis.Core } /// - /// Restarts the application + /// Restarts the application /// /// Whether the application should be restarted with elevated permissions /// Delay in seconds before killing process and restarting @@ -90,6 +89,11 @@ namespace Artemis.Core } } + private static void OnRestartRequested(RestartEventArgs e) + { + RestartRequested?.Invoke(null, e); + } + #region Events /// @@ -98,20 +102,15 @@ namespace Artemis.Core public static event EventHandler? ShutdownRequested; /// - /// Occurs when the core has requested an application restart + /// Occurs when the core has requested an application restart /// public static event EventHandler? RestartRequested; - + private static void OnShutdownRequested() { ShutdownRequested?.Invoke(null, EventArgs.Empty); } #endregion - - private static void OnRestartRequested(RestartEventArgs e) - { - RestartRequested?.Invoke(null, e); - } } } \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index db296971a..d185a83fb 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -40,7 +40,7 @@ - + false @@ -318,11 +318,20 @@ True Resources.resx + + True + True + Settings.settings + PreserveNewest + + SettingsSingleFileGenerator + Settings.Designer.cs + diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 400ded1a6..2a0bf0f65 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -17,6 +17,7 @@ using Artemis.UI.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Stylet; +using Artemis.UI.Utilities; using Ninject; using Serilog; using SharpVectors.Dom.Resources; @@ -70,16 +71,10 @@ namespace Artemis.UI }); // Initialize the core async so the UI can show the progress - Task.Run(async () => + Task.Run(() => { try { - if (StartupArguments.Contains("--autorun")) - { - logger.Information("Sleeping for 15 seconds on auto run to allow vendor applications required for SDKs to start"); - await Task.Delay(TimeSpan.FromSeconds(15)); - } - _core.StartupArguments = StartupArguments; _core.IsElevated = new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator); _core.Initialize(); @@ -144,15 +139,25 @@ namespace Artemis.UI private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) { - ProcessStartInfo info = new() + string args = Args.Any() ? "-ArgumentList " + string.Join(',', Args.Select(a => "'" + a + "'")) : ""; + + if (e.Elevate) { - Arguments = - $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; (Get-Process 'Artemis.UI').kill(); Start-Process -FilePath '{Constants.ExecutablePath}' {(e.Elevate ? "-Verb RunAs" : "")}}}\"", - WindowStyle = ProcessWindowStyle.Hidden, - CreateNoWindow = true, - FileName = "PowerShell.exe" - }; - Process.Start(info); + ProcessStartInfo info = new() + { + Arguments = + $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; (Get-Process 'Artemis.UI').kill(); Start-Process -FilePath '{Constants.ExecutablePath}' {args} -Verb RunAs}}\"", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + } + else + { + ProcessUtilities.RunAsDesktopUser(Constants.ExecutablePath); + } + Execute.OnUIThread(() => Application.Current.Shutdown()); } diff --git a/src/Artemis.UI/Properties/Resources.Designer.cs b/src/Artemis.UI/Properties/Resources.Designer.cs index a0fb0a67b..38a2162d0 100644 --- a/src/Artemis.UI/Properties/Resources.Designer.cs +++ b/src/Artemis.UI/Properties/Resources.Designer.cs @@ -190,6 +190,30 @@ namespace Artemis.UI.Properties { } } + /// + /// Looks up a localized string similar to <?xml version="1.0" encoding="UTF-16"?> + ///<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task"> + /// <RegistrationInfo> + /// <Date>2021-01-24T15:05:33.7954017</Date> + /// <Author>DESKTOP-8CH3TD6\Robert</Author> + /// <URI>\Artemis 2 - Autorun</URI> + /// </RegistrationInfo> + /// <Triggers> + /// <LogonTrigger> + /// <Enabled>true</Enabled> + /// <Delay>PT15S</Delay> + /// </LogonTrigger> + /// </Triggers> + /// <Principals> + /// <Principal id="Author"> + /// <UserId>S-1-5-21-184080501-1733858 [rest of string was truncated]";. + /// + internal static string artemis_autorun { + get { + return ResourceManager.GetString("artemis-autorun", resourceCulture); + } + } + /// /// Looks up a localized resource of type System.Byte[]. /// diff --git a/src/Artemis.UI/Properties/Resources.resx b/src/Artemis.UI/Properties/Resources.resx index 96a38f334..a7f768ddc 100644 --- a/src/Artemis.UI/Properties/Resources.resx +++ b/src/Artemis.UI/Properties/Resources.resx @@ -112,12 +112,12 @@ 2.0 - System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceReader, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - + ..\resources\cursors\aero_crosshair.cur;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 @@ -157,6 +157,9 @@ ..\resources\cursors\aero_rotate_tr.cur;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + ..\Resources\Artemis 2 - Autorun.xml;System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089;utf-16 + ..\resources\images\logo\bow.svg;System.Byte[], mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 diff --git a/src/Artemis.UI/Properties/Settings.Designer.cs b/src/Artemis.UI/Properties/Settings.Designer.cs index 93cac3d15..e1e14bb61 100644 --- a/src/Artemis.UI/Properties/Settings.Designer.cs +++ b/src/Artemis.UI/Properties/Settings.Designer.cs @@ -12,7 +12,7 @@ namespace Artemis.UI.Properties { [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.0.0.0")] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase { private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); diff --git a/src/Artemis.UI/Properties/launchSettings.json b/src/Artemis.UI/Properties/launchSettings.json index a11fc1923..948a8e1bb 100644 --- a/src/Artemis.UI/Properties/launchSettings.json +++ b/src/Artemis.UI/Properties/launchSettings.json @@ -1,7 +1,8 @@ { "profiles": { "Artemis.UI": { - "commandName": "Project" + "commandName": "Project", + "commandLineArgs": "--test --test2" } } } \ No newline at end of file diff --git a/src/Artemis.UI/Resources/Artemis 2 - Autorun.xml b/src/Artemis.UI/Resources/Artemis 2 - Autorun.xml new file mode 100644 index 0000000000000000000000000000000000000000..e31980c8baf57014140df2b687258d8853f66c19 GIT binary patch literal 3410 zcmbW4+fN!n6voeUll~8ccLni=hH9vnN@}YSj4!@WK_qf9ELQ#J+y1^8w!5>7h-S0y zaOTW+Zr_|4|Ng1jzCGK}23D}&*0qU6*0-^Z?28p`%~q|?`xEm;MR%eKL&%(&#%PWet}UKMA47VxiT7g*_m5HUYNW6D_3H+XLu z`Iw~Z^$(p+7rE?Ih<$~x{#LNrVzfyNW%qa8*70-OcKEg7w6?KWvIIlom+qAQ881-F`X;Jliq@HK<*5%PJ94}8k`_#b>wX%sb=d0M9UX@0cMn3;Zh33zvYgmIvU92lIb;b0Y&&m({9=Q1j zzFn6Q`P)YShTjn$jG0k?6F(cEjF3#2`^o#{a{peGFv>&FBP{mGtc%bs{O*B3ivSp7Ad5B1DsF0`0uMx2o>$|B7W^NM_^)2rkARavZg8X?9J z_tP%jQTG#nH*4`7B0F{8K5~DqZ*z*B%%^MZ*&?R+ zGjB{Kbk{l{3;sNO#)59Wi%$3&Hya;Caw3WUeh&z1nBrRz^j+?E@>cgMow!4EkBLUR zHOk%nvUkB%ZhQ^yt6gWc=<=Ag5=Wl+q5EWy+Nd6iO().Collection.OneActive { + private readonly ICoreService _coreService; + public DebugViewModel( ISettingsService settingsService, + ICoreService coreService, RenderDebugViewModel renderDebugViewModel, DataModelDebugViewModel dataModelDebugViewModel, LogsDebugViewModel logsDebugViewModel) { + _coreService = coreService; Items.Add(renderDebugViewModel); Items.Add(dataModelDebugViewModel); Items.Add(logsDebugViewModel); @@ -51,7 +55,7 @@ namespace Artemis.UI.Screens.Settings.Debug public void Restart() { - Core.Utilities.Restart(false, TimeSpan.FromMilliseconds(500)); + Core.Utilities.Restart(_coreService.IsElevated, TimeSpan.FromMilliseconds(500)); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index 8615fd8fc..3a41acea1 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -3,10 +3,15 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; +using System.Net.Mime; using System.Threading.Tasks; +using System.Windows; +using System.Xml.Linq; +using System.Xml.XPath; using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.Services; +using Artemis.UI.Properties; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services; using Artemis.UI.Services.Interfaces; @@ -290,24 +295,103 @@ namespace Artemis.UI.Screens.Settings.Tabs.General private void ApplyAutorun() { + if (!StartWithWindows) + StartMinimized = false; + + // Remove the old auto-run method of placing a shortcut in shell:startup + string autoRunFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), "Artemis.lnk"); + if (File.Exists(autoRunFile)) + File.Delete(autoRunFile); + + // Create or remove the task if necessary try { - string autoRunFile = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Startup), "Artemis.lnk"); - string executableFile = Constants.ExecutablePath; + Process schtasks = new() + { + StartInfo = + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), + Arguments = "/TN \"Artemis 2 autorun\"" + } + }; - if (File.Exists(autoRunFile)) - File.Delete(autoRunFile); - if (StartWithWindows) - ShortcutUtilities.Create(autoRunFile, executableFile, "--autorun", new FileInfo(executableFile).DirectoryName, "Artemis", "", ""); - else - StartMinimized = false; + schtasks.Start(); + schtasks.WaitForExit(); + bool taskCreated = schtasks.ExitCode == 0; + + if (StartWithWindows && !taskCreated) + CreateAutoRunTask(); + else if (!StartWithWindows && taskCreated) + RemoveAutoRunTask(); } catch (Exception e) { - _dialogService.ShowExceptionDialog("An exception occured while trying to apply the auto run setting", e); + Execute.PostToUIThread(() => _dialogService.ShowExceptionDialog("An exception occured while trying to apply the auto run setting", e)); throw; } } + + private void CreateAutoRunTask() + { + XDocument document = XDocument.Parse(Resources.artemis_autorun); + XElement task = document.Descendants().First(); + + task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Date") + .SetValue(DateTime.Now); + task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Author") + .SetValue(System.Security.Principal.WindowsIdentity.GetCurrent().Name); + + task.Descendants().First(d => d.Name.LocalName == "Principals").Descendants().First(d => d.Name.LocalName == "Principal").Descendants().First(d => d.Name.LocalName == "UserId") + .SetValue(System.Security.Principal.WindowsIdentity.GetCurrent().User.Value); + + task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "WorkingDirectory") + .SetValue(Constants.ApplicationFolder); + task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "Command") + .SetValue("\"" + Constants.ExecutablePath + "\""); + + string xmlPath = Path.GetTempFileName(); + using (Stream fileStream = new FileStream(xmlPath, FileMode.Create)) + { + document.Save(fileStream); + } + + Process schtasks = new() + { + StartInfo = + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + Verb = "runas", + FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), + Arguments = $"/Create /XML \"{xmlPath}\" /tn \"Artemis 2 autorun\"" + } + }; + + schtasks.Start(); + schtasks.WaitForExit(); + + File.Delete(xmlPath); + } + + private void RemoveAutoRunTask() + { + Process schtasks = new() + { + StartInfo = + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + Verb = "runas", + FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), + Arguments = "/Delete /TN \"Artemis 2 autorun\" /f" + } + }; + + schtasks.Start(); + schtasks.WaitForExit(); + } } public enum ApplicationColorScheme diff --git a/src/Artemis.UI/Utilities/ProcessUtilities.cs b/src/Artemis.UI/Utilities/ProcessUtilities.cs new file mode 100644 index 000000000..18c251774 --- /dev/null +++ b/src/Artemis.UI/Utilities/ProcessUtilities.cs @@ -0,0 +1,245 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Threading.Tasks; + +namespace Artemis.UI.Utilities +{ + public static class ProcessUtilities + { + public static Task RunProcessAsync(string fileName, string arguments) + { + TaskCompletionSource tcs = new(); + + Process process = new() + { + StartInfo = {FileName = fileName, Arguments = arguments}, + EnableRaisingEvents = true + }; + + process.Exited += (sender, args) => + { + tcs.SetResult(process.ExitCode); + process.Dispose(); + }; + + process.Start(); + + return tcs.Task; + } + + public static void RunAsDesktopUser(string fileName) + { + if (string.IsNullOrWhiteSpace(fileName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName)); + + // To start process as shell user you will need to carry out these steps: + // 1. Enable the SeIncreaseQuotaPrivilege in your current token + // 2. Get an HWND representing the desktop shell (GetShellWindow) + // 3. Get the Process ID(PID) of the process associated with that window(GetWindowThreadProcessId) + // 4. Open that process(OpenProcess) + // 5. Get the access token from that process (OpenProcessToken) + // 6. Make a primary token with that token(DuplicateTokenEx) + // 7. Start the new process with that primary token(CreateProcessWithTokenW) + + IntPtr hProcessToken = IntPtr.Zero; + // Enable SeIncreaseQuotaPrivilege in this process. (This won't work if current process is not elevated.) + try + { + IntPtr process = GetCurrentProcess(); + if (!OpenProcessToken(process, 0x0020, ref hProcessToken)) + return; + + TOKEN_PRIVILEGES tkp = new() + { + PrivilegeCount = 1, + Privileges = new LUID_AND_ATTRIBUTES[1] + }; + + if (!LookupPrivilegeValue(null, "SeIncreaseQuotaPrivilege", ref tkp.Privileges[0].Luid)) + return; + + tkp.Privileges[0].Attributes = 0x00000002; + + if (!AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero)) + return; + } + finally + { + CloseHandle(hProcessToken); + } + + // Get an HWND representing the desktop shell. + // CAVEATS: This will fail if the shell is not running (crashed or terminated), or the default shell has been + // replaced with a custom shell. This also won't return what you probably want if Explorer has been terminated and + // restarted elevated. + IntPtr hwnd = GetShellWindow(); + if (hwnd == IntPtr.Zero) + return; + + IntPtr hShellProcess = IntPtr.Zero; + IntPtr hShellProcessToken = IntPtr.Zero; + IntPtr hPrimaryToken = IntPtr.Zero; + try + { + // Get the PID of the desktop shell process. + uint dwPID; + if (GetWindowThreadProcessId(hwnd, out dwPID) == 0) + return; + + // Open the desktop shell process in order to query it (get the token) + hShellProcess = OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID); + if (hShellProcess == IntPtr.Zero) + return; + + // Get the process token of the desktop shell. + if (!OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken)) + return; + + uint dwTokenRights = 395U; + + // Duplicate the shell's process token to get a primary token. + // Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation). + if (!DuplicateTokenEx(hShellProcessToken, dwTokenRights, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hPrimaryToken)) + return; + + // Start the target process with the new token. + STARTUPINFO si = new(); + PROCESS_INFORMATION pi = new(); + if (!CreateProcessWithTokenW(hPrimaryToken, 0, fileName, "", 0, IntPtr.Zero, Path.GetDirectoryName(fileName), ref si, out pi)) + return; + } + finally + { + CloseHandle(hShellProcessToken); + CloseHandle(hPrimaryToken); + CloseHandle(hShellProcess); + } + } + + #region Interop + + private struct TOKEN_PRIVILEGES + { + public uint PrivilegeCount; + + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + public LUID_AND_ATTRIBUTES[] Privileges; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + private struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public uint Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + private struct LUID + { + public readonly uint LowPart; + public readonly int HighPart; + } + + [Flags] + private enum ProcessAccessFlags : uint + { + All = 0x001F0FFF, + Terminate = 0x00000001, + CreateThread = 0x00000002, + VirtualMemoryOperation = 0x00000008, + VirtualMemoryRead = 0x00000010, + VirtualMemoryWrite = 0x00000020, + DuplicateHandle = 0x00000040, + CreateProcess = 0x000000080, + SetQuota = 0x00000100, + SetInformation = 0x00000200, + QueryInformation = 0x00000400, + QueryLimitedInformation = 0x00001000, + Synchronize = 0x00100000 + } + + private enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + private enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + [StructLayout(LayoutKind.Sequential)] + private struct PROCESS_INFORMATION + { + public readonly IntPtr hProcess; + public readonly IntPtr hThread; + public readonly int dwProcessId; + public readonly int dwThreadId; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + private struct STARTUPINFO + { + public readonly int cb; + public readonly string lpReserved; + public readonly string lpDesktop; + public readonly string lpTitle; + public readonly int dwX; + public readonly int dwY; + public readonly int dwXSize; + public readonly int dwYSize; + public readonly int dwXCountChars; + public readonly int dwYCountChars; + public readonly int dwFillAttribute; + public readonly int dwFlags; + public readonly short wShowWindow; + public readonly short cbReserved2; + public readonly IntPtr lpReserved2; + public readonly IntPtr hStdInput; + public readonly IntPtr hStdOutput; + public readonly IntPtr hStdError; + } + + [DllImport("kernel32.dll", ExactSpelling = true)] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + private static extern bool OpenProcessToken(IntPtr h, int acc, ref IntPtr phtok); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern bool LookupPrivilegeValue(string host, string name, ref LUID pluid); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + private static extern bool AdjustTokenPrivileges(IntPtr htok, bool disall, ref TOKEN_PRIVILEGES newst, int len, IntPtr prev, IntPtr relen); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool CloseHandle(IntPtr hObject); + + + [DllImport("user32.dll")] + private static extern IntPtr GetShellWindow(); + + [DllImport("user32.dll", SetLastError = true)] + private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + private static extern IntPtr OpenProcess(ProcessAccessFlags processAccess, bool bInheritHandle, uint processId); + + [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + private static extern bool DuplicateTokenEx(IntPtr hExistingToken, uint dwDesiredAccess, IntPtr lpTokenAttributes, SECURITY_IMPERSONATION_LEVEL impersonationLevel, TOKEN_TYPE tokenType, + out IntPtr phNewToken); + + [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)] + private static extern bool CreateProcessWithTokenW(IntPtr hToken, int dwLogonFlags, string lpApplicationName, string lpCommandLine, int dwCreationFlags, IntPtr lpEnvironment, + string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); + + #endregion + } +} \ No newline at end of file From ca7af1d142bca483abcad5679026df11d991e994 Mon Sep 17 00:00:00 2001 From: Robert Date: Mon, 25 Jan 2021 19:33:21 +0100 Subject: [PATCH 2/2] Auto-update - Don't check for updates until after core init finished Tray - Attempt to more reliably hide icon on application close Auto-run - Implemented dropping of permissions --- src/Artemis.Core/Events/RestartEventArgs.cs | 9 ++- src/Artemis.Core/Services/CoreService.cs | 11 ++- .../Services/Interfaces/ICoreService.cs | 2 +- .../Interfaces/IPluginManagementService.cs | 2 +- .../Services/PluginManagementService.cs | 5 +- src/Artemis.Core/Utilities/Utilities.cs | 7 +- src/Artemis.UI/Bootstrapper.cs | 39 +++++++++-- src/Artemis.UI/Properties/launchSettings.json | 2 +- .../Screens/Settings/Debug/DebugView.xaml | 27 ++++++- .../Screens/Settings/Debug/DebugViewModel.cs | 10 ++- src/Artemis.UI/Screens/TrayViewModel.cs | 20 ++++-- src/Artemis.UI/Utilities/ProcessUtilities.cs | 70 +++++++++++-------- 12 files changed, 150 insertions(+), 54 deletions(-) diff --git a/src/Artemis.Core/Events/RestartEventArgs.cs b/src/Artemis.Core/Events/RestartEventArgs.cs index 6df873143..1fe2b1fae 100644 --- a/src/Artemis.Core/Events/RestartEventArgs.cs +++ b/src/Artemis.Core/Events/RestartEventArgs.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace Artemis.Core { @@ -7,10 +8,11 @@ namespace Artemis.Core /// public class RestartEventArgs : EventArgs { - internal RestartEventArgs(bool elevate, TimeSpan delay) + internal RestartEventArgs(bool elevate, TimeSpan delay, List? extraArgs) { Elevate = elevate; Delay = delay; + ExtraArgs = extraArgs; } /// @@ -22,5 +24,10 @@ namespace Artemis.Core /// Gets the delay before killing process and restarting /// public TimeSpan Delay { get; } + + /// + /// A list of extra arguments to pass to Artemis when restarting + /// + public List? ExtraArgs { get; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 4177b475a..fbcb9a22d 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -63,6 +63,7 @@ namespace Artemis.Core.Services _loggingLevel = settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Debug); _renderScale = settingsService.GetSetting("Core.RenderScale", 0.5); _frameStopWatch = new Stopwatch(); + StartupArguments = new List(); UpdatePluginCache(); @@ -77,7 +78,7 @@ namespace Artemis.Core.Services public TimeSpan FrameTime { get; private set; } public bool ModuleRenderingDisabled { get; set; } - public List? StartupArguments { get; set; } + public List StartupArguments { get; set; } public bool IsElevated { get; set; } public void Dispose() @@ -109,12 +110,16 @@ namespace Artemis.Core.Services // Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version; _logger.Debug("Forcing plugins to use HidSharp {hidSharpVersion}", hidSharpVersion); - + DeserializationLogger.Initialize(Kernel); // Initialize the services _pluginManagementService.CopyBuiltInPlugins(); - _pluginManagementService.LoadPlugins(StartupArguments != null && StartupArguments.Contains("--ignore-plugin-lock"), IsElevated); + _pluginManagementService.LoadPlugins( + StartupArguments.Contains("--ignore-plugin-lock"), + StartupArguments.Contains("--force-elevation"), + IsElevated + ); ArtemisSurface surfaceConfig = _surfaceService.ActiveSurface; _logger.Information("Initialized with active surface entity {surfaceConfig}-{guid}", surfaceConfig.Name, surfaceConfig.EntityId); diff --git a/src/Artemis.Core/Services/Interfaces/ICoreService.cs b/src/Artemis.Core/Services/Interfaces/ICoreService.cs index 6e13fba3b..f3d934d11 100644 --- a/src/Artemis.Core/Services/Interfaces/ICoreService.cs +++ b/src/Artemis.Core/Services/Interfaces/ICoreService.cs @@ -26,7 +26,7 @@ namespace Artemis.Core.Services /// /// Gets or sets a list of startup arguments /// - List? StartupArguments { get; set; } + List StartupArguments { get; set; } /// /// Gets a boolean indicating whether Artemis is running in an elevated environment (admin permissions) diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index 170f87771..0aa3a66a3 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -26,7 +26,7 @@ namespace Artemis.Core.Services /// /// Loads all installed plugins. If plugins already loaded this will reload them all /// - void LoadPlugins(bool ignorePluginLock, bool isElevated); + void LoadPlugins(bool ignorePluginLock, bool stayElevated, bool isElevated); /// /// Unloads all installed plugins. diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index a6ca023be..39ef62b89 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -172,7 +172,7 @@ namespace Artemis.Core.Services #region Plugins - public void LoadPlugins(bool ignorePluginLock, bool isElevated) + public void LoadPlugins(bool ignorePluginLock, bool stayElevated, bool isElevated) { if (LoadingPlugins) throw new ArtemisCoreException("Cannot load plugins while a previous load hasn't been completed yet."); @@ -208,9 +208,10 @@ namespace Artemis.Core.Services return; } - if (isElevated && !adminRequired) + if (isElevated && !adminRequired && !stayElevated) { // No need for a delay this early on, nothing that needs graceful shutdown is happening yet + _logger.Information("Restarting because no plugin requires elevation and --force-elevation was not supplied"); Utilities.Restart(false, TimeSpan.Zero); return; } diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index fa03f516d..9d7e7de8e 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Linq; using System.Security.AccessControl; using System.Security.Principal; @@ -38,9 +40,10 @@ namespace Artemis.Core /// /// Whether the application should be restarted with elevated permissions /// Delay in seconds before killing process and restarting - public static void Restart(bool elevate, TimeSpan delay) + /// A list of extra arguments to pass to Artemis when restarting + public static void Restart(bool elevate, TimeSpan delay, params string[] extraArgs) { - OnRestartRequested(new RestartEventArgs(elevate, delay)); + OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList())); } /// diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 2a0bf0f65..c9c24459a 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -2,7 +2,9 @@ using System.Collections.Generic; using System.Diagnostics; using System.Globalization; +using System.IO; using System.Linq; +using System.Runtime.InteropServices; using System.Security.Principal; using System.Threading.Tasks; using System.Windows; @@ -20,7 +22,6 @@ using Artemis.UI.Stylet; using Artemis.UI.Utilities; using Ninject; using Serilog; -using SharpVectors.Dom.Resources; using Stylet; namespace Artemis.UI @@ -40,6 +41,7 @@ namespace Artemis.UI protected override void Launch() { + // TODO: Move shutdown code out of bootstrapper Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; Core.Utilities.PrepareFirstLaunch(); @@ -139,25 +141,47 @@ namespace Artemis.UI private void UtilitiesOnRestartRequested(object sender, RestartEventArgs e) { - string args = Args.Any() ? "-ArgumentList " + string.Join(',', Args.Select(a => "'" + a + "'")) : ""; - + List argsList = new(); + argsList.AddRange(Args); + if (e.ExtraArgs != null) + argsList.AddRange(e.ExtraArgs.Except(argsList)); + string args = argsList.Any() ? "-ArgumentList " + string.Join(',', argsList) : ""; + string command = + $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; " + + $"(Get-Process 'Artemis.UI').kill(); " + + $"Start-Process -FilePath '{Constants.ExecutablePath}' -WorkingDirectory '{Constants.ApplicationFolder}' {args}}}\""; + // Elevated always runs with RunAs if (e.Elevate) { ProcessStartInfo info = new() { - Arguments = - $"-Command \"& {{Start-Sleep -Milliseconds {(int) e.Delay.TotalMilliseconds}; (Get-Process 'Artemis.UI').kill(); Start-Process -FilePath '{Constants.ExecutablePath}' {args} -Verb RunAs}}\"", + Arguments = command.Replace("}\"", " -Verb RunAs}\""), WindowStyle = ProcessWindowStyle.Hidden, CreateNoWindow = true, FileName = "PowerShell.exe" }; Process.Start(info); } + // Non-elevated runs regularly if currently not elevated + else if (!_core.IsElevated) + { + ProcessStartInfo info = new() + { + Arguments = command, + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + } + // Non-elevated runs via a utility method is currently elevated (de-elevating is hacky) else { - ProcessUtilities.RunAsDesktopUser(Constants.ExecutablePath); + string powerShell = Path.Combine(Environment.SystemDirectory, "WindowsPowerShell", "v1.0", "powershell.exe"); + ProcessUtilities.RunAsDesktopUser(powerShell, command, true); } + // Lets try a graceful shutdown, PowerShell will kill if needed Execute.OnUIThread(() => Application.Current.Shutdown()); } @@ -176,5 +200,8 @@ namespace Artemis.UI Environment.Exit(1); }); } + + [DllImport("user32.dll")] + private static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); } } \ No newline at end of file diff --git a/src/Artemis.UI/Properties/launchSettings.json b/src/Artemis.UI/Properties/launchSettings.json index 948a8e1bb..be8d62f45 100644 --- a/src/Artemis.UI/Properties/launchSettings.json +++ b/src/Artemis.UI/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "Artemis.UI": { "commandName": "Project", - "commandLineArgs": "--test --test2" + "commandLineArgs": "--force-elevation" } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml b/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml index 46eb707f5..068b39899 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml +++ b/src/Artemis.UI/Screens/Settings/Debug/DebugView.xaml @@ -39,9 +39,30 @@ Stay on top - + + + diff --git a/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs b/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs index e4d884880..e661158ea 100644 --- a/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Debug/DebugViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Screens.Settings.Debug.Tabs; @@ -29,6 +30,8 @@ namespace Artemis.UI.Screens.Settings.Debug public PluginSetting StayOnTopSetting { get; } public string Title => "Debugger"; + public bool CanElevate => !_coreService.IsElevated; + public bool CanDrop => _coreService.IsElevated; public void ToggleStayOnTop() { @@ -50,7 +53,12 @@ namespace Artemis.UI.Screens.Settings.Debug public void Elevate() { - Core.Utilities.Restart(true, TimeSpan.FromMilliseconds(500)); + Core.Utilities.Restart(true, TimeSpan.FromMilliseconds(500), "--force-elevation"); + } + + public void Drop() + { + Core.Utilities.Restart(false, TimeSpan.Zero); } public void Restart() diff --git a/src/Artemis.UI/Screens/TrayViewModel.cs b/src/Artemis.UI/Screens/TrayViewModel.cs index 302bae074..666c8cd59 100644 --- a/src/Artemis.UI/Screens/TrayViewModel.cs +++ b/src/Artemis.UI/Screens/TrayViewModel.cs @@ -46,6 +46,9 @@ namespace Artemis.UI.Screens _debugService = debugService; CanShowRootViewModel = true; + Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; + Core.Utilities.RestartRequested += UtilitiesOnShutdownRequested; + windowService.ConfigureMainWindowProvider(this); messageService.ConfigureNotificationProvider(this); bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun"); @@ -56,7 +59,9 @@ namespace Artemis.UI.Screens coreService.Initialized += (_, _) => TrayBringToForeground(); } else - updateService.AutoUpdate(); + { + coreService.Initialized += (_, _) => updateService.AutoUpdate(); + } } public bool CanShowRootViewModel @@ -111,12 +116,14 @@ namespace Artemis.UI.Screens public void OnTrayBalloonTipClicked(object sender, EventArgs e) { if (CanShowRootViewModel) + { TrayBringToForeground(); + } else { // Wrestle the main window to the front - Window mainWindow = ((Window) _rootViewModel.View); - if (mainWindow.WindowState == WindowState.Minimized) + Window mainWindow = (Window) _rootViewModel.View; + if (mainWindow.WindowState == WindowState.Minimized) mainWindow.WindowState = WindowState.Normal; mainWindow.Activate(); mainWindow.Topmost = true; @@ -125,6 +132,11 @@ namespace Artemis.UI.Screens } } + private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) + { + Execute.OnUIThread(() => _taskBarIcon?.Dispose()); + } + private void ShowSplashScreen() { Execute.OnUIThread(() => @@ -156,7 +168,7 @@ namespace Artemis.UI.Screens PackIcon packIcon = new() {Kind = icon}; Geometry geometry = Geometry.Parse(packIcon.Data); - + // Scale the icon up to fit a 256x256 image and draw it geometry = Geometry.Combine(geometry, Geometry.Empty, GeometryCombineMode.Union, new ScaleTransform(256 / geometry.Bounds.Right, 256 / geometry.Bounds.Bottom)); drawingContext.DrawGeometry(new SolidColorBrush(Colors.White), null, geometry); diff --git a/src/Artemis.UI/Utilities/ProcessUtilities.cs b/src/Artemis.UI/Utilities/ProcessUtilities.cs index 18c251774..dc418ace1 100644 --- a/src/Artemis.UI/Utilities/ProcessUtilities.cs +++ b/src/Artemis.UI/Utilities/ProcessUtilities.cs @@ -29,7 +29,7 @@ namespace Artemis.UI.Utilities return tcs.Task; } - public static void RunAsDesktopUser(string fileName) + public static Process RunAsDesktopUser(string fileName, string arguments, bool hideWindow) { if (string.IsNullOrWhiteSpace(fileName)) throw new ArgumentException("Value cannot be null or whitespace.", nameof(fileName)); @@ -49,7 +49,7 @@ namespace Artemis.UI.Utilities { IntPtr process = GetCurrentProcess(); if (!OpenProcessToken(process, 0x0020, ref hProcessToken)) - return; + return null; TOKEN_PRIVILEGES tkp = new() { @@ -58,12 +58,12 @@ namespace Artemis.UI.Utilities }; if (!LookupPrivilegeValue(null, "SeIncreaseQuotaPrivilege", ref tkp.Privileges[0].Luid)) - return; + return null; tkp.Privileges[0].Attributes = 0x00000002; if (!AdjustTokenPrivileges(hProcessToken, false, ref tkp, 0, IntPtr.Zero, IntPtr.Zero)) - return; + return null; } finally { @@ -76,7 +76,7 @@ namespace Artemis.UI.Utilities // restarted elevated. IntPtr hwnd = GetShellWindow(); if (hwnd == IntPtr.Zero) - return; + return null; IntPtr hShellProcess = IntPtr.Zero; IntPtr hShellProcessToken = IntPtr.Zero; @@ -86,29 +86,41 @@ namespace Artemis.UI.Utilities // Get the PID of the desktop shell process. uint dwPID; if (GetWindowThreadProcessId(hwnd, out dwPID) == 0) - return; + return null; // Open the desktop shell process in order to query it (get the token) hShellProcess = OpenProcess(ProcessAccessFlags.QueryInformation, false, dwPID); if (hShellProcess == IntPtr.Zero) - return; + return null; // Get the process token of the desktop shell. if (!OpenProcessToken(hShellProcess, 0x0002, ref hShellProcessToken)) - return; + return null; uint dwTokenRights = 395U; // Duplicate the shell's process token to get a primary token. // Based on experimentation, this is the minimal set of rights required for CreateProcessWithTokenW (contrary to current documentation). if (!DuplicateTokenEx(hShellProcessToken, dwTokenRights, IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out hPrimaryToken)) - return; + return null; // Start the target process with the new token. STARTUPINFO si = new(); + if (hideWindow) + { + si.dwFlags = 0x00000001; + si.wShowWindow = 0; + } + PROCESS_INFORMATION pi = new(); - if (!CreateProcessWithTokenW(hPrimaryToken, 0, fileName, "", 0, IntPtr.Zero, Path.GetDirectoryName(fileName), ref si, out pi)) - return; + if (!CreateProcessWithTokenW(hPrimaryToken, 0, fileName, $"\"{fileName}\" {arguments}", 0, IntPtr.Zero, Path.GetDirectoryName(fileName), ref si, out pi)) + { + // Get the last error and display it. + int error = Marshal.GetLastWin32Error(); + return null; + } + + return Process.GetProcessById(pi.dwProcessId); } finally { @@ -186,24 +198,24 @@ namespace Artemis.UI.Utilities [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] private struct STARTUPINFO { - public readonly int cb; - public readonly string lpReserved; - public readonly string lpDesktop; - public readonly string lpTitle; - public readonly int dwX; - public readonly int dwY; - public readonly int dwXSize; - public readonly int dwYSize; - public readonly int dwXCountChars; - public readonly int dwYCountChars; - public readonly int dwFillAttribute; - public readonly int dwFlags; - public readonly short wShowWindow; - public readonly short cbReserved2; - public readonly IntPtr lpReserved2; - public readonly IntPtr hStdInput; - public readonly IntPtr hStdOutput; - public readonly IntPtr hStdError; + public int cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public int dwX; + public int dwY; + public int dwXSize; + public int dwYSize; + public int dwXCountChars; + public int dwYCountChars; + public int dwFillAttribute; + public int dwFlags; + public short wShowWindow; + public short cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; } [DllImport("kernel32.dll", ExactSpelling = true)]