using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.Serialization.Formatters.Binary;
using System.Text;
using System.Windows.Forms;
using System.Reflection;
namespace Sessions
{
public delegate void SessionNotifiedEventHandler(object objectReceivedFromCallingProcess);
public class SessionManager : IDisposable
{
#region Private fields
/// <summary>
/// Name of the active session
/// </summary>
private string _sessionName;
/// <summary>
/// String to identify the current assembly
/// This string will be attached to the session window name
/// to make it unique. This id could also be used to identify
/// groups of sessions.
/// </summary>
private readonly string _appendToSessionNametoEnsureItsUnique;
/// <summary>
/// The native Window to communicate across processes using
/// WM_COPYDATA
/// </summary>
private CommunicationWindow _communicator;
#endregion
#region Singleton implementation - This object exists only once per process
/// <summary>
/// Holds SessionManager instance
/// </summary>
private static SessionManager _instance;
/// <summary>
/// Private Constructor to allow initialization only from within this class
/// </summary>
private SessionManager()
{
_sessionName = "";
// This could be any string, I use the Assembly GUID here.
// It will be trimmed from the window title later to read the session name
_appendToSessionNametoEnsureItsUnique = GetAssemblyGUID();
}
/// <summary>
/// Creates the SessionManager Instance if required
/// and returns it (or the existing one)
/// </summary>
private static SessionManager Instance
{
get
{
if (_instance == null)
_instance = new SessionManager();
return _instance;
}
}
#endregion
#region Methods - These methods will be used by the static Interface methods
/// <summary>
/// Starts a new Session with the given name
/// by creating a (hidden) session window
/// </summary>
/// <param name="sessionName">Name of the session</param>
private void StartNew(string sessionName)
{
_sessionName = sessionName;
// Initialize the Native Window
// The window will be invisibly running in background, only to be found by the findwindow method
_communicator = new CommunicationWindow(_sessionName + _appendToSessionNametoEnsureItsUnique);
}
/// <summary>
/// Shuts the session down by closing and destroying the native window
/// </summary>
private void Shutdown()
{
if (_communicator != null)
_communicator.DestroyHandle();
}
/// <summary>
/// Searches for the Session Window
/// </summary>
/// <param name="sessionName"></param>
/// <returns></returns>
private bool IsActive(string sessionName)
{
IntPtr handle = FindWindow(IntPtr.Zero, sessionName + _appendToSessionNametoEnsureItsUnique);
if (handle != IntPtr.Zero)
{
return true;
}
return false;
}
/// <summary>
/// Notifies another session about the launch of a new process.
/// The target session should now act as it was called.
/// The calling Session will exit immediately after calling.
/// </summary>
/// <param name="sessionName">Name of the session to send the notification to</param>
/// <param name="attachment">Attachment to be contained within the message. Usually command line string</param>
/// <returns>true if the message was sent, false if the target session was not found</returns>
private bool NotifySession(string sessionName, object attachment)
{
// Search for the window of the session to notify
IntPtr hWnd = FindWindow(IntPtr.Zero, sessionName + _appendToSessionNametoEnsureItsUnique);
if (hWnd != IntPtr.Zero)
{
// This is the Trick! Use a pinned handle to have a global pointer to the byte array.
// The pinned pointer allows us to access the memory location from another process
GCHandle bufferHandle = new GCHandle();
try
{
byte[] buffer;
COPYDATASTRUCT dataStructForMessage = new COPYDATASTRUCT();
if (attachment != null)
{
buffer = MakeBytes(attachment);
bufferHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned); // here we pin the buffer
dataStructForMessage.dwData = 0;
dataStructForMessage.cbData = buffer.Length;
//get the address of the pinned buffer
dataStructForMessage.lpData = bufferHandle.AddrOfPinnedObject();
}
GCHandle dataStructHandle = GCHandle.Alloc(dataStructForMessage, GCHandleType.Pinned); // the COPYDATASTRUCT must also be pinned!!
try
{
// Send the message to the target process and attach the structure
SendMessage(hWnd, WM_COPYDATA, IntPtr.Zero, dataStructHandle.AddrOfPinnedObject());
return true;
}
finally
{
dataStructHandle.Free(); // Free the handle to let the memory cleanup do its magic!
}
}
finally
{
if (bufferHandle.IsAllocated)
bufferHandle.Free(); // Free second handle too!
}
}
return false;
}
/// <summary>
/// Enumerates all Windows of the system and returns a list of those
/// ending with the string defined in _appendToSessionNametoEnsureItsUnique
/// (without _appendToSessionNametoEnsureItsUnique)
/// </summary>
/// <returns>List of Session Names</returns>
private List<string> GetListOfSessionWidows()
{
List<string> windowNames = new List<string>();
// Enumeration with nested delegate function for API callback
EnumWindows(delegate(IntPtr hWnd, IntPtr lParam)
{
string windowText = GetWindowText(hWnd);
if (windowText.EndsWith(_appendToSessionNametoEnsureItsUnique))
windowNames.Add(windowText.Replace(_appendToSessionNametoEnsureItsUnique, ""));
return 1;
}, new IntPtr(0));
return windowNames;
}
#endregion
#region Static Interface Methods - Use these to call from the outside
/// <summary>
/// Occurs when another process was launched targetting the same session name.
/// The other process will exit after this event was handled.
/// </summary>
public static event SessionNotifiedEventHandler Notified;
/// <summary>
/// Starts the Session.
/// </summary>
/// <param name="sessionName">Name of the session.</param>
public static void SessionStart(string sessionName)
{
Instance.StartNew(sessionName);
}
/// <summary>
/// Shuts the Session down.
/// </summary>
public static void SessionShutdown()
{
Instance.Shutdown();
}
/// <summary>
/// Determinse if a session with the given name is already active.
/// </summary>
/// <param name="sessionName">Name of the session.</param>
/// <returns>true if a session name is already used by another session</returns>
public static bool SessionIsActice(string sessionName)
{
return Instance.IsActive(sessionName);
}
/// <summary>
/// Notifies the active session and attaches an object to be sent.
/// </summary>
/// <param name="sessionName">Name of the session.</param>
/// <param name="attachment">The attachment.</param>
/// <returns>true if the message was sent, false if the target session was not found</returns>
public static bool NotifyActiveSession(string sessionName, object attachment)
{
return Instance.NotifySession(sessionName, attachment);
}
/// <summary>
/// Gets all active session names.
/// </summary>
/// <returns>List of names of running sessions</returns>
public static List<string> GetAllActiveSessionNames()
{
return Instance.GetListOfSessionWidows();
}
#endregion
#region Windows API Methods
[DllImport("user32.dll", EntryPoint = "FindWindow", SetLastError = true)]
static extern IntPtr FindWindow(IntPtr zeroOnly, string lpWindowName);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
private delegate int EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern int GetWindowText(IntPtr hWnd, [Out] StringBuilder lpString, int nMaxCount);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern int GetWindowTextLength(IntPtr hWnd);
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
static extern IntPtr SendMessage(IntPtr hWnd, int nMsg, IntPtr wParam, IntPtr lParam);
const short WM_COPYDATA = 74;
struct COPYDATASTRUCT
{
public int dwData;
public int cbData;
public IntPtr lpData;
}
#endregion
#region NativeWindow used to send and receive Inter Process Messages
/// <summary>
/// The CommunicationWindow class is used to identify the current session
/// from any process by using FindWindow. The Window Name will be the
/// Session id followed by a GUID to make sure the window name is unique.
/// </summary>
private sealed class CommunicationWindow : NativeWindow
{
public CommunicationWindow(string sessionname)
{
CreateParams cp = new CreateParams();
cp.Caption = sessionname;
CreateHandle(cp);
}
/// <summary>
/// Override WndProc to receive Messages from other Processes
///
/// Messages to catch are WM_COPYDATA messages. the lParam Value
/// points to a structure containing the commandline used to open the session
/// </summary>
/// <param name="m">The Message received</param>
protected override void WndProc(ref Message m)
{
if (m.Msg == WM_COPYDATA)
{
// Get the Copydata structure from the pointer in lParam
COPYDATASTRUCT dataStructFromMessage = (COPYDATASTRUCT)Marshal.PtrToStructure(m.LParam, typeof(COPYDATASTRUCT));
// This will be the object encapsulated in the message
object receivedObject = null;
if (dataStructFromMessage.cbData > 0 && dataStructFromMessage.lpData != IntPtr.Zero)
{
// Copy the bytes in the struct to our local buffer
byte[] buffer = new byte[dataStructFromMessage.cbData];
Marshal.Copy(dataStructFromMessage.lpData, buffer, 0, buffer.Length);
//deserialize the buffer to a new object
receivedObject = MakeObject(buffer);
}
// Raise event
if (Notified != null) // if evenHandlers are connected
Notified(receivedObject);
}
else
base.WndProc(ref m);
}
}
#endregion
#region Tool functions
/// <summary>
/// Determines the GUID of the current assembly
/// </summary>
/// <returns>GUID as string</returns>
private string GetAssemblyGUID()
{
string id = "";
foreach (object attr in Assembly.GetExecutingAssembly().GetCustomAttributes(true))
{
if (attr is GuidAttribute)
id = ((GuidAttribute)attr).Value;
}
return id;
}
/// <summary>
/// Gets the window text.
/// </summary>
/// <param name="hWnd">The handle to the window.</param>
/// <returns>Caption of the window</returns>
private string GetWindowText(IntPtr hWnd)
{
StringBuilder sb = new StringBuilder(GetWindowTextLength(hWnd) + 1);
GetWindowText(hWnd, sb, sb.Capacity);
return sb.ToString();
}
/// <summary>
/// Deserializes an object in memory.
/// </summary>
/// <param name="buffer">Byte array of the object received from an external process</param>
/// <returns>Deserialized object. Requires casting into its correct type.</returns>
private static object MakeObject(byte[] buffer)
{
using (MemoryStream stream = new MemoryStream(buffer))
{
return new BinaryFormatter().Deserialize(stream);
}
}
/// <summary>
/// Serializes an object in memory into a byte array.
/// This can be used to attach any object to the COPYDATASTRUCT
/// structure and send it to another process.the receivubg process
/// must know which type to expect to make this work.
/// </summary>
/// <param name="obj">An object to serialize</param>
/// <returns>A byte array containing the object.</returns>
private static byte[] MakeBytes(Object obj)
{
using (MemoryStream stream = new MemoryStream())
{
new BinaryFormatter().Serialize(stream, obj);
return stream.ToArray();
}
}
#endregion
#region IDisposable Members
public void Dispose()
{
Shutdown();
}
#endregion
}
}