I am relatively new to programming and I took it upon myself to make a simple chat application; think AIM from back in the day. This is more of a request for a code review than a list of questions. For the most part, the application works with a few minor errors which I will highlight below. Please see the code from the project: https://github.com/N8STROMO/Basic-Chat-Tcp as well as the code listed below. Questions are embedded along the way. Thanks for any help you may be able to provide!
Here is the Server Code: Server.cs
namespace Server
{
public class Server
{
// This is the port that the server will be listening on
const int PORT = 500;
// This is where the usernames and their connections will be held
readonly Dictionary<string, ServerUserConnection> userToConnections = new Dictionary<string, ServerUserConnection>();
readonly TcpListener listener;
public static void Main()
{
// Constructor for the ChatServer
Server server = new Server();
while(true)
{
Thread.Sleep(1); // Temp to keep alive
}
}
/// <summary>
/// Listens for data from clients
/// </summary>
public Server()
{
listener = TcpListener.Create(PORT);
listener.Start();
WaitForConnections();
}
/// <summary>
/// Begins an asynchronous operation to accept an incoming connection attempt
/// </summary>
private void WaitForConnections()
{
listener.BeginAcceptTcpClient(OnConnect, null);
}
/// <summary>
/// This method is executed asynchronously
/// Connects the client to the server
/// Broadcasts the user to client to be displayed on the chatform
/// Then waits for another connection to be established
/// </summary>
/// <param name="ar"></param>
void OnConnect(IAsyncResult ar)
{
//Asynchronously accepts an incoming connection attempt and creates a new TcpClient to handle remote host communication.
TcpClient client = listener.EndAcceptTcpClient(ar);
Console.WriteLine("Connected");
ReceiveUser(client);
BroadcastUserList();
WaitForConnections();
}
/// <summary>
/// Connects a user to the server and adds them to the dictionary userToConnections
/// </summary>
/// <param name="client"></param>
public void ReceiveUser(TcpClient client)
{
ServerUserConnection connection = new ServerUserConnection(this, client); // Constructor
userToConnections.Add(connection.userName, connection);
}
/// <summary>
/// For each user that is connected append the userList to include that user
/// TODO Do not need to keep a running list of users; send the user over then throw it away
/// </summary>
void BroadcastUserList()
{
string userList = "";
foreach(var connection in userToConnections)
{
userList += $"{connection.Value.userName},";
}
SendMsgToAll(MessageType.UserList, null, userList);
}
/// <summary>
/// Pushes out messages to the connected clients
/// </summary>
/// <param name="type"></param>
/// <param name="user"></param>
/// <param name="message"></param>
public void SendMsgToAll(MessageType type, ServerUserConnection user, string message)
{
Console.WriteLine($"{user?.userName}: {message}");
foreach(var connection in userToConnections)
{
Console.WriteLine($"Sending to {connection.Value.userName}");
Utils.SendInformation(type, connection.Value.stream, message);
}
}
}
}
Here is the ServerUserConnection.cs: I am having trouble understanding how the inheritance here works and what the base keyword does.
namespace Server
{
public class ServerUserConnection : UserConnection
{
readonly Server server;
/// <summary>
/// Facilitates connection
/// </summary>
/// <param name="server"></param>
/// <param name="client"></param>
public ServerUserConnection(Server server, TcpClient client) : base(client, GetUsername(client)) // Inherits from UserConnection()
{
this.server = server;
}
private static string GetUsername(TcpClient client)
{
NetworkStream stream = client.GetStream();
if(stream.CanRead)
{
// Receives infromation from the stream, determines MessageType, and returns username
string userName = Utils.ReceiveInformation(stream, client, out MessageType type);
Console.WriteLine(userName);
return userName;
}
return null;
}
/// <summary>
///
/// </summary>
/// <param name="type"></param>
/// <param name="message"></param>
protected override void OnRead(MessageType type, string message)
{
if(type != MessageType.ChatMessage)
{
return;
}
server.SendMsgToAll(MessageType.ChatMessage, this, $"{userName} {message}");
}
}
}
Here is the Client Code: Client.cs
namespace Client
{
public class ChatClient
{
private const int PORT = 500;
TcpClient client = new TcpClient();
public ChatForm chatForm;
public ClientUserConnection userConnection;
string data;
[STAThread]
public static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
var client = new ChatClient();
var form = new ConnectForm(client);
Application.Run(form);
}
/// <summary>
/// This is called when the ConnectForm btnSubmit is pressed.
/// </summary>
/// <param name="ipAddress"></param>
/// <param name="userName"></param>
public void ConnectToServer(string ipAddress, string userName)
{
client.Connect(ipAddress, PORT);
SendUserName(userName);
}
/// <summary>
/// Sends user to the server
/// </summary>
public void SendUserName(string user)
{
userConnection = new ClientUserConnection(client, this, user);
Utils.SendInformation(MessageType.Connect, client.GetStream(), user);
}
/// <summary>
/// Sends a message to the server
/// </summary>
/// <param name="msg"></param>
public void SendMessage(string msg)
{
Utils.SendInformation(MessageType.ChatMessage, userConnection.stream, msg);
}
}
}
Here is the ClientUserConnection.cs: Is there a way to refactor or optimize this so that instead of keeping a running list of messages just send the message over and throw it away? Again, the same question about inheritance.
public class ClientUserConnection : UserConnection
{
public readonly Client.ChatClient chatClient;
public string message = "";
public string userListText;
/// <summary>
///
/// </summary>
/// <param name="client"></param>
/// <param name="chatClient"></param>
/// <param name="userName"></param>
public ClientUserConnection(TcpClient client, Client.ChatClient chatClient, string userName) : base(client, userName) // Inherits from UserConnection()
{
this.chatClient = chatClient;
}
/// <summary>
/// When the data is reads determine what kind of message it is
/// Parse/split out the message and user; display only relevant data
/// TODO Do not need to keep a running list of messages; send the message over then throw it away
/// </summary>
/// <param name="type"></param>
/// <param name="message"></param>
protected override void OnRead(MessageType type, string message)
{
if(type == MessageType.ChatMessage)
{
int iSpace = message.IndexOf(" ");
if(iSpace < 0)
{
// if error
return;
}
string from = message.Substring(0, iSpace);
string chatMessage = message.Substring(iSpace + 1, message.Length - iSpace - 1);
this.message += $"[{from}]: {chatMessage}{Environment.NewLine}";
}
else if(type == MessageType.UserList)
{
string[] userList = message.Split(',');
string userListText = "";
for(int i = 0; i < userList.Length; i++)
{
userListText += $"{userList[i]}{Environment.NewLine}";
}
this.userListText = userListText;
}
}
}
There are also three files within a class library that are used within this project:
This is the MessageType.cs:
public enum MessageType
{
Connect, UserList, ChatMessage
}
This is the UserConnection.cs: I still need to deal with disconnecting users. If one of the chat forms is closed, it crashes the application.
public abstract class UserConnection
{
public readonly TcpClient client;
public readonly NetworkStream stream;
public readonly string userName;
byte[] data;
/// <summary>
///
/// </summary>
/// <param name="client"></param>
/// <param name="userName"></param>
public UserConnection(TcpClient client, string userName)
{
this.client = client;
this.userName = userName;
stream = client.GetStream();
data = new byte[client.ReceiveBufferSize];
WaitForData();
}
/// <summary>
///
/// </summary>
private void WaitForData()
{
Console.WriteLine("Wait");
stream.BeginRead(data, 0, data.Length, OnReadData, null);
}
/// <summary>
/// SocketException: An existing connection was forcibly closed by the remote host
/// </summary>
/// <param name="ar"></param>
void OnReadData(IAsyncResult ar)
{
Console.WriteLine("Read");
int result = stream.EndRead(ar); // TODO disconnect & error handling
Console.WriteLine("Read done");
if(result <= 0)
{
Console.WriteLine("Error reading");
return;
}
string message = Utils.ReceiveInformation(data, result, out MessageType type);
OnRead(type, message);
WaitForData();
}
/// <summary>
///
/// </summary>
/// <param name="type"></param>
/// <param name="message"></param>
protected abstract void OnRead(MessageType type, string message);
}
This is the Utils.cs: I am having trouble understanding how there are two methods called ReceiveInformation() and how they interact with each other
public class Utils
{
/// <summary>
///
/// </summary>
/// <param name="stream"></param>
/// <param name="connection"></param>
/// <param name="type"></param>
/// <returns></returns>
public static string ReceiveInformation(NetworkStream stream, TcpClient connection, out MessageType type)
{
byte[] bytes = new byte[connection.ReceiveBufferSize];
int length = stream.Read(bytes, 0, bytes.Length);
return ReceiveInformation(bytes, length, out type);
}
/// <summary>
///
/// </summary>
/// <param name="bytes"></param>
/// <param name="length"></param>
/// <param name="type"></param>
/// <returns></returns>
public static string ReceiveInformation(byte[] bytes, int length, out MessageType type)
{
string data = Encoding.ASCII.GetString(bytes, 0, length);
int iSpace = data.IndexOf(' ');
if(iSpace < 0)
{
// TODO
}
string typeString = data.Substring(0, iSpace);
type = (MessageType)Enum.Parse(typeof(MessageType), typeString);
string message = data.Substring(iSpace + 1, data.Length - iSpace - 1);
return message;
}
/// <summary>
///
/// </summary>
/// <param name="type"></param>
/// <param name="stream"></param>
/// <param name="message"></param>
public static void SendInformation(MessageType type, NetworkStream stream, string message)
{
Byte[] sendBytes = Encoding.UTF8.GetBytes($"{type} {message}");
stream.Write(sendBytes, 0, sendBytes.Length);
}
}
I have been trying to add a Write text file section to this project but i keep getting told that lots of things 'dont exist in the current context'.
The text tile is meant to be created my documents showing 'First line,new line 1, new line 2'
This should work alongside the kinect sample.
I am sorry that i am very new to this and may be doing it completely wrong.
// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MainWindow.xaml.cs" company="Microsoft">
// Copyright (c) Microsoft Corporation. All rights reserved.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Microsoft.Samples.Kinect.HDFaceBasics
{
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Media3D;
using Microsoft.Kinect;
using Microsoft.Kinect.Face;
using System.IO;
/// <summary>
/// Main Window
/// </summary>
public partial class MainWindow : Window, INotifyPropertyChanged, IDisposable
{
// Create a string array with the lines of text
string text = "First line" + Environment.NewLine;
// Set a variable to the My Documents path.
string mydocpath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
// Write the text to a new file named "WriteFile.txt".
File.WriteAllText(mydocpath + #"\WriteFile.txt", text);
// Create a string array with the additional lines of text
string[] lines = { "New line 1", "New line 2" };
// Append new lines of text to the file
File.AppendAllLines(mydocpath + #"\WriteFile.txt", lines);
/// <summary>
/// Currently used KinectSensor
/// </summary>
private KinectSensor sensor = null;
/// <summary>
/// Body frame source to get a BodyFrameReader
/// </summary>
private BodyFrameSource bodySource = null;
/// <summary>
/// Body frame reader to get body frames
/// </summary>
private BodyFrameReader bodyReader = null;
/// <summary>
/// HighDefinitionFaceFrameSource to get a reader and a builder from.
/// Also to set the currently tracked user id to get High Definition Face Frames of
/// </summary>
private HighDefinitionFaceFrameSource highDefinitionFaceFrameSource = null;
/// <summary>
/// HighDefinitionFaceFrameReader to read HighDefinitionFaceFrame to get FaceAlignment
/// </summary>
private HighDefinitionFaceFrameReader highDefinitionFaceFrameReader = null;
/// <summary>
/// FaceAlignment is the result of tracking a face, it has face animations location and orientation
/// </summary>
private FaceAlignment currentFaceAlignment = null;
/// <summary>
/// FaceModel is a result of capturing a face
/// </summary>
private FaceModel currentFaceModel = null;
/// <summary>
/// FaceModelBuilder is used to produce a FaceModel
/// </summary>
private FaceModelBuilder faceModelBuilder = null;
/// <summary>
/// The currently tracked body
/// </summary>
private Body currentTrackedBody = null;
/// <summary>
/// The currently tracked body
/// </summary>
private ulong currentTrackingId = 0;
/// <summary>
/// Gets or sets the current tracked user id
/// </summary>
private string currentBuilderStatus = string.Empty;
/// <summary>
/// Gets or sets the current status text to display
/// </summary>
private string statusText = "Ready To Start Capture";
/// <summary>
/// Initializes a new instance of the MainWindow class.
/// </summary>
public MainWindow()
{
this.InitializeComponent();
this.DataContext = this;
}
/// <summary>
/// INotifyPropertyChangedPropertyChanged event to allow window controls to bind to changeable data
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Gets or sets the current status text to display
/// </summary>
public string StatusText
{
get
{
return this.statusText;
}
set
{
if (this.statusText != value)
{
this.statusText = value;
// notify any bound elements that the text has changed
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs("StatusText"));
}
}
}
}
/// <summary>
/// Gets or sets the current tracked user id
/// </summary>
private ulong CurrentTrackingId
{
get
{
return this.currentTrackingId;
}
set
{
this.currentTrackingId = value;
this.StatusText = this.MakeStatusText();
}
}
/// <summary>
/// Gets or sets the current Face Builder instructions to user
/// </summary>
private string CurrentBuilderStatus
{
get
{
return this.currentBuilderStatus;
}
set
{
this.currentBuilderStatus = value;
this.StatusText = this.MakeStatusText();
}
}
/// <summary>
/// Called when disposed of
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Dispose based on whether or not managed or native resources should be freed
/// </summary>
/// <param name="disposing">Set to true to free both native and managed resources, false otherwise</param>
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
if (this.currentFaceModel != null)
{
this.currentFaceModel.Dispose();
this.currentFaceModel = null;
}
}
}
/// <summary>
/// Returns the length of a vector from origin
/// </summary>
/// <param name="point">Point in space to find it's distance from origin</param>
/// <returns>Distance from origin</returns>
private static double VectorLength(CameraSpacePoint point)
{
var result = Math.Pow(point.X, 2) + Math.Pow(point.Y, 2) + Math.Pow(point.Z, 2);
result = Math.Sqrt(result);
return result;
}
/// <summary>
/// Finds the closest body from the sensor if any
/// </summary>
/// <param name="bodyFrame">A body frame</param>
/// <returns>Closest body, null of none</returns>
private static Body FindClosestBody(BodyFrame bodyFrame)
{
Body result = null;
double closestBodyDistance = double.MaxValue;
Body[] bodies = new Body[bodyFrame.BodyCount];
bodyFrame.GetAndRefreshBodyData(bodies);
foreach (var body in bodies)
{
if (body.IsTracked)
{
var currentLocation = body.Joints[JointType.SpineBase].Position;
var currentDistance = VectorLength(currentLocation);
if (result == null || currentDistance < closestBodyDistance)
{
result = body;
closestBodyDistance = currentDistance;
}
}
}
return result;
}
/// <summary>
/// Find if there is a body tracked with the given trackingId
/// </summary>
/// <param name="bodyFrame">A body frame</param>
/// <param name="trackingId">The tracking Id</param>
/// <returns>The body object, null of none</returns>
private static Body FindBodyWithTrackingId(BodyFrame bodyFrame, ulong trackingId)
{
Body result = null;
Body[] bodies = new Body[bodyFrame.BodyCount];
bodyFrame.GetAndRefreshBodyData(bodies);
foreach (var body in bodies)
{
if (body.IsTracked)
{
if (body.TrackingId == trackingId)
{
result = body;
break;
}
}
}
return result;
}
/// <summary>
/// Gets the current collection status
/// </summary>
/// <param name="status">Status value</param>
/// <returns>Status value as text</returns>
private static string GetCollectionStatusText(FaceModelBuilderCollectionStatus status)
{
string res = string.Empty;
if ((status & FaceModelBuilderCollectionStatus.FrontViewFramesNeeded) != 0)
{
res = "FrontViewFramesNeeded";
return res;
}
if ((status & FaceModelBuilderCollectionStatus.LeftViewsNeeded) != 0)
{
res = "LeftViewsNeeded";
return res;
}
if ((status & FaceModelBuilderCollectionStatus.RightViewsNeeded) != 0)
{
res = "RightViewsNeeded";
return res;
}
if ((status & FaceModelBuilderCollectionStatus.TiltedUpViewsNeeded) != 0)
{
res = "TiltedUpViewsNeeded";
return res;
}
if ((status & FaceModelBuilderCollectionStatus.Complete) != 0)
{
res = "Complete";
return res;
}
if ((status & FaceModelBuilderCollectionStatus.MoreFramesNeeded) != 0)
{
res = "TiltedUpViewsNeeded";
return res;
}
return res;
}
/// <summary>
/// Helper function to format a status message
/// </summary>
/// <returns>Status text</returns>
private string MakeStatusText()
{
string status = string.Format(System.Globalization.CultureInfo.CurrentCulture, "Builder Status: {0}, Current Tracking ID: {1}", this.CurrentBuilderStatus, this.CurrentTrackingId);
return status;
}
/// <summary>
/// Fires when Window is Loaded
/// </summary>
/// <param name="sender">object sending the event</param>
/// <param name="e">event arguments</param>
private void Window_Loaded(object sender, RoutedEventArgs e)
{
this.InitializeHDFace();
}
/// <summary>
/// Initialize Kinect object
/// </summary>
private void InitializeHDFace()
{
this.CurrentBuilderStatus = "Ready To Start Capture";
this.sensor = KinectSensor.GetDefault();
this.bodySource = this.sensor.BodyFrameSource;
this.bodyReader = this.bodySource.OpenReader();
this.bodyReader.FrameArrived += this.BodyReader_FrameArrived;
this.highDefinitionFaceFrameSource = new HighDefinitionFaceFrameSource(this.sensor);
this.highDefinitionFaceFrameSource.TrackingIdLost += this.HdFaceSource_TrackingIdLost;
this.highDefinitionFaceFrameReader = this.highDefinitionFaceFrameSource.OpenReader();
this.highDefinitionFaceFrameReader.FrameArrived += this.HdFaceReader_FrameArrived;
this.currentFaceModel = new FaceModel();
this.currentFaceAlignment = new FaceAlignment();
this.InitializeMesh();
this.UpdateMesh();
this.sensor.Open();
}
/// <summary>
/// Initializes a 3D mesh to deform every frame
/// </summary>
private void InitializeMesh()
{
var vertices = this.currentFaceModel.CalculateVerticesForAlignment(this.currentFaceAlignment);
var triangleIndices = this.currentFaceModel.TriangleIndices;
var indices = new Int32Collection(triangleIndices.Count);
for (int i = 0; i < triangleIndices.Count; i += 3)
{
uint index01 = triangleIndices[i];
uint index02 = triangleIndices[i + 1];
uint index03 = triangleIndices[i + 2];
indices.Add((int)index03);
indices.Add((int)index02);
indices.Add((int)index01);
}
this.theGeometry.TriangleIndices = indices;
this.theGeometry.Normals = null;
this.theGeometry.Positions = new Point3DCollection();
this.theGeometry.TextureCoordinates = new PointCollection();
foreach (var vert in vertices)
{
this.theGeometry.Positions.Add(new Point3D(vert.X, vert.Y, -vert.Z));
this.theGeometry.TextureCoordinates.Add(new Point());
}
}
/// <summary>
/// Sends the new deformed mesh to be drawn
/// </summary>
private void UpdateMesh()
{
var vertices = this.currentFaceModel.CalculateVerticesForAlignment(this.currentFaceAlignment);
for (int i = 0; i < vertices.Count; i++)
{
var vert = vertices[i];
this.theGeometry.Positions[i] = new Point3D(vert.X, vert.Y, -vert.Z);
}
}
/// <summary>
/// Start a face capture on clicking the button
/// </summary>
/// <param name="sender">object sending the event</param>
/// <param name="e">event arguments</param>
private void StartCapture_Button_Click(object sender, RoutedEventArgs e)
{
this.StartCapture();
}
/// <summary>
/// This event fires when a BodyFrame is ready for consumption
/// </summary>
/// <param name="sender">object sending the event</param>
/// <param name="e">event arguments</param>
private void BodyReader_FrameArrived(object sender, BodyFrameArrivedEventArgs e)
{
this.CheckOnBuilderStatus();
var frameReference = e.FrameReference;
using (var frame = frameReference.AcquireFrame())
{
if (frame == null)
{
// We might miss the chance to acquire the frame, it will be null if it's missed
return;
}
if (this.currentTrackedBody != null)
{
this.currentTrackedBody = FindBodyWithTrackingId(frame, this.CurrentTrackingId);
if (this.currentTrackedBody != null)
{
return;
}
}
Body selectedBody = FindClosestBody(frame);
if (selectedBody == null)
{
return;
}
this.currentTrackedBody = selectedBody;
this.CurrentTrackingId = selectedBody.TrackingId;
this.highDefinitionFaceFrameSource.TrackingId = this.CurrentTrackingId;
}
}
/// <summary>
/// This event is fired when a tracking is lost for a body tracked by HDFace Tracker
/// </summary>
/// <param name="sender">object sending the event</param>
/// <param name="e">event arguments</param>
private void HdFaceSource_TrackingIdLost(object sender, TrackingIdLostEventArgs e)
{
var lostTrackingID = e.TrackingId;
if (this.CurrentTrackingId == lostTrackingID)
{
this.CurrentTrackingId = 0;
this.currentTrackedBody = null;
if (this.faceModelBuilder != null)
{
this.faceModelBuilder.Dispose();
this.faceModelBuilder = null;
}
this.highDefinitionFaceFrameSource.TrackingId = 0;
}
}
/// <summary>
/// This event is fired when a new HDFace frame is ready for consumption
/// </summary>
/// <param name="sender">object sending the event</param>
/// <param name="e">event arguments</param>
private void HdFaceReader_FrameArrived(object sender, HighDefinitionFaceFrameArrivedEventArgs e)
{
using (var frame = e.FrameReference.AcquireFrame())
{
// We might miss the chance to acquire the frame; it will be null if it's missed.
// Also ignore this frame if face tracking failed.
if (frame == null || !frame.IsFaceTracked)
{
return;
}
frame.GetAndRefreshFaceAlignmentResult(this.currentFaceAlignment);
this.UpdateMesh();
}
}
/// <summary>
/// Start a face capture operation
/// </summary>
private void StartCapture()
{
this.StopFaceCapture();
this.faceModelBuilder = null;
this.faceModelBuilder = this.highDefinitionFaceFrameSource.OpenModelBuilder(FaceModelBuilderAttributes.None);
this.faceModelBuilder.BeginFaceDataCollection();
this.faceModelBuilder.CollectionCompleted += this.HdFaceBuilder_CollectionCompleted;
}
/// <summary>
/// Cancel the current face capture operation
/// </summary>
private void StopFaceCapture()
{
if (this.faceModelBuilder != null)
{
this.faceModelBuilder.Dispose();
this.faceModelBuilder = null;
}
}
/// <summary>
/// This event fires when the face capture operation is completed
/// </summary>
/// <param name="sender">object sending the event</param>
/// <param name="e">event arguments</param>
private void HdFaceBuilder_CollectionCompleted(object sender, FaceModelBuilderCollectionCompletedEventArgs e)
{
var modelData = e.ModelData;
this.currentFaceModel = modelData.ProduceFaceModel();
this.faceModelBuilder.Dispose();
this.faceModelBuilder = null;
this.CurrentBuilderStatus = "Capture Complete";
}
/// <summary>
/// Check the face model builder status
/// </summary>
private void CheckOnBuilderStatus()
{
if (this.faceModelBuilder == null)
{
return;
}
string newStatus = string.Empty;
var captureStatus = this.faceModelBuilder.CaptureStatus;
newStatus += captureStatus.ToString();
var collectionStatus = this.faceModelBuilder.CollectionStatus;
newStatus += ", " + GetCollectionStatusText(collectionStatus);
this.CurrentBuilderStatus = newStatus;
}
}
}
create a method that you can call that will allow you to create as well as append to the file
private static void WriteAndOrAppendText(string path, string strText)
{
using (StreamWriter fileStream = new StreamWriter(path, true))
{
fileStream.WriteLine(strText);
fileStream.Flush();
fileStream.Close();
}
}
// Set a variable to the My Documents path.
string mydocpath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
// Write the text to a new file named "WriteFile.txt".
WriteAndOrAppendText(mydocpath + #"\WriteFile.txt", text);
string[] lines = { "New line 1", "New line 2" };
WriteAndOrAppendText(mydocpath , string.Join(",", lines.ToArray()));
I have a well established console application in c# 2.0 that uses plugin architecture.
As of right now, the program uses basic multi-threading that can run several instances. The threads are created and continue on until the application is stopped.
Each instance can load its own variety of plugins and configured separately.
Plugins are inherited from a base plugin. This system has been working like a charm for years.
The plugins are event driven, they all read various events to see if they are called upon, if not they return and let the next plugin read the events to see if they are called out to fire.
This system has been working for years. However, I would like to further the scope of multi-threading to allow the plugins to listen to the events in an asynchronous fashion rather than synchronous. One of the drawbacks of this setup is that once a plugin fires and does its work, it locks out the instance. When the next event is fired it has to wait for the previous work to be completed. Then it will allow the next process to take place.
What I would like it to do, is execute the plugin and not have to wait for the process to end before moving on to the next process to begin by an event.
I am stuck with .Net 2.0 for the time being, and must find a solution in that framework. I have looked at numerous examples and I can not find one that meets the criteria. One of the problems is that each plugin has its own time that it may take to process, and there is no way to count to track the percentage that the plugin is complete. The plugins will start and ends its process when it is done. Depending on the parameters of the event, and the plugin it can take any range of time to complete.
My question would be what would be the best way to handle multi-threading in this situation where plugins are executed by events. I have looked at pages such as http://msdn.microsoft.com/en-us/library/2e08f6yc(v=vs.80).aspx and I can figure out where I would be able to have an entry point in an event driven plugin architecture.
If anyone has any clue, I would appreciate it. The lack of multi-threading in this manner has been the Achilles' heel for this application for years.
Plugin base: These contain some functions that are triggered by events:
using System;
using VhaBot.Communication;
namespace VhaBot
{
/// <summary>
/// Plugin BaseClass, must be inherited by all plugins
/// </summary>
public abstract class PluginBase : MarshalByRefObject
{
private bool _locked;
private string _name;
private string _internalName;
private int _version;
private string _author;
private string[] _contributors;
private string _description;
private PluginState _defaultState;
private string[] _dependencies;
private Command[] _commands;
/// <summary>
/// Friendly display name of plugin
/// </summary>
/// <example>
/// <code>
/// this.Name = "Message of the Day";
/// </code>
/// </example>
public string Name
{
set
{
if (_locked)
{
throw new Exception();
}
_name = value;
}
get { return _name; }
}
/// <summary>
/// Internal name of the plugin
/// </summary>
/// <example>
/// <code>
/// this.InternalName = "VhMotd";
/// </code>
/// </example>
public string InternalName
{
set
{
if (_locked)
{
throw new Exception();
}
_internalName = value.ToLower();
}
get { return _internalName; }
}
/// <summary>
/// Pluigin Version
/// </summary>
/// <remarks>
/// Versions are stored as integers only. Version 1.0.0 would have a value of 100
/// </remarks>
/// <example>
/// <code>
/// this.Version = 100;
/// </code>
/// </example>
public int Version
{
set
{
if (_locked)
{
throw new Exception();
}
_version = value;
}
get { return _version; }
}
/// <summary>
/// Author of the plugin
/// </summary>
/// <example>
/// <code>
/// this.Author = "Vhab";
/// </code>
/// </example>
public string Author
{
set
{
if (_locked)
{
throw new Exception();
}
_author = value;
}
get { return _author; }
}
/// <summary>
/// List of contributors to the development of the plugin.
/// </summary>
/// <example>
/// <code>
/// this.Contributors = new string[] { "Iriche", "Kilmanagh" };
/// </code>
/// </example>
public string[] Contributors
{
set
{
if (_locked)
{
throw new Exception();
}
_contributors = value;
}
get
{
if (_contributors != null)
{
return _contributors;
}
return new string[0];
}
}
/// <summary>
/// Description of the plugin
/// </summary>
/// <example>
/// <code>
/// this.Description = "Provides an interface to the user to view who is online and/or on the private channel.";
/// </code>
/// </example>
public string Description
{
set
{
if (_locked)
{
throw new Exception();
}
_description = value;
}
get { return _description; }
}
/// <summary>
/// The default <see cref="VhaBot.PluginState" /> of the plugin
/// </summary>
/// <example>
/// <code>
/// this.DefaultState = PluginState.Installed;
/// </code>
/// </example>
/// <seealso cref="VhaBot.PluginState" />
public PluginState DefaultState
{
set
{
if (_locked)
{
throw new Exception();
}
_defaultState = value;
}
get { return _defaultState; }
}
/// <summary>
/// List of other plugins that a plugin is dependent on to function
/// </summary>
/// <remarks>
/// Plugins are referred to using their internal names. See <see cref="VhaBot.PluginBase.InternalName" />
/// </remarks>
/// <example>
/// <code>
/// this.Dependencies = new string[] { "vhItems" };
/// </code>
/// </example>
public string[] Dependencies
{
set
{
if (_locked)
{
throw new Exception();
}
_dependencies = value;
}
get
{
if (_dependencies != null)
{
return _dependencies;
}
return new string[0];
}
}
public Command[] Commands
{
set
{
if (_locked)
{
throw new Exception();
}
_commands = value;
}
get
{
if (_commands != null)
{
return _commands;
}
return new Command[0];
}
}
internal void Init()
{
_locked = true;
}
/// <summary>
/// A plugin has loaded in response to <see cref="VhaBot.ShellModules.Plugins.Load" />
/// </summary>
/// <param name="bot"></param>
/// ///
/// <remarks>Code inside this method will be executed when a plugin is loading</remarks>
public virtual void OnLoad(BotShell bot)
{
}
/// <summary>
/// A plugin has unloaded in response to <see cref="VhaBot.ShellModules.Plugins.Unload" />
/// </summary>
/// <param name="bot"></param>
/// <remarks>Code inside this method will be executed when a plugin is unloading</remarks>
public virtual void OnUnload(BotShell bot)
{
}
/// <summary>
/// A plugin has installed in response to <see cref="VhaBot.ShellModules.Plugins.Install" />
/// </summary>
/// <param name="bot"></param>
public virtual void OnInstall(BotShell bot)
{
}
/// <summary>
/// A plugin as been uninstalled in response to <see cref="VhaBot.ShellModules.Plugins.Uninstall" />
/// </summary>
/// <param name="bot"></param>
public virtual void OnUninstall(BotShell bot)
{
}
/// <summary>
/// A plugin has been upgraded (Unused)
/// </summary>
/// <param name="bot"></param>
/// <param name="version"></param>
/// <remarks>This function is not active</remarks>
public virtual void OnUpgrade(BotShell bot, Int32 version)
{
}
/// <summary>
/// Response to a command
/// </summary>
/// <param name="bot"></param>
/// <param name="e"></param>
public virtual void OnCommand(BotShell bot, CommandArgs e)
{
}
/// <summary>
/// Response to an unauthorized command
/// </summary>
/// <param name="bot"></param>
/// <param name="e"></param>
public virtual void OnUnauthorizedCommand(BotShell bot, CommandArgs e)
{
}
/// <summary>
/// Response to a command help query <see cref="VhaBot.ShellModules.Commands.GetHelp." />
/// </summary>
/// <param name="bot"></param>
/// <param name="command"></param>
/// <returns></returns>
/// <remarks>Code inside this method will be executed when help is requested</remarks>
public virtual string OnHelp(BotShell bot, string command)
{
return null;
}
/// <summary>
/// Response to a custom configuration
/// </summary>
/// <param name="bot"></param>
/// <param name="key"></param>
/// <returns></returns>
public virtual string OnCustomConfiguration(BotShell bot, string key)
{
return null;
}
/// <summary>
/// Response to a plugin message
/// </summary>
/// <param name="bot"></param>
/// <param name="message"></param>
public virtual void OnPluginMessage(BotShell bot, PluginMessage message)
{
}
/// <summary>
/// Response to a bot message
/// </summary>
/// <param name="bot"></param>
/// <param name="message"></param>
public virtual void OnBotMessage(BotShell bot, BotMessage message)
{
}
/// <summary>
/// Returns display name of bot and current version
/// </summary>
/// <returns></returns>
public override string ToString()
{
return Name + " v" + Version;
}
/// <summary>
/// There is no information to document this command
/// </summary>
/// <param name="bot"></param>
/// <param name="args"></param>
public void FireOnCommand(BotShell bot, CommandArgs args)
{
try
{
if (args.Authorized)
OnCommand(bot, args);
else
OnUnauthorizedCommand(bot, args);
}
catch (Exception ex)
{
CommandArgs e = args;
var window = new RichTextWindow(bot);
window.AppendTitle("Error Report");
window.AppendHighlight("Error: ");
window.AppendNormal(ex.Message);
window.AppendLinkEnd();
window.AppendLineBreak();
window.AppendHighlight("Source: ");
window.AppendNormal(ex.Source);
window.AppendLinkEnd();
window.AppendLineBreak();
window.AppendHighlight("Target Site: ");
window.AppendNormal(ex.TargetSite.ToString());
window.AppendLinkEnd();
window.AppendLineBreak();
window.AppendHighlight("Stack Trace:");
window.AppendLineBreak();
window.AppendNormal(ex.StackTrace);
window.AppendLinkEnd();
window.AppendLineBreak();
bot.SendReply(e,
"There has been an error while executing this command »» " +
window.ToString("More Information"));
BotShell.Output("[Plugin Execution Error] " + ex);
}
}
}
}
Events Class:
namespace VhaBot.ShellModules
{
/// <summary>
/// VhaBot Events
/// </summary>
public class Events
{
public event BotStateChangedHandler BotStateChangedEvent;
public event ChannelJoinEventHandler ChannelJoinEvent;
public event UserJoinChannelHandler UserJoinChannelEvent;
public event UserLeaveChannelHandler UserLeaveChannelEvent;
public event UserLogonHandler UserLogonEvent;
public event UserLogoffHandler UserLogoffEvent;
public event PrivateMessageHandler PrivateMessageEvent;
public event PrivateChannelMessageHandler PrivateChannelMessageEvent;
public event ChannelMessageHandler ChannelMessageEvent;
public event MemberAddedHandler MemberAddedEvent;
public event MemberRemovedHandler MemberRemovedEvent;
public event MemberUpdatedHandler MemberUpdatedEvent;
public event AltAddedHandler AltAddedEvent;
public event AltRemovedHandler AltRemovedEvent;
/// <summary>
/// A message was sent to the IRC channel in response to a <see cref="VhaBot.BotShell.SendIrcMessage" /> request
/// </summary>
public event IrcMessageHandler IrcMessageEvent;
public event ConfigurationChangedHandler ConfigurationChangedEvent;
internal void OnBotStateChanged(BotShell bot, BotStateChangedArgs e)
{
if (BotStateChangedEvent != null)
try
{
BotStateChangedEvent(bot, e);
}
catch
{
}
}
internal void OnChannelJoin(BotShell bot, ChannelJoinEventArgs e)
{
if (ChannelJoinEvent != null)
try
{
ChannelJoinEvent(bot, e);
}
catch
{
}
}
internal void OnUserJoinChannel(BotShell bot, UserJoinChannelArgs e)
{
if (UserJoinChannelEvent != null)
try
{
UserJoinChannelEvent(bot, e);
}
catch
{
}
}
internal void OnUserLeaveChannel(BotShell bot, UserLeaveChannelArgs e)
{
if (UserLeaveChannelEvent != null)
try
{
UserLeaveChannelEvent(bot, e);
}
catch
{
}
}
internal void OnUserLogon(BotShell bot, UserLogonArgs e)
{
if (UserLogonEvent != null)
try
{
UserLogonEvent(bot, e);
}
catch
{
}
}
internal void OnUserLogoff(BotShell bot, UserLogoffArgs e)
{
if (UserLogoffEvent != null)
try
{
UserLogoffEvent(bot, e);
}
catch
{
}
}
internal void OnPrivateMessage(BotShell bot, PrivateMessageArgs e)
{
if (PrivateMessageEvent != null)
try
{
PrivateMessageEvent(bot, e);
}
catch
{
}
}
internal void OnPrivateChannelMessage(BotShell bot, PrivateChannelMessageArgs e)
{
if (PrivateChannelMessageEvent != null)
try
{
PrivateChannelMessageEvent(bot, e);
}
catch
{
}
}
internal void OnChannelMessage(BotShell bot, ChannelMessageArgs e)
{
if (ChannelMessageEvent != null)
try
{
ChannelMessageEvent(bot, e);
}
catch
{
}
}
internal void OnMemberAdded(BotShell bot, MemberAddedArgs e)
{
if (MemberAddedEvent != null)
try
{
MemberAddedEvent(bot, e);
}
catch
{
}
}
internal void OnMemberRemoved(BotShell bot, MemberRemovedArgs e)
{
if (MemberRemovedEvent != null)
try
{
MemberRemovedEvent(bot, e);
}
catch
{
}
}
internal void OnMemberUpdated(BotShell bot, MemberUpdatedArgs e)
{
if (MemberUpdatedEvent != null)
try
{
MemberUpdatedEvent(bot, e);
}
catch
{
}
}
internal void OnAltAdded(BotShell bot, AltAddedArgs e)
{
if (AltAddedEvent != null)
try
{
AltAddedEvent(bot, e);
}
catch
{
}
}
internal void OnAltRemoved(BotShell bot, AltRemovedArgs e)
{
if (AltRemovedEvent != null)
try
{
AltRemovedEvent(bot, e);
}
catch
{
}
}
internal void OnConfigurationChanged(BotShell bot, ConfigurationChangedArgs e)
{
if (ConfigurationChangedEvent != null)
try
{
ConfigurationChangedEvent(bot, e);
}
catch
{
}
}
internal void OnIrcMessage(BotShell bot, IrcMessageArgs e)
{
if (IrcMessageEvent != null)
{
IrcMessageEvent(bot, e);
}
try
{
}
catch
{
}
}
}
}
I've got little to go on as your description of the system is a bit vague but I'll give it a shot.
From your description it seems you have some plugin, say
interface IPlugin {
PluginResult ReadAndExecuteEvents(Events e);
// Added asynchronous methods.
IAsyncResult BeginReadAndExecuteEvents(Events e, AsyncCallback cb, Object state);
PluginResult EndReadAndExecuteEvents(IAsyncResult result);
}
with
class PluginResult
{
public Boolean Stop;
// etc.
}
also you don't seem to be using .NET events, but rather some sort of Event class/enumeration.
Your old code seems to be something like:
foreach (var eventList in ReadEvents())
foreach (var plugin in pluginList)
if (plugin.ReadAndExecuteEvents(eventList).Stop)
break;
You can make this asynchronous doing something like:
foreach (var eventList in ReadEvents())
{
// It seems this is what you want, only one event processed at a time by an "instance"? So block here until unlocked.
LockProcess();
var pluginIndex = 0;
AsyncCallback handleResult = null;
handleResult = delegate(IAsyncResult result)
{
if (pluginList[pluginIndex].EndReadAndExecuteEvents(result).Stop)
goto STOP;
pluginIndex += 1;
if (pluginIndex == pluginList.Count)
goto STOP;
Events e = (Events)result.AsyncState;
pluginList[pluginIndex].BeginReadAndExecuteEvents(e, handleResult, e);
return;
STOP:
UnlockProcess();
};
pluginList[0].BeginReadAndExecuteEvents(eventList, handleResult, eventList);
}
So in .NET 2 style you could add some BeginXxx method and in its AsyncCallback do your stuff.
Of course it is up to the actual plugin to do its multithreading/asynchronisity, say if it writes a file by using BeginWrite to a FileStream etc.
I have conveniently ignored exception handling here.
So, to make your whole application use this asynchronisity you can put this code in a BeginRunEvents method, say, following the same "APM" pattern. You can then schedule this to the threadpool if you wish.
If this is not at all what you are looking for please provide some more code examples/info.
WatiN seems to not handle repeated download dialogs consistently:
foreach (string file in lstFiles)
{
// continue if no download link
if (!ie.Element([search criteria]).Exists) continue;
var btnDownload = ie.Element([search criteria]);
string fullFilename = workingDir + "\\" + file;
FileDownloadHandler download = new FileDownloadHandler(fullFilename);
using (new UseDialogOnce(ie.DialogWatcher, download))
{
btnDownload.ClickNoWait();
download.WaitUntilFileDownloadDialogIsHandled(30);
download.WaitUntilDownloadCompleted(150);
ie.RemoveDialogHandler(download);
}
}
Basically, I loop through a list of filenames that I expect to be available and click the download button. This usually works, but after so many downloads (it varies, sometimes everything that's available downloads, sometimes nothing) it will hang while waiting to handle the dialog. The button's identified correctly, the download dialog appears, it just isn't detected and handled. It isn't site-specific as similar methods on other sites are also met with variable success. Anyone encounter this before and know of a resolution?
edit: Repeated downloads do not work whatsoever in Server 2008. In Win7, this happens randomly after one or more successful repeated downloads.
The problem appears because IE File Download Dialog consist of 2 windows. WatiN DialogWatcher gets all the system windows and tries to handle them in foreach loop. After handling first correct dialog window DialogWatcher gets the next window wich has the same properties and is a valid Download Dialog. DialogWatcher starts waiting until this window is visible but it closes immediately after previos window is handled.
My solution is to return from foreach loop after any dialog is handled:
#region WatiN Copyright (C) 2006-2011 Jeroen van Menen
//Copyright 2006-2011 Jeroen van Menen
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
#endregion Copyright
using System;
using System.Collections.Generic;
using System.Threading;
using WatiN.Core.Exceptions;
using WatiN.Core.Interfaces;
using WatiN.Core.Logging;
using WatiN.Core.Native.InternetExplorer;
using WatiN.Core.Native.Windows;
using WatiN.Core.UtilityClasses;
namespace WatiN.Core.DialogHandlers
{
/// <summary>
/// This class handles alert/popup dialogs. Every second it checks if a dialog
/// is shown. If so, it stores it's message in the alertQueue and closses the dialog
/// by clicking the close button in the title bar.
/// </summary>
public class DialogWatcher : IDisposable
{
private static IList<DialogWatcher> dialogWatchers = new List<DialogWatcher>();
private bool _keepRunning = true;
private readonly IList<IDialogHandler> _handlers;
private readonly Thread _watcherThread;
private bool _closeUnhandledDialogs = Settings.AutoCloseDialogs;
public Window MainWindow { get; private set; }
/// <summary>
/// Gets the dialog watcher for the specified (main) internet explorer window.
/// It creates new instance if no dialog watcher for the specified window exists.
/// </summary>
/// <param name="mainWindowHwnd">The (main) internet explorer window.</param>
/// <returns></returns>
public static DialogWatcher GetDialogWatcher(IntPtr mainWindowHwnd)
{
var window = new Window(mainWindowHwnd);
Logger.LogDebug("GetDialogWatcher mainhwnd: " + window.Hwnd + ", " + window.Title);
var toplevelWindow = window.ToplevelWindow;
Logger.LogDebug("GetDialogWatcher mainhwnd: " + toplevelWindow.Hwnd + ", " + toplevelWindow.Title);
CleanupDialogWatcherCache();
var dialogWatcher = GetDialogWatcherFromCache(toplevelWindow);
// If no dialogwatcher exists for the ieprocessid then
// create a new one, store it and return it.
if (dialogWatcher == null)
{
dialogWatcher = new DialogWatcher(toplevelWindow);
dialogWatchers.Add(dialogWatcher);
}
return dialogWatcher;
}
public static DialogWatcher GetDialogWatcherFromCache(Window mainWindow)
{
// Loop through already created dialogwatchers and
// return a dialogWatcher if one exists for the given processid
foreach (var dialogWatcher in dialogWatchers)
{
if (dialogWatcher.MainWindow.Equals(mainWindow))
{
return dialogWatcher;
}
}
return null;
}
public static void CleanupDialogWatcherCache()
{
var cleanedupDialogWatcherCache = new List<DialogWatcher>();
foreach (var dialogWatcher in dialogWatchers)
{
if (!dialogWatcher.IsRunning)
{
dialogWatcher.Dispose();
}
else
{
cleanedupDialogWatcherCache.Add(dialogWatcher);
}
}
dialogWatchers = cleanedupDialogWatcherCache;
}
/// <summary>
/// Initializes a new instance of the <see cref="DialogWatcher"/> class.
/// You are encouraged to use the Factory method <see cref="DialogWatcher.GetDialogWatcherFromCache"/>
/// instead.
/// </summary>
/// <param name="mainWindow">The main window handle of internet explorer.</param>
public DialogWatcher(Window mainWindow)
{
MainWindow = mainWindow;
_handlers = new List<IDialogHandler>();
// Create thread to watch windows
_watcherThread = new Thread(Start);
// Start the thread.
_watcherThread.Start();
}
/// <summary>
/// Increases the reference count of this DialogWatcher instance with 1.
/// </summary>
public void IncreaseReferenceCount()
{
ReferenceCount++;
}
/// <summary>
/// Decreases the reference count of this DialogWatcher instance with 1.
/// When reference count becomes zero, the Dispose method will be
/// automatically called. This method will throw an <see cref="ReferenceCountException"/>
/// if the reference count is zero.
/// </summary>
public void DecreaseReferenceCount()
{
if (ReferenceCount > 0)
{
ReferenceCount--;
}
else
{
throw new ReferenceCountException();
}
if (ReferenceCount == 0)
{
Dispose();
}
}
/// <summary>
/// Adds the specified handler.
/// </summary>
/// <param name="handler">The handler.</param>
public void Add(IDialogHandler handler)
{
lock (this)
{
_handlers.Add(handler);
}
}
/// <summary>
/// Removes the specified handler.
/// </summary>
/// <param name="handler">The handler.</param>
public void Remove(IDialogHandler handler)
{
lock (this)
{
_handlers.Remove(handler);
}
}
/// <summary>
/// Removes all instances that match <paramref name="handler"/>.
/// This method determines equality by calling Object.Equals.
/// </summary>
/// <param name="handler">The object implementing IDialogHandler.</param>
/// <example>
/// If you want to use RemoveAll with your custom dialog handler to
/// remove all instances of your dialog handler from a DialogWatcher instance,
/// you should override the Equals method in your custom dialog handler class
/// like this:
/// <code>
/// public override bool Equals(object obj)
/// {
/// if (obj == null) return false;
///
/// return (obj is YourDialogHandlerClassNameGoesHere);
/// }
/// </code>
/// You could also inherit from <see cref="BaseDialogHandler"/> instead of implementing
/// <see cref="IDialogHandler"/> in your custom dialog handler. <see cref="BaseDialogHandler"/> provides
/// overrides for Equals and GetHashCode that work with RemoveAll.
/// </example>
public void RemoveAll(IDialogHandler handler)
{
while (Contains(handler))
{
Remove(handler);
}
}
/// <summary>
/// Removes all registered dialog handlers.
/// </summary>
public void Clear()
{
lock (this)
{
_handlers.Clear();
}
}
/// <summary>
/// Determines whether this <see cref="DialogWatcher"/> contains the specified dialog handler.
/// </summary>
/// <param name="handler">The dialog handler.</param>
/// <returns>
/// <c>true</c> if [contains] [the specified handler]; otherwise, <c>false</c>.
/// </returns>
public bool Contains(IDialogHandler handler)
{
lock (this)
{
return _handlers.Contains(handler);
}
}
/// <summary>
/// Gets the count of registered dialog handlers.
/// </summary>
/// <value>The count.</value>
public int Count
{
get
{
lock (this)
{
return _handlers.Count;
}
}
}
/// <summary>
/// Gets or sets a value indicating whether unhandled dialogs should be closed automaticaly.
/// The initial value is set to the value of <cref name="Settings.AutoCloseDialogs" />.
/// </summary>
/// <value>
/// <c>true</c> if unhandled dialogs should be closed automaticaly; otherwise, <c>false</c>.
/// </value>
public bool CloseUnhandledDialogs
{
get
{
lock (this)
{
return _closeUnhandledDialogs;
}
}
set
{
lock (this)
{
_closeUnhandledDialogs = value;
}
}
}
/// <summary>
/// Gets the (main) internet explorer window handle this dialog watcher watches.
/// </summary>
/// <value>The process id.</value>
public IntPtr MainWindowHwnd
{
get { return MainWindow.Hwnd; }
}
/// <summary>
/// Called by the constructor to start watching popups
/// on a separate thread.
/// </summary>
private void Start()
{
while (_keepRunning)
{
if (MainWindow.Exists())
{
var winEnumerator = new WindowsEnumerator();
var windows = winEnumerator.GetWindows(win => true);
foreach (var window in windows)
{
if (!_keepRunning) return;
if(HandleWindow(window))
break;
}
// Keep DialogWatcher responsive during 1 second sleep period
var count = 0;
while (_keepRunning && count < 5)
{
Thread.Sleep(200);
count++;
}
}
else
{
_keepRunning = false;
}
}
}
public bool IsRunning
{
get { return _watcherThread.IsAlive; }
}
public int ReferenceCount { get; private set; }
/// <summary>
/// Get the last stored exception thrown by a dialog handler while
/// calling the <see cref="IDialogHandler.HandleDialog"/> method of the
/// dialog handler.
/// </summary>
/// <value>The last exception.</value>
public Exception LastException { get; private set; }
/// <summary>
/// If the window is a dialog and visible, it will be passed to
/// the registered dialog handlers. I none if these can handle
/// it, it will be closed if <see cref="CloseUnhandledDialogs"/>
/// is <c>true</c>.
/// </summary>
/// <param name="window">The window.</param>
/// <returns>
/// <c>true</c> if dialog is handled by one of handlers; otherwise, <c>false</c>.
/// </returns>
public bool HandleWindow(Window window)
{
if (!window.IsDialog()) return false;
if (!HasDialogSameProcessNameAsBrowserWindow(window)) return false;
// This is needed otherwise the window Style will return a "wrong" result.
WaitUntilVisibleOrTimeOut(window);
// Lock the thread and see if a handler will handle
// this dialog window
lock (this)
{
foreach (var dialogHandler in _handlers)
{
try
{
if (dialogHandler.CanHandleDialog(window, MainWindow.Hwnd))
{
if (dialogHandler.HandleDialog(window)) return true;
}
}
catch (Exception e)
{
LastException = e;
Logger.LogAction((LogFunction log) => { log("Exception was thrown while DialogWatcher called HandleDialog: {0}",e.ToString()); });
}
}
// If no handler handled the dialog, see if the dialog
// should be closed automatically.
if (!CloseUnhandledDialogs || !MainWindow.Equals(window.ToplevelWindow)) return false;
Logger.LogAction((LogFunction log) => { log("Auto closing dialog with title: '{0}', text: {1}, style: ", window.Title, window.Message, window.StyleInHex); });
window.ForceClose();
}
return false;
}
private bool HasDialogSameProcessNameAsBrowserWindow(Window window)
{
var comparer = new Comparers.StringComparer(window.ProcessName, true);
return comparer.Compare(MainWindow.ProcessName);
}
private static void WaitUntilVisibleOrTimeOut(Window window)
{
// Wait untill window is visible so all properties
// of the window class (like Style and StyleInHex)
// will return valid values.
var tryActionUntilTimeOut = new TryFuncUntilTimeOut(TimeSpan.FromSeconds(Settings.WaitForCompleteTimeOut));
var success = tryActionUntilTimeOut.Try(() => window.Visible);
if (!success)
{
Logger.LogAction((LogFunction log) => { log("Dialog with title '{0}' not visible after {1} seconds.", window.Title, Settings.WaitForCompleteTimeOut); });
}
}
#region IDisposable Members
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or
/// resetting unmanaged resources.
/// </summary>
public void Dispose()
{
lock (this)
{
_keepRunning = false;
}
if (IsRunning)
{
_watcherThread.Join();
}
Clear();
}
#endregion
}
}
My team ran into this as well while automating IE8 with WatiN. The problem seems to be with IE, possibly doing some time consuming house-cleaning. The work-around we ultimately used was to invoke a new instance of IE within the outer loop, disposing of it each iteration and then waiting for 5 seconds for whatever was going on in the background to resolve. It was a hack but got the job done.
foreach (var file in lstFiles)
{
string fullFilename = workingDir + "\\" + file;
using (var browser = new IE(fullFilename))
{
//page manipulations...
FileDownloadHandler download = new FileDownloadHandler(fullFilename);
using (new UseDialogOnce(browser.DialogWatcher, download))
{ //lnkFile.ClickNoWait();
download.WaitUntilFileDownloadDialogIsHandled(15);
download.WaitUntilDownloadCompleted(150);
}
}
Thread.Sleep(5000);
}
I need to track the current in user (the one using the console) on Windows XP SP3.
I tried the following:
Microsoft.Win32.SystemEvents.SessionSwitch: Works for single logon/logout events, but fails to detect switch user.
If the following occurs:
userA log in
userA switch user
userB login
userB logout
userA restore session
Events 3 and 4 are not detected by SystemEvents.SessionSwitch
Monitoring the "Security" EventLog: Events are inconsistent and arrive out of order. For instance, if the list above is replayed, I receive an event id 528 (Logon), followed by two 538 (Logoff) for userA after he restores his session. Checking event.TimeGenerated doesn't help. This method also does not work if auditing is disabled on SecPol.msc.
P/Invoking WTSRegisterSessionNotification: Works fine. I had to create a hidden form, override its WndProc to handle WM_WTSSESSION_CHANGE messages, and then call WTSQuerySessionInformation to get the username associated with the event. This method looks too complex, is there a simpler way?
Edit:
Calling WTSGetActiveConsoleSessionId every n milliseconds works too, but I'm looking for an event based method.
If you're making a service, your class is derived from ServiceBase. Consider investigating the OnSessionChange method. Override this method to detect different types of session changes. It provides both a reason for the session change, and the new session identifier. Be sure in your constructor to set CanHandleSessionChangeEvent to true, otherwise your override will not be called.
Here ist the last part, I use this to query session changed events:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using System.Windows.Forms;
using Rdp.Interfaces;
using Microsoft.Win32;
using System.ServiceProcess;
using System.Diagnostics;
using System.Threading;
using Balzers.Misc.Helpers;
namespace Rdp.Service {
/// <summary>
/// <para> Terminal session info provider has 2 main functions:</para>
/// <list type="number">
/// <item>
/// <description>Provide all current terminal session information: <see cref="M:Oerlikon.Balzers.Rdp.Service.RdpSessionInfo.ListSessions(System.Boolean)"/></description>
/// </item>
/// <item>
/// <description>Observer terminal session changes: <see cref="E:Oerlikon.Balzers.Rdp.Service.RdpSessionInfo.SessionChanged"/></description>
/// </item>
/// </list>
/// </summary>
public class RdpSessionInfo : IDisposable {
/************************************************************************************************/
/* DllImports */
/************************************************************************************************/
#region DllImports
[DllImport("wtsapi32.dll", SetLastError = true)]
static extern IntPtr WTSOpenServer([MarshalAs(UnmanagedType.LPStr)] String pServerName);
[DllImport("wtsapi32.dll")]
static extern void WTSCloseServer(IntPtr hServer);
[DllImport("wtsapi32.dll")]
static extern int WTSEnumerateSessions(
IntPtr pServer,
[MarshalAs(UnmanagedType.U4)] int iReserved,
[MarshalAs(UnmanagedType.U4)] int iVersion,
ref IntPtr pSessionInfo,
[MarshalAs(UnmanagedType.U4)] ref int iCount);
[DllImport("Wtsapi32.dll")]
static extern bool WTSQuerySessionInformation(
System.IntPtr pServer,
int iSessionID,
WTS_INFO_CLASS oInfoClass,
out System.IntPtr pBuffer,
out uint iBytesReturned);
[DllImport("Wtsapi32.dll")]
public static extern bool WTSWaitSystemEvent(
IntPtr hServer,
UInt32 EventMask,
out IntPtr pEventFlags);
[DllImport("wtsapi32.dll")]
static extern void WTSFreeMemory(IntPtr pMemory);
[DllImport("user32.dll")]
public static extern int ExitWindowsEx(int uFlags, int dwReason);
[DllImport("WtsApi32.dll")]
private static extern bool WTSRegisterSessionNotification(IntPtr hWnd, [MarshalAs(UnmanagedType.U4)]int dwFlags);
[DllImport("WtsApi32.dll")]
private static extern bool WTSUnRegisterSessionNotification(IntPtr hWnd);
public delegate int ServiceControlHandlerEx(int control, int eventType, IntPtr eventData, IntPtr context);
[DllImport("advapi32.dll", SetLastError = true)]
public static extern IntPtr RegisterServiceCtrlHandlerEx(string lpServiceName, ServiceControlHandlerEx cbex, IntPtr context);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr hObject);
[DllImport("user32.dll")]
static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent);
#endregion
#region Constants
public const int SERVICE_CONTROL_STOP = 1;
public const int SERVICE_CONTROL_DEVICEEVENT = 11;
public const int SERVICE_CONTROL_SHUTDOWN = 5;
public const int SERVICE_CONTROL_SESSIONCHANGE = 0x0000000E;
// WTSWaitSystemEvent local server handle
public const int WTS_CURRENT_SERVER_HANDLE = 0;
public const int WTS_CURRENT_SESSION = 0;
[Flags]
public enum WaitSystemEventFlags {
/* =====================================================================
== EVENT - Event flags for WTSWaitSystemEvent
===================================================================== */
None = 0x00000000, // return no event
CreatedWinstation = 0x00000001, // new WinStation created
DeletedWinstation = 0x00000002, // existing WinStation deleted
RenamedWinstation = 0x00000004, // existing WinStation renamed
ConnectedWinstation = 0x00000008, // WinStation connect to client
DisconnectedWinstation = 0x00000010, // WinStation logged on without client
LogonUser = 0x00000020, // user logged on to existing WinStation
LogoffUser = 0x00000040, // user logged off from existing WinStation
WinstationStateChange = 0x00000080, // WinStation state change
LicenseChange = 0x00000100, // license state change
AllEvents = 0x7fffffff, // wait for all event types
// Unfortunately cannot express this as an unsigned long...
//FlushEvent = 0x80000000 // unblock all waiters
}
public const UInt32 FlushEvent = 0x80000000;
#endregion
/************************************************************************************************/
/* Private members */
/************************************************************************************************/
#region Private members
private String m_ServerName = Environment.MachineName;
private bool m_unregistered = false;
private ServiceControlHandlerEx myCallback;
private bool tsObserverRunning = false;
private Thread tsObserverThread;
IntPtr hServ;
#endregion
/************************************************************************************************/
/* Constructors */
/************************************************************************************************/
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="T:Oerlikon.Balzers.Rdp.Service.RdpSessionInfo">RdpSessionInfo</see> class.
/// </summary>
/// <remarks></remarks>
public RdpSessionInfo() : this(Environment.MachineName) { }
/// <summary>
/// Initializes a new instance of the <see cref="T:Oerlikon.Balzers.Rdp.Service.RdpSessionInfo"/> class.
/// </summary>
/// <param name="ServerName"></param>
public RdpSessionInfo(String ServerName) : base() {
this.m_ServerName = ServerName;
this.hServ = WTSOpenServer(this.m_ServerName);
tsObserverThread = new Thread(StartTerminalSessionObservation);
tsObserverThread.Start(hServ);
}
~RdpSessionInfo() {
}
#endregion Constructors
/************************************************************************************************/
/* Methods */
/************************************************************************************************/
#region Public methods
public void StartTerminalSessionObservation(object hServ) {
string msg;
IntPtr pEvents = IntPtr.Zero;
IntPtr hServer = (IntPtr)hServ;
List<TerminalSessionInfo> oldSessions, newSessions;
TerminalSessionInfo tsi;
WM_WTSSESSION_CHANGE_TYPE changeType;
// initial read actual sessions
oldSessions = ListSessions(false);
newSessions = new List<TerminalSessionInfo>(oldSessions.ToArray());
tsObserverRunning = true;
while(this.tsObserverRunning) {
if(WTSWaitSystemEvent(hServer, (UInt32)WaitSystemEventFlags.AllEvents, out pEvents)) {
WaitSystemEventFlags eventType = GetSystemEventType(pEvents);
switch(eventType) {
case WaitSystemEventFlags.ConnectedWinstation:
case WaitSystemEventFlags.CreatedWinstation:
case WaitSystemEventFlags.LogonUser:
newSessions = ListSessions(false);
tsi = GetChangedTerminalSession(oldSessions, newSessions, out changeType);
oldSessions.Clear();
oldSessions.AddRange(newSessions.ToArray());
if(tsi != null && tsi.SessionInfo.iSessionID != 0)
OnSessionChanged(new TerminalSessionChangedEventArgs(changeType, tsi.SessionInfo.iSessionID, tsi));
break;
case WaitSystemEventFlags.DeletedWinstation:
case WaitSystemEventFlags.DisconnectedWinstation:
case WaitSystemEventFlags.LogoffUser:
case WaitSystemEventFlags.WinstationStateChange:
newSessions = ListSessions(false);
tsi = GetChangedTerminalSession(oldSessions, newSessions, out changeType);
oldSessions.Clear();
oldSessions.AddRange(newSessions.ToArray());
if(tsi != null && tsi.SessionInfo.iSessionID != 0)
OnSessionChanged(new TerminalSessionChangedEventArgs(changeType, tsi.SessionInfo.iSessionID, tsi));
break;
default:
break;
}
}
else {
uint winErrorCode = Win32Sec.GetLastError();
msg = new System.ComponentModel.Win32Exception((int)winErrorCode).Message;
WindowsEventLogHelper.WriteEventLog(msg, EventLogEntryType.Error);
WindowsEventLogHelper.WriteEventLog(RdpControl.SVC_NAME + " " + System.Reflection.MethodInfo.GetCurrentMethod().Name + " - methode failed: " + msg, EventLogEntryType.Error);
}
Thread.Sleep(100);
}
WTSCloseServer(hServer);
}
public void StopTerminalSessionObservation(object hServ) {
this.tsObserverRunning = false;
IntPtr pEvents = IntPtr.Zero;
// unlock the waiter
WTSWaitSystemEvent((IntPtr)hServ, FlushEvent, out pEvents);
tsObserverThread.Join(200);
}
public static IntPtr OpenServer(String Name) {
IntPtr server = WTSOpenServer(Name);
return server;
}
public static void CloseServer(IntPtr ServerHandle) {
WTSCloseServer(ServerHandle);
}
/// <summary>
/// Read all session info running on the system.
/// </summary>
/// <param name="RdpOnly">If set to <see langword="true"/>, then only Rdp sessions
/// will be listed; otherwise, all session types <see cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_PROTOCOL_TYPE"/> .</param>
public List<TerminalSessionInfo> ListSessions(bool RdpOnly) {
IntPtr server = IntPtr.Zero;
List<TerminalSessionInfo> ret = new List<TerminalSessionInfo>();
//server = OpenServer(this.m_ServerName);
try {
IntPtr ppSessionInfo = IntPtr.Zero;
Int32 count = 0;
Int32 retval = WTSEnumerateSessions(this.hServ, 0, 1, ref ppSessionInfo, ref count);
Int32 dataSize = Marshal.SizeOf(typeof(WTS_SESSION_INFO));
Int64 current = (int)ppSessionInfo;
if(retval != 0) {
for(int i = 0; i < count; i++) {
TerminalSessionInfo tsi = GetSessionInfo(this.hServ, (System.IntPtr)current);
current += dataSize;
if(tsi.ProtocolType == WTS_CLIENT_PROTOCOL_TYPE.RDP || !RdpOnly)
ret.Add(tsi);
}
WTSFreeMemory(ppSessionInfo);
}
}
finally {
//CloseServer(server);
}
return ret;
}
#endregion Public methods
#region Private methods
private TerminalSessionInfo GetChangedTerminalSession(List<TerminalSessionInfo> oldSessions, List<TerminalSessionInfo> newSessions, out WM_WTSSESSION_CHANGE_TYPE sessionChangeType) {
TerminalSessionInfo retval = new TerminalSessionInfo(0);
sessionChangeType = (WM_WTSSESSION_CHANGE_TYPE)0;
// session added
if(newSessions.Count > oldSessions.Count) {
retval = newSessions.Where(s => oldSessions.Where(old => old.SessionInfo.iSessionID == s.SessionInfo.iSessionID).ToList().Count == 0).FirstOrDefault();
if(retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSConnected
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSActive
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSConnectQuery)
sessionChangeType = (retval.ProtocolType == WTS_CLIENT_PROTOCOL_TYPE.RDP) ? WM_WTSSESSION_CHANGE_TYPE.WTS_REMOTE_CONNECT : WM_WTSSESSION_CHANGE_TYPE.WTS_CONSOLE_CONNECT;
}
else if(newSessions.Count < oldSessions.Count) {
retval = oldSessions.Where(s => newSessions.Where(old => old.SessionInfo.iSessionID == s.SessionInfo.iSessionID).ToList().Count == 0).FirstOrDefault();
retval.SessionInfo.oState = WTS_CONNECTSTATE_CLASS.WTSDisconnected;
retval.WtsInfo.State = WTS_CONNECTSTATE_CLASS.WTSDisconnected;
sessionChangeType = (retval.ProtocolType == WTS_CLIENT_PROTOCOL_TYPE.RDP) ? WM_WTSSESSION_CHANGE_TYPE.WTS_REMOTE_DISCONNECT : WM_WTSSESSION_CHANGE_TYPE.WTS_CONSOLE_DISCONNECT;
}
else {
retval = newSessions.Where(s => oldSessions.Where(old => old.SessionInfo.iSessionID == s.SessionInfo.iSessionID && old.SessionInfo.oState != s.SessionInfo.oState).ToList().Count > 0 && s.SessionInfo.iSessionID != 0).FirstOrDefault();
if(retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSConnected
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSActive
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSConnectQuery)
sessionChangeType = (retval.ProtocolType == WTS_CLIENT_PROTOCOL_TYPE.RDP) ? WM_WTSSESSION_CHANGE_TYPE.WTS_REMOTE_CONNECT : WM_WTSSESSION_CHANGE_TYPE.WTS_CONSOLE_CONNECT;
else if(retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSDisconnected
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSDown
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSIdle
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSListen
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSReset
|| retval.SessionInfo.oState == WTS_CONNECTSTATE_CLASS.WTSShadow)
sessionChangeType = (retval.ProtocolType == WTS_CLIENT_PROTOCOL_TYPE.RDP) ? WM_WTSSESSION_CHANGE_TYPE.WTS_REMOTE_DISCONNECT : WM_WTSSESSION_CHANGE_TYPE.WTS_CONSOLE_DISCONNECT;
}
return retval;
}
private WaitSystemEventFlags GetSystemEventType(IntPtr pEvents) {
if(((int)pEvents & (int)WaitSystemEventFlags.ConnectedWinstation) == (int)WaitSystemEventFlags.ConnectedWinstation)
return WaitSystemEventFlags.ConnectedWinstation;
else if(((int)pEvents & (int)WaitSystemEventFlags.CreatedWinstation) == (int)WaitSystemEventFlags.CreatedWinstation)
return WaitSystemEventFlags.CreatedWinstation;
else if(((int)pEvents & (int)WaitSystemEventFlags.DisconnectedWinstation) == (int)WaitSystemEventFlags.DisconnectedWinstation)
return WaitSystemEventFlags.DisconnectedWinstation;
else if(((int)pEvents & (int)WaitSystemEventFlags.LicenseChange) == (int)WaitSystemEventFlags.LicenseChange)
return WaitSystemEventFlags.LicenseChange;
else if(((int)pEvents & (int)WaitSystemEventFlags.LogoffUser) == (int)WaitSystemEventFlags.LogoffUser)
return WaitSystemEventFlags.LogoffUser;
else if(((int)pEvents & (int)WaitSystemEventFlags.LogonUser) == (int)WaitSystemEventFlags.LogonUser)
return WaitSystemEventFlags.LogonUser;
else if(((int)pEvents & (int)WaitSystemEventFlags.RenamedWinstation) == (int)WaitSystemEventFlags.RenamedWinstation)
return WaitSystemEventFlags.RenamedWinstation;
else if(((int)pEvents & (int)WaitSystemEventFlags.WinstationStateChange) == (int)WaitSystemEventFlags.WinstationStateChange)
return WaitSystemEventFlags.WinstationStateChange;
else return WaitSystemEventFlags.None;
}
/// <param name="pServer"></param>
/// <param name="pSessionInfo"></param>
private TerminalSessionInfo GetSessionInfo(IntPtr pServer, IntPtr pSessionInfo) {
int iCurrent = (int)pSessionInfo;
uint iReturned = 0;
WTS_CLIENT_ADDRESS oClientAddres = new WTS_CLIENT_ADDRESS();
WTS_CLIENT_DISPLAY oClientDisplay = new WTS_CLIENT_DISPLAY();
WTS_CLIENT_PROTOCOL_TYPE oClientProtocol = WTS_CLIENT_PROTOCOL_TYPE.UNKNOWN;
WTS_CLIENT_INFO oClientInfo = new WTS_CLIENT_INFO();
WTSINFO oWtsInfo = new WTSINFO();
string sIPAddress = string.Empty;
string sUserName = string.Empty, sClientName = string.Empty;
string sDomain = string.Empty;
string sClientApplicationDirectory = string.Empty;
TerminalSessionInfo retval = new TerminalSessionInfo(0);
// Get session info structure
WTS_SESSION_INFO oSessionInfo = (WTS_SESSION_INFO)Marshal.PtrToStructure((System.IntPtr)iCurrent, typeof(WTS_SESSION_INFO));
//Get the IP address of the Terminal Services User
IntPtr pAddress = IntPtr.Zero;
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientAddress, out pAddress, out iReturned) == true) {
oClientAddres = (WTS_CLIENT_ADDRESS)Marshal.PtrToStructure(pAddress, oClientAddres.GetType());
sIPAddress = oClientAddres.bAddress[2] + "." + oClientAddres.bAddress[3] + "." + oClientAddres.bAddress[4] + "." + oClientAddres.bAddress[5];
}
//Get the User Name of the Terminal Services User
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSUserName, out pAddress, out iReturned) == true) {
sUserName = Marshal.PtrToStringAnsi(pAddress);
}
//Get the Client Name of the Terminal Services User
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientName, out pAddress, out iReturned) == true) {
sClientName = Marshal.PtrToStringAnsi(pAddress);
}
//Get the Domain Name of the Terminal Services User
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSDomainName, out pAddress, out iReturned) == true) {
sDomain = Marshal.PtrToStringAnsi(pAddress);
}
//Get the Display Information of the Terminal Services User
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientDisplay, out pAddress, out iReturned) == true) {
oClientDisplay = (WTS_CLIENT_DISPLAY)Marshal.PtrToStructure(pAddress, oClientDisplay.GetType());
}
//Get the Application Directory of the Terminal Services User
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientDirectory, out pAddress, out iReturned) == true) {
sClientApplicationDirectory = Marshal.PtrToStringAnsi(pAddress);
}
//Get protocol type
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientProtocolType, out pAddress, out iReturned) == true) {
oClientProtocol = (WTS_CLIENT_PROTOCOL_TYPE)Marshal.ReadInt16(pAddress);
}
//Get client info
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSClientInfo, out pAddress, out iReturned) == true) {
oClientInfo = (WTS_CLIENT_INFO)Marshal.PtrToStructure(pAddress, oClientInfo.GetType());
//sUserName = String.IsNullOrEmpty(sUserName) ? oClientInfo.UserName : sUserName;
}
//Get WTS info
if(WTSQuerySessionInformation(pServer, oSessionInfo.iSessionID, WTS_INFO_CLASS.WTSSessionInfo, out pAddress, out iReturned) == true) {
oWtsInfo = (WTSINFO)Marshal.PtrToStructure(pAddress, oWtsInfo.GetType());
}
// fill result
retval.SessionInfo = oSessionInfo;
//retval.SessionInfo.oState = oSessionInfo.oState;
//retval.SessionInfo.sWinsWorkstationName = oSessionInfo.sWinsWorkstationName == null ? "" : oSessionInfo.sWinsWorkstationName;
retval.UserName = sUserName == null ? "" : sUserName;
retval.ClientMachineName = sClientName == null ? "" : sClientName;
retval.ClientIPAddress = sIPAddress == null ? "" : sIPAddress;
retval.Domain = sDomain == null ? "" : sDomain;
retval.ProtocolType = oClientProtocol;
retval.ClientInfo = oClientInfo;
retval.WtsInfo = oWtsInfo;
return retval;
}
#endregion Private methods
#region Handlers
private event TerminalSessionChangedEventHandler mSessionChangedEventHandler;
/// <summary>
/// Occurs when a terminal session has changed.
/// </summary>
/// <remarks>
/// Following change types will be observed: <see cref="T:Oerlikon.Balzers.Rdp.Interfaces.WM_WTSSESSION_CHANGE"/> and <see cref="F:Oerlikon.Balzers.Rdp.Interfaces.WM_WTSSESSION_CHANGE.WM_WTSSESSION_CHANGE"/>
/// </remarks>
/// <seealso href="http://pinvoke.net/default.aspx/wtsapi32/WTSRegisterSessionNotification.html">WTSRegisterSessionNotification</seealso>
public event TerminalSessionChangedEventHandler SessionChanged {
add {
if(mSessionChangedEventHandler == null || !mSessionChangedEventHandler.GetInvocationList().Contains(value))
mSessionChangedEventHandler += value;
}
remove {
mSessionChangedEventHandler -= value;
}
}
public void OnSessionChanged(TerminalSessionChangedEventArgs SessionChangedEventArg) {
if(mSessionChangedEventHandler != null) {
TerminalSessionChangedSaveInvoker.SafeInvokeEvent(mSessionChangedEventHandler, SessionChangedEventArg);
}
}
#endregion Handlers
#region IDisposable Members
public void Dispose() {
if(!m_unregistered) {
StopTerminalSessionObservation(this.hServ);
m_unregistered = true;
}
}
#endregion
}
}
There is some ballast and unused waste in it but you can pick out the essentials.
This should work within desktop session as windows service too.
Ok the WTSRegisterSessionNotification - solution only works with a control or form in your hand, but what about a console applicaton or class running in a windows service environment and have no the ServiceBase object because its a plugin - based architecture.
Here are some structure definitions first:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
namespace Rdp.Interfaces {
/************************************************************************************************/
/* struct members */
/************************************************************************************************/
/// <summary>
/// Terminal Session Info data holder struct.
/// </summary>
/// <remarks>
/// Structures, interfaces, p-invoke members for Terminal Service Session
/// </remarks>
[Serializable]
public struct TerminalSessionInfo {
/// <summary>
/// Remote Desktop Services API Structure member
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_SESSION_INFO"/>
public WTS_SESSION_INFO SessionInfo;
/// <summary>
/// Remote Desktop Services API Structure member
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_PROTOCOL_TYPE"/>
public WTS_CLIENT_PROTOCOL_TYPE ProtocolType;
/// <summary>
/// Remote Desktop Services API Structure member
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO"/>
public WTS_CLIENT_INFO ClientInfo;
/// <summary>
/// Remote Desktop Services API Structure member
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTSINFO"/>
public WTSINFO WtsInfo;
/// <summary>
/// The client user name.
/// </summary>
public string UserName;
/// <summary>
/// The domain name of the client computer.
/// </summary>
public string Domain;
/// <summary>
/// The client network address.
/// </summary>
public string ClientIPAddress;
/// <summary>
/// The machine name of the client computer.
/// </summary>
public string ClientMachineName;
/// <summary>
/// Initializes a new instance of the <see cref="T:Oerlikon.Balzers.Rdp.Interfaces.TerminalSessionInfo"/> structure.
/// </summary>
/// <param name="SessionId">Only used to force an initialization of members.</param>
public TerminalSessionInfo(int SessionId) {
this.SessionInfo = new WTS_SESSION_INFO();
this.SessionInfo.iSessionID = SessionId;
this.SessionInfo.sWinsWorkstationName = String.Empty;
this.UserName = String.Empty;
this.Domain = String.Empty;
this.ClientIPAddress = String.Empty;
this.ProtocolType = WTS_CLIENT_PROTOCOL_TYPE.UNKNOWN;
this.ClientMachineName = String.Empty;
this.ClientInfo = new WTS_CLIENT_INFO();
this.ClientInfo.ClientMachineName = String.Empty;
this.ClientInfo.Domain = String.Empty;
this.ClientInfo.UserName = String.Empty;
this.ClientInfo.WorkDirectory = String.Empty;
this.ClientInfo.InitialProgram = String.Empty;
this.ClientInfo.ClientDirectory = String.Empty;
this.ClientInfo.DeviceId = String.Empty;
this.WtsInfo = new WTSINFO();
this.WtsInfo.Domain = String.Empty;
this.WtsInfo.UserName = String.Empty;
this.WtsInfo.WinStationName = String.Empty;
}
/// <summary>
/// Returns the fully qualified type name of this instance.
/// </summary>
/// <returns>
/// A <see cref="T:System.String"/> containing a fully qualified type name.
/// </returns>
/// <filterpriority>2</filterpriority>
public override string ToString() {
string retval = "SessionID: " + this.SessionInfo.iSessionID.ToString();
retval += String.IsNullOrEmpty(this.Domain) ? "" : Environment.NewLine + "Domain: " + this.Domain;
retval += String.IsNullOrEmpty(this.UserName) ? "" : Environment.NewLine + "UserName: " + this.UserName;
retval += String.IsNullOrEmpty(this.ClientMachineName) ? "" : Environment.NewLine + "ClientMachineName: " + this.ClientMachineName;
retval += String.IsNullOrEmpty(this.ClientIPAddress) ? "" : Environment.NewLine + "ClientIPAddress: " + this.ClientIPAddress;
retval += String.IsNullOrEmpty(this.SessionInfo.sWinsWorkstationName) ? "" : Environment.NewLine + "WinsWorkstationName: " + this.SessionInfo.sWinsWorkstationName;
retval += this.ProtocolType == WTS_CLIENT_PROTOCOL_TYPE.UNKNOWN ? "" : Environment.NewLine + "ProtocolType: " + this.ProtocolType.ToString();
retval += String.IsNullOrEmpty(this.SessionInfo.oState.ToString()) ? "" : Environment.NewLine + "State: " + this.SessionInfo.oState.ToString();
retval += String.IsNullOrEmpty(this.ClientInfo.ClientMachineName) ? "" : Environment.NewLine + "ClientInfoMachineName: " + this.ClientInfo.ClientMachineName;
retval += String.IsNullOrEmpty(this.ClientInfo.Domain) ? "" : Environment.NewLine + "ClientInfoDomain: " + this.ClientInfo.Domain;
retval += String.IsNullOrEmpty(this.ClientInfo.UserName) ? "" : Environment.NewLine + "ClientInfoUserName: " + this.ClientInfo.UserName;
retval += String.IsNullOrEmpty(this.ClientInfo.WorkDirectory) ? "" : Environment.NewLine + "ClientInfoWorkDirectory: " + this.ClientInfo.WorkDirectory;
retval += String.IsNullOrEmpty(this.ClientInfo.ClientDirectory) ? "" : Environment.NewLine + "ClientInfoDirectory: " + this.ClientInfo.ClientDirectory;
retval += String.IsNullOrEmpty(this.ClientInfo.DeviceId) ? "" : Environment.NewLine + "ClientInfoDeviceId: " + this.ClientInfo.DeviceId;
retval += this.ClientInfo.ClientBuildNumber == 0 ? "" : Environment.NewLine + "ClientInfoBuildNumber: " + this.ClientInfo.ClientBuildNumber.ToString();
retval += this.ClientInfo.ClientHardwareId == 0 ? "" : Environment.NewLine + "ClientInfoHardwareId: " + this.ClientInfo.ClientHardwareId.ToString();
retval += this.ClientInfo.ClientProductId == 0 ? "" : Environment.NewLine + "ClientInfoProductId: " + this.ClientInfo.ClientProductId.ToString();
retval += String.IsNullOrEmpty(this.WtsInfo.Domain) ? "" : Environment.NewLine + "WtsInfoDomain: " + this.WtsInfo.Domain;
retval += String.IsNullOrEmpty(this.WtsInfo.UserName) ? "" : Environment.NewLine + "WtsInfoUserName: " + this.WtsInfo.UserName;
retval += String.IsNullOrEmpty(this.WtsInfo.WinStationName) ? "" : Environment.NewLine + "WtsInfoWinStationName: " + this.WtsInfo.WinStationName;
retval += this.WtsInfo.ConnectTime == 0 ? "" : Environment.NewLine + "WtsInfoConnectTime: " + ToCSharpTime(this.WtsInfo.ConnectTime, true).ToString();
retval += this.WtsInfo.CurrentTime == 0 ? "" : Environment.NewLine + "WtsInfoCurrentTime: " + ToCSharpTime(this.WtsInfo.CurrentTime, true).ToString();
retval += this.WtsInfo.DisconnectTime == 0 ? "" : Environment.NewLine + "WtsInfoDisconnectTime: " + ToCSharpTime(this.WtsInfo.DisconnectTime, true).ToString();
retval += this.WtsInfo.LogonTime == 0 ? "" : Environment.NewLine + "WtsInfoLogonTime: " + ToCSharpTime(this.WtsInfo.LogonTime, true).ToString();
retval += this.WtsInfo.LastInputTime == 0 ? "" : Environment.NewLine + "WtsInfoLogonTime: " + ToCSharpTime(this.WtsInfo.LastInputTime, true).ToString();
retval += this.WtsInfo.IncomingBytes == 0 ? "" : Environment.NewLine + "WtsInfoIncomingBytes: " + this.WtsInfo.IncomingBytes.ToString();
retval += this.WtsInfo.OutgoingBytes == 0 ? "" : Environment.NewLine + "WtsInfoOutgoingBytes: " + this.WtsInfo.OutgoingBytes.ToString();
return retval;
}
/// <summary>
/// Help method to find C++ corresponding long value of C# DateTime (starting at 01
/// / 01 / 1970 00:00:00).
/// </summary>
/// <param name="date">.NET object</param>
/// <param name="localTime">If set to <see langword="true"/>, then date will be
/// assummed as local time and converted to UTC - time; otherwise, UTC will be
/// assumed.</param>
/// <returns>
/// C++ corresponding long value
/// </returns>
public static long ToUnixtime(DateTime date, bool localTime) {
DateTime unixStartTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
//DateTime unixStartTime = DateTime.MinValue;
if(localTime) date = date.ToUniversalTime();
TimeSpan timeSpan = date - unixStartTime;
return Convert.ToInt64(timeSpan.TotalMilliseconds);
}
/// <summary>
/// Help method to find C# DateTime from C++ corresponding long value.
/// </summary>
/// <param name="unixTime">Unix value of date time starting at 01 / 01 / 1970
/// 00:00:00</param>
/// <param name="localTime">If set to <see langword="true"/>, then ; otherwise,
/// .</param>
public static DateTime ToCSharpTime(long unixTime, bool localTime) {
DateTime unixStartTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
//DateTime unixStartTime = DateTime.MinValue;
if(localTime) return unixStartTime.AddTicks(unixTime).ToLocalTime();
return unixStartTime.AddTicks(unixTime);
}
#region IComparable Members
/// <summary>
/// Overriding Operator ==
/// </summary>
/// <param name="a">Object to compare</param>
/// <param name="b">Object to compare</param>
/// <returns>Return true if the segment start / end values match.</returns>
public static bool operator ==(TerminalSessionInfo a, TerminalSessionInfo b) {
return Equals(a, b);
}
/// <summary>
/// Overriding Operator !=
/// </summary>
/// <param name="a">Object to compare</param>
/// <param name="b">Object to compare</param>
/// <returns>Return true if the segment start / end values match.</returns>
public static bool operator !=(TerminalSessionInfo a, TerminalSessionInfo b) {
return !Equals(a, b);
}
/// <summary>
/// Overriding Equals
/// </summary>
/// <param name="obj">Object to compare with own instance.</param>
/// <returns>Return true if the segment start / end values match.</returns>
public override bool Equals(object obj) {
// If parameter is null return false.
if(obj == null) {
return false;
}
// If parameter cannot be cast to Point return false.
TerminalSessionInfo p = (TerminalSessionInfo)obj;
if((System.Object)p == null) {
return false;
}
// Return true if the segment start / end values match:
return Equals(this, p);
}
/// <summary>
/// Memberwise comparison
/// </summary>
/// <param name="a"></param>
/// <param name="b"></param>
public static bool Equals(TerminalSessionInfo a, TerminalSessionInfo b) {
bool retval = false;
if(((System.Object)a == null) && (System.Object)b == null) {
return false;
}
if(((System.Object)a == null) ^ (System.Object)b == null) {
return false;
}
// check property members
string[] properties = new string[] {
"UserName",
"Domain",
"ClientIPAddress",
"ClientMachineName",
"SessionInfo.iSessionID",
"SessionInfo.sWinsWorkstationName",
"SessionInfo.oState",
"ProtocolType",
"ClientInfo.ClientMachineName",
"ClientInfo.Domain",
"ClientInfo.UserName",
"ClientInfo.WorkDirectory",
"ClientInfo.InitialProgram",
"ClientInfo.EncryptionLevel",
"ClientInfo.ClientAddressFamily",
"ClientInfo.ClientAddress",
"ClientInfo.HRes",
"ClientInfo.VRes",
"ClientInfo.ColorDepth",
"ClientInfo.ClientDirectory",
"ClientInfo.ClientBuildNumber",
"ClientInfo.ClientHardwareId",
"ClientInfo.ClientProductId",
"ClientInfo.DeviceId",
"WtsInfo.State",
"WtsInfo.SessionId",
"WtsInfo.WinStationName",
"WtsInfo.Domain",
"WtsInfo.UserName",
"WtsInfo.ConnectTime",
"WtsInfo.DisconnectTime",
"WtsInfo.LogonTime" };
retval = true;
object vala, valb;
foreach(string prop in properties) {
try {
vala = GetFieldItem(a, prop);
valb = GetFieldItem(b, prop);
if(((System.Object)vala == null) && (System.Object)valb == null)
continue;
if(((System.Object)vala == null) ^ (System.Object)valb == null)
return false;
if(!Object.Equals(vala, valb))
return false;
}
catch(Exception ex) {
retval = false;
}
}
return retval;
}
/* Ein Property TopDown suchen. Wir graben uns durch die Hierarchiestufen einer Klasse nach unten,
// Bis wir das erste mal auf das Property "name" stossen. Dieses wird dann zurückgemolden
// Bei überladenen Properties wird dann erst das überladene gefunden.
// Über den normalen Type.GetProperty(name) würde sonst ein ambigous error erzeugt.*/
/// <summary>
/// Gets property with path <see paramref="name"/> from <see paramref="obj"/>.
/// Using System.Type.GetProperty(name) throws an exception if a property is overloaded. This method
/// does not throw an ambigous exception instead it returns the overloaded property value.
/// </summary>
/// <param name="obj">Object with properties.</param>
/// <param name="name">Path to property (e.g.: TimeOfDay.Hours or Ticks)</param>
/// <returns></returns>
static public System.Reflection.PropertyInfo GetPropertyTopDown(System.Object obj, System.String name) {
System.Type trs = obj.GetType();
for(trs = obj.GetType(); trs != null; trs = trs.BaseType) {
// Nur Properties, die in dieser Hierarchiestufe der Klasse deklariert wurden
System.Reflection.PropertyInfo[] pis = trs.GetProperties(
System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.DeclaredOnly
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Static);
foreach(System.Reflection.PropertyInfo pi in pis) {
System.Diagnostics.Debug.Assert(trs == pi.DeclaringType);
if(pi.Name == name) {
//System.Diagnostics.Debug.WriteLine(pi.DeclaringType + " -> " + pi.Name);
return pi;
}
}
}
return null;
}
/* Ein Property TopDown suchen. Wir graben uns durch die Hierarchiestufen einer Klasse nach unten,
// Bis wir das erste mal auf das Property "name" stossen. Dieses wird dann zurückgemolden
// Bei überladenen Properties wird dann erst das überladene gefunden.
// Über den normalen Type.GetProperty(name) würde sonst ein ambigous error erzeugt.*/
/// <summary>
/// Gets property with path <see cref=""/> from <see cref=""/>. Using
/// System.Type.GetField(name) throws an exception if a property is overloaded. This
/// method does not throw an ambigous exception instead it returns the overloaded
/// property value.
/// </summary>
/// <param name="obj">Object with properties.</param>
/// <param name="name">Path to property (e.g.: TimeOfDay.Hours or Ticks)</param>
static public System.Reflection.FieldInfo GetFieldTopDown(System.Object obj, System.String name) {
System.Type trs = obj.GetType();
for(trs = obj.GetType(); trs != null; trs = trs.BaseType) {
// Nur Properties, die in dieser Hierarchiestufe der Klasse deklariert wurden
System.Reflection.FieldInfo[] pis = trs.GetFields(
System.Reflection.BindingFlags.Public
| System.Reflection.BindingFlags.Instance
| System.Reflection.BindingFlags.DeclaredOnly
| System.Reflection.BindingFlags.NonPublic
| System.Reflection.BindingFlags.Static);
foreach(System.Reflection.FieldInfo fi in pis) {
System.Diagnostics.Debug.Assert(trs == fi.DeclaringType);
if(fi.Name == name) {
//System.Diagnostics.Debug.WriteLine(pi.DeclaringType + " -> " + pi.Name);
return fi;
}
}
}
return null;
}
/// <summary>
/// Gets value of property with path <paramref name="name"/>.
/// </summary>
/// <param name="obj">Object with properties.</param>
/// <param name="name">Property path.</param>
/// <example>
/// <code><![CDATA[
/// System.DateTime date = new System.DateTime();
/// int hours = (int)Balzers.Misc.ReflectionHelper.GetItem(date, "TimeOfDay.Hours");
/// long ticks = (long)Balzers.Misc.ReflectionHelper.GetItem(date, "Ticks");
/// ]]></code>
/// </example>
static public System.Object GetFieldItem(System.Object obj, System.String name) {
System.Reflection.FieldInfo fi = null;
System.String[] s = name.Split(new char[] { '.' }, 2);
while(s.Length > 1) {
//pi = Balzers.Misc.ReflectionHelper.GetPropertyTopDown(obj, name);
//System.Diagnostics.Debug.Assert(pi != null, "GetItem(obj, " + name + ")");
fi = GetFieldTopDown(obj, s[0]);
System.Diagnostics.Debug.Assert(fi != null, "GetFieldItem(obj, " + name + ")");
obj = fi.GetValue(obj);
//obj = obj.GetType().GetProperty(s[0]).GetValue(obj, null);
s = s[1].Split(new char[] { '.' }, 2);
}
//pi = Balzers.Misc.ReflectionHelper.GetPropertyTopDown(obj, name);
//System.Diagnostics.Debug.Assert(pi != null, "GetItem(obj, " + name + ")");
fi = GetFieldTopDown(obj, s[0]);
System.Diagnostics.Debug.Assert(fi != null, "GetFieldItem(obj, " + s[0] + ")");
System.Object value = fi.GetValue(obj);
return value;
//return obj.GetType().GetProperty(s[0]).GetValue(obj, null);
}
#endregion
}
}
Here ist the next part:
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using System.Windows.Forms;
using System.Reflection;
namespace Rdp.Interfaces {
#region struct members
//Structure for Terminal Service Client IP Address
/// </remarks>
[StructLayout(LayoutKind.Sequential)]
public struct WTS_CLIENT_ADDRESS {
/// <summary>
/// Address family. This member can
/// be <b>AF_INET</b>, <b>AF_INET6</b>, <b>AF_IPX</b>, <b>AF_NETBIOS</b>,
/// or <b>AF_UNSPEC</b>.
/// </summary>
public int iAddressFamily;
/// <summary>
/// Client network address. The format of the field of <b>Address</b> depends on the
/// address type as specified by the <b>AddressFamily</b> member.
/// <para>For an address family <b>AF_INET</b>: <b>Address </b>contains the IPV4
/// address of the client as a null-terminated string.</para>
/// <para>For an family <b>AF_INET6</b>: <b>Address </b>contains the IPV6 address of
/// the client as raw byte values. (For example, the address "FFFF::1"
/// would be represented as the following series of byte values: "0xFF 0xFF
/// 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
/// 0x01")</para>
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 20)]
public byte[] bAddress;
}
/// <summary>
/// Maximum string lengths constants used within RDP API structures <see href="https://msdn.microsoft.com/en-us/library/bb736369(v=vs.85).aspx#">MSDN</see>
/// </summary>
/// <seealso href="https://msdn.microsoft.com/en-us/library/bb736369(v=vs.85).aspx">MSDN
/// Example</seealso>
public struct WTSAPI32_CONSTANTS {
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int USERNAME_LENGTH = 20;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int CLIENTNAME_LENGTH = 20;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int CLIENTADDRESS_LENGTH = 30;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int MAX_ELAPSED_TIME_LENGTH = 15;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int MAX_DATE_TIME_LENGTH = 15;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int WINSTATIONNAME_LENGTH = 32;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int DOMAIN_LENGTH = 17;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int WTS_DRIVE_LENGTH = 3;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int WTS_LISTENER_NAME_LENGTH = 32;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int WTS_COMMENT_LENGTH = 60;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int PRODUCTINFO_COMPANYNAME_LENGTH = 256;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int PRODUCTINFO_PRODUCTID_LENGTH = 4;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int VALIDATIONINFORMATION_LICENSE_LENGTH = 16384;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int VALIDATIONINFORMATION_HARDWAREID_LENGTH = 20;
/// <summary>
/// Maximum string lengths constants used within RDP API structures
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO">Example
/// WTS_CLIENT_INFO</seealso>
public const int MAX_PATH = 260;
}
//Structure for Terminal Service Client Infostructure
/// <summary>
/// Contains information about a Remote Desktop Connection (RDC) client.
/// </summary>
/// <seealso href="https://msdn.microsoft.com/en-us/library/bb736369(v=vs.85).aspx#">MSDN</seealso>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
[Serializable]
public struct WTS_CLIENT_INFO {
/// <summary>
/// The NetBIOS name of the client computer.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.CLIENTNAME_LENGTH + 1)]
public string ClientMachineName;
/// <summary>
/// The domain name of the client computer.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.DOMAIN_LENGTH + 1)]
public string Domain;
/// <summary>
/// The client user name.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.USERNAME_LENGTH + 1)]
public string UserName;
/// <summary>
/// The folder for the initial program.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.MAX_PATH + 1)]
public string WorkDirectory;
/// <summary>
/// The program to start on connection.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.MAX_PATH + 1)]
public string InitialProgram;
/// <summary>
/// The security level of encryption.
/// </summary>
[MarshalAs(UnmanagedType.U1)]
public byte EncryptionLevel;
/// <summary>
/// The address family. This member can
/// be <b>AF_INET</b>, <b>AF_INET6</b>, <b>AF_IPX</b>, <b>AF_NETBIOS</b>,
/// or <b>AF_UNSPEC</b>.
/// </summary>
[MarshalAs(UnmanagedType.U4)]
public UInt32 ClientAddressFamily;
/// <summary>
/// The client network address.
/// </summary>
[MarshalAs(UnmanagedType.ByValArray, SizeConst = WTSAPI32_CONSTANTS.CLIENTADDRESS_LENGTH + 1)]
public UInt16[] ClientAddress;
/// <summary>
/// Horizontal dimension, in pixels, of the client's display.
/// </summary>
[MarshalAs(UnmanagedType.U2)]
public UInt16 HRes;
/// <summary>
/// Vertical dimension, in pixels, of the client's display.
/// </summary>
[MarshalAs(UnmanagedType.U2)]
public UInt16 VRes;
/// <summary>
/// Color depth of the client's display. For possible values, see
/// the <b>ColorDepth</b> member of the <b>WTS_CLIENT_DISPLAY</b> structure.
/// </summary>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_DISPLAY"/>
[MarshalAs(UnmanagedType.U2)]
public UInt16 ColorDepth;
/// <summary>
/// The location of the client ActiveX control DLL.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.MAX_PATH + 1)]
public string ClientDirectory;
/// <summary>
/// The client build number.
/// </summary>
[MarshalAs(UnmanagedType.U4)]
public UInt32 ClientBuildNumber;
/// <summary>
/// Reserved.
/// </summary>
[MarshalAs(UnmanagedType.U4)]
public UInt32 ClientHardwareId;
/// <summary>
/// Reserved.
/// </summary>
[MarshalAs(UnmanagedType.U2)]
public UInt16 ClientProductId;
/// <summary>
/// The number of output buffers on the server per session.
/// </summary>
[MarshalAs(UnmanagedType.U2)]
public UInt16 OutBufCountHost;
/// <summary>
/// The number of output buffers on the client.
/// </summary>
[MarshalAs(UnmanagedType.U2)]
public UInt16 OutBufCountClient;
/// <summary>
/// The length of the output buffers, in bytes.
/// </summary>
[MarshalAs(UnmanagedType.U2)]
public UInt16 OutBufLength;
/// <summary>
/// The device ID of the network adapter.
/// </summary>
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.MAX_PATH + 1)]
public string DeviceId;
}
/// <summary>
/// Contains information about a Remote Desktop Services session.
/// </summary>
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
[Serializable]
public struct WTSINFO {
/// <summary>
/// A value of the <b>WTS_CONNECTSTATE_CLASS</b> enumeration type that indicates the
/// session's current connection state.
/// </summary>
public WTS_CONNECTSTATE_CLASS State;
public UInt32 SessionId;
public UInt32 IncomingBytes;
public UInt32 OutgoingBytes;
public UInt32 IncomingFrames;
public UInt32 OutgoingFrames;
public UInt32 IncomingCompressedBytes;
public UInt32 OutgoingCompressedBytes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.WINSTATIONNAME_LENGTH)]
public String WinStationName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.DOMAIN_LENGTH)]
public String Domain;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = WTSAPI32_CONSTANTS.USERNAME_LENGTH + 1)]
public String UserName;
[MarshalAs(UnmanagedType.I8)]
public Int64 ConnectTime;
[MarshalAs(UnmanagedType.I8)]
public Int64 DisconnectTime;
[MarshalAs(UnmanagedType.I8)]
public Int64 LastInputTime;
[MarshalAs(UnmanagedType.I8)]
public Int64 LogonTime;
[MarshalAs(UnmanagedType.I8)]
public Int64 CurrentTime;
}
/// <summary>
/// <para>Contains information about a client session on a Remote Desktop Session
/// Host (RD Session Host) server.</para>
/// </summary>
/// <seealso href="http://pinvoke.net/default.aspx/Structures/_WTS_SESSION_INFO.html">http://pinvoke.net/</seealso>
/// <seealso href="https://msdn.microsoft.com/en-us/library/aa383864(v=vs.85).aspx">MSDN</seealso>
[StructLayout(LayoutKind.Sequential)]
[Serializable]
public struct WTS_SESSION_INFO {
/// <summary>
/// A Terminal Services session identifier. To indicate the session in which the
/// calling application is running.<br/>
/// </summary>
public int iSessionID;
/// <summary>
/// Pointer to a null-terminated string that contains the WinStation name of this
/// session. The WinStation name is a name that Windows associates with the session,
/// for example, "services", "console", or
/// "RDP-Tcp#0".
/// </summary>
[MarshalAs(UnmanagedType.LPStr)]
public string sWinsWorkstationName;
/// <summary>
/// A value from the <b>WTS_CONNECTSTATE_CLASS</b> enumeration type that indicates
/// the session's current connection state.
/// </summary>
/// <seealso href="https://msdn.microsoft.com/en-us/library/aa383860(v=vs.85).aspx">MSDN</seealso>
/// <seealso cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CONNECTSTATE_CLASS"/>
public WTS_CONNECTSTATE_CLASS oState;
}
// Structure for Terminal Service Session Client Display
/// <summary>
/// Contains information about the display of a Remote Desktop Connection (RDC)
/// client.
/// </summary>
/// <seealso href="https://msdn.microsoft.com/en-us/library/aa383858(v=vs.85).aspx">MSDN</seealso>
[StructLayout(LayoutKind.Sequential)]
public struct WTS_CLIENT_DISPLAY {
/// <summary>
/// Horizontal dimension, in pixels, of the client's display.
/// </summary>
public int iHorizontalResolution;
/// <summary>
/// Vertical dimension, in pixels, of the client's display.
/// </summary>
public int iVerticalResolution;
//1 = The display uses 4 bits per pixel for a maximum of 16 colors.
//2 = The display uses 8 bits per pixel for a maximum of 256 colors.
//4 = The display uses 16 bits per pixel for a maximum of 2^16 colors.
//8 = The display uses 3-byte RGB values for a maximum of 2^24 colors.
//16 = The display uses 15 bits per pixel for a maximum of 2^15 colors.
public int iColorDepth;
}
#endregion struct members
/************************************************************************************************/
/* enum members */
/************************************************************************************************/
#region enum members
/// <summary>
/// Specifies the connection state of a Remote Desktop Services session.
/// </summary>
/// <seealso href="https://msdn.microsoft.com/en-us/library/aa383860(v=vs.85).aspx">MSDN</seealso>
public enum WTS_CONNECTSTATE_CLASS {
/// <summary>
/// A user is logged on to the WinStation.
/// </summary>
WTSActive,
/// <summary>
/// The WinStation is connected to the client.
/// </summary>
WTSConnected,
/// <summary>
/// The WinStation is in the process of connecting to the client.
/// </summary>
WTSConnectQuery,
/// <summary>
/// The WinStation is shadowing another WinStation.
/// </summary>
WTSShadow,
/// <summary>
/// The WinStation is active but the client is disconnected.
/// </summary>
WTSDisconnected,
/// <summary>
/// The WinStation is waiting for a client to connect.
/// </summary>
WTSIdle,
/// <summary>
/// The WinStation is listening for a connection. A listener session waits for
/// requests for new client connections. No user is logged on a listener session. A
/// listener session cannot be reset, shadowed, or changed to a regular client
/// session.
/// </summary>
WTSListen,
/// <summary>
/// The WinStation is being reset.
/// </summary>
WTSReset,
/// <summary>
/// The WinStation is down due to an error.
/// </summary>
WTSDown,
/// <summary>
/// The WinStation is initializing.
/// </summary>
WTSInit
}
/// <summary>
/// <para>A <b>USHORT</b> value that specifies information about the protocol type
/// for the session. This is one of the following values:</para>
/// </summary>
/// <seealso href="https://msdn.microsoft.com/en-us/library/aa383861(v=vs.85).aspx">MSDN</seealso>
public enum WTS_CLIENT_PROTOCOL_TYPE : ushort {
/// <summary>
/// The console session.
/// </summary>
CONSOLE = 0,
/// <summary>
/// This value is retained for legacy purposes.
/// </summary>
LEGACY,
/// <summary>
/// The RDP protocol
/// </summary>
RDP,
/// <summary>
/// Custom value for internal use
/// </summary>
UNKNOWN
}
/// <summary>
/// Contains values that indicate the type of session information to retrieve in a call to the <see cref="WTSQuerySessionInformation"/> function.
/// </summary>
public enum WTS_INFO_CLASS {
/// <summary>
/// A null-terminated string that contains the name of the initial program that Remote Desktop Services runs when the user logs on.
/// </summary>
WTSInitialProgram,
/// <summary>
/// A null-terminated string that contains the published name of the application that the session is running.
/// </summary>
WTSApplicationName,
/// <summary>
/// A null-terminated string that contains the default directory used when launching the initial program.
/// </summary>
WTSWorkingDirectory,
/// <summary>
/// This value is not used.
/// </summary>
WTSOEMId,
/// <summary>
/// A <B>ULONG</B> value that contains the session identifier.
/// </summary>
WTSSessionId,
/// <summary>
/// A null-terminated string that contains the name of the user associated with the session.
/// </summary>
WTSUserName,
/// <summary>
/// A null-terminated string that contains the name of the Remote Desktop Services session.
/// </summary>
/// <remarks>
/// <B>Note</B> Despite its name, specifying this type does not return the window station name.
/// Rather, it returns the name of the Remote Desktop Services session.
/// Each Remote Desktop Services session is associated with an interactive window station.
/// Because the only supported window station name for an interactive window station is "WinSta0",
/// each session is associated with its own "WinSta0" window station. For more information, see <see href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms687096(v=vs.85).aspx">Window Stations</see>.
/// </remarks>
WTSWinStationName,
/// <summary>
/// A null-terminated string that contains the name of the domain to which the logged-on user belongs.
/// </summary>
WTSDomainName,
/// <summary>
/// The session's current connection state. For more information, see <see cref="WTS_CONNECTSTATE_CLASS"/>.
/// </summary>
WTSConnectState,
/// <summary>
/// A <B>ULONG</B> value that contains the build number of the client.
/// </summary>
WTSClientBuildNumber,
/// <summary>
/// A null-terminated string that contains the name of the client.
/// </summary>
WTSClientName,
/// <summary>
/// A null-terminated string that contains the directory in which the client is installed.
/// </summary>
WTSClientDirectory,
/// <summary>
/// A <B>USHORT</B> client-specific product identifier.
/// </summary>
WTSClientProductId,
/// <summary>
/// A <b>ULONG</b> value that contains a client-specific hardware identifier. This
/// option is reserved for future use. PInvoke function
/// WTSQuerySessionInformation will always return a value of 0.
/// </summary>
WTSClientHardwareId,
/// <summary>
/// The network type and network address of the client. For more information, see <see cref="WTS_CLIENT_ADDRESS"/>.
/// </summary>
/// <remarks>The IP address is offset by two bytes from the start of the <B>Address</B> member of the <see cref="WTS_CLIENT_ADDRESS"/> structure.</remarks>
WTSClientAddress,
/// <summary>
/// Information about the display resolution of the client. For more information, see <see cref="WTS_CLIENT_DISPLAY"/>.
/// </summary>
WTSClientDisplay,
/// <summary>
/// A USHORT value that specifies information about the protocol type for the session. This is one of the following values:<BR/>
/// 0 - The console session.<BR/>
/// 1 - This value is retained for legacy purposes.<BR/>
/// 2 - The RDP protocol.<BR/>
/// </summary>
WTSClientProtocolType,
/// <summary>
/// <para>This value returns <b>FALSE</b>. If you call PInvoke function
/// GetLastError to get extended error information, <b>GetLastError</b> returns
/// <b>ERROR_NOT_SUPPORTED</b>.</para>
/// </summary>
/// <remarks>
/// <b>Windows Server 2008, Windows Vista, Windows Server 2003, and Windows XP:</b>
/// This value is not used.
/// </remarks>
WTSIdleTime,
/// <summary>
/// This value returns <B>FALSE</B>. If you call the pinvoke GetLastError() to get extended error information, <B>GetLastError</B> returns <B>ERROR_NOT_SUPPORTED</B>.
/// </summary>
/// <remarks>
/// <B>Windows Server 2008, Windows Vista, Windows Server 2003, and Windows XP:</B> This value is not used.
/// </remarks>
WTSLogonTime,
/// <summary>
/// This value returns <B>FALSE</B>. If you call the pinvoke GetLastError() to get extended error information, <B>GetLastError</B> returns <B>ERROR_NOT_SUPPORTED</B>.
/// </summary>
/// <remarks>
/// <B>Windows Server 2008, Windows Vista, Windows Server 2003, and Windows XP:</B> This value is not used.
/// </remarks>
WTSIncomingBytes,
/// <summary>
/// This value returns <B>FALSE</B>. If you call the pinvoke GetLastError() to get extended error information, <B>GetLastError</B> returns <B>ERROR_NOT_SUPPORTED</B>.
/// </summary>
/// <remarks>
/// <B>Windows Server 2008, Windows Vista, Windows Server 2003, and Windows XP:</B> This value is not used.
/// </remarks>
WTSOutgoingBytes,
/// <summary>
/// This value returns <B>FALSE</B>. If you call the pinvoke GetLastError() to get extended error information, <B>GetLastError</B> returns <B>ERROR_NOT_SUPPORTED</B>.
/// </summary>
/// <remarks>
/// <B>Windows Server 2008, Windows Vista, Windows Server 2003, and Windows XP:</B> This value is not used.
/// </remarks>
WTSIncomingFrames,
/// <summary>
/// This value returns <B>FALSE</B>. If you call the pinvoke GetLastError() to get extended error information, <B>GetLastError</B> returns <B>ERROR_NOT_SUPPORTED</B>.
/// </summary>
/// <remarks>
/// <B>Windows Server 2008, Windows Vista, Windows Server 2003, and Windows XP:</B> This value is not used.
/// </remarks>
WTSOutgoingFrames,
/// <summary>
/// Information about a Remote Desktop Connection (RDC) client. For more
/// information, see <see cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_CLIENT_INFO"/>.
/// </summary>
/// <remarks>
/// <b>Windows Vista, Windows Server 2003, and Windows XP:</b> This value is not
/// supported. This value is supported beginning with Windows Server 2008 and
/// Windows Vista with SP1.
/// </remarks>
WTSClientInfo,
/// <summary>
/// Information about a client session on an RD Session Host server. For more
/// information, see <see cref="T:Oerlikon.Balzers.Rdp.Interfaces.WTS_SESSION_INFO"/>.
/// </summary>
/// <remarks>
/// <b>Windows Vista, Windows Server 2003, and Windows XP:</b> This value is not
/// supported. This value is supported beginning with Windows Server 2008 and
/// Windows Vista with SP1.
/// </remarks>
WTSSessionInfo
}
#endregion
}
4.userB logout
you shall use SessionSwitchReason.ConsoleDisConnect as the reason.
5.userA restore session
you shall use SessionSwitchReason.ConsoleConnect as the reason.