From 62fe4d7687e22344924c0a6235e49b4a445dcbe5 Mon Sep 17 00:00:00 2001 From: Andreas Saurwein Date: Fri, 8 Sep 2023 17:55:42 +0100 Subject: [PATCH 1/2] started adding alwaysadmin option, elevation, autostart --- VSLauncherX/App.config | 3 + VSLauncherX/Helpers/AdminInfo.cs | 22 +++ VSLauncherX/Helpers/AutoRun.cs | 164 ++++++++++++++++++++ VSLauncherX/MainDialog.cs | 25 ++- VSLauncherX/Program.cs | 44 +++++- VSLauncherX/Properties/Settings.Designer.cs | 12 ++ VSLauncherX/Properties/Settings.settings | 3 + VSLauncherX/VSLauncherX.csproj | 3 + 8 files changed, 270 insertions(+), 6 deletions(-) create mode 100644 VSLauncherX/Helpers/AdminInfo.cs create mode 100644 VSLauncherX/Helpers/AutoRun.cs diff --git a/VSLauncherX/App.config b/VSLauncherX/App.config index fe59ae3..915598a 100644 --- a/VSLauncherX/App.config +++ b/VSLauncherX/App.config @@ -40,6 +40,9 @@ True + + False + \ No newline at end of file diff --git a/VSLauncherX/Helpers/AdminInfo.cs b/VSLauncherX/Helpers/AdminInfo.cs new file mode 100644 index 0000000..533a2f5 --- /dev/null +++ b/VSLauncherX/Helpers/AdminInfo.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +namespace VSLauncher.Helpers +{ + internal class AdminInfo + { + public static bool IsCurrentUserAdmin() + { + return UACHelper.UACHelper.IsAdministrator; + } + + internal static bool IsElevated() + { + return UACHelper.UACHelper.IsElevated; + } + } +} diff --git a/VSLauncherX/Helpers/AutoRun.cs b/VSLauncherX/Helpers/AutoRun.cs new file mode 100644 index 0000000..047ff6f --- /dev/null +++ b/VSLauncherX/Helpers/AutoRun.cs @@ -0,0 +1,164 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Security.Principal; +using System.Text; +using System.Threading.Tasks; + +using Microsoft.Win32.TaskScheduler; + +namespace VSLauncher.Helpers +{ + /// + /// The auto run. + /// + public class AutoRun + { + private const string FolderName = "VSLauncherX"; + private const string TaskName = "AppStart for "; + + /// + /// Initializes a new instance of the class. + /// + public AutoRun() + { + } + + /// + /// Setups the launcher task. + /// + /// If true, start elevated. + /// If true, start as autostart. + public static void SetupLauncherTask(bool bElevated, bool asAutostart) + { + // Get the service on the local machine + using (TaskService ts = new TaskService()) + { + TaskFolder folder; + var bFolderExists = ts.RootFolder.SubFolders.Any(sf => sf.Name == FolderName); + + if (!bFolderExists) + { + folder = ts.RootFolder.CreateFolder(FolderName); + } + else + { + folder = ts.RootFolder.SubFolders.First(sf => sf.Name == FolderName); + } + + var user = System.Security.Principal.WindowsIdentity.GetCurrent(); + + string location = Assembly.GetExecutingAssembly().Location.Replace(".dll", ".exe"); + var execAction = new ExecAction(location, "autostart" ,workingDirectory: Path.GetDirectoryName(location)); + var taskName = TaskName + GetUserName(user); + + // create the task if it doesn't exist + if (folder.AllTasks.Any(t => t.Name == taskName)) + { + folder.DeleteTask(taskName); + } + + // Create a new task definition and assign properties + TaskDefinition td = ts.NewTask(); + td.RegistrationInfo.Author = user.Name; + td.RegistrationInfo.Description = "Visual Studio Launcher"; + + td.Principal.RunLevel = bElevated ? TaskRunLevel.Highest : TaskRunLevel.LUA; + td.Principal.LogonType = TaskLogonType.InteractiveToken; + + td.Settings.StartWhenAvailable = true; + td.Settings.AllowDemandStart = true; + td.Settings.IdleSettings.StopOnIdleEnd = false; + td.Settings.DisallowStartIfOnBatteries = false; + td.Settings.StopIfGoingOnBatteries = false; + td.Settings.ExecutionTimeLimit = TimeSpan.Zero; + td.Settings.AllowHardTerminate = true; + td.Settings.MultipleInstances = TaskInstancesPolicy.Parallel; + + if (asAutostart) + { + // Create a trigger that will fire the task when the user logs on + var logonTrigger = new LogonTrigger + { + UserId = user.User.ToString(), + Delay = TimeSpan.FromSeconds(10) // give task manager time to start too + }; + + td.Triggers.Add(logonTrigger); + } + + td.Actions.Add(execAction); + + // Register the task in the root folder + folder.RegisterTaskDefinition(taskName, td); + } + } + + /// + /// Gets a simple user name. + /// + /// The user. + /// A string. + private static string GetUserName(WindowsIdentity user) + { + return user.Name.Replace("\\", "_").Replace(".", "_"); + } + + /// + /// Runs the. + /// + internal static void Run() + { + // Get the service on the local machine + using (TaskService ts = new TaskService()) + { + TaskFolder folder; + var bFolderExists = ts.RootFolder.SubFolders.Any(sf => sf.Name == FolderName); + + if (!bFolderExists) + { + throw new Exception("Task Folder not found"); + } + else + { + folder = ts.RootFolder.SubFolders.First(sf => sf.Name == FolderName); + } + var taskName = TaskName + GetUserName(System.Security.Principal.WindowsIdentity.GetCurrent()); + + if (folder.AllTasks.Any(t => t.Name == taskName)) + { + folder.Tasks[taskName].Run(); + } + } + } + + /// + /// Removes the launcher task. + /// + internal static void RemoveLauncherTask() + { + using (TaskService ts = new TaskService()) + { + TaskFolder folder; + var bFolderExists = ts.RootFolder.SubFolders.Any(sf => sf.Name == FolderName); + + if (!bFolderExists) + { + return; + } + + folder = ts.RootFolder.SubFolders.First(sf => sf.Name == FolderName); + + var user = System.Security.Principal.WindowsIdentity.GetCurrent(); + var taskName = TaskName + GetUserName(user); + + // create the task if it doesn't exist + if (folder.AllTasks.Any(t => t.Name == taskName)) + { + folder.DeleteTask(taskName); + } + } + } + } +} diff --git a/VSLauncherX/MainDialog.cs b/VSLauncherX/MainDialog.cs index 30bb812..417acd8 100644 --- a/VSLauncherX/MainDialog.cs +++ b/VSLauncherX/MainDialog.cs @@ -15,6 +15,7 @@ using LibGit2Sharp; using Windows.Devices.Geolocation; using static System.Windows.Forms.AxHost; +using System.Runtime.InteropServices; namespace VSLauncher { @@ -188,6 +189,10 @@ private void InitializeListview() // this.olvFiles.SetObjects(list); } + [DllImport("shell32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + static extern bool IsUserAnAdmin(); + /// /// Initializes a new instance of the class. /// @@ -198,7 +203,16 @@ public MainDialog() InitializeListview(); SetupDragAndDrop(); - // BuildTestData(); + // if(IsUserAnAdmin()) + if(AdminInfo.IsCurrentUserAdmin()) + { + this.Text += " (Administrator)"; + } + + if(AdminInfo.IsElevated()) + { + this.Text += " *"; + } this.bInUpdate = true; @@ -573,6 +587,15 @@ private void mainSettings_Click(object sender, EventArgs e) if (dlg.ShowDialog() == DialogResult.OK) { + if(Properties.Settings.Default.AlwaysAdmin || Properties.Settings.Default.AutoStart) + { + Program.UpdateTaskScheduler(); + } + else + { + Program.RemoveTaskScheduler(); + } + _ = SolutionData_OnChanged(true); } } diff --git a/VSLauncherX/Program.cs b/VSLauncherX/Program.cs index 7af985c..210046e 100644 --- a/VSLauncherX/Program.cs +++ b/VSLauncherX/Program.cs @@ -1,5 +1,9 @@ +using System.Diagnostics; + using VSLauncher; using VSLauncher.DataModel; +using VSLauncher.Helpers; +using VSLauncher.Properties; namespace VSLauncher { @@ -24,14 +28,44 @@ static void Main() VisualStudioFileIcons16.GetIcon("Solution"); VisualStudioFileIcons32.GetIcon("Solution"); - Application.Run(new MainDialog()); + // add logic to start either normal, or as part of autostart, with or without elevation + var args = Environment.GetCommandLineArgs(); + + if((args.Length > 1 && args[1] == "autostart") || Debugger.IsAttached) + { + // we have command line arguments, so we are started as part of autostart + Application.Run(new MainDialog()); + } + else + { + if (!Settings.Default.AlwaysAdmin) + { + Application.Run(new MainDialog()); + } + else + { + // we are started normally + AutoRun.Run(); + } + } } - /// - /// Tests the. - /// - private static void Test() + internal static void UpdateTaskScheduler() + { + if(Settings.Default.AlwaysAdmin && !UACHelper.UACHelper.IsAdministrator) + { + // restart ourselves with elevation + ProcessStartInfo psi = new ProcessStartInfo() + { + + }; + } + AutoRun.SetupLauncherTask(Settings.Default.AlwaysAdmin, Settings.Default.AutoStart); + } + + internal static void RemoveTaskScheduler() { + AutoRun.RemoveLauncherTask(); } } } \ No newline at end of file diff --git a/VSLauncherX/Properties/Settings.Designer.cs b/VSLauncherX/Properties/Settings.Designer.cs index 8ad31d9..32d7074 100644 --- a/VSLauncherX/Properties/Settings.Designer.cs +++ b/VSLauncherX/Properties/Settings.Designer.cs @@ -154,5 +154,17 @@ public bool ColumnOptionsVisible { this["ColumnOptionsVisible"] = value; } } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool AutoStart { + get { + return ((bool)(this["AutoStart"])); + } + set { + this["AutoStart"] = value; + } + } } } diff --git a/VSLauncherX/Properties/Settings.settings b/VSLauncherX/Properties/Settings.settings index d08b45c..7332391 100644 --- a/VSLauncherX/Properties/Settings.settings +++ b/VSLauncherX/Properties/Settings.settings @@ -35,5 +35,8 @@ True + + False + \ No newline at end of file diff --git a/VSLauncherX/VSLauncherX.csproj b/VSLauncherX/VSLauncherX.csproj index 0411b4b..d0ee4ec 100644 --- a/VSLauncherX/VSLauncherX.csproj +++ b/VSLauncherX/VSLauncherX.csproj @@ -51,8 +51,11 @@ + + + From cb7dfd57d3008580c69257c66acd2089dc3b9244 Mon Sep 17 00:00:00 2001 From: Andreas Saurwein Date: Mon, 11 Sep 2023 14:11:56 +0100 Subject: [PATCH 2/2] finalized Admin/elevation detection imported some code from gsudo finalized autostart and always start as admin Shuffled some code around --- VSLauncherX/Controls/VisualStudioCombobox.cs | 218 +++++++++ .../Forms/dlgExecuteVisualStudio.Designer.cs | 6 +- VSLauncherX/Forms/dlgSettings.Designer.cs | 27 +- VSLauncherX/Forms/dlgSettings.cs | 2 + VSLauncherX/Forms/dlgSettings.resx | 2 +- VSLauncherX/Helpers/AdminInfo.cs | 39 +- VSLauncherX/Helpers/ItemLauncher.cs | 140 ++++++ VSLauncherX/Helpers/Native/NtDllApi.cs | 64 +++ VSLauncherX/Helpers/Native/ProcessApi.cs | 237 ++++++++++ VSLauncherX/Helpers/Native/SafeTokenHandle.cs | 24 + VSLauncherX/Helpers/Native/TokensApi.cs | 420 +++++++++++++++++ VSLauncherX/Helpers/ProcessHelper.cs | 141 ++++++ VSLauncherX/Helpers/SecurityHelper.cs | 77 ++++ VSLauncherX/Helpers/Tokens/IntegrityLevel.cs | 31 ++ VSLauncherX/Helpers/Tokens/NativeMethods.cs | 104 +++++ VSLauncherX/Helpers/Tokens/Privilege.cs | 41 ++ .../Helpers/Tokens/PrivilegeManager.cs | 75 +++ VSLauncherX/Helpers/Tokens/TokenProvider.cs | 433 ++++++++++++++++++ VSLauncherX/ItemLauncher.cs | 140 ------ VSLauncherX/MainDialog.Designer.cs | 2 +- VSLauncherX/MainDialog.cs | 54 ++- VSLauncherX/Program.cs | 14 +- VSLauncherX/VisualStudioCombobox.cs | 218 --------- 23 files changed, 2115 insertions(+), 394 deletions(-) create mode 100644 VSLauncherX/Controls/VisualStudioCombobox.cs create mode 100644 VSLauncherX/Helpers/ItemLauncher.cs create mode 100644 VSLauncherX/Helpers/Native/NtDllApi.cs create mode 100644 VSLauncherX/Helpers/Native/ProcessApi.cs create mode 100644 VSLauncherX/Helpers/Native/SafeTokenHandle.cs create mode 100644 VSLauncherX/Helpers/Native/TokensApi.cs create mode 100644 VSLauncherX/Helpers/ProcessHelper.cs create mode 100644 VSLauncherX/Helpers/SecurityHelper.cs create mode 100644 VSLauncherX/Helpers/Tokens/IntegrityLevel.cs create mode 100644 VSLauncherX/Helpers/Tokens/NativeMethods.cs create mode 100644 VSLauncherX/Helpers/Tokens/Privilege.cs create mode 100644 VSLauncherX/Helpers/Tokens/PrivilegeManager.cs create mode 100644 VSLauncherX/Helpers/Tokens/TokenProvider.cs delete mode 100644 VSLauncherX/ItemLauncher.cs delete mode 100644 VSLauncherX/VisualStudioCombobox.cs diff --git a/VSLauncherX/Controls/VisualStudioCombobox.cs b/VSLauncherX/Controls/VisualStudioCombobox.cs new file mode 100644 index 0000000..bf2be35 --- /dev/null +++ b/VSLauncherX/Controls/VisualStudioCombobox.cs @@ -0,0 +1,218 @@ +using VSLauncher.DataModel; + +namespace VSLauncher.Controls +{ + /// + /// The visual studio combobox. + /// + public class VisualStudioCombobox : ComboBox + { + private VisualStudioInstanceManager visualStudioVersions = new VisualStudioInstanceManager(); + private bool showDefault; + + public VisualStudioCombobox() + { + DrawMode = DrawMode.OwnerDrawFixed; + DropDownStyle = ComboBoxStyle.DropDownList; + IntegralHeight = false; + ItemHeight = 26; + DrawItem += CustomDrawItem; + + Items.AddRange(visualStudioVersions.All.ToArray()); + } + + /// + /// Gets or sets a value indicating whether to show default. + /// + public bool ShowDefault + { + get => showDefault; + set + { + showDefault = value; + + if (value) + { + Items.Insert(0, ""); + } + else + { + if (Items.Count > 0 && Items[0] is string) + { + Items.RemoveAt(0); + } + } + } + } + + /// + /// Gets the versions. + /// + public List Versions { get { return visualStudioVersions.All; } } + + /// + /// Gets the selected item. + /// + public new VisualStudioInstance? SelectedItem + { + get + { + if (ShowDefault) + { + return base.SelectedIndex > 0 ? (VisualStudioInstance)base.SelectedItem : null; + } + + return (VisualStudioInstance)base.SelectedItem; + } + set + { + base.SelectedItem = value; + } + } + + /// + /// Gets or sets the selected index, accounting for the default item which will result in -1 + /// + public new int SelectedIndex + { + get + { + return ShowDefault ? base.SelectedIndex - 1 : base.SelectedIndex; + } + set + { + base.SelectedIndex = value; + } + } + + /// + /// Gets a value indicating whether default is selected. + /// + public bool IsDefaultSelected + { + get + { + return ShowDefault && base.SelectedIndex == 0; + } + } + + /// + /// Selects the default item if existing + /// + public void SelectDefault() + { + if (ShowDefault) + { + SelectedIndex = 0; + } + } + + protected override void OnDropDown(EventArgs e) + { + Items.Clear(); + + if (ShowDefault) + { + Items.Add(""); + } + + Items.AddRange(visualStudioVersions.All.ToArray()); + + base.OnDropDown(e); + } + + /// + /// Selects the active item from a version string (exact or major version), a shortname or an identifier + /// + /// The v. + internal void SelectFromVersion(string? v) + { + if (string.IsNullOrWhiteSpace(v)) + { + if (ShowDefault) + { + if (v != null) // v is "" so select the default + { + SelectDefault(); + return; + } + } + + // any NULL value will return the highest version + v = visualStudioVersions.HighestVersion().Version; + } + + // find either by exact version or by short name containing the version + var i = visualStudioVersions.All.FindIndex(x => x.Version == v); + if (i >= 0) + { + SelectedIndex = ShowDefault ? 1 + i : i; + return; + } + + var k = visualStudioVersions.All.FindIndex(x => x.Version.StartsWith(v)); + if (k >= 0) + { + SelectedIndex = ShowDefault ? 1 + k : k; + return; + } + + // by year + var n = visualStudioVersions.All.FindIndex(x => x.ShortName.Contains(v)); + if (n >= 0) + { + SelectedIndex = ShowDefault ? 1 + n : n; + return; + } + + // by identifier + var o = visualStudioVersions.All.FindIndex(x => x.Identifier == v); + if (o >= 0) + { + SelectedIndex = ShowDefault ? 1 + o : o; + return; + } + + SelectedIndex = -1; + } + + /// + /// Custom draws the item + /// + /// The sender. + /// The e. + private void CustomDrawItem(object sender, DrawItemEventArgs e) + { + // draw the selected item with the Visual Studio Icon and the version as text + if (e.Index >= 0 && e.Index <= visualStudioVersions.Count) + { + e.DrawBackground(); + + var height = 16; + + Rectangle iconRect = new Rectangle(e.Bounds.Left + Margin.Left, + e.Bounds.Top + (ItemHeight - height) / 2, + height, height); + if (ShowDefault) + { + if (e.Index > 0) + { + e.Graphics.DrawIcon(visualStudioVersions[e.Index - 1].AppIcon, iconRect); + e.Graphics.DrawString(visualStudioVersions[e.Index - 1].Name, e.Font, Brushes.Black, e.Bounds.Left + 20, e.Bounds.Top + 4); + } + else + { + e.Graphics.DrawIcon(Resources.AppLogo, iconRect); + e.Graphics.DrawString("", e.Font, Brushes.Black, e.Bounds.Left + 20, e.Bounds.Top + 4); + } + } + else + { + e.Graphics.DrawIcon(visualStudioVersions[e.Index].AppIcon, iconRect); + e.Graphics.DrawString(visualStudioVersions[e.Index].Name, e.Font, Brushes.Black, e.Bounds.Left + 20, e.Bounds.Top + 4); + } + } + } + + } +} \ No newline at end of file diff --git a/VSLauncherX/Forms/dlgExecuteVisualStudio.Designer.cs b/VSLauncherX/Forms/dlgExecuteVisualStudio.Designer.cs index 0a92ed1..5c0ca26 100644 --- a/VSLauncherX/Forms/dlgExecuteVisualStudio.Designer.cs +++ b/VSLauncherX/Forms/dlgExecuteVisualStudio.Designer.cs @@ -1,6 +1,8 @@ -namespace VSLauncher +using VSLauncher.Controls; + +namespace VSLauncher { - partial class dlgExecuteVisualStudio + partial class dlgExecuteVisualStudio { /// /// Required designer variable. diff --git a/VSLauncherX/Forms/dlgSettings.Designer.cs b/VSLauncherX/Forms/dlgSettings.Designer.cs index ef51df5..19bba36 100644 --- a/VSLauncherX/Forms/dlgSettings.Designer.cs +++ b/VSLauncherX/Forms/dlgSettings.Designer.cs @@ -34,6 +34,7 @@ private void InitializeComponent() chkAlwaysAdmin = new CheckBox(); chkSync = new CheckBox(); chkShowPath = new CheckBox(); + chkAutostart = new CheckBox(); SuspendLayout(); // // btnCancel @@ -44,7 +45,7 @@ private void InitializeComponent() btnCancel.Location = new Point(334, 261); btnCancel.Name = "btnCancel"; btnCancel.Size = new Size(100, 40); - btnCancel.TabIndex = 5; + btnCancel.TabIndex = 4; btnCancel.Text = " Cancel"; btnCancel.TextImageRelation = TextImageRelation.ImageBeforeText; btnCancel.UseVisualStyleBackColor = true; @@ -57,7 +58,7 @@ private void InitializeComponent() btnOk.Location = new Point(440, 261); btnOk.Name = "btnOk"; btnOk.Size = new Size(100, 40); - btnOk.TabIndex = 6; + btnOk.TabIndex = 5; btnOk.Text = " Ok"; btnOk.TextImageRelation = TextImageRelation.ImageBeforeText; btnOk.UseVisualStyleBackColor = true; @@ -69,30 +70,40 @@ private void InitializeComponent() chkAlwaysAdmin.Location = new Point(12, 12); chkAlwaysAdmin.Name = "chkAlwaysAdmin"; chkAlwaysAdmin.Size = new Size(142, 19); - chkAlwaysAdmin.TabIndex = 7; + chkAlwaysAdmin.TabIndex = 0; chkAlwaysAdmin.Text = "Always start as Admin"; chkAlwaysAdmin.UseVisualStyleBackColor = true; // // chkSync // chkSync.AutoSize = true; - chkSync.Location = new Point(12, 37); + chkSync.Location = new Point(12, 62); chkSync.Name = "chkSync"; chkSync.Size = new Size(192, 19); - chkSync.TabIndex = 7; + chkSync.TabIndex = 2; chkSync.Text = "Synchronize with VS Recent List"; chkSync.UseVisualStyleBackColor = true; // // chkShowPath // chkShowPath.AutoSize = true; - chkShowPath.Location = new Point(12, 62); + chkShowPath.Location = new Point(12, 87); chkShowPath.Name = "chkShowPath"; chkShowPath.Size = new Size(219, 19); - chkShowPath.TabIndex = 7; + chkShowPath.TabIndex = 3; chkShowPath.Text = "Show path for solutions and projects"; chkShowPath.UseVisualStyleBackColor = true; // + // chkAutostart + // + chkAutostart.AutoSize = true; + chkAutostart.Location = new Point(12, 37); + chkAutostart.Name = "chkAutostart"; + chkAutostart.Size = new Size(126, 19); + chkAutostart.TabIndex = 1; + chkAutostart.Text = "Autostart on logon"; + chkAutostart.UseVisualStyleBackColor = true; + // // dlgSettings // this.AcceptButton = btnOk; @@ -103,6 +114,7 @@ private void InitializeComponent() this.ControlBox = false; this.Controls.Add(chkShowPath); this.Controls.Add(chkSync); + this.Controls.Add(chkAutostart); this.Controls.Add(chkAlwaysAdmin); this.Controls.Add(btnCancel); this.Controls.Add(btnOk); @@ -126,5 +138,6 @@ private void InitializeComponent() private CheckBox chkAlwaysAdmin; private CheckBox chkSync; private CheckBox chkShowPath; + private CheckBox chkAutostart; } } \ No newline at end of file diff --git a/VSLauncherX/Forms/dlgSettings.cs b/VSLauncherX/Forms/dlgSettings.cs index 28574bb..a3ba402 100644 --- a/VSLauncherX/Forms/dlgSettings.cs +++ b/VSLauncherX/Forms/dlgSettings.cs @@ -23,6 +23,7 @@ public dlgSettings() private void btnOk_Click(object sender, EventArgs e) { Properties.Settings.Default.AlwaysAdmin = chkAlwaysAdmin.Checked; + Properties.Settings.Default.AutoStart = chkAutostart.Checked; Properties.Settings.Default.SynchronizeVS = chkSync.Checked; Properties.Settings.Default.ShowPathForSolutions = chkShowPath.Checked; Properties.Settings.Default.Save(); @@ -30,6 +31,7 @@ private void btnOk_Click(object sender, EventArgs e) private void dlgSettings_Load(object sender, EventArgs e) { + chkAutostart.Checked = Properties.Settings.Default.AutoStart; chkAlwaysAdmin.Checked = Properties.Settings.Default.AlwaysAdmin; chkSync.Checked = Properties.Settings.Default.SynchronizeVS; chkShowPath.Checked = Properties.Settings.Default.ShowPathForSolutions; diff --git a/VSLauncherX/Forms/dlgSettings.resx b/VSLauncherX/Forms/dlgSettings.resx index 2d622b3..f5307a3 100644 --- a/VSLauncherX/Forms/dlgSettings.resx +++ b/VSLauncherX/Forms/dlgSettings.resx @@ -18,7 +18,7 @@ System.Resources.ResXResourceReader, System.Windows.Forms, ... System.Resources.ResXResourceWriter, System.Windows.Forms, ... this is my long stringthis is a comment - Blue + Blue [base64 mime encoded serialized .NET Framework object] diff --git a/VSLauncherX/Helpers/AdminInfo.cs b/VSLauncherX/Helpers/AdminInfo.cs index 533a2f5..85b69e5 100644 --- a/VSLauncherX/Helpers/AdminInfo.cs +++ b/VSLauncherX/Helpers/AdminInfo.cs @@ -1,22 +1,55 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; using System.Linq; +using System.Runtime.InteropServices; using System.Security.Principal; using System.Text; using System.Threading.Tasks; +using Newtonsoft.Json.Linq; +using VSLauncher.Helpers.Tokens; +using VSLauncherX.Helpers; + namespace VSLauncher.Helpers { - internal class AdminInfo + internal class AdminInfo { + private static WindowsIdentity _currentUserIdentity; + + private static WindowsIdentity CachedOwner => _currentUserIdentity ?? (_currentUserIdentity = Owner); + + public static WindowsIdentity Owner => WindowsIdentity.GetCurrent(); + + /// + /// Is the current user admin. + /// + /// A bool. public static bool IsCurrentUserAdmin() { - return UACHelper.UACHelper.IsAdministrator; + return SecurityHelper.IsAdministrator() | SecurityHelper.IsMemberOfLocalAdmins(); } + /// + /// Is the app elevated. + /// + /// A bool. internal static bool IsElevated() { - return UACHelper.UACHelper.IsElevated; + IntegrityLevel currentIntegrity = (IntegrityLevel)SecurityHelper.GetCurrentIntegrityLevel(); + + try + { + TokenProvider tp = TokenProvider.CreateFromCurrentProcessToken(); + var et = tp.GetTokenElevationType(); + + return et == TokenProvider.TokenElevationType.Full; + } + catch + { + return currentIntegrity > IntegrityLevel.Medium; + } } } } diff --git a/VSLauncherX/Helpers/ItemLauncher.cs b/VSLauncherX/Helpers/ItemLauncher.cs new file mode 100644 index 0000000..b1e8010 --- /dev/null +++ b/VSLauncherX/Helpers/ItemLauncher.cs @@ -0,0 +1,140 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; + +using Newtonsoft.Json; + +using VSLauncher.DataModel; + +namespace VSLauncher.Helpers +{ + /// + /// The item launcher. + /// + public class ItemLauncher + { + /// + /// Initializes a new instance of the class. + /// + /// The item. + /// The target. + public ItemLauncher(VsItem item, VisualStudioInstance target) + { + Target = target; + SingleItem = item; + } + + /// + /// Initializes a new instance of the class. + /// + /// The item. + /// The target. + public ItemLauncher(VsFolder item, VisualStudioInstance target) + { + Solution = item; + Target = target; + } + + /// + /// Gets the launch item. + /// + public VsFolder? Solution { get; } + + /// + /// Gets the single item. + /// + public VsItem? SingleItem { get; } + + /// + /// Gets the target. + /// + public VisualStudioInstance Target { get; } + + /// + /// Gets the last exception. + /// + public Exception LastException { get; private set; } + + /// + /// Launches the. + /// + /// If true, b force admin. + public Task Launch(bool bForceAdmin = false) + { + // Execute the item, run the before and after items if set, elevate to admin if required + return Task.Run(() => + { + string s = CreateLaunchInfoString(); + + Debug.WriteLine(s); + if (Solution is null && SingleItem is null) + { + throw new NullReferenceException(); + } + + bool bRequireAdmin = bForceAdmin | (Solution is null ? SingleItem!.RunAsAdmin : Solution.RunAsAdmin); + var psi = new ProcessStartInfo + { + FileName = GetLauncherPath(), + Arguments = s, + ErrorDialog = false, + UseShellExecute = bRequireAdmin, + Verb = bRequireAdmin ? "runas" : "run", + WindowStyle = ProcessWindowStyle.Hidden, + }; + + try + { + var p = Process.Start(psi); + } + catch (Exception ex) + { + LastException = ex; + Debug.WriteLine(ex.ToString()); + } + }); + } + + /// + /// Gets the launcher path. + /// + /// A string. + public string GetLauncherPath() + { + string env = Environment.CurrentDirectory; + + // get working directory of current assembly + string current = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? env; + + return Path.Combine(current, "BackgroundLaunch.exe"); + } + + /// + /// Creates the launch info string. + /// + /// A string. + public string CreateLaunchInfoString() + { + var li = new LaunchInfo() + { + Solution = Solution ?? new VsFolder(SingleItem ?? throw new NullReferenceException()), + Target = Target.Location + }; + + JsonSerializerSettings settings = new JsonSerializerSettings() + { + Formatting = Formatting.None, + }; + + string s = JsonConvert.SerializeObject(li, settings); + + s = s.Replace('\"', '«').Replace(' ', '»'); + + return s; + } + } +} diff --git a/VSLauncherX/Helpers/Native/NtDllApi.cs b/VSLauncherX/Helpers/Native/NtDllApi.cs new file mode 100644 index 0000000..659ed56 --- /dev/null +++ b/VSLauncherX/Helpers/Native/NtDllApi.cs @@ -0,0 +1,64 @@ +using System; +using System.Runtime.InteropServices; + +namespace VSLauncher.Helpers.Native +{ + internal static class NtDllApi + { + internal class NativeMethods + { + [DllImport("ntdll.dll", SetLastError = true)] + public static extern int NtSetInformationProcess(IntPtr hProcess, PROCESS_INFORMATION_CLASS processInformationClass, ref PROCESS_ACCESS_TOKEN processInformation, int processInformationLength); + + [DllImport("ntdll.dll", SetLastError = true)] + public static extern int NtQueryInformationProcess(IntPtr processHandle, int processInformationClass, ref PROCESS_BASIC_INFORMATION processInformation, int processInformationLength, out int returnLength); + } + + public enum PROCESS_INFORMATION_CLASS + { + ProcessBasicInformation, + ProcessQuotaLimits, + ProcessIoCounters, + ProcessVmCounters, + ProcessTimes, + ProcessBasePriority, + ProcessRaisePriority, + ProcessDebugPort, + ProcessExceptionPort, + ProcessAccessToken, + ProcessLdtInformation, + ProcessLdtSize, + ProcessDefaultHardErrorMode, + ProcessIoPortHandlers, + ProcessPooledUsageAndLimits, + ProcessWorkingSetWatch, + ProcessUserModeIOPL, + ProcessEnableAlignmentFaultFixup, + ProcessPriorityClass, + ProcessWx86Information, + ProcessHandleCount, + ProcessAffinityMask, + ProcessPriorityBoost, + MaxProcessInfoClass + } + + internal struct PROCESS_BASIC_INFORMATION + { + public uint /*NtStatus*/ ExitStatus; + public IntPtr PebBaseAddress; + public UIntPtr AffinityMask; + public int BasePriority; + public UIntPtr UniqueProcessId; + public UIntPtr InheritedFromUniqueProcessId; + } + + + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_ACCESS_TOKEN + { + public IntPtr Token; + public IntPtr Thread; + } + + } +} diff --git a/VSLauncherX/Helpers/Native/ProcessApi.cs b/VSLauncherX/Helpers/Native/ProcessApi.cs new file mode 100644 index 0000000..41ffe41 --- /dev/null +++ b/VSLauncherX/Helpers/Native/ProcessApi.cs @@ -0,0 +1,237 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security; +using System.Security.Permissions; + +namespace VSLauncher.Helpers.Native +{ + /// + /// PInvoke signatures for win32 process api + /// + public static class ProcessApi + { + internal const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000; + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct STARTUPINFOEX + { + public STARTUPINFO StartupInfo; + public IntPtr lpAttributeList; + } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + internal struct STARTUPINFO + { + 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; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + [StructLayout(LayoutKind.Sequential)] + internal struct SECURITY_ATTRIBUTES + { + public int nLength; + public IntPtr lpSecurityDescriptor; + public int bInheritHandle; + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool InitializeProcThreadAttributeList( + IntPtr lpAttributeList, int dwAttributeCount, int dwFlags, ref IntPtr lpSize); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool UpdateProcThreadAttribute( + IntPtr lpAttributeList, uint dwFlags, IntPtr attribute, IntPtr lpValue, + IntPtr cbSize, IntPtr lpPreviousValue, IntPtr lpReturnSize); + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CreateProcess( + string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, CreateProcessFlags dwCreationFlags, + IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFOEX lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [Flags] + public enum CreateProcessFlags : uint + { + DEBUG_PROCESS = 0x00000001, + DEBUG_ONLY_THIS_PROCESS = 0x00000002, + CREATE_SUSPENDED = 0x00000004, + DETACHED_PROCESS = 0x00000008, + CREATE_NEW_CONSOLE = 0x00000010, + NORMAL_PRIORITY_CLASS = 0x00000020, + IDLE_PRIORITY_CLASS = 0x00000040, + HIGH_PRIORITY_CLASS = 0x00000080, + REALTIME_PRIORITY_CLASS = 0x00000100, + CREATE_NEW_PROCESS_GROUP = 0x00000200, + CREATE_UNICODE_ENVIRONMENT = 0x00000400, + CREATE_SEPARATE_WOW_VDM = 0x00000800, + CREATE_SHARED_WOW_VDM = 0x00001000, + CREATE_FORCEDOS = 0x00002000, + BELOW_NORMAL_PRIORITY_CLASS = 0x00004000, + ABOVE_NORMAL_PRIORITY_CLASS = 0x00008000, + INHERIT_PARENT_AFFINITY = 0x00010000, + INHERIT_CALLER_PRIORITY = 0x00020000, + CREATE_PROTECTED_PROCESS = 0x00040000, + EXTENDED_STARTUPINFO_PRESENT = 0x00080000, + PROCESS_MODE_BACKGROUND_BEGIN = 0x00100000, + PROCESS_MODE_BACKGROUND_END = 0x00200000, + CREATE_BREAKAWAY_FROM_JOB = 0x01000000, + CREATE_PRESERVE_CODE_AUTHZ_LEVEL = 0x02000000, + CREATE_DEFAULT_ERROR_MODE = 0x04000000, + CREATE_NO_WINDOW = 0x08000000, + PROFILE_USER = 0x10000000, + PROFILE_KERNEL = 0x20000000, + PROFILE_SERVER = 0x40000000, + CREATE_IGNORE_SYSTEM_DEFAULT = 0x80000000, + } + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool DeleteProcThreadAttributeList(IntPtr lpAttributeList); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool CloseHandle(IntPtr hObject); + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool GetExitCodeProcess(SafeProcessHandle processHandle, out int exitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool GetNamedPipeClientProcessId(IntPtr Pipe, out uint ClientProcessId); + + #region PID traversing + internal const int ERROR_NO_MORE_FILES = 0x12; + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern SafeSnapshotHandle CreateToolhelp32Snapshot(SnapshotFlags flags, uint id); + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool Process32First(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool Process32Next(SafeSnapshotHandle hSnapshot, ref PROCESSENTRY32 lppe); + + [Flags] + internal enum SnapshotFlags : uint + { + HeapList = 0x00000001, + Process = 0x00000002, + Thread = 0x00000004, + Module = 0x00000008, + Module32 = 0x00000010, + All = HeapList | Process | Thread | Module, + Inherit = 0x80000000, + NoHeaps = 0x40000000 + } + [StructLayout(LayoutKind.Sequential)] + internal struct PROCESSENTRY32 + { + public uint dwSize; + public uint cntUsage; + public uint th32ProcessID; + public IntPtr th32DefaultHeapID; + public uint th32ModuleID; + public uint cntThreads; + public uint th32ParentProcessID; + public int pcPriClassBase; + public uint dwFlags; + [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] public string szExeFile; + }; + + [SuppressUnmanagedCodeSecurity] + internal sealed class SafeSnapshotHandle : SafeHandleMinusOneIsInvalid + { + internal SafeSnapshotHandle() : base(true) + { + } + + [SecurityPermission(SecurityAction.LinkDemand, UnmanagedCode = true)] + internal SafeSnapshotHandle(IntPtr handle) : base(true) + { + SetHandle(handle); + } + + protected override bool ReleaseHandle() + { + return CloseHandle(handle); + } + + [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = true)] + private static extern bool CloseHandle(IntPtr handle); + } + #endregion + + #region Query Process Info + public const uint PROCESS_QUERY_INFORMATION = 0x0400; + public const uint PROCESS_SET_INFORMATION = 0x0200; + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern bool OpenProcessToken(IntPtr hProcess, uint dwDesiredAccess, out IntPtr hToken); + #endregion + + /// Checks whether a process is being debugged. + /// + /// The "remote" in CheckRemoteDebuggerPresent does not imply that the debugger + /// necessarily resides on a different computer; instead, it indicates that the + /// debugger resides in a separate and parallel process. + /// + /// Use the IsDebuggerPresent function to detect whether the calling process + /// is running under the debugger. + /// + [DllImport("Kernel32.dll", SetLastError = true, ExactSpelling = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CheckRemoteDebuggerPresent( + SafeHandle hProcess, + [MarshalAs(UnmanagedType.Bool)] ref bool isDebuggerPresent); + + [DllImport("kernel32.dll", CharSet = CharSet.Ansi, SetLastError = true)] + internal static extern IntPtr GetCurrentProcess(); + + [DllImport("kernel32.dll")] + internal static extern int GetCurrentProcessId(); + + + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern bool CreatePipe(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, SECURITY_ATTRIBUTES lpPipeAttributes, int nSize); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern uint ResumeThread(IntPtr hThread); + + [DllImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool TerminateProcess(IntPtr hProcess, uint uExitCode); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern uint WaitForSingleObject(IntPtr hHandle, uint dwMilliseconds); + } +} diff --git a/VSLauncherX/Helpers/Native/SafeTokenHandle.cs b/VSLauncherX/Helpers/Native/SafeTokenHandle.cs new file mode 100644 index 0000000..2998b18 --- /dev/null +++ b/VSLauncherX/Helpers/Native/SafeTokenHandle.cs @@ -0,0 +1,24 @@ +using Microsoft.Win32.SafeHandles; +using System; + +namespace VSLauncher.Helpers.Native +{ + internal class SafeTokenHandle : SafeHandleZeroOrMinusOneIsInvalid + { + internal SafeTokenHandle(IntPtr handle) + : base(true) + { + SetHandle(handle); + } + + private SafeTokenHandle() + : base(true) + { + } + + protected override bool ReleaseHandle() + { + return ProcessApi.CloseHandle(handle); + } + } +} diff --git a/VSLauncherX/Helpers/Native/TokensApi.cs b/VSLauncherX/Helpers/Native/TokensApi.cs new file mode 100644 index 0000000..800b78e --- /dev/null +++ b/VSLauncherX/Helpers/Native/TokensApi.cs @@ -0,0 +1,420 @@ +using System; +using System.Runtime.InteropServices; +using static VSLauncher.Helpers.Native.ProcessApi; + +namespace VSLauncher.Helpers.Native +{ + internal static class TokensApi + { + public enum LogonFlags + { + /// + /// Log on, then load the user's profile in the HKEY_USERS registry key. The function + /// returns after the profile has been loaded. Loading the profile can be time-consuming, + /// so it is best to use this value only if you must access the information in the + /// HKEY_CURRENT_USER registry key. + /// NOTE: Windows Server 2003: The profile is unloaded after the new process has been + /// terminated, regardless of whether it has created child processes. + /// + /// See LOGON_WITH_PROFILE + WithProfile = 1, + /// + /// Log on, but use the specified credentials on the network only. The new process uses the + /// same token as the caller, but the system creates a new logon session within LSA, and + /// the process uses the specified credentials as the default credentials. + /// This value can be used to create a process that uses a different set of credentials + /// locally than it does remotely. This is useful in inter-domain scenarios where there is + /// no trust relationship. + /// The system does not validate the specified credentials. Therefore, the process can start, + /// but it may not have access to network resources. + /// + /// See LOGON_NETCREDENTIALS_ONLY + NetCredentialsOnly + } + + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + + internal const int ERROR_NOT_ALL_ASSIGNED = 1300; + + internal const uint STANDARD_RIGHTS_REQUIRED = 0x000F0000; + internal const uint STANDARD_RIGHTS_READ = 0x00020000; + internal const uint TOKEN_ASSIGN_PRIMARY = 0x0001; + internal const uint TOKEN_DUPLICATE = 0x0002; + internal const uint TOKEN_IMPERSONATE = 0x0004; + internal const uint TOKEN_QUERY = 0x0008; + internal const uint TOKEN_QUERY_SOURCE = 0x0010; + internal const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; + internal const uint TOKEN_ADJUST_GROUPS = 0x0040; + internal const uint TOKEN_ADJUST_DEFAULT = 0x0080; + internal const uint TOKEN_ADJUST_SESSIONID = 0x0100; + internal const uint TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY; + internal const uint TOKEN_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_IMPERSONATE | + TOKEN_QUERY | + TOKEN_QUERY_SOURCE | + TOKEN_ADJUST_PRIVILEGES | + TOKEN_ADJUST_GROUPS | + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID; + + [DllImport("advapi32", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CreateProcessWithTokenW(IntPtr hToken, LogonFlags dwLogonFlags, string lpApplicationName, string lpCommandLine, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, [In] ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); + + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous = 0, + SecurityIdentification = 1, + SecurityImpersonation = 2, + SecurityDelegation = 3 + } + + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + public const uint SE_GROUP_INTEGRITY = 0x00000020; + + [DllImport("advapi32", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public extern static bool DuplicateTokenEx( + IntPtr hExistingToken, + uint dwDesiredAccess, + IntPtr lpTokenAttributes, + SECURITY_IMPERSONATION_LEVEL ImpersonationLevel, + TOKEN_TYPE TokenType, + out SafeTokenHandle phNewToken); + + /* + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool CreateProcessWithLogonW( + String userName, + String domain, + String password, + LogonFlags logonFlags, + String applicationName, + String commandLine, + CreateProcessFlags creationFlags, + UInt32 environment, + String currentDirectory, + ref STARTUPINFO startupInfo, + out PROCESS_INFORMATION processInformation); + */ + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool CreateProcessAsUser( + SafeTokenHandle hToken, + string applicationName, + string commandLine, + IntPtr pProcessAttributes, + IntPtr pThreadAttributes, + bool bInheritHandles, + uint dwCreationFlags, + IntPtr pEnvironment, + string currentDirectory, + ref STARTUPINFO startupInfo, + out PROCESS_INFORMATION processInformation); + + /* + [DllImport("advapi32.dll", SetLastError = true)] + public static extern Boolean CreateProcessAsUserW(IntPtr hToken, string lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, Boolean bInheritHandles, CreateProcessFlags dwCreationFlags, IntPtr lpEnvironment, IntPtr lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInfo); + */ + + // Integrity Levels + public enum TOKEN_INFORMATION_CLASS + { + /// + /// The buffer receives a TOKEN_USER structure that contains the user account of the token. + /// + TokenUser = 1, + + /// + /// The buffer receives a TOKEN_GROUPS structure that contains the group accounts associated with the token. + /// + TokenGroups, + + /// + /// The buffer receives a TOKEN_PRIVILEGES structure that contains the privileges of the token. + /// + TokenPrivileges, + + /// + /// The buffer receives a TOKEN_OWNER structure that contains the default owner security identifier (SID) for newly created objects. + /// + TokenOwner, + + /// + /// The buffer receives a TOKEN_PRIMARY_GROUP structure that contains the default primary group SID for newly created objects. + /// + TokenPrimaryGroup, + + /// + /// The buffer receives a TOKEN_DEFAULT_DACL structure that contains the default DACL for newly created objects. + /// + TokenDefaultDacl, + + /// + /// The buffer receives a TOKEN_SOURCE structure that contains the source of the token. TOKEN_QUERY_SOURCE access is needed to retrieve this information. + /// + TokenSource, + + /// + /// The buffer receives a TOKEN_TYPE value that indicates whether the token is a primary or impersonation token. + /// + TokenType, + + /// + /// The buffer receives a SECURITY_IMPERSONATION_LEVEL value that indicates the impersonation level of the token. If the access token is not an impersonation token, the function fails. + /// + TokenImpersonationLevel, + + /// + /// The buffer receives a TOKEN_STATISTICS structure that contains various token statistics. + /// + TokenStatistics, + + /// + /// The buffer receives a TOKEN_GROUPS structure that contains the list of restricting SIDs in a restricted token. + /// + TokenRestrictedSids, + + /// + /// The buffer receives a DWORD value that indicates the Terminal Services session identifier that is associated with the token. + /// + TokenSessionId, + + /// + /// The buffer receives a TOKEN_GROUPS_AND_PRIVILEGES structure that contains the user SID, the group accounts, the restricted SIDs, and the authentication ID associated with the token. + /// + TokenGroupsAndPrivileges, + + /// + /// Reserved. + /// + TokenSessionReference, + + /// + /// The buffer receives a DWORD value that is nonzero if the token includes the SANDBOX_INERT flag. + /// + TokenSandBoxInert, + + /// + /// Reserved. + /// + TokenAuditPolicy, + + /// + /// The buffer receives a TOKEN_ORIGIN value. + /// + TokenOrigin, + + /// + /// The buffer receives a TOKEN_ELEVATION_TYPE value that specifies the elevation level of the token. + /// + TokenElevationType, + + /// + /// The buffer receives a TOKEN_LINKED_TOKEN structure that contains a handle to another token that is linked to this token. + /// + TokenLinkedToken, + + /// + /// The buffer receives a TOKEN_ELEVATION structure that specifies whether the token is elevated. + /// + TokenElevation, + + /// + /// The buffer receives a DWORD value that is nonzero if the token has ever been filtered. + /// + TokenHasRestrictions, + + /// + /// The buffer receives a TOKEN_ACCESS_INFORMATION structure that specifies security information contained in the token. + /// + TokenAccessInformation, + + /// + /// The buffer receives a DWORD value that is nonzero if virtualization is allowed for the token. + /// + TokenVirtualizationAllowed, + + /// + /// The buffer receives a DWORD value that is nonzero if virtualization is enabled for the token. + /// + TokenVirtualizationEnabled, + + /// + /// The buffer receives a TOKEN_MANDATORY_LABEL structure that specifies the token's integrity level. + /// + TokenIntegrityLevel, + + /// + /// The buffer receives a DWORD value that is nonzero if the token has the UIAccess flag set. + /// + TokenUIAccess, + + /// + /// The buffer receives a TOKEN_MANDATORY_POLICY structure that specifies the token's mandatory integrity policy. + /// + TokenMandatoryPolicy, + + /// + /// The buffer receives the token's logon security identifier (SID). + /// + TokenLogonSid, + + /// + /// The maximum value for this enumeration + /// + MaxTokenInfoClass + } + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool GetTokenInformation( + IntPtr TokenHandle, + TOKEN_INFORMATION_CLASS TokenInformationClass, + IntPtr TokenInformation, + int TokenInformationLength, + out int ReturnLength); + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_MANDATORY_LABEL + { + public SID_AND_ATTRIBUTES Label; + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_LINKED_TOKEN + { + public IntPtr LinkedToken; + } + + [StructLayout(LayoutKind.Sequential)] + public struct SID_AND_ATTRIBUTES + { + public IntPtr Sid; + public uint Attributes; + } + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern IntPtr GetSidSubAuthority(IntPtr pSid, int nSubAuthority); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern IntPtr GetSidSubAuthorityCount(IntPtr pSid); + + [DllImport("advapi32.dll", SetLastError = true)] + public static extern bool SetTokenInformation(IntPtr TokenHandle, TOKEN_INFORMATION_CLASS TokenInformationClass, + IntPtr TokenInformation, uint TokenInformationLength); + + [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool ConvertStringSidToSid( + string StringSid, + out IntPtr ptrSid + ); + + [DllImport("advapi32.dll")] + public static extern int GetLengthSid(IntPtr pSid); + + #region Safer + + [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)] + public static extern bool SaferCreateLevel( + SaferScopes dwScopeId, + SaferLevels dwLevelId, + int OpenFlags, + out IntPtr pLevelHandle, + IntPtr lpReserved); + + [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)] + public static extern bool SaferCloseLevel( + IntPtr pLevelHandle); + + [DllImport("advapi32", SetLastError = true, CallingConvention = CallingConvention.StdCall)] + public static extern bool SaferComputeTokenFromLevel( + IntPtr levelHandle, + IntPtr inAccessToken, + out SafeTokenHandle outAccessToken, + SaferComputeTokenFlags dwFlags, + IntPtr lpReserved + ); + + [Flags] + public enum SaferLevels : uint + { + Disallowed = 0, + Untrusted = 0x1000, + Constrained = 0x10000, + NormalUser = 0x20000, + FullyTrusted = 0x40000 + } + + [Flags] + public enum SaferComputeTokenFlags : uint + { + None = 0x0, + NullIfEqual = 0x1, + CompareOnly = 0x2, + MakeIntert = 0x4, + WantFlags = 0x8 + } + + [Flags] + public enum SaferScopes : uint + { + Machine = 1, + User = 2 + } + #endregion + + [DllImport("advapi32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CreateRestrictedToken( + SafeTokenHandle ExistingTokenHandle, + uint Flags, + uint DisableSidCount, + IntPtr SidsToDisable, + uint DeletePrivilegeCount, + IntPtr PrivilegesToDelete, + uint RestrictedSidCount, + IntPtr SidsToRestrict, + out SafeTokenHandle NewTokenHandle + ); + + #region AdjustToken + /* + const int ANYSIZE_ARRAY = 1; + public struct TOKEN_PRIVILEGES + { + public int PrivilegeCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = ANYSIZE_ARRAY)] + public LUID_AND_ATTRIBUTES[] Privileges; + } + + [StructLayout(LayoutKind.Sequential, Pack = 4)] + public struct LUID_AND_ATTRIBUTES + { + public LUID Luid; + public UInt32 Attributes; + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + public UInt32 LowPart; + public Int32 HighPart; + } + + [DllImport("advapi32.dll")] + public static extern bool LookupPrivilegeValue(string lpSystemName, string lpName, ref LUID lpLuid); + */ + #endregion + + [DllImport("Advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)] + internal static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(string StringSecurityDescriptor, uint StringSDRevision, out IntPtr SecurityDescriptor, out UIntPtr SecurityDescriptorSize); + + } +} diff --git a/VSLauncherX/Helpers/ProcessHelper.cs b/VSLauncherX/Helpers/ProcessHelper.cs new file mode 100644 index 0000000..1006cbc --- /dev/null +++ b/VSLauncherX/Helpers/ProcessHelper.cs @@ -0,0 +1,141 @@ +using static VSLauncher.Helpers.Native.ProcessApi; +using Microsoft.Win32.SafeHandles; +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Principal; +using System.Threading; +using VSLauncher.Helpers.Native; + +namespace VSLauncherX.Helpers +{ + /// + /// Borrowed from gsudo + /// + internal static class ProcessHelper + { + private static string _cacheOwnExeName; + + public static WindowsIdentity GetProcessUser(this Process process) + { + IntPtr processHandle = IntPtr.Zero; + try + { + OpenProcessToken(process.Handle, 8, out processHandle); + WindowsIdentity wi = new WindowsIdentity(processHandle); + return wi; + } + catch + { + return null; + } + finally + { + if (processHandle != IntPtr.Zero) + { + CloseHandle(processHandle); + } + } + } + + static internal int GetProcessIntegrityLevel(IntPtr processHandle) + { + /* + * https://docs.microsoft.com/en-us/previous-versions/dotnet/articles/bb625963(v=msdn.10)?redirectedfrom=MSDN + * https://support.microsoft.com/en-us/help/243330/well-known-security-identifiers-in-windows-operating-systems + S-1-16-0 Untrusted Mandatory Level An untrusted integrity level. + S-1-16-4096 Low Mandatory Level A low integrity level. + S-1-16-8192 Medium Mandatory Level A medium integrity level. + S-1-16-8448 Medium Plus Mandatory Level A medium plus integrity level. + S-1-16-12288 High Mandatory Level A high integrity level. + S-1-16-16384 System Mandatory Level A system integrity level. + S-1-16-20480 Protected Process Mandatory Level A protected-process integrity level. + S-1-16-28672 Secure Process Mandatory Level A secure process integrity level. + */ + int IL = -1; + //SafeWaitHandle hToken = null; + IntPtr hToken = IntPtr.Zero; + + int cbTokenIL = 0; + IntPtr pTokenIL = IntPtr.Zero; + + try + { + // Open the access token of the current process with TOKEN_QUERY. + if (!OpenProcessToken(processHandle, + TokensApi.TOKEN_QUERY, out hToken)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Then we must query the size of the integrity level information + // associated with the token. Note that we expect GetTokenInformation + // to return false with the ERROR_INSUFFICIENT_BUFFER error code + // because we've given it a null buffer. On exit cbTokenIL will tell + // the size of the group information. + if (!TokensApi.GetTokenInformation(hToken, + TokensApi.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, IntPtr.Zero, 0, + out cbTokenIL)) + { + int error = Marshal.GetLastWin32Error(); + const int ERROR_INSUFFICIENT_BUFFER = 0x7a; + if (error != ERROR_INSUFFICIENT_BUFFER) + { + // When the process is run on operating systems prior to + // Windows Vista, GetTokenInformation returns false with the + // ERROR_INVALID_PARAMETER error code because + // TokenIntegrityLevel is not supported on those OS's. + throw new Win32Exception(error); + } + } + + // Now we allocate a buffer for the integrity level information. + pTokenIL = Marshal.AllocHGlobal(cbTokenIL); + if (pTokenIL == IntPtr.Zero) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Now we ask for the integrity level information again. This may fail + // if an administrator has added this account to an additional group + // between our first call to GetTokenInformation and this one. + if (!TokensApi.GetTokenInformation(hToken, + TokensApi.TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, pTokenIL, cbTokenIL, + out cbTokenIL)) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + // Marshal the TOKEN_MANDATORY_LABEL struct from native to .NET object. + TokensApi.TOKEN_MANDATORY_LABEL tokenIL = (TokensApi.TOKEN_MANDATORY_LABEL) + Marshal.PtrToStructure(pTokenIL, typeof(TokensApi.TOKEN_MANDATORY_LABEL)); + + IntPtr pIL = TokensApi.GetSidSubAuthority(tokenIL.Label.Sid, 0); + IL = Marshal.ReadInt32(pIL); + } + finally + { + // Centralized cleanup for all allocated resources. Clean up only + // those which were allocated, and clean them up in the right order. + + if (hToken != IntPtr.Zero) + { + CloseHandle(hToken); + // Marshal.FreeHGlobal(hToken); + // hToken.Close(); + // hToken = null; + } + + if (pTokenIL != IntPtr.Zero) + { + Marshal.FreeHGlobal(pTokenIL); + pTokenIL = IntPtr.Zero; + cbTokenIL = 0; + } + } + + return IL; + } + } +} diff --git a/VSLauncherX/Helpers/SecurityHelper.cs b/VSLauncherX/Helpers/SecurityHelper.cs new file mode 100644 index 0000000..12ac371 --- /dev/null +++ b/VSLauncherX/Helpers/SecurityHelper.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Security.Principal; +using VSLauncher.Helpers.Tokens; +using VSLauncher.Helpers.Native; + +namespace VSLauncherX.Helpers +{ + /// + /// Borrowed from gsudo + /// + internal static class SecurityHelper + { + public static bool IsMemberOfLocalAdmins() + { + var principal = new WindowsPrincipal(WindowsIdentity.GetCurrent()); + var claims = principal.Claims; + return claims.Any(c => c.Value == "S-1-5-32-544"); + } + + private static int? _cacheGetCurrentIntegrityLevelCache; + public static bool IsHighIntegrity() + { + return GetCurrentIntegrityLevel() >= (int)IntegrityLevel.High; + } + + /// + /// The function gets the integrity level of the current process. + /// + /// + /// Returns the integrity level of the current process. It is usually one of + /// these values: + /// + /// SECURITY_MANDATORY_UNTRUSTED_RID - means untrusted level + /// SECURITY_MANDATORY_LOW_RID - means low integrity level. + /// SECURITY_MANDATORY_MEDIUM_RID - means medium integrity level. + /// SECURITY_MANDATORY_HIGH_RID - means high integrity level. + /// SECURITY_MANDATORY_SYSTEM_RID - means system integrity level. + /// + /// + /// + /// When any native Windows API call fails, the function throws a Win32Exception + /// with the last error code. + /// + static internal int GetCurrentIntegrityLevel() + { + if (_cacheGetCurrentIntegrityLevelCache.HasValue) + { + return _cacheGetCurrentIntegrityLevelCache.Value; + } + + _cacheGetCurrentIntegrityLevelCache = ProcessHelper.GetProcessIntegrityLevel(ProcessApi.GetCurrentProcess()); + + return _cacheGetCurrentIntegrityLevelCache.Value; + } + + private static bool? _cacheIsAdmin; + + public static bool IsAdministrator() + { + if (_cacheIsAdmin.HasValue) return _cacheIsAdmin.Value; + + try + { + WindowsIdentity identity = WindowsIdentity.GetCurrent(); + WindowsPrincipal principal = new WindowsPrincipal(identity); + _cacheIsAdmin = principal.IsInRole(WindowsBuiltInRole.Administrator); + + return _cacheIsAdmin.Value; + } + catch (Exception) + { + return false; + } + } + } +} diff --git a/VSLauncherX/Helpers/Tokens/IntegrityLevel.cs b/VSLauncherX/Helpers/Tokens/IntegrityLevel.cs new file mode 100644 index 0000000..30b4978 --- /dev/null +++ b/VSLauncherX/Helpers/Tokens/IntegrityLevel.cs @@ -0,0 +1,31 @@ +using static VSLauncher.Helpers.Native.TokensApi; + +namespace VSLauncher.Helpers.Tokens +{ + public enum IntegrityLevel + { + Untrusted = 0, + Low = 4096, + Medium = 8192, + MediumRestricted = 8193, // Experimental, like medium but without the split token. It can call RunAs but won't elevate. + MediumPlus = 8448, + High = 12288, + System = 16384, + Protected = 20480, + Secure = 28672 + } + + static class IntegrityLevelExtensions + { + public static SaferLevels ToSaferLevel(this IntegrityLevel integrityLevel) + { + if (integrityLevel >= IntegrityLevel.High) + return SaferLevels.FullyTrusted; + if (integrityLevel >= IntegrityLevel.Medium) + return SaferLevels.NormalUser; + if (integrityLevel >= IntegrityLevel.Low) + return SaferLevels.Constrained; + return SaferLevels.Untrusted; + } + } +} \ No newline at end of file diff --git a/VSLauncherX/Helpers/Tokens/NativeMethods.cs b/VSLauncherX/Helpers/Tokens/NativeMethods.cs new file mode 100644 index 0000000..6239e24 --- /dev/null +++ b/VSLauncherX/Helpers/Tokens/NativeMethods.cs @@ -0,0 +1,104 @@ +using System; +using System.Runtime.ConstrainedExecution; +using System.Runtime.InteropServices; +using System.Security.Principal; +using static VSLauncher.Helpers.Native.TokensApi; +using VSLauncher.Helpers.Native; + +namespace VSLauncher.Helpers.Tokens +{ + internal static partial class NativeMethods + { + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool LookupPrivilegeValue(string lpsystemname, string lpname, [MarshalAs(UnmanagedType.Struct)] ref LUID lpLuid); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool AdjustTokenPrivileges(IntPtr TokenHandle, bool DisableAllPrivileges, ref TOKEN_PRIVILEGES NewState, uint Bufferlength, IntPtr PreviousState, IntPtr ReturnLength); + + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int SE_PRIVILEGE_DISABLED = 0x00000000; + internal const int ERROR_NOT_ALL_ASSIGNED = 0x00000514; + + internal const uint STANDARD_RIGHTS_REQUIRED = 0x000F0000; + internal const uint STANDARD_RIGHTS_READ = 0x00020000; + internal const uint TOKEN_ASSIGN_PRIMARY = 0x0001; + internal const uint TOKEN_DUPLICATE = 0x0002; + internal const uint TOKEN_IMPERSONATE = 0x0004; + internal const uint TOKEN_QUERY = 0x0008; + internal const uint TOKEN_QUERY_SOURCE = 0x0010; + internal const uint TOKEN_ADJUST_PRIVILEGES = 0x0020; + internal const uint TOKEN_ADJUST_GROUPS = 0x0040; + internal const uint TOKEN_ADJUST_DEFAULT = 0x0080; + internal const uint TOKEN_ADJUST_SESSIONID = 0x0100; + internal const uint TOKEN_READ = STANDARD_RIGHTS_READ | TOKEN_QUERY; + internal const uint TOKEN_ALL_ACCESS = STANDARD_RIGHTS_REQUIRED | + TOKEN_ASSIGN_PRIMARY | + TOKEN_DUPLICATE | + TOKEN_IMPERSONATE | + TOKEN_QUERY | + TOKEN_QUERY_SOURCE | + TOKEN_ADJUST_PRIVILEGES | + TOKEN_ADJUST_GROUPS | + TOKEN_ADJUST_DEFAULT | + TOKEN_ADJUST_SESSIONID; + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + internal static extern IntPtr GetCurrentProcess(); + + [DllImport("Advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool OpenProcessToken(IntPtr processHandle, + uint desiredAccesss, + out IntPtr tokenHandle); + + [DllImport("Advapi32.dll", SetLastError = true)] + internal static extern bool OpenThreadToken(IntPtr ThreadToken, TokenAccessLevels DesiredAccess, bool OpenAsSelf, out SafeTokenHandle TokenHandle); + + [DllImport("kernel32.dll", SetLastError = true)] + internal static extern IntPtr GetCurrentThread(); + + [DllImport("Advapi32.dll", SetLastError = true)] + public static extern bool SetThreadToken(IntPtr Thread, SafeTokenHandle Token); + + + [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static extern bool CloseHandle(IntPtr hObject); + + [StructLayout(LayoutKind.Sequential)] + public struct LUID + { + private uint lowPart; + private int highPart; + + public uint LowPart { get => lowPart; set => lowPart = value; } + + public int HighPart { get => highPart; set => highPart = value; } + } + + [StructLayout(LayoutKind.Sequential)] + public struct LUID_AND_ATTRIBUTES + { + private LUID luid; + private uint attributes; + + public LUID Luid { get => luid; set => luid = value; } + + public uint Attributes { get => attributes; set => attributes = value; } + } + + [StructLayout(LayoutKind.Sequential)] + public struct TOKEN_PRIVILEGES + { + private uint privilegeCount; + [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1)] + private LUID_AND_ATTRIBUTES[] privileges; + + public uint PrivilegeCount { get => privilegeCount; set => privilegeCount = value; } + + public LUID_AND_ATTRIBUTES[] Privileges { get => privileges; set => privileges = value; } + } + } +} diff --git a/VSLauncherX/Helpers/Tokens/Privilege.cs b/VSLauncherX/Helpers/Tokens/Privilege.cs new file mode 100644 index 0000000..fb6c689 --- /dev/null +++ b/VSLauncherX/Helpers/Tokens/Privilege.cs @@ -0,0 +1,41 @@ +namespace VSLauncher.Helpers.Tokens +{ + public enum Privilege + { + SeAssignPrimaryTokenPrivilege, + SeAuditPrivilege, + SeBackupPrivilege, + SeChangeNotifyPrivilege, + SeCreateGlobalPrivilege, + SeCreatePagefilePrivilege, + SeCreatePermanentPrivilege, + SeCreateSymbolicLinkPrivilege, + SeCreateTokenPrivilege, + SeDebugPrivilege, + SeEnableDelegationPrivilege, + SeImpersonatePrivilege, + SeIncreaseBasePriorityPrivilege, + SeIncreaseQuotaPrivilege, + SeIncreaseWorkingSetPrivilege, + SeLoadDriverPrivilege, + SeLockMemoryPrivilege, + SeMachineAccountPrivilege, + SeManageVolumePrivilege, + SeProfileSingleProcessPrivilege, + SeRelabelPrivilege, + SeRemoteShutdownPrivilege, + SeRestorePrivilege, + SeSecurityPrivilege, + SeShutdownPrivilege, + SeSyncAgentPrivilege, + SeSystemEnvironmentPrivilege, + SeSystemProfilePrivilege, + SeSystemtimePrivilege, + SeTakeOwnershipPrivilege, + SeTcbPrivilege, + SeTimeZonePrivilege, + SeTrustedCredManAccessPrivilege, + SeUndockPrivilege, + SeDelegateSessionUserImpersonatePrivilege, + } +} diff --git a/VSLauncherX/Helpers/Tokens/PrivilegeManager.cs b/VSLauncherX/Helpers/Tokens/PrivilegeManager.cs new file mode 100644 index 0000000..8849671 --- /dev/null +++ b/VSLauncherX/Helpers/Tokens/PrivilegeManager.cs @@ -0,0 +1,75 @@ +using System; +using System.ComponentModel; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Security.Principal; +using static VSLauncher.Helpers.Native.TokensApi; +using static VSLauncher.Helpers.Tokens.NativeMethods; + +namespace VSLauncher.Helpers.Tokens +{ + //Enable a privilege in the current thread by implementing the following line in your code: + //PrivilegeManager.EnablePrivilege(SecurityEntity.SE_SHUTDOWN_NAME); + + public static class PrivilegeManager + { + public static void DisableAllPrivileges(IntPtr tokenHandle) + { + var TOKEN_PRIVILEGES = new TOKEN_PRIVILEGES(); + + if (!AdjustTokenPrivileges(tokenHandle, true, ref TOKEN_PRIVILEGES, 0, IntPtr.Zero, IntPtr.Zero)) + throw new Win32Exception(); + } + + public static void SetPrivilegeState(Privilege securityEntity, bool enabled) + { + const int ERROR_NO_TOKEN = 0x3f0; + + var locallyUniqueIdentifier = new LUID(); + + if (!LookupPrivilegeValue(null, securityEntity.ToString(), ref locallyUniqueIdentifier)) + throw new Win32Exception(); + + if (!OpenThreadToken(GetCurrentThread(), TokenAccessLevels.Query | TokenAccessLevels.AdjustPrivileges, true, out var token)) + { + var error = Marshal.GetLastWin32Error(); + if (error != ERROR_NO_TOKEN) + { + throw new Win32Exception(error); + } + + // No token is on the thread, copy from process + if (!OpenProcessToken(GetCurrentProcess(), (uint)TokenAccessLevels.Duplicate, out var processToken)) + { + throw new Win32Exception(); + } + + if (!DuplicateTokenEx(processToken, (uint)(TokenAccessLevels.Impersonate | TokenAccessLevels.Query | TokenAccessLevels.AdjustPrivileges), + IntPtr.Zero, SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenImpersonation, out token)) + { + throw new Win32Exception(); + } + + if (!SetThreadToken(IntPtr.Zero, token)) + { + throw new Win32Exception(); + } + } + + var tp = new TOKEN_PRIVILEGES(); + tp.PrivilegeCount = 1; + + tp.Privileges = new LUID_AND_ATTRIBUTES[1]; + tp.Privileges[0].Attributes = (uint)(enabled ? NativeMethods.SE_PRIVILEGE_ENABLED : SE_PRIVILEGE_DISABLED); + tp.Privileges[0].Luid = locallyUniqueIdentifier; + + if (!AdjustTokenPrivileges(token.DangerousGetHandle(), false, ref tp, (uint)Marshal.SizeOf(tp), + IntPtr.Zero, IntPtr.Zero)) + throw new Win32Exception(); + + Debug.WriteLine($"Privilege {securityEntity} was {(enabled ? "ENABLED" : "DISABLED")}"); + token.Close(); + // todo: proper close. + } + } +} \ No newline at end of file diff --git a/VSLauncherX/Helpers/Tokens/TokenProvider.cs b/VSLauncherX/Helpers/Tokens/TokenProvider.cs new file mode 100644 index 0000000..c1c1134 --- /dev/null +++ b/VSLauncherX/Helpers/Tokens/TokenProvider.cs @@ -0,0 +1,433 @@ +using System; +using static VSLauncher.Helpers.Native.ProcessApi; +using static VSLauncher.Helpers.Native.TokensApi; +using System.ComponentModel; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Linq; +using System.Security.Principal; +using VSLauncherX.Helpers; +using VSLauncher.Helpers.Native; + +namespace VSLauncher.Helpers.Tokens +{ + internal class TokenProvider : IDisposable + { + public const uint MAXIMUM_ALLOWED = 0x02000000; + + private SafeTokenHandle Token; + public SafeTokenHandle GetToken() => Token; + + private TokenProvider() + { + } + + public static TokenProvider CreateFromSystemAccount() + { + var winlogon = Process.GetProcesses().Where(p => p.ProcessName == "winlogon").FirstOrDefault(); + return CreateFromProcessToken(winlogon.Id); + } + + public static TokenProvider CreateFromProcessToken(int pidWithToken, uint tokenAccess = MAXIMUM_ALLOWED) + { + IntPtr existingProcessHandle = OpenProcess(PROCESS_QUERY_INFORMATION, true, (uint)pidWithToken); + if (existingProcessHandle == IntPtr.Zero) + { + throw new Win32Exception(); + } + + IntPtr existingProcessToken; + try + { + if (!OpenProcessToken(existingProcessHandle, + MAXIMUM_ALLOWED + //| TOKEN_ALL_ACCESS, + //| TOKEN_DUPLICATE + //| TokensApi.TOKEN_ADJUST_DEFAULT | + //| TokensApi.TOKEN_QUERY + //| TokensApi.TOKEN_ASSIGN_PRIMARY + //| TOKEN_QUERY_SOURCE + //| TOKEN_IMPERSONATE + //| TOKEN_READ + //| TOKEN_ALL_ACCESS ==> access denied + //| STANDARD_RIGHTS_REQUIRED ==> access denied. + , + out existingProcessToken)) + { + throw new Win32Exception(); + } + } + finally + { + CloseHandle(existingProcessHandle); + } + + if (existingProcessToken == IntPtr.Zero) return null; + + var sa = new SECURITY_ATTRIBUTES(); + sa.nLength = 0; + uint desiredAccess = MAXIMUM_ALLOWED; + + SafeTokenHandle newToken; + + if (!DuplicateTokenEx(existingProcessToken, desiredAccess, IntPtr.Zero, + SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out newToken)) + { + CloseHandle(existingProcessToken); + throw new Win32Exception(); + } + + CloseHandle(existingProcessToken); + + return new TokenProvider() + { + Token = newToken + }; + } + + public static TokenProvider CreateFromSaferApi(SaferLevels saferLevel) + { + IntPtr hSaferLevel; + if (!SaferCreateLevel(SaferScopes.User, saferLevel, 1, out hSaferLevel, IntPtr.Zero)) + throw new Win32Exception(); + + SafeTokenHandle hToken; + try + { + if (!SaferComputeTokenFromLevel(hSaferLevel, IntPtr.Zero, out hToken, + SaferComputeTokenFlags.None, IntPtr.Zero)) + throw new Win32Exception(); + } + finally + { + SaferCloseLevel(hSaferLevel); + } + + return new TokenProvider() { Token = hToken }; + } + + public static TokenProvider CreateFromCurrentProcessToken(uint access = + TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | + TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY + | TOKEN_ADJUST_PRIVILEGES + | TOKEN_ADJUST_GROUPS) + { + var tm = OpenCurrentProcessToken(access); + return tm.Duplicate(MAXIMUM_ALLOWED); + } + + public TokenProvider Duplicate(uint desiredAccess = 0x02000000) + { + if (!DuplicateTokenEx(Token.DangerousGetHandle(), desiredAccess, IntPtr.Zero, + SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out var newToken)) + throw new Win32Exception(); + + Token.Close(); + Token = newToken; + return this; + } + + public static TokenProvider OpenCurrentProcessToken(uint access = + TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | + TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY + | TOKEN_ADJUST_PRIVILEGES + | TOKEN_ADJUST_GROUPS) + { + IntPtr existingProcessToken; + + if (!OpenProcessToken(Process.GetCurrentProcess().Handle, + access + // internal const UInt32 TOKEN_ADJUST_SESSIONID = 0x0100; + , + out existingProcessToken)) + throw new Win32Exception(); + + if (existingProcessToken == IntPtr.Zero) + throw new Win32Exception(); + + return new TokenProvider() { Token = new SafeTokenHandle(existingProcessToken) }; + } + + public TokenProvider GetLinkedToken(uint desiredAccess = MAXIMUM_ALLOWED) + { + // Now we allocate a buffer for the integrity level information. + int cb = Marshal.SizeOf(); + var pLinkedToken = Marshal.AllocHGlobal(cb); + if (pLinkedToken == IntPtr.Zero) + { + throw new Win32Exception(); + } + + try + { + // Now we ask for the integrity level information again. This may fail + // if an administrator has added this account to an additional group + // between our first call to GetTokenInformation and this one. + if (!GetTokenInformation(Token.DangerousGetHandle(), + TOKEN_INFORMATION_CLASS.TokenLinkedToken, pLinkedToken, cb, + out cb)) + { + throw new Win32Exception(); + } + + // Marshal the TOKEN_MANDATORY_LABEL struct from native to .NET object. + TOKEN_LINKED_TOKEN linkedTokenStruct = (TOKEN_LINKED_TOKEN) + Marshal.PtrToStructure(pLinkedToken, typeof(TOKEN_LINKED_TOKEN)); + + var sa = new SECURITY_ATTRIBUTES(); + sa.nLength = 0; + + SafeTokenHandle newToken; + + if (!DuplicateTokenEx(linkedTokenStruct.LinkedToken, desiredAccess, IntPtr.Zero, + SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation, TOKEN_TYPE.TokenPrimary, out newToken)) + { + throw new Win32Exception(); + } + + CloseHandle(linkedTokenStruct.LinkedToken); + Token.Close(); + Token = newToken; + } + finally + { + Marshal.FreeHGlobal(pLinkedToken); + } + + return this; + } + + internal static TokenProvider CreateUnelevated(IntegrityLevel level) + { + if (SecurityHelper.IsAdministrator()) + { + // Have you impersonated system first? + if (!WindowsIdentity.GetCurrent().IsSystem) + { + try + { + return + CreateFromSystemAccount() + .EnablePrivilege(Privilege.SeAssignPrimaryTokenPrivilege, true) + .Impersonate(() => + { + return CreateFromCurrentProcessToken().GetLinkedToken().Duplicate() + .SetIntegrity(level); + }); + } + catch (Exception e) + { + Debug.WriteLine("Unable to get unelevated token, will try SaferApi Token. " + e.ToString()); + return CreateFromSaferApi(SaferLevels.NormalUser); + } + } + else + { + // IntPtr hwnd = ConsoleApi.GetShellWindow(); + // _ = ConsoleApi.GetWindowThreadProcessId(hwnd, out uint pid); + return CreateFromProcessToken(0); + } + } + else + { + return CreateFromCurrentProcessToken(); + } + } + + public TokenProvider SetIntegrity(IntegrityLevel integrityLevel) + { + return SetIntegrity((int)integrityLevel); + } + + public TokenProvider SetIntegrity(int integrityLevel) + { + string integritySid = + "S-1-16-" + integrityLevel.ToString(System.Globalization.CultureInfo.InvariantCulture); + IntPtr pIntegritySid; + if (!ConvertStringSidToSid(integritySid, out pIntegritySid)) + throw new Win32Exception(); + + TOKEN_MANDATORY_LABEL TIL = new TOKEN_MANDATORY_LABEL(); + TIL.Label.Attributes = 0x00000020 /* SE_GROUP_INTEGRITY */; + TIL.Label.Sid = pIntegritySid; + + var pTIL = Marshal.AllocHGlobal(Marshal.SizeOf()); + Marshal.StructureToPtr(TIL, pTIL, false); + + if (!SetTokenInformation(Token.DangerousGetHandle(), + TOKEN_INFORMATION_CLASS.TokenIntegrityLevel, + pTIL, + (uint)(Marshal.SizeOf() + GetLengthSid(pIntegritySid)))) + throw new Win32Exception(); + + return this; + } + + /// + /// Use this only to restrict a non-elevated token. + /// This method is useless if you are already elevated because it can't + /// remove you from Admin's group. (You are still administrator able to write in C:\windows). + /// + /// + public TokenProvider RestrictTokenMaxPrivilege(bool ignoreErrors = false) + { + uint DISABLE_MAX_PRIVILEGE = 0x1; + uint LUA_TOKEN = 0x4; + SafeTokenHandle result; + + if (!CreateRestrictedToken( + Token, + LUA_TOKEN | DISABLE_MAX_PRIVILEGE, + //WRITE_RESTRICTED, + 0, + IntPtr.Zero, //pSA, + 0, + IntPtr.Zero, + 0, + IntPtr.Zero, + out result) && !ignoreErrors) + throw new Win32Exception(); + + Token.Close(); + Token = result; + return this; + } + + public TokenProvider Impersonate(Action ActionImpersonated) + { + WindowsIdentity.RunImpersonated(new Microsoft.Win32.SafeHandles.SafeAccessTokenHandle(GetToken().DangerousGetHandle()), ActionImpersonated); + + /* using (var ctx = System.Security.Principal.WindowsIdentity.Impersonate(GetToken().DangerousGetHandle())) + { + try + { + ActionImpersonated(); + } + finally + { + ctx.Undo(); + } + } + + */ + return this; + } + + public T Impersonate(Func ActionImpersonated) + + { + T res = default; + WindowsIdentity.RunImpersonated(new Microsoft.Win32.SafeHandles.SafeAccessTokenHandle(GetToken().DangerousGetHandle()), () => { res = ActionImpersonated(); }); + return res; + } + + public TokenProvider EnablePrivileges(bool throwOnFailure, params Privilege[] priviledgesList) + { + // todo: rewrite to use just 1 api call, handle exceptions, + foreach (var priv in priviledgesList) + EnablePrivilege(priv, throwOnFailure); + + return this; + } + + public TokenProvider EnablePrivilege(Privilege securityEntity, bool throwOnFailure) + { + // todo: rewrite to use just 1 api call, handle exceptions, + var locallyUniqueIdentifier = new NativeMethods.LUID(); + + if (!NativeMethods.LookupPrivilegeValue(null, securityEntity.ToString(), ref locallyUniqueIdentifier)) + throw new Win32Exception(); + + var tp = new NativeMethods.TOKEN_PRIVILEGES(); + tp.PrivilegeCount = 1; + tp.Privileges = new NativeMethods.LUID_AND_ATTRIBUTES[1]; + tp.Privileges[0].Attributes = NativeMethods.SE_PRIVILEGE_ENABLED; + tp.Privileges[0].Luid = locallyUniqueIdentifier; + + if (!NativeMethods.AdjustTokenPrivileges(Token.DangerousGetHandle(), false, ref tp, 0, IntPtr.Zero, IntPtr.Zero)) + if (throwOnFailure) + throw new Win32Exception(); + + // if (throwOnFailure && Microsoft.Win32.GetLastError() != 0) + // throw new Win32Exception(); //throw new Exception("The token does not have the specified privilege. \n"); + + return this; + } + + public TokenProvider SetSessionId(int sessionId) + { + int size = Marshal.SizeOf(); + IntPtr pValue = Marshal.AllocHGlobal(size); + try + { + Marshal.WriteInt32(pValue, sessionId); + + if (!SetTokenInformation( + Token.DangerousGetHandle(), TOKEN_INFORMATION_CLASS.TokenSessionId, + pValue, (uint)size)) + throw new Win32Exception(); + + return this; + } + finally + { + Marshal.FreeHGlobal(pValue); + } + } + + public static TokenElevationType GetTokenElevationType(IntPtr token) + { + int size = Marshal.SizeOf(); + IntPtr pValue = Marshal.AllocHGlobal(size); + if (!GetTokenInformation(token, TOKEN_INFORMATION_CLASS.TokenElevationType, pValue, size, out size)) + { + throw new Win32Exception(); + } + + return (TokenElevationType)Marshal.ReadInt32(pValue); + } + + public TokenElevationType GetTokenElevationType() + { + int size = Marshal.SizeOf(); + IntPtr pValue = Marshal.AllocHGlobal(size); + if (!GetTokenInformation(Token.DangerousGetHandle(), TOKEN_INFORMATION_CLASS.TokenElevationType, pValue, size, out size)) + { + throw new Win32Exception(); + } + + return (TokenElevationType)Marshal.ReadInt32(pValue); + } + + public int GetSessionId() + { + int size = Marshal.SizeOf(); + IntPtr pValue = Marshal.AllocHGlobal(size); + try + { + if (!GetTokenInformation(Token.DangerousGetHandle(), TOKEN_INFORMATION_CLASS.TokenSessionId, pValue, size, out size)) + { + throw new Win32Exception(); + } + + return Marshal.ReadInt32(pValue); + } + finally + { + Marshal.FreeHGlobal(pValue); + } + } + + public void Dispose() + { + } + + internal enum TokenElevationType : uint + { + Default = 1u, + Full, + Limited + } + + } +} \ No newline at end of file diff --git a/VSLauncherX/ItemLauncher.cs b/VSLauncherX/ItemLauncher.cs deleted file mode 100644 index 7964160..0000000 --- a/VSLauncherX/ItemLauncher.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; - -using Newtonsoft.Json; - -using VSLauncher.DataModel; - -namespace VSLauncher -{ - /// - /// The item launcher. - /// - public class ItemLauncher - { - /// - /// Initializes a new instance of the class. - /// - /// The item. - /// The target. - public ItemLauncher(VsItem item, VisualStudioInstance target) - { - this.Target = target; - this.SingleItem = item; - } - - /// - /// Initializes a new instance of the class. - /// - /// The item. - /// The target. - public ItemLauncher(VsFolder item, VisualStudioInstance target) - { - this.Solution = item; - this.Target = target; - } - - /// - /// Gets the launch item. - /// - public VsFolder? Solution { get; } - - /// - /// Gets the single item. - /// - public VsItem? SingleItem { get; } - - /// - /// Gets the target. - /// - public VisualStudioInstance Target { get; } - - /// - /// Gets the last exception. - /// - public Exception LastException { get; private set; } - - /// - /// Launches the. - /// - /// If true, b force admin. - public Task Launch(bool bForceAdmin = false) - { - // Execute the item, run the before and after items if set, elevate to admin if required - return Task.Run(() => - { - string s = CreateLaunchInfoString(); - - Debug.WriteLine(s); - if(this.Solution is null && this.SingleItem is null) - { - throw new NullReferenceException(); - } - - bool bRequireAdmin = bForceAdmin | (this.Solution is null ? this.SingleItem!.RunAsAdmin : this.Solution.RunAsAdmin); - var psi = new System.Diagnostics.ProcessStartInfo - { - FileName = this.GetLauncherPath(), - Arguments = s, - ErrorDialog = false, - UseShellExecute = bRequireAdmin, - Verb = bRequireAdmin ? "runas" : "run", - WindowStyle = System.Diagnostics.ProcessWindowStyle.Hidden, - }; - - try - { - var p = System.Diagnostics.Process.Start(psi); - } - catch (Exception ex) - { - this.LastException = ex; - Debug.WriteLine(ex.ToString()); - } - }); - } - - /// - /// Gets the launcher path. - /// - /// A string. - public string GetLauncherPath() - { - string env = Environment.CurrentDirectory; - - // get working directory of current assembly - string current = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location) ?? env; - - return Path.Combine(current, "BackgroundLaunch.exe"); - } - - /// - /// Creates the launch info string. - /// - /// A string. - public string CreateLaunchInfoString() - { - var li = new LaunchInfo() - { - Solution = this.Solution ?? new VsFolder(this.SingleItem ?? throw new NullReferenceException()), - Target = this.Target.Location - }; - - JsonSerializerSettings settings = new JsonSerializerSettings() - { - Formatting = Formatting.None, - }; - - string s = JsonConvert.SerializeObject(li, settings); - - s = s.Replace('\"', '«').Replace(' ', '»'); - - return s; - } - } -} diff --git a/VSLauncherX/MainDialog.Designer.cs b/VSLauncherX/MainDialog.Designer.cs index e0f3943..8345117 100644 --- a/VSLauncherX/MainDialog.Designer.cs +++ b/VSLauncherX/MainDialog.Designer.cs @@ -4,7 +4,7 @@ namespace VSLauncher { - partial class MainDialog + partial class MainDialog { /// /// Required designer variable. diff --git a/VSLauncherX/MainDialog.cs b/VSLauncherX/MainDialog.cs index 417acd8..850081b 100644 --- a/VSLauncherX/MainDialog.cs +++ b/VSLauncherX/MainDialog.cs @@ -19,10 +19,10 @@ namespace VSLauncher { - /// - /// The main dialog. - /// - public partial class MainDialog : Form + /// + /// The main dialog. + /// + public partial class MainDialog : Form { /// /// used to indicate that some internal update is going on @@ -198,20 +198,21 @@ private void InitializeListview() /// public MainDialog() { - InitializeComponent(); InitializeListview(); SetupDragAndDrop(); - // if(IsUserAnAdmin()) - if(AdminInfo.IsCurrentUserAdmin()) - { - this.Text += " (Administrator)"; - } + bool bAdmin = AdminInfo.IsCurrentUserAdmin(); + bool bElevated = AdminInfo.IsElevated(); - if(AdminInfo.IsElevated()) + if(bAdmin || bElevated) { - this.Text += " *"; + if(bAdmin && bElevated) + this.Text += $" (Administrator, Elevated)"; + else if(bAdmin) + this.Text += $" (Administrator)"; + else + this.Text += $" (Elevated)"; } this.bInUpdate = true; @@ -587,9 +588,17 @@ private void mainSettings_Click(object sender, EventArgs e) if (dlg.ShowDialog() == DialogResult.OK) { - if(Properties.Settings.Default.AlwaysAdmin || Properties.Settings.Default.AutoStart) + if(Properties.Settings.Default.AlwaysAdmin || Properties.Settings.Default.AutoStart) { - Program.UpdateTaskScheduler(); + if (Properties.Settings.Default.AlwaysAdmin) + { + RestartOurselves(); + Application.Exit(); + } + else + { + Program.UpdateTaskScheduler(); + } } else { @@ -599,6 +608,23 @@ private void mainSettings_Click(object sender, EventArgs e) _ = SolutionData_OnChanged(true); } } + + /// + /// Restarts the application to register it with admin rights. + /// + private void RestartOurselves() + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = Assembly.GetExecutingAssembly().Location.Replace(".dll", ".exe"), + Arguments = "register", + Verb = "runas", + UseShellExecute = true + }; + + Process.Start(startInfo); + } + #endregion #region ListView event handling diff --git a/VSLauncherX/Program.cs b/VSLauncherX/Program.cs index 210046e..cf9d603 100644 --- a/VSLauncherX/Program.cs +++ b/VSLauncherX/Program.cs @@ -38,6 +38,12 @@ static void Main() } else { + if(args.Length > 1 && args[1] == "register") + { + // at this point the app was started with admin privs, so we can register the task + UpdateTaskScheduler(); + } + if (!Settings.Default.AlwaysAdmin) { Application.Run(new MainDialog()); @@ -52,14 +58,6 @@ static void Main() internal static void UpdateTaskScheduler() { - if(Settings.Default.AlwaysAdmin && !UACHelper.UACHelper.IsAdministrator) - { - // restart ourselves with elevation - ProcessStartInfo psi = new ProcessStartInfo() - { - - }; - } AutoRun.SetupLauncherTask(Settings.Default.AlwaysAdmin, Settings.Default.AutoStart); } diff --git a/VSLauncherX/VisualStudioCombobox.cs b/VSLauncherX/VisualStudioCombobox.cs deleted file mode 100644 index 01dd86d..0000000 --- a/VSLauncherX/VisualStudioCombobox.cs +++ /dev/null @@ -1,218 +0,0 @@ -using VSLauncher.DataModel; - -namespace VSLauncher -{ - /// - /// The visual studio combobox. - /// - public class VisualStudioCombobox : ComboBox - { - private VisualStudioInstanceManager visualStudioVersions = new VisualStudioInstanceManager(); - private bool showDefault; - - public VisualStudioCombobox() - { - this.DrawMode = DrawMode.OwnerDrawFixed; - this.DropDownStyle = ComboBoxStyle.DropDownList; - this.IntegralHeight = false; - this.ItemHeight = 26; - this.DrawItem += CustomDrawItem; - - this.Items.AddRange(this.visualStudioVersions.All.ToArray()); - } - - /// - /// Gets or sets a value indicating whether to show default. - /// - public bool ShowDefault - { - get => showDefault; - set - { - showDefault = value; - - if (value) - { - this.Items.Insert(0, ""); - } - else - { - if (this.Items.Count > 0 && this.Items[0] is string) - { - this.Items.RemoveAt(0); - } - } - } - } - - /// - /// Gets the versions. - /// - public List Versions { get { return visualStudioVersions.All; } } - - /// - /// Gets the selected item. - /// - public new VisualStudioInstance? SelectedItem - { - get - { - if(this.ShowDefault) - { - return base.SelectedIndex > 0 ? (VisualStudioInstance)base.SelectedItem : null; - } - - return (VisualStudioInstance)base.SelectedItem; - } - set - { - base.SelectedItem = value; - } - } - - /// - /// Gets or sets the selected index, accounting for the default item which will result in -1 - /// - public new int SelectedIndex - { - get - { - return this.ShowDefault ? base.SelectedIndex - 1 : base.SelectedIndex; - } - set - { - base.SelectedIndex = value; - } - } - - /// - /// Gets a value indicating whether default is selected. - /// - public bool IsDefaultSelected - { - get - { - return this.ShowDefault && base.SelectedIndex == 0; - } - } - - /// - /// Selects the default item if existing - /// - public void SelectDefault() - { - if(ShowDefault) - { - this.SelectedIndex = 0; - } - } - - protected override void OnDropDown(EventArgs e) - { - this.Items.Clear(); - - if (ShowDefault) - { - this.Items.Add(""); - } - - this.Items.AddRange(this.visualStudioVersions.All.ToArray()); - - base.OnDropDown(e); - } - - /// - /// Selects the active item from a version string (exact or major version), a shortname or an identifier - /// - /// The v. - internal void SelectFromVersion(string? v) - { - if(string.IsNullOrWhiteSpace(v)) - { - if (ShowDefault) - { - if (v != null) // v is "" so select the default - { - SelectDefault(); - return; - } - } - - // any NULL value will return the highest version - v = this.visualStudioVersions.HighestVersion().Version; - } - - // find either by exact version or by short name containing the version - var i = this.visualStudioVersions.All.FindIndex(x => x.Version == v); - if (i >= 0) - { - this.SelectedIndex = ShowDefault ? 1 + i : i; - return; - } - - var k = this.visualStudioVersions.All.FindIndex(x => x.Version.StartsWith(v)); - if (k >= 0) - { - this.SelectedIndex = ShowDefault ? 1 + k : k; - return; - } - - // by year - var n = this.visualStudioVersions.All.FindIndex(x => x.ShortName.Contains(v)); - if (n >= 0) - { - this.SelectedIndex = ShowDefault ? 1 + n : n; - return; - } - - // by identifier - var o = this.visualStudioVersions.All.FindIndex(x => x.Identifier == v); - if (o >= 0) - { - this.SelectedIndex = ShowDefault ? 1 + o : o; - return; - } - - this.SelectedIndex = -1; - } - - /// - /// Custom draws the item - /// - /// The sender. - /// The e. - private void CustomDrawItem(object sender, DrawItemEventArgs e) - { - // draw the selected item with the Visual Studio Icon and the version as text - if (e.Index >= 0 && e.Index <= this.visualStudioVersions.Count) - { - e.DrawBackground(); - - var height = 16; - - Rectangle iconRect = new Rectangle(e.Bounds.Left + this.Margin.Left, - e.Bounds.Top + ((this.ItemHeight - height) / 2), - height, height); - if(ShowDefault) - { - if(e.Index > 0) - { - e.Graphics.DrawIcon(visualStudioVersions[e.Index - 1].AppIcon, iconRect); - e.Graphics.DrawString(visualStudioVersions[e.Index - 1].Name, e.Font, Brushes.Black, e.Bounds.Left + 20, e.Bounds.Top + 4); - } - else - { - e.Graphics.DrawIcon(Resources.AppLogo, iconRect); - e.Graphics.DrawString("", e.Font, Brushes.Black, e.Bounds.Left + 20, e.Bounds.Top + 4); - } - } - else - { - e.Graphics.DrawIcon(visualStudioVersions[e.Index].AppIcon, iconRect); - e.Graphics.DrawString(visualStudioVersions[e.Index].Name, e.Font, Brushes.Black, e.Bounds.Left + 20, e.Bounds.Top + 4); - } - } - } - - } -} \ No newline at end of file