From f6f80d5971edcf3484e0cc47b8dfca9dcf3bec89 Mon Sep 17 00:00:00 2001 From: ryannewington Date: Sat, 12 Aug 2017 13:14:21 +1000 Subject: [PATCH] Adds the ability to set the type of action to peform after the idle timeout. Supports logoff, reboot, and shutdown actions --- src/Lithnet.IdleLogoff/EventLogging.cs | 1 + src/Lithnet.IdleLogoff/IdleTimeoutAction.cs | 15 +++ src/Lithnet.IdleLogoff/NativeMethods.cs | 96 ++++++++++++++++++- .../en-US/lithnet.idlelogoff.adml | 5 + .../PolicyDefinitions/lithnet.idlelogoff.admx | 37 ++++++- src/Lithnet.IdleLogoff/Program.cs | 10 +- src/Lithnet.IdleLogoff/Settings.cs | 27 ++++++ src/Lithnet.IdleLogoff/TokenPrivileges.cs | 13 +++ .../frmSettings.Designer.cs | 34 ++++++- src/Lithnet.IdleLogoff/frmSettings.cs | 55 +++++++---- .../lithnet.idlelogoff.csproj | 5 + .../lithnet.idlelogoff.setup.wixproj | 3 + 12 files changed, 267 insertions(+), 34 deletions(-) create mode 100644 src/Lithnet.IdleLogoff/IdleTimeoutAction.cs create mode 100644 src/Lithnet.IdleLogoff/TokenPrivileges.cs diff --git a/src/Lithnet.IdleLogoff/EventLogging.cs b/src/Lithnet.IdleLogoff/EventLogging.cs index 350137a..6a0ccc7 100644 --- a/src/Lithnet.IdleLogoff/EventLogging.cs +++ b/src/Lithnet.IdleLogoff/EventLogging.cs @@ -10,6 +10,7 @@ public class EventLogging internal const int EVT_LOGOFFEVENT = 3; internal const int EVT_TIMERINTERVALCHANGED = 4; internal const int EVT_LOGOFFFAILED = 5; + internal const int EVT_RESTARTFAILED = 6; internal static string evtSource = "Lithnet.idlelogoff"; internal static bool LogEnabled = true; diff --git a/src/Lithnet.IdleLogoff/IdleTimeoutAction.cs b/src/Lithnet.IdleLogoff/IdleTimeoutAction.cs new file mode 100644 index 0000000..a6f59be --- /dev/null +++ b/src/Lithnet.IdleLogoff/IdleTimeoutAction.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Lithnet.idlelogoff +{ + public enum IdleTimeoutAction + { + Logoff = 0, + Reboot = 1, + Shutdown = 2, + } +} diff --git a/src/Lithnet.IdleLogoff/NativeMethods.cs b/src/Lithnet.IdleLogoff/NativeMethods.cs index f2e06e1..ad926aa 100644 --- a/src/Lithnet.IdleLogoff/NativeMethods.cs +++ b/src/Lithnet.IdleLogoff/NativeMethods.cs @@ -6,6 +6,11 @@ namespace Lithnet.idlelogoff { public class NativeMethods { + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int TOKEN_QUERY = 0x00000008; + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + internal const string SE_SHUTDOWN_NAME = "SeShutdownPrivilege"; + [DllImport("powrprof.dll")] private static extern int CallNtPowerInformation( POWER_INFORMATION_LEVEL informationLevel, @@ -15,12 +20,28 @@ private static extern int CallNtPowerInformation( int nOutputBufferSize ); + + [DllImport("kernel32.dll")] + private static extern IntPtr GetCurrentProcess(); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern int OpenProcessToken(IntPtr processHandle, int desiredAccess, ref IntPtr tokenHandle); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern int LookupPrivilegeValue(string systemName, string name, ref long luid); + + [DllImport("advapi32.dll", SetLastError = true)] + private static extern int AdjustTokenPrivileges(IntPtr tokenHandle, bool disableAllPrivileges, ref TokenPrivileges newState, int bufferLength, IntPtr previousState, IntPtr length); + [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool GetLastInputInfo(ref LastInputInfo lastInputInfo); [DllImport("User32.dll", CharSet = CharSet.Unicode, SetLastError = true)] private static extern bool ExitWindowsEx(ShutdownFlags flags, int reason); + [DllImport("kernel32", SetLastError = true)] + private static extern bool CloseHandle(IntPtr handle); + public static bool IsDisplayRequested() { EXECUTION_STATE state; @@ -50,12 +71,83 @@ public static int GetLastInputTime() public static void LogOffUser() { - ShutdownFlags flags = ShutdownFlags.Logoff | ShutdownFlags.Force; - + ShutdownFlags flags; + bool elevated = false; + + if (Settings.Action == IdleTimeoutAction.Reboot || Settings.Action == IdleTimeoutAction.Shutdown) + { + try + { + ElevatePrivileges(); + elevated = true; + } + catch (Exception ex) + { + EventLogging.TryLogEvent($"Could not get workstation shutdown permissions. Logging off instead\n{ex}", EventLogging.EVT_RESTARTFAILED, System.Diagnostics.EventLogEntryType.Error); + } + } + + if (elevated && Settings.Action == IdleTimeoutAction.Shutdown) + { + flags = ShutdownFlags.Shutdown | ShutdownFlags.Force; + } + else if (elevated && Settings.Action == IdleTimeoutAction.Reboot) + { + flags = ShutdownFlags.Reboot | ShutdownFlags.Force; + } + else { + flags = ShutdownFlags.Logoff | ShutdownFlags.Force; + } + if (!ExitWindowsEx(flags, 0)) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } + + private static void ElevatePrivileges() + { + IntPtr currentProcess = GetCurrentProcess(); + IntPtr tokenHandle = IntPtr.Zero; + + try + { + int result = OpenProcessToken(currentProcess, TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, ref tokenHandle); + + if (result == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + TokenPrivileges tokenPrivileges; + tokenPrivileges.PrivilegeCount = 1; + tokenPrivileges.Luid = 0; + tokenPrivileges.Attributes = SE_PRIVILEGE_ENABLED; + + result = LookupPrivilegeValue(null, SE_SHUTDOWN_NAME, ref tokenPrivileges.Luid); + if (result == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + + result = AdjustTokenPrivileges(tokenHandle, false, ref tokenPrivileges, 0, IntPtr.Zero, IntPtr.Zero); + if (result == 0) + { + throw new Win32Exception(Marshal.GetLastWin32Error()); + } + } + finally + { + if (currentProcess != IntPtr.Zero) + { + CloseHandle(currentProcess); + } + + if (tokenHandle != IntPtr.Zero) + { + CloseHandle(tokenHandle); + } + } + } } } \ No newline at end of file diff --git a/src/Lithnet.IdleLogoff/PolicyDefinitions/en-US/lithnet.idlelogoff.adml b/src/Lithnet.IdleLogoff/PolicyDefinitions/en-US/lithnet.idlelogoff.adml index d51fd60..0655c91 100644 --- a/src/Lithnet.IdleLogoff/PolicyDefinitions/en-US/lithnet.idlelogoff.adml +++ b/src/Lithnet.IdleLogoff/PolicyDefinitions/en-US/lithnet.idlelogoff.adml @@ -8,6 +8,9 @@ Lithnet IdleLogoff Log off inactive users + Log off + Reboot + Shutdown At least Microsoft Windows At least Microsoft Windows XP @@ -15,10 +18,12 @@ Idle timeout (minutes) Ignore sleep prevention requests from applications such as media playback + Action Idle timeout (minutes) Ignore sleep prevention requests from applications such as media playback + Action diff --git a/src/Lithnet.IdleLogoff/PolicyDefinitions/lithnet.idlelogoff.admx b/src/Lithnet.IdleLogoff/PolicyDefinitions/lithnet.idlelogoff.admx index c2c6e01..d2443ae 100644 --- a/src/Lithnet.IdleLogoff/PolicyDefinitions/lithnet.idlelogoff.admx +++ b/src/Lithnet.IdleLogoff/PolicyDefinitions/lithnet.idlelogoff.admx @@ -31,6 +31,23 @@ + + + + + + + + + + + + + + + + + @@ -45,7 +62,25 @@ - + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Lithnet.IdleLogoff/Program.cs b/src/Lithnet.IdleLogoff/Program.cs index ca250cd..ca143af 100644 --- a/src/Lithnet.IdleLogoff/Program.cs +++ b/src/Lithnet.IdleLogoff/Program.cs @@ -44,11 +44,11 @@ private static void InitTimer() if (Settings.Enabled) { - EventLogging.TryLogEvent("The application has started. User " + Environment.UserDomainName + "\\" + Environment.UserName + " will be logged off after being idle for " + Settings.IdleLimit + " minutes", EventLogging.EVT_TIMERSTARTED); + EventLogging.TryLogEvent($"The application has started. {Settings.Action} will be performed for user {Environment.UserDomainName}\\{Environment.UserName} after being idle for {Settings.IdleLimit} minutes", EventLogging.EVT_TIMERSTARTED); } else { - EventLogging.TryLogEvent("The application has started, but is not enabled. User " + Environment.UserDomainName + "\\" + Environment.UserName + " will not be logged off automatically", EventLogging.EVT_TIMERSTARTED); + EventLogging.TryLogEvent($"The application has started, but is not enabled. User {Environment.UserDomainName}\\{Environment.UserName} will not be logged off automatically", EventLogging.EVT_TIMERSTARTED); } Program.eventTimer = new Timer(); @@ -122,7 +122,7 @@ private static void EventTimer_Tick(object sender, EventArgs e) if (initialTime != logoffidletime) { - EventLogging.TryLogEvent($"Idle timeout limit has changed. User {Environment.UserDomainName}\\{Environment.UserName} will now be logged off after {Settings.IdleLimit} minutes", EventLogging.EVT_TIMERINTERVALCHANGED); + EventLogging.TryLogEvent($"Idle timeout limit has changed. {Settings.Action} will be performed for user {Environment.UserDomainName}\\{Environment.UserName} after {Settings.IdleLimit} minutes", EventLogging.EVT_TIMERINTERVALCHANGED); initialTime = logoffidletime; } @@ -147,7 +147,7 @@ private static void EventTimer_Tick(object sender, EventArgs e) if (DateTime.Now.Subtract(Program.lastDateTime).TotalMilliseconds > logoffidletime) { - EventLogging.TryLogEvent($"User {Environment.UserName} has passed the idle time limit of {Settings.IdleLimit} minutes. Initiating logoff.", EventLogging.EVT_LOGOFFEVENT); + EventLogging.TryLogEvent($"User {Environment.UserName} has passed the idle time limit of {Settings.IdleLimit} minutes. Initiating {Settings.Action}.", EventLogging.EVT_LOGOFFEVENT); if (!Settings.Debug) { @@ -160,7 +160,7 @@ private static void EventTimer_Tick(object sender, EventArgs e) } catch (Exception ex) { - EventLogging.TryLogEvent("An error occurred trying to log off the user\n" + ex.Message, EventLogging.EVT_LOGOFFFAILED); + EventLogging.TryLogEvent($"An error occurred trying to perform the {Settings.Action} operation\n" + ex.Message, EventLogging.EVT_LOGOFFFAILED); } } else diff --git a/src/Lithnet.IdleLogoff/Settings.cs b/src/Lithnet.IdleLogoff/Settings.cs index 3524d63..a1c96a6 100644 --- a/src/Lithnet.IdleLogoff/Settings.cs +++ b/src/Lithnet.IdleLogoff/Settings.cs @@ -289,6 +289,33 @@ public static bool Enabled } } + public static IdleTimeoutAction Action + { + get + { + int retval = 0; + + object regvalue = Settings.GetPolicyOrSetting("Action"); + if (regvalue != null) + { + try + { + retval = (int)regvalue; + } + catch + { + //unable to cast from an object to a string + } + } + + return (IdleTimeoutAction)retval; + } + set + { + SaveSetting("Action", (int)value, RegistryValueKind.DWord); + } + } + public static bool IgnoreDisplayRequested { get diff --git a/src/Lithnet.IdleLogoff/TokenPrivileges.cs b/src/Lithnet.IdleLogoff/TokenPrivileges.cs new file mode 100644 index 0000000..62f6130 --- /dev/null +++ b/src/Lithnet.IdleLogoff/TokenPrivileges.cs @@ -0,0 +1,13 @@ +using System; +using System.Runtime.InteropServices; + +namespace Lithnet.idlelogoff +{ + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TokenPrivileges + { + public int PrivilegeCount; + public long Luid; + public int Attributes; + } +} diff --git a/src/Lithnet.IdleLogoff/frmSettings.Designer.cs b/src/Lithnet.IdleLogoff/frmSettings.Designer.cs index 16fc202..08d5518 100644 --- a/src/Lithnet.IdleLogoff/frmSettings.Designer.cs +++ b/src/Lithnet.IdleLogoff/frmSettings.Designer.cs @@ -38,6 +38,8 @@ private void InitializeComponent() this.lbGPControlled = new System.Windows.Forms.Label(); this.lbHiddenRefresh = new System.Windows.Forms.Label(); this.ckIgnoreDisplayRequested = new System.Windows.Forms.CheckBox(); + this.cbAction = new System.Windows.Forms.ComboBox(); + this.label2 = new System.Windows.Forms.Label(); ((System.ComponentModel.ISupportInitialize)(this.udMinutes)).BeginInit(); this.SuspendLayout(); // @@ -84,7 +86,7 @@ private void InitializeComponent() // // btOK // - this.btOK.Location = new System.Drawing.Point(344, 142); + this.btOK.Location = new System.Drawing.Point(344, 181); this.btOK.Name = "btOK"; this.btOK.Size = new System.Drawing.Size(71, 24); this.btOK.TabIndex = 4; @@ -95,7 +97,7 @@ private void InitializeComponent() // btCancel // this.btCancel.DialogResult = System.Windows.Forms.DialogResult.Cancel; - this.btCancel.Location = new System.Drawing.Point(267, 142); + this.btCancel.Location = new System.Drawing.Point(267, 181); this.btCancel.Name = "btCancel"; this.btCancel.Size = new System.Drawing.Size(71, 24); this.btCancel.TabIndex = 3; @@ -106,7 +108,7 @@ private void InitializeComponent() // lbProductName // this.lbProductName.AutoSize = true; - this.lbProductName.Location = new System.Drawing.Point(7, 144); + this.lbProductName.Location = new System.Drawing.Point(7, 183); this.lbProductName.Name = "lbProductName"; this.lbProductName.Size = new System.Drawing.Size(99, 13); this.lbProductName.TabIndex = 4; @@ -114,7 +116,7 @@ private void InitializeComponent() // // lbProductVersion // - this.lbProductVersion.Location = new System.Drawing.Point(6, 158); + this.lbProductVersion.Location = new System.Drawing.Point(7, 196); this.lbProductVersion.Name = "lbProductVersion"; this.lbProductVersion.Size = new System.Drawing.Size(119, 13); this.lbProductVersion.TabIndex = 5; @@ -147,13 +149,33 @@ private void InitializeComponent() this.ckIgnoreDisplayRequested.Text = "Ignore sleep prevention requests from applications such as media playback"; this.ckIgnoreDisplayRequested.UseVisualStyleBackColor = true; // + // cbAction + // + this.cbAction.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; + this.cbAction.FormattingEnabled = true; + this.cbAction.Location = new System.Drawing.Point(154, 131); + this.cbAction.Name = "cbAction"; + this.cbAction.Size = new System.Drawing.Size(166, 21); + this.cbAction.TabIndex = 10; + // + // label2 + // + this.label2.AutoSize = true; + this.label2.Location = new System.Drawing.Point(97, 131); + this.label2.Name = "label2"; + this.label2.Size = new System.Drawing.Size(40, 13); + this.label2.TabIndex = 11; + this.label2.Text = "Action"; + // // frmSettings // this.AcceptButton = this.btOK; this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; this.CancelButton = this.btCancel; - this.ClientSize = new System.Drawing.Size(427, 178); + this.ClientSize = new System.Drawing.Size(427, 217); + this.Controls.Add(this.label2); + this.Controls.Add(this.cbAction); this.Controls.Add(this.ckIgnoreDisplayRequested); this.Controls.Add(this.lbHiddenRefresh); this.Controls.Add(this.lbGPControlled); @@ -189,5 +211,7 @@ private void InitializeComponent() private System.Windows.Forms.Label lbGPControlled; private System.Windows.Forms.Label lbHiddenRefresh; private System.Windows.Forms.CheckBox ckIgnoreDisplayRequested; + private System.Windows.Forms.ComboBox cbAction; + private System.Windows.Forms.Label label2; } } \ No newline at end of file diff --git a/src/Lithnet.IdleLogoff/frmSettings.cs b/src/Lithnet.IdleLogoff/frmSettings.cs index b53ecc5..ec75cb5 100644 --- a/src/Lithnet.IdleLogoff/frmSettings.cs +++ b/src/Lithnet.IdleLogoff/frmSettings.cs @@ -7,41 +7,49 @@ public partial class frmSettings : Form { public frmSettings() { - InitializeComponent(); + this.InitializeComponent(); } private void frmSettings_Load(object sender, EventArgs e) { - - RefreshUI(); + this.RefreshUI(); } private void RefreshUI() { + this.lbProductName.Text = "Lithnet.idlelogoff"; + this.lbProductVersion.Text = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(3); + + this.ckEnableIdleLogoff.Checked = Settings.Enabled; + this.ckEnableIdleLogoff.Enabled = !Settings.IsSettingFromPolicy(nameof(Settings.Enabled)); - lbProductName.Text = "Lithnet.idlelogoff"; - lbProductVersion.Text = System.Reflection.Assembly.GetExecutingAssembly().GetName().Version.ToString(3); + this.ckIgnoreDisplayRequested.Checked = Settings.IgnoreDisplayRequested; + this.ckIgnoreDisplayRequested.Enabled = !Settings.IsSettingFromPolicy(nameof(Settings.IgnoreDisplayRequested)); - ckEnableIdleLogoff.Checked = Settings.Enabled; - ckEnableIdleLogoff.Enabled = !Settings.IsSettingFromPolicy(nameof(Settings.Enabled)); + this.cbAction.Items.Clear(); + + foreach (string item in Enum.GetNames(typeof(IdleTimeoutAction))) + { + this.cbAction.Items.Add(item); + } - ckIgnoreDisplayRequested.Checked = Settings.IgnoreDisplayRequested; - ckIgnoreDisplayRequested.Enabled = !Settings.IsSettingFromPolicy(nameof(Settings.IgnoreDisplayRequested)); + this.cbAction.SelectedItem = Settings.Action.ToString(); + this.cbAction.Enabled = !Settings.IsSettingFromPolicy(nameof(Settings.Action)); try { - udMinutes.Value = Settings.IdleLimit; + this.udMinutes.Value = Settings.IdleLimit; } catch { - udMinutes.Value = 60; + this.udMinutes.Value = 60; } - udMinutes.Enabled = !Settings.IsSettingFromPolicy(nameof(Settings.IdleLimit)); + this.udMinutes.Enabled = !Settings.IsSettingFromPolicy(nameof(Settings.IdleLimit)); - if (!udMinutes.Enabled | !ckEnableIdleLogoff.Enabled | !ckIgnoreDisplayRequested.Enabled) + if (!this.udMinutes.Enabled | !this.ckEnableIdleLogoff.Enabled | !this.ckIgnoreDisplayRequested.Enabled) { - lbGPControlled.Visible = true; + this.lbGPControlled.Visible = true; } } @@ -49,19 +57,24 @@ private void btOK_Click(object sender, EventArgs e) { try { - if (ckEnableIdleLogoff.Enabled) + if (this.ckEnableIdleLogoff.Enabled) + { + Settings.Enabled = this.ckEnableIdleLogoff.Checked; + } + + if (this.udMinutes.Enabled) { - Settings.Enabled = ckEnableIdleLogoff.Checked; + Settings.IdleLimit = (int) this.udMinutes.Value; } - if (udMinutes.Enabled) + if (this.ckIgnoreDisplayRequested.Enabled) { - Settings.IdleLimit = (int) udMinutes.Value; + Settings.IgnoreDisplayRequested = this.ckIgnoreDisplayRequested.Checked; } - if (ckIgnoreDisplayRequested.Enabled) + if (this.cbAction.Enabled) { - Settings.IgnoreDisplayRequested = ckIgnoreDisplayRequested.Checked; + Settings.Action = (IdleTimeoutAction)Enum.Parse(typeof(IdleTimeoutAction), (string)this.cbAction.SelectedItem, true); } } catch (Exception ex) @@ -96,7 +109,7 @@ private void btOK_Click(object sender, EventArgs e) private void lbHiddenRefresh_Click(object sender, EventArgs e) { - RefreshUI(); + this.RefreshUI(); } private void btCancel_Click(object sender, EventArgs e) diff --git a/src/Lithnet.IdleLogoff/lithnet.idlelogoff.csproj b/src/Lithnet.IdleLogoff/lithnet.idlelogoff.csproj index a9c6bba..7e35364 100644 --- a/src/Lithnet.IdleLogoff/lithnet.idlelogoff.csproj +++ b/src/Lithnet.IdleLogoff/lithnet.idlelogoff.csproj @@ -97,6 +97,7 @@ + @@ -111,6 +112,7 @@ + @@ -145,6 +147,9 @@ + + "C:\Program Files (x86)\Microsoft SDKs\ClickOnce\SignTool\signtool.exe" sign /sha1 5ce168989b1359309dddee460c10e96fa60fa78b /t http://time.certum.pl /v $(TargetFileName) +