SafeHandle vs IntPtr in WinAPIs and CloseHandle in C# - c#

I read many articles about SafeHandle and IDiposable but I still don't understand whether I should use SafeHandle and CloseHandle or not in C#.
MSDN SafeHandle example
IntPtr, SafeHandle and HandleRef - Explained
https://blog.benoitblanchon.fr/safehandle/.
The first source is similar to my question but however it doesn't answer my question.
Let's say I have the following 3 different examples that I got from Internet:
// Example 1
byte[] temp = new byte[IntPtr.Size];
fixed (byte* p = temp)
{
IntPtr SectionHandle = new IntPtr(p);
LARGE_INTEGER MaximumSize = new LARGE_INTEGER();
MaximumSize.LowPart = pNTHeader->OptionalHeader.SizeOfImage;
status = NtCreateSection(
SectionHandle,
SectionAccess.SECTION_MAP_EXECUTE | SectionAccess.SECTION_MAP_READ | SectionAccess.SECTION_MAP_WRITE,
IntPtr.Zero,
ref MaximumSize,
MemoryProtectionConstants.PAGE_EXECUTE_READWRITE,
AllocationTypes.SEC_COMMIT,
IntPtr.Zero);
}
// Example 2
IntPtr hFile = CreateFile(
path,
GenericRights.GENERIC_READ,
FileFlags.FILE_SHARE_DELETE | FileFlags.FILE_SHARE_READ | FileFlags.FILE_SHARE_WRITE,
IntPtr.Zero,
FileCreationFlags.OPEN_EXISTING,
FileFlags.FILE_ATTRIBUTE_NORMAL,
IntPtr.Zero);
if (hFile == INVALID_HANDLE_VALUE)
return false;
// Example 3
IntPtr hMap = CreateFileMapping(
hFile,
IntPtr.Zero,
MemoryProtectionConstants.PAGE_READONLY,
0,
0,
IntPtr.Zero);
if (hMap == IntPtr.Zero)
return false;
The examples are Windows APIs, so they are unmanaged. What I don't get is:
1) Should the all 3 examples use SafeHandle over IntPtr because they are unmanaged and if one of them shouldn't, why?
2) I find CloseHandle for a good practice in C/C++ but in C# I don't know if the Garbage Collector closes the handle automatically in the end? Should CloseHandle be used in the above examples and why?
3) The first example uses an unmanaged byte* to allocate memory on the heap. Can this procedure be done by not using unmanaged pointer? The code below is my guess, what do you think about it?
IntPtr SectionHandle = Marshal.AllocateHGlobal(sizeof(int));
... code ...
Marshal.FreeHGlobal(SectionHandle);
Edit:
Posting the code I edited:
[SecurityCritical]
public sealed class SafeHandleBuffer : SafeBuffer
{
public SafeHandleBuffer()
: base(true)
{
handle = Marshal.AllocHGlobal(IntPtr.Size);
}
public int GetValue() =>
Marshal.ReadInt32(handle);
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle()
{
Marshal.FreeHGlobal(handle);
return true;
}
}
public sealed class SafeHandleToken : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeHandleToken()
: base(true)
{
}
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.MayFail)]
protected override bool ReleaseHandle() =>
CloseHandle(handle);
}
// Including the IDisposable implementation in the class where the next example calls are found
private SafeHandleToken _fileHandle, _mappingHandle;
private SafeHandleBuffer _sectionHandle;
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
[SecurityPermission(SecurityAction.Demand, UnmanagedCode = true)]
protected virtual void Dispose(bool disposing)
{
if (_fileHandle != null && !_fileHandle.IsInvalid)
{
_fileHandle.Dispose();
}
if (_mappingHandle != null && !_mappingHandle.IsInvalid)
{
_mappingHandle.Dispose();
}
if (_sectionHandle != null && !_sectionHandle.IsInvalid)
{
_sectionHandle.Dispose();
}
}
// Example 1
SafeHandleBuffer tempHandle = new SafeHandleBuffer();
LARGE_INTEGER MaximumSize = new LARGE_INTEGER();
MaximumSize.LowPart = pNTHeader->OptionalHeader.SizeOfImage;
status = NtCreateSection(
tempHandle,
SectionAccess.SECTION_MAP_EXECUTE | SectionAccess.SECTION_MAP_READ | SectionAccess.SECTION_MAP_WRITE,
IntPtr.Zero,
ref MaximumSize,
MemoryProtectionConstants.PAGE_EXECUTE_READWRITE,
AllocationTypes.SEC_COMMIT,
IntPtr.Zero);
// Example 2
SafeHandleToken tempHandle = CreateFile(
path,
GenericRights.GENERIC_READ,
FileShare.ReadWrite | FileShare.Delete,
IntPtr.Zero,
FileMode.Open,
FileAttributes.Normal,
IntPtr.Zero);
Thread.Sleep(500);
_fileHandle = tempHandle;
if (_fileHandle.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());
// Example 3
tempHandle = CreateFileMapping(
_fileHandle,
IntPtr.Zero,
(uint)MemoryProtectionConstants.PAGE_READONLY | (uint)AllocationTypes.SEC_IMAGE,
0,
0,
IntPtr.Zero);
Thread.Sleep(500);
_mappingHandle = tempHandle;
if (_mappingHandle.IsInvalid)
throw new Win32Exception(Marshal.GetLastWin32Error());
Do you think I did everything alright? Should SafeHandle be used on every Windows API like GetModuleHandle, GetProcAddress, MapViewOfFile and some more I don't remember right now. Some of them are returning LPVOID.

Related

What's wrong with this unmanaged code in C#?

I was trying to get text from each control in hierarchy. The following code runs fine if I use the unsafe method. However, using the unmanaged version seems to break hWnd, which in result hWnd = GetAncestor(hWnd, GetAncestorFlags.GA_PARENT) complains:
System.AccessViolationException: 'Attempted to read or write protected memory. This is often an indication that other memory is corrupt.'
I have checked hWnd was not changed after returning from GetWindowTextRaw function, and if I comment out the second SendMessage in this function will not cause the issue (although it will clearly not get the window text).
(PS: I'm using PInvoke.User32 in NuGet)
// using static PInvoke.User32;
public static string GetWindowTextRaw(IntPtr hWnd) {
// Allocate correct string length first
int length = (int)SendMessage(hWnd, WindowMessage.WM_GETTEXTLENGTH, IntPtr.Zero, IntPtr.Zero);
char[] buff = new char[length + 1];
IntPtr iptr = Marshal.AllocHGlobal(buff.Length);
SendMessage(hWnd, WindowMessage.WM_GETTEXT, (IntPtr)(length + 1), iptr);
Marshal.Copy(iptr, buff, 0, length + 1);
Marshal.FreeHGlobal(iptr);
//unsafe
//{
// fixed (char* p = buff)
// SendMessage(hWnd, WindowMessage.WM_GETTEXT, (IntPtr)(length + 1), (IntPtr)p);
//}
return new string(buff).TrimEnd('\0');
}
private void button1_Click(object sender, EventArgs {
POINT p;
IntPtr hWnd;
//while (true)
if (GetCursorPos(out p)) {
hWnd = WindowFromPoint(p); ;
Debug.Print($"{p.x} {p.y} 0x{(int)hWnd:x8}");
while (hWnd != IntPtr.Zero) {
Debug.Print($"{GetWindowTextRaw(hWnd)}");
hWnd = GetAncestor(hWnd, GetAncestorFlags.GA_PARENT);
}
Thread.Sleep(500);
}
}
IntPtr iptr = Marshal.AllocHGlobal(buff.Length);
Wrong size, you need buff.Length * sizeof(char). Twice as much as it allocates now. As written, the code corrupts the same heap that the OS uses, anything can happen next. An AVE is a normal and happy outcome, but not guaranteed.

Can't send WM_INPUTLANGCHANGEREQUEST to some controls

I'm working on (yet another) keyboard layout switcher and got strange troubles with Skype window (ver 6.22 on win7 x64). Any combinations of GetForegroundWindow() / GetFocus() / GetParentWindow() don't succeed to change the layout only inside the message input and, even more strange, only if more than one character is entered. Other cases work perfectly nice except wpf apps which refuse to obey without focusedHandle stuff.
public static void SetNextKeyboardLayout()
{
IntPtr hWnd = GetForegroundWindow();
uint processId;
uint activeThreadId = GetWindowThreadProcessId(hWnd, out processId);
uint currentThreadId = GetCurrentThreadId();
AttachThreadInput(activeThreadId, currentThreadId, true);
IntPtr focusedHandle = GetFocus();
AttachThreadInput(activeThreadId, currentThreadId, false);
PostMessage(focusedHandle == IntPtr.Zero ? hWnd : focusedHandle, WM_INPUTLANGCHANGEREQUEST, INPUTLANGCHANGE_FORWARD, HKL_NEXT);
}
I'm new to winapi things, so any help will be kindly appreciated, thank you.
After disassembling some of working products i've figured out that i was close to the right algorythm which looks like this:
public static void SetNextKeyboardLayout()
{
IntPtr hWnd = IntPtr.Zero;
var threadId = GetWindowThreadProcessId(GetForegroundWindow(), IntPtr.Zero);
var currentThreadId = GetCurrentThreadId();
var info = new GUITHREADINFO();
info.cbSize = Marshal.SizeOf(info);
var success = GetGUIThreadInfo(threadId, ref info);
// target = hwndCaret || hwndFocus || (AttachThreadInput + GetFocus) || hwndActive || GetForegroundWindow
AttachThreadInput(threadId, currentThreadId, true);
IntPtr focusedHandle = GetFocus();
AttachThreadInput(threadId, currentThreadId, false);
if (success)
{
if (info.hwndCaret != IntPtr.Zero) { hWnd = info.hwndCaret; }
else if (info.hwndFocus != IntPtr.Zero) { hWnd = info.hwndFocus; }
else if (focusedHandle != IntPtr.Zero) { hWnd = focusedHandle; }
else if (info.hwndActive != IntPtr.Zero) { hWnd = info.hwndActive; }
}
else
{
hWnd = focusedHandle;
}
if (hWnd == IntPtr.Zero) { hWnd = GetForegroundWindow(); }
PostMessage(hWnd, WM_INPUTLANGCHANGEREQUEST, INPUTLANGCHANGE_FORWARD, HKL_NEXT);
}
But the problem was not in finding PostMessage target hWnd, but in skype's input handling. I have solved it by adding a tiny delay before WM_INPUTLANGCHANGEREQUEST so skype can properly process all the input sent to it. Now i have to get things working without this delay but this is another story.
You should try this: PostMessage(hWnd,WM_INPUTLANGCHANGEREQUEST,0,(LPARAM)HKL_NEXT);
P.S.:
Under Windows 10 any WM_INPUTLANGCHANGEREQUEST crashes Skype.
Best way with Windows 10 -- is emulate keys for switch keyboard layout, like this:
keybd_event(VK_LWIN, 0, KEYEVENTF_EXTENDEDKEY, 0);
keybd_event(VK_SPACE,0, KEYEVENTF_EXTENDEDKEY, 0);
Sleep(10);
keybd_event(VK_SPACE,0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);
keybd_event(VK_LWIN, 0, KEYEVENTF_EXTENDEDKEY | KEYEVENTF_KEYUP, 0);

DeviceIoControl can't eject a non-empty CDROM drive?

I tried using DeviceIoControl function (Win32 API function) to eject my CDROM drive, it works perfectly when my CDROM drive has no disk, but after inserting a disk in it, the Marshal.GetLastWin32Error() returned 32 (ERROR_SHARING_VIOLATION: The process cannot access the file because it is being used by another process), the driveHandle passed in the DeviceIoControl is created by CreateFile() function.
Could you please help me out? I like this way of manipulating the CD ROM related stuff, I can use winmm.dll to eject my CDROM but I think this way is worth to try.
OK, here is the code:
using System;
using System.ComponentModel;
using System.Text;
using System.Windows.Forms;
using System.IO;
using System.Runtime.InteropServices;
namespace DVD_ejector
{
public partial class Form1 : Form
{
const int OPENEXISTING = 3;
const int IOCTL_STORAGE_EJECT_MEDIA = 2967560;
const uint GENERICREAD = 0x80000000;
const int INVALID_HANDLE = -1;
public Form1()
{
InitializeComponent();
DriveInfo[] drs = DriveInfo.GetDrives();
List<DriveInfo> cdRoms = new List<DriveInfo>();
foreach (DriveInfo dInfo in drs)
{
if (dInfo.DriveType == DriveType.CDRom)
{
cdRoms.Add(dInfo);
}
}
comboBox1.DataSource = cdRoms;
comboBox1.DisplayMember = "Name";
if (comboBox1.Items.Count > 0) comboBox1.SelectedIndex = 0;
button1.Click += (sender, e) =>
{
Eject(#"\\.\" + ((DriveInfo)comboBox1.SelectedItem).Name[0]+":");
};
}
[DllImport("kernel32", SetLastError=true)]
static extern IntPtr CreateFile(string fileName, uint desiredAccess, uint shareMode, IntPtr attributes,uint creationDisposition, uint flagsAndAttribute, IntPtr fileTemplate);
[DllImport("kernel32")]
static extern int CloseHandle(IntPtr fileHandle);
[DllImport("kernel32")]
static extern bool DeviceIoControl(IntPtr driveHandle, int ctrlCode, IntPtr inBuffer, int inBufferSize, IntPtr outBuffer, int outBufferSize, ref int bytesReturned, IntPtr overlapped);
int bytesReturned;
private void Eject(string cdDrive)
{
IntPtr driveHandle = CreateFile(cdDrive, GENERICREAD, 0, IntPtr.Zero, OPENEXISTING, 0, IntPtr.Zero);
try
{
if((int)driveHandle != INVALID_HANDLE)
DeviceIoControl(driveHandle, IOCTL_STORAGE_EJECT_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, ref bytesReturned, IntPtr.Zero);
}
catch (Exception ex)
{
MessageBox.Show(ex.Message);
}
finally
{
CloseHandle(driveHandle);
}
}
}
}
As the error states, the device is being used by something else, but it's failing on the CreateFile call instead of the DeviceIoControl, and your code is not correctly checking for the failure.
The reason you're getting a sharing violation is because you're trying to open the device exclusively which will fail if ANYTHING has tried to open it or a file on it, including anti virus, Explorer, Search indexer, etc.
This updated Eject function fixes the share mode and the error handling and now reports the errors in the correct places.
private void Eject(string cdDrive) {
IntPtr driveHandle = new IntPtr(INVALID_HANDLE);
try {
// Open the device
driveHandle = CreateFile(cdDrive, GENERICREAD, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, IntPtr.Zero, OPENEXISTING, 0, IntPtr.Zero);
if ((int)driveHandle == INVALID_HANDLE) { throw new Win32Exception(); }
// Try and eject
bool ejected = DeviceIoControl(driveHandle, IOCTL_STORAGE_EJECT_MEDIA, IntPtr.Zero, 0, IntPtr.Zero, 0, ref bytesReturned, IntPtr.Zero);
if (!ejected) { throw new Win32Exception(); }
} catch (Exception ex) {
MessageBox.Show(ex.Message);
} finally {
if ((int)driveHandle != INVALID_HANDLE) { CloseHandle(driveHandle); }
}
}

Taskbar location

How can i detect where the taskbar is located? I need to know for displaying my notification in the right corner. Thanks
Edit:
Thank you Hans Passant. I used that with this to get location. I hope is ok.
GetTaskbarLocation(TaskbarPosition.GetTaskbarPosition());
private void GetTaskbarLocation(Rectangle rc)
{
if (rc.X == rc.Y)
{
if (rc.Right < rc.Bottom)
taskbarLocation = TaskbarLocation.Left;
if (rc.Right > rc.Bottom)
taskbarLocation = TaskbarLocation.Top;
}
if (rc.X > rc.Y)
taskbarLocation = TaskbarLocation.Right;
if (rc.X < rc.Y)
taskbarLocation = TaskbarLocation.Bottom;
}
public static Rectangle GetTaskbarPosition() {
var data = new APPBARDATA();
data.cbSize = System.Runtime.InteropServices.Marshal.SizeOf(data);
IntPtr retval = SHAppBarMessage(ABM_GETTASKBARPOS, ref data);
if (retval == IntPtr.Zero) throw new Win32Exception("Please re-install Windows");
return new Rectangle(data.rc.left, data.rc.top,
data.rc.right - data.rc.left, data.rc.bottom - data.rc.top);
}
// P/Invoke goo:
private const int ABM_GETTASKBARPOS = 5;
[System.Runtime.InteropServices.DllImport("shell32.dll")]
private static extern IntPtr SHAppBarMessage(int msg, ref APPBARDATA data);
private struct APPBARDATA {
public int cbSize;
public IntPtr hWnd;
public int uCallbackMessage;
public int uEdge;
public RECT rc;
public IntPtr lParam;
}
private struct RECT {
public int left, top, right, bottom;
}
SHAppBarMessage(ABM_GETTASKBARPOS)
See the SHAppBarMessage Function and the ABM_GETTASKBARPOS Message for more info and the pinvoke page for SHAppBarMessage has a VB.Net sample that shouldn't be too difficult to translate.
The SHAppBarMessage function will return you information about the taskbar if you pass in the ABM_GETTASKBARPOS message. It has an out parameter which is a pointer to APPBARDATA that contains the screen cooridinates of the task bar. You can use to work out where on screen it is.
It's probably best to use the available API: NotifyIcon.ShowBalloonTip:
void Form1_DoubleClick(object sender, EventArgs e)
{
notifyIcon1.Visible = true;
notifyIcon1.ShowBalloonTip(20000, "Information", "This is the text",
ToolTipIcon.Info );
}
In Java, using JNA (adapted from other C# solutions above)
public static Rectangle getTaskbarPosition() throws Exception {
APPBARDATA data = new APPBARDATA();
data.cbSize = new WinDef.DWORD(data.size());
WinDef.UINT_PTR retval = Shell32.INSTANCE.SHAppBarMessage(ABM_GETTASKBARPOS, data);
if (retval == null) {
throw new Exception("Please re-install Windows");
}
return new Rectangle(data.rc.left, data.rc.top, data.rc.right - data.rc.left, data.rc.bottom - data.rc.top);
}

Programmatically Determine a Duration of a Locked Workstation?

How can one determine, in code, how long the machine is locked?
Other ideas outside of C# are also welcome.
I like the windows service idea (and have accepted it) for simplicity and cleanliness, but unfortunately I don't think it will work for me in this particular case. I wanted to run this on my workstation at work rather than home (or in addition to home, I suppose), but it's locked down pretty hard courtesy of the DoD. That's part of the reason I'm rolling my own, actually.
I'll write it up anyway and see if it works. Thanks everyone!
I hadn't found this before, but from any application you can hookup a SessionSwitchEventHandler. Obviously your application will need to be running, but so long as it is:
Microsoft.Win32.SystemEvents.SessionSwitch += new Microsoft.Win32.SessionSwitchEventHandler(SystemEvents_SessionSwitch);
void SystemEvents_SessionSwitch(object sender, Microsoft.Win32.SessionSwitchEventArgs e)
{
if (e.Reason == SessionSwitchReason.SessionLock)
{
//I left my desk
}
else if (e.Reason == SessionSwitchReason.SessionUnlock)
{
//I returned to my desk
}
}
I would create a Windows Service (a visual studio 2005 project type) that handles the OnSessionChange event as shown below:
protected override void OnSessionChange(SessionChangeDescription changeDescription)
{
if (changeDescription.Reason == SessionChangeReason.SessionLock)
{
//I left my desk
}
else if (changeDescription.Reason == SessionChangeReason.SessionUnlock)
{
//I returned to my desk
}
}
What and how you log the activity at that point is up to you, but a Windows Service provides quick and easy access to windows events like startup, shutdown, login/out, along with the lock and unlock events.
The solution below uses the Win32 API. OnSessionLock is called when the workstation is locked, and OnSessionUnlock is called when it is unlocked.
[DllImport("wtsapi32.dll")]
private static extern bool WTSRegisterSessionNotification(IntPtr hWnd,
int dwFlags);
[DllImport("wtsapi32.dll")]
private static extern bool WTSUnRegisterSessionNotification(IntPtr
hWnd);
private const int NotifyForThisSession = 0; // This session only
private const int SessionChangeMessage = 0x02B1;
private const int SessionLockParam = 0x7;
private const int SessionUnlockParam = 0x8;
protected override void WndProc(ref Message m)
{
// check for session change notifications
if (m.Msg == SessionChangeMessage)
{
if (m.WParam.ToInt32() == SessionLockParam)
OnSessionLock(); // Do something when locked
else if (m.WParam.ToInt32() == SessionUnlockParam)
OnSessionUnlock(); // Do something when unlocked
}
base.WndProc(ref m);
return;
}
void OnSessionLock()
{
Debug.WriteLine("Locked...");
}
void OnSessionUnlock()
{
Debug.WriteLine("Unlocked...");
}
private void Form1Load(object sender, EventArgs e)
{
WTSRegisterSessionNotification(this.Handle, NotifyForThisSession);
}
// and then when we are done, we should unregister for the notification
// WTSUnRegisterSessionNotification(this.Handle);
I know this is an old question but i have found a method to get the Lock State for a given session.
I found my answer here but it was in C++ so i translated as much as i can to C# to get the Lock State.
So here goes:
static class SessionInfo {
private const Int32 FALSE = 0;
private static readonly IntPtr WTS_CURRENT_SERVER = IntPtr.Zero;
private const Int32 WTS_SESSIONSTATE_LOCK = 0;
private const Int32 WTS_SESSIONSTATE_UNLOCK = 1;
private static bool _is_win7 = false;
static SessionInfo() {
var os_version = Environment.OSVersion;
_is_win7 = (os_version.Platform == PlatformID.Win32NT && os_version.Version.Major == 6 && os_version.Version.Minor == 1);
}
[DllImport("wtsapi32.dll")]
private static extern Int32 WTSQuerySessionInformation(
IntPtr hServer,
[MarshalAs(UnmanagedType.U4)] UInt32 SessionId,
[MarshalAs(UnmanagedType.U4)] WTS_INFO_CLASS WTSInfoClass,
out IntPtr ppBuffer,
[MarshalAs(UnmanagedType.U4)] out UInt32 pBytesReturned
);
[DllImport("wtsapi32.dll")]
private static extern void WTSFreeMemoryEx(
WTS_TYPE_CLASS WTSTypeClass,
IntPtr pMemory,
UInt32 NumberOfEntries
);
private enum WTS_INFO_CLASS {
WTSInitialProgram = 0,
WTSApplicationName = 1,
WTSWorkingDirectory = 2,
WTSOEMId = 3,
WTSSessionId = 4,
WTSUserName = 5,
WTSWinStationName = 6,
WTSDomainName = 7,
WTSConnectState = 8,
WTSClientBuildNumber = 9,
WTSClientName = 10,
WTSClientDirectory = 11,
WTSClientProductId = 12,
WTSClientHardwareId = 13,
WTSClientAddress = 14,
WTSClientDisplay = 15,
WTSClientProtocolType = 16,
WTSIdleTime = 17,
WTSLogonTime = 18,
WTSIncomingBytes = 19,
WTSOutgoingBytes = 20,
WTSIncomingFrames = 21,
WTSOutgoingFrames = 22,
WTSClientInfo = 23,
WTSSessionInfo = 24,
WTSSessionInfoEx = 25,
WTSConfigInfo = 26,
WTSValidationInfo = 27,
WTSSessionAddressV4 = 28,
WTSIsRemoteSession = 29
}
private enum WTS_TYPE_CLASS {
WTSTypeProcessInfoLevel0,
WTSTypeProcessInfoLevel1,
WTSTypeSessionInfoLevel1
}
public enum WTS_CONNECTSTATE_CLASS {
WTSActive,
WTSConnected,
WTSConnectQuery,
WTSShadow,
WTSDisconnected,
WTSIdle,
WTSListen,
WTSReset,
WTSDown,
WTSInit
}
public enum LockState {
Unknown,
Locked,
Unlocked
}
[StructLayout(LayoutKind.Sequential)]
private struct WTSINFOEX {
public UInt32 Level;
public UInt32 Reserved; /* I have observed the Data field is pushed down by 4 bytes so i have added this field as padding. */
public WTSINFOEX_LEVEL Data;
}
[StructLayout(LayoutKind.Sequential)]
private struct WTSINFOEX_LEVEL {
public WTSINFOEX_LEVEL1 WTSInfoExLevel1;
}
[StructLayout(LayoutKind.Sequential)]
private struct WTSINFOEX_LEVEL1 {
public UInt32 SessionId;
public WTS_CONNECTSTATE_CLASS SessionState;
public Int32 SessionFlags;
/* I can't figure out what the rest of the struct should look like but as i don't need anything past the SessionFlags i'm not going to. */
}
public static LockState GetSessionLockState(UInt32 session_id) {
IntPtr ppBuffer;
UInt32 pBytesReturned;
Int32 result = WTSQuerySessionInformation(
WTS_CURRENT_SERVER,
session_id,
WTS_INFO_CLASS.WTSSessionInfoEx,
out ppBuffer,
out pBytesReturned
);
if (result == FALSE)
return LockState.Unknown;
var session_info_ex = Marshal.PtrToStructure<WTSINFOEX>(ppBuffer);
if (session_info_ex.Level != 1)
return LockState.Unknown;
var lock_state = session_info_ex.Data.WTSInfoExLevel1.SessionFlags;
WTSFreeMemoryEx(WTS_TYPE_CLASS.WTSTypeSessionInfoLevel1, ppBuffer, pBytesReturned);
if (_is_win7) {
/* Ref: https://msdn.microsoft.com/en-us/library/windows/desktop/ee621019(v=vs.85).aspx
* Windows Server 2008 R2 and Windows 7: Due to a code defect, the usage of the WTS_SESSIONSTATE_LOCK
* and WTS_SESSIONSTATE_UNLOCK flags is reversed. That is, WTS_SESSIONSTATE_LOCK indicates that the
* session is unlocked, and WTS_SESSIONSTATE_UNLOCK indicates the session is locked.
* */
switch (lock_state) {
case WTS_SESSIONSTATE_LOCK:
return LockState.Unlocked;
case WTS_SESSIONSTATE_UNLOCK:
return LockState.Locked;
default:
return LockState.Unknown;
}
}
else {
switch (lock_state) {
case WTS_SESSIONSTATE_LOCK:
return LockState.Locked;
case WTS_SESSIONSTATE_UNLOCK:
return LockState.Unlocked;
default:
return LockState.Unknown;
}
}
}
}
Note: The above code was extracted from a much larger project so if i missed a bit sorry. I havn't got time to test the above code but plan to come back in a week or two to check everything. I only posted it now because i didn't want to forget to do it.
NOTE: This is not an answer, but a (contribution) to Timothy Carter answer, because my reputation doesn't allow me to comment so far.
Just in case somebody tried the code from Timothy Carter's answer and did not get it to work right away in a Windows service, there's one property that need to be set to true in the constructor of the service.
Just add the line in the constructor:
CanHandleSessionChangeEvent = true;
And be sure not to set this property after the service is started otherwise an InvalidOperationException will be thrown.
If you're interested in writing a windows-service to "find" these events, topshelf (the library/framework that makes writing windows services much easier) has a hook.
public interface IMyServiceContract
{
void Start();
void Stop();
void SessionChanged(Topshelf.SessionChangedArguments args);
}
public class MyService : IMyServiceContract
{
public void Start()
{
}
public void Stop()
{
}
public void SessionChanged(SessionChangedArguments e)
{
Console.WriteLine(e.ReasonCode);
}
}
and now the code to wire up the topshelf service to the interface/concrete above
Everything below is "typical" topshelf setup.... except for 2 lines which I marked as
/* THIS IS MAGIC LINE */
Those are what get the SessionChanged method to fire.
I tested this with windows 10 x64. I locked and unlocked my machine and I got the desired result.
IMyServiceContract myServiceObject = new MyService(); /* container.Resolve<IMyServiceContract>(); */
HostFactory.Run(x =>
{
x.Service<IMyServiceContract>(s =>
{
s.ConstructUsing(name => myServiceObject);
s.WhenStarted(sw => sw.Start());
s.WhenStopped(sw => sw.Stop());
s.WhenSessionChanged((csm, hc, chg) => csm.SessionChanged(chg)); /* THIS IS MAGIC LINE */
});
x.EnableSessionChanged(); /* THIS IS MAGIC LINE */
/* use command line variables for the below commented out properties */
/*
x.RunAsLocalService();
x.SetDescription("My Description");
x.SetDisplayName("My Display Name");
x.SetServiceName("My Service Name");
x.SetInstanceName("My Instance");
*/
x.StartManually(); // Start the service manually. This allows the identity to be tweaked before the service actually starts
/* the below map to the "Recover" tab on the properties of the Windows Service in Control Panel */
x.EnableServiceRecovery(r =>
{
r.OnCrashOnly();
r.RestartService(1); ////first
r.RestartService(1); ////second
r.RestartService(1); ////subsequents
r.SetResetPeriod(0);
});
x.DependsOnEventLog(); // Windows Event Log
x.UseLog4Net();
x.EnableShutdown();
x.OnException(ex =>
{
/* Log the exception */
/* not seen, I have a log4net logger here */
});
});
My packages.config to provide hints about versions:
<package id="log4net" version="2.0.5" targetFramework="net45" />
<package id="Topshelf" version="4.0.3" targetFramework="net461" />
<package id="Topshelf.Log4Net" version="4.0.3" targetFramework="net461" />
In Windows Task Scheduler, you could create tasks that trigger on workstation lock and on workstation unlock. Each task could write a flag and timestamp to a file to state if the workstation is locked or unlocked and when it happened.
I realize that this is not a programmatic way. It is simpler than writing a service. It won't miss an event because your program happens to not be running at the time of lock/unlock transition.
Below is the 100% working code to find if the PC is locked or not.
Before using this use the namespace System.Runtime.InteropServices.
[DllImport("user32", EntryPoint = "OpenDesktopA", CharSet = CharSet.Ansi,SetLastError = true, ExactSpelling = true)]
private static extern Int32 OpenDesktop(string lpszDesktop, Int32 dwFlags, bool fInherit, Int32 dwDesiredAccess);
[DllImport("user32", CharSet = CharSet.Ansi, SetLastError = true, ExactSpelling = true)]
private static extern Int32 CloseDesktop(Int32 hDesktop);
[DllImport("user32", CharSet = CharSet.Ansi,SetLastError = true,ExactSpelling = true)]
private static extern Int32 SwitchDesktop(Int32 hDesktop);
public static bool IsWorkstationLocked()
{
const int DESKTOP_SWITCHDESKTOP = 256;
int hwnd = -1;
int rtn = -1;
hwnd = OpenDesktop("Default", 0, false, DESKTOP_SWITCHDESKTOP);
if (hwnd != 0)
{
rtn = SwitchDesktop(hwnd);
if (rtn == 0)
{
// Locked
CloseDesktop(hwnd);
return true;
}
else
{
// Not locked
CloseDesktop(hwnd);
}
}
else
{
// Error: "Could not access the desktop..."
}
return false;
}

Categories