Wednesday, October 22, 2008

Synergy on Vista

I heard about Synergy several years back, hailed in some of my circles as the coolest thing since sliced bread, however I've only ever a single laptop or PC.  If you've never heard of it, it let's you share a keyboard and mouse between multiple computers.  It's a KVM without the V

In a strange twist of events, I've gone from one laptop to three.  While tools like KeyPass have eased the pain of floating passwords between machines, the worst challenge is adjusting to the radically different keyboard layouts.  Synergy with my new Bluetooth keyboard/mouse may be the answer.

Some useful links:

A few notes about configuration:

  • Make sure you can successfully ping between machines.  Consider adding entries to your hosts file to ensure proper name resolution
  • Don't forget bi-directional relationships!  If you only define one link, you can't drag your mouse back onto the other screen.
  • The configuration screens are klugey.  Just remember to click the "+" buttons when defining links -- huge grief saver.

Tuesday, October 07, 2008

Detecting Lock Workstation Session Events using WPF

A recent project at work needed to detect when the user locked and unlocked their workstation.  As I'd seen some great examples do this previously, I didn't think much of it.  However, because WPF is a very different beast compared to Windows Forms, it's a slightly different approach.

Neat stuff like detecting operating system events isn't part of the .NET framework and it requires calling outside to native Win32 API using P/Invoke features of the CLR.  If you've done any Win32 programming, you'd know that it's largely based on Handles, IntrPtr's and messages.  And as a breath of fresh air, the WPF API is focused primarily on building rich user-interfaces and is completely devoid of legacy Win32 programming concepts.  WPF is a huge leap, but worth it.

Fortunately, interoperability with Win32 is a breeze so if you want to tap into native Windows API, it's available if you're willing to write some code.

I should point out that most of this code has been adapted from this post over at the .NET Security Blog.  As shawnfa's post goes over the API in detail, I won't cover it here, rather I'll focus on how to pull this off in WPF.  Also, as I'm a big fan of composition in favour over inheritance, I've pulled all the session management stuff into an encapsulated class to make it easier to "mix in".  I haven't found any issues with this approach, but I'd welcome feedback.

Tapping into the Windows API can be mixed into your app using the HwndSource class.  It requires a handle to the calling window, and since the Handle property doesn't exist on the WPF Window class, you'll have to use the WindowInteropHelper to expose it.  The only gotcha here is that the Handle isn't available until after the window has been loaded.  This is analogous to the Form.OnHandleCreated method in Windows Form programming.

Here's my adapted sample:

using System;
using System.Runtime.InteropServices;
using System.Windows;
using System.Windows.Interop;

namespace LockedWorkstationExample
{
  public class SessionNotificationUtil : IDisposable
  {
     // from wtsapi32.h
     private const int NotifyForThisSession = 0;

     // from winuser.h
     private const int SessionChangeMessage = 0x02B1;
     private const int SessionLockParam = 0x7;
     private const int SessionUnlockParam = 0x8;

     [DllImport("wtsapi32.dll")]
     private static extern bool WTSRegisterSessionNotification(IntPtr hWnd, int dwFlags);

     [DllImport("wtsapi32.dll")]
     private static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd);

     // flag to indicate if we've registered for notifications or not
     private bool registered = false;

     WindowInteropHelper interopHelper;

     /// <summary>
     /// Constructor
     /// </summary>
     /// <param name="window"></param>
     public SessionNotificationUtil(Window window)
     {
         interopHelper = new WindowInteropHelper(window);
         window.Loaded += new RoutedEventHandler(window_Loaded);
     }

     // deferred initialization logic
     void window_Loaded(object sender, RoutedEventArgs e)
     {
         HwndSource source = HwndSource.FromHwnd(interopHelper.Handle);
         source.AddHook(new HwndSourceHook(WndProc));
         EnableRaisingEvents = true;
     }

     protected bool EnableRaisingEvents
     {
         get { return registered; }
         set
         {
            // WtsRegisterSessionNotification requires Windows XP or higher
            bool haveXp =   Environment.OSVersion.Platform == PlatformID.Win32NT &&
                 (Environment.OSVersion.Version.Major > 5 || 
                 (Environment.OSVersion.Version.Major == 5 &&
                 Environment.OSVersion.Version.Minor >= 1));

            if (!haveXp)
            {
                 registered = false;
                 return;
            }

            if (value == true && !registered)
            {
                 WTSRegisterSessionNotification(interopHelper.Handle, NotifyForThisSession);
                 registered = true;
            }
            else if (value == false && registered)
            {
                 WTSUnRegisterSessionNotification(interopHelper.Handle);
                 registered = false;
            }
         }
     }

     private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
     {
         if (msg == SessionChangeMessage)
         {
            if (wParam.ToInt32() == SessionLockParam)
            {
                OnSessionLock();
            }
            else if (wParam.ToInt32() == SessionUnlockParam)
            {
                OnSessionUnLock();
            }
         }
         return IntPtr.Zero;
     }

     private void OnSessionLock()
     {
         if (SessionChanged != null)
         {
            SessionChanged(this, new SessionNotificationEventArgs(SessionNotification.Lock));
         }
     }

     private void OnSessionUnLock()
     {
         if (SessionChanged != null)
         {
            SessionChanged(this, new SessionNotificationEventArgs(SessionNotification.Unlock));
         }
     }

     public event EventHandler<SessionNotificationEventArgs> SessionChanged;

     #region IDisposable Members
     public void Dispose()
     {
         // unhook from wtsapi
         if (registered)
         {
            EnableRaisingEvents = false;
         }
     }
     #endregion
 }

 public class SessionNotificationEventArgs : EventArgs
 {
     public SessionNotificationEventArgs(SessionNotification notification)
     {
        _notification = notification;
        _timestamp = DateTime.Now;
     }

     public SessionNotification Notification
     {
        get { return _notification; }
     }

     public DateTime TimeStamp
     {
        get { return _timestamp; }
     }

     private SessionNotification _notification;
     private DateTime _timestamp;
 }

 public enum SessionNotification
 {
     Lock = 0,
     Unlock
 }

}

At this point, I can mix in session notification wherever needed without having to introduce a base window class:

public partial class Window1 : Window
{
    public Window1()
    {
        InitializeComponent();
        SessionNotificationUtil util = new SessionNotificationUtil(this);
        util.SessionChanged += new EventHandler<SessionNotificationEventArgs>(util_SessionChanged);
    }

    void util_SessionChanged(object sender, SessionNotificationEventArgs e)
    {
        this.txtOutput.Text += String.Format("Recieved {0} notification at {1}\n", e.Notification, e.TimeStamp);
    }
}
Update 10/23/2008: Source code now available for download.

Casey Alexander

Well, it has been pretty quiet here on the blog for the last month only because my personal life has been pretty loud.

IMG_5263

6lbs-14oz, 20" long, October 1st.  Mom, baby and big brother are all well.