// TARGET:Windows365.exe
// START_IN:
/* TODO:
- Comments
- Minimize PowerShell window
- Clean up code issues
- Data
*/
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Linq;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using LoginPI.Engine.ScriptBase;
using LoginPI.Engine.ScriptBase.Components;
namespace LoginVSI
{
public class UniversalConnector : ScriptBase
{
// Constants
private const int EngineTimeoutSeconds = 60;
private const int MaxTypingSpeed = 1000;
private const string MutexName = @"Global\CustomLoginVSIConnector";
private const string ActionParamClassName = "className";
private const string ActionParamData = "data";
private const string ActionParamOperation = "operation";
private const string ActionParamPostOpWait = "postOpWait";
private const string ActionParamProcess = "process";
private const string ActionParamSkip = "skip";
private const string ActionParamText = "text";
private const string ActionParamTitle = "title";
private const string ActionParamUseMainWindow = "useMainWindow";
private const string ActionParamWindowClass = "windowClass";
private const string DefaultTempFolder = $@"C:\temp";
private const string OperationNameClick = "click";
private const string OperationNameType = "type";
// Private Fields and Properties
private bool _loginSucceeded;
private int _typingSpeed = 500;
private string _connectionUrl = "";
private string _connectorLogFile = "";
private string _connectorLogFolder = "";
private string _domain = "";
private string _password = "";
private string _resource = "";
private string _saveFile = "";
private string _userName = "";
private readonly string _startTimestamp = DateTime.Now.ToString("yyyyMMddHHmmss");
///
/// An array of dictionaries that define the actions to be performed to download the RDP file.
///
private Dictionary[] GetStartRemoteConnectionActions() {
return new Dictionary[] {
new Dictionary {
{ActionParamClassName, "Button:*"},
{ActionParamTitle, "Sign in"},
{ActionParamText, "*"},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamOperation, OperationNameClick},
{ActionParamPostOpWait, "1"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Button:*"},
{ActionParamTitle, "Sign in with another account"},
{ActionParamText, "*"},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamOperation, OperationNameClick},
{ActionParamPostOpWait, "1"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Edit"},
{ActionParamTitle, "Email address"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameType},
{ActionParamData, GetUserName()},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamPostOpWait, "1"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Button"},
{ActionParamTitle, "Next"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameClick},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamPostOpWait, "2"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Edit"},
{ActionParamTitle, $"Enter the password*"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameType},
{ActionParamData, GetPassword() + "{ENTER}"},
{ActionParamProcess, "explorer"},
{ActionParamWindowClass, "Win32 Window:ApplicationFrameWindow"},
{ActionParamPostOpWait, "2"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Button*"},
{ActionParamTitle, "Devices*"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameClick},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamPostOpWait, "1"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Edit:*"},
{ActionParamTitle, "Search"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameType},
{ActionParamData, GetResource() + "{ENTER}"},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamPostOpWait, "1"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Button:*"},
{ActionParamTitle, "Connect"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameClick},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamPostOpWait, "10"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Button:*"},
{ActionParamTitle, "Account"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameClick},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamPostOpWait, "1"},
{ActionParamSkip, bool.FalseString}
},
new Dictionary {
{ActionParamClassName, "Button:*"},
{ActionParamTitle, "Sign out"},
{ActionParamText, "*"},
{ActionParamOperation, OperationNameClick},
{ActionParamUseMainWindow, bool.TrueString},
{ActionParamPostOpWait, "1"},
{ActionParamSkip, bool.FalseString}
},
};
}
// Public Methods
///
/// Retrieve the value for the UserName property.
///
/// The username that will be used to access the resource as a string.
public string GetUserName()
{
return _userName;
}
///
/// Retrieve the value for the StartTimestamp property.
///
/// A date and time string formatted as yyyyMMddHHmmss.
public string GetStartTimestamp()
{
return _startTimestamp;
}
///
/// Set the value for the UserName property
///
/// The username to use to login to the AVD desktop.
public void SetUserName(string userName)
{
if (string.IsNullOrWhiteSpace(userName))
{
Log("UserName cannot be null or empty");
throw new ArgumentException("UserName cannot be null or empty", nameof(userName));
}
_userName = userName;
}
///
/// Retrieve the value for the Password property
///
/// The value of the Passwor as a string.
public string GetPassword()
{
return _password;
}
///
/// Assign a value to the Password property
///
/// The string to use as the user's password.
/// Thrown if the value of the password parameter is empty or null.
public void SetPassword(string password)
{
if (string.IsNullOrWhiteSpace(password))
{
Log("Password cannot be null or empty");
throw new ArgumentException("Password cannot be null or empty", nameof(password));
}
_password = password;
}
///
/// Retrieve the value for the Resource property
///
/// The value of the Resource as a string.
public string GetResource()
{
return _resource;
}
///
/// Assign a value to the Resource property.
///
/// The name of the resource to connect to.
/// The value of the Resource property.
/// Thrown if the value of the resource parameter is empty or null.
public void SetResource(string resource)
{
if (string.IsNullOrWhiteSpace(resource))
{
Log("Resource cannot be null or empty");
throw new ArgumentException("Resource cannot be null or empty", nameof(resource));
}
_resource = resource;
}
///
/// Retrieve the value for the Domain property.
///
/// The value of the Domain as a string.
public string GetDomain()
{
return _domain;
}
///
/// Retrieve the value for the Domain property.
///
/// The name of the domain the user should log in to.
/// Thrown when the value of the domain parameter is empty or null.
public void SetDomain(string domain)
{
if (string.IsNullOrWhiteSpace(domain))
{
Log("Domain cannot be null or empty");
throw new ArgumentException("Domain cannot be null or empty", nameof(domain));
}
_domain = domain;
}
///
/// Retrieve the value for the WvdAddress property.
///
/// The connection URL as a string.
public string GetConnectionUrl()
{
return _connectionUrl;
}
///
/// Set the value for the ConnectionUrl property.
///
/// The URL to be used to start the remote connection.
/// Thrown if the ConnectionUrl is empty or null.
public void SetConnectionUrl(string connectionUrl)
{
const string errorMsg = "ConnectionUrl cannot be null or empty";
if (string.IsNullOrWhiteSpace(connectionUrl))
{
Log(errorMsg);
throw new ArgumentException(errorMsg, nameof(connectionUrl));
}
_connectionUrl = connectionUrl;
}
///
/// Retrieve the value for the TypingSpeed property.
///
/// The typing speed as an integer.
public int GetTypingSpeed()
{
return _typingSpeed;
}
public void SetTypingSpeed(string typingSpeed)
{
if (string.IsNullOrWhiteSpace(typingSpeed))
{
Log("TypingSpeed cannot be null or empty");
throw new ArgumentException("TypingSpeed cannot be null or empty", nameof(typingSpeed));
}
try
{
var typingSpeedInt = int.Parse(typingSpeed);
if (typingSpeedInt <= 0 || typingSpeedInt > MaxTypingSpeed)
{
Log("TypingSpeed cannot be null or empty");
throw new ArgumentException($"TypingSpeed must be greater than zero and no more than {MaxTypingSpeed}",
nameof(typingSpeed));
}
_typingSpeed = typingSpeedInt;
}
catch (Exception ex)
{
Log("TypingSpeed must be a valid integer");
throw new ArgumentException("TypingSpeed must be a valid integer", nameof(typingSpeed), ex);
}
}
public string GetConnectorLogFile() {
return _connectorLogFile;
}
public void SetConnectorLogFile(string logFile) {
_connectorLogFile = logFile;
}
public string GetConnectorLogFolder() {
return _connectorLogFolder;
}
public void SetConnectorLogFolder(string logFolder) {
if (!Directory.Exists(logFolder))
{
Directory.CreateDirectory(logFolder);
}
_connectorLogFolder = logFolder;
}
public string GetSaveFile()
{
if (!string.IsNullOrWhiteSpace(_saveFile)) return _saveFile;
// Construct the save file path
_saveFile = $@"{DefaultTempFolder}\{Guid.NewGuid()}.rdpw";
return _saveFile;
}
public string GetDomainUserName()
{
return GetUserName().Split('@')[0];
}
public bool GetLoginSucceeded()
{
return _loginSucceeded;
}
public void SetLoginSucceeded(bool loginSucceeded)
{
_loginSucceeded = loginSucceeded;
}
public string GetTempFolder()
{
// Check to see if the TEMP environment variable is set.
var tempFolder = Environment.GetEnvironmentVariable("TEMP") ?? DefaultTempFolder;
// Check that the Temp folder exists. Create it if it does not.
if (!Directory.Exists(tempFolder))
{
Directory.CreateDirectory(tempFolder);
}
return tempFolder;
}
// ReSharper disable once UnusedMember.Local
private void Execute()
{
InitializeConnector();
Wait(2);
var totalWaitTime = 0;
var maxWaitTime = 60;
var pollingInterval = 5;
while (Process.GetProcessesByName("LoginEnterprise.Engine.Standalone").Length > 2)
{
Log("Waiting for the previous instance to finish.");
Wait(pollingInterval);
totalWaitTime += pollingInterval;
if (totalWaitTime >= maxWaitTime)
{
ABORT($"Waited {maxWaitTime} for previous engine to finish.");
}
}
Log($"Starting connection process for {GetUserName()} to {GetResource()}");
START();
Wait(2);
PerformActions(GetStartRemoteConnectionActions());
Log($"SUCCESS: Connection to {GetResource()} for user {GetUserName()} was established.");
Log($"Connecter has completed. The log file is located at {GetConnectorLogFile()}");
STOP();
}
private void InitializeConnector()
{
SetUserName(Console.ReadLine());
SetPassword(Console.ReadLine());
SetResource(Console.ReadLine());
SetConnectionUrl(Console.ReadLine());
SetTypingSpeed("900");
SetConnectorLogFolder($@"{GetTempFolder()}\LoginVSILogs");
SetConnectorLogFile($@"{GetConnectorLogFolder()}\connector_{GetStartTimestamp()}.log");
// Report the values that were provided to from the console (by the PowerShell initiator).
// The password value will be hidden.
Log("Recieved the following input values from the console:");
foreach (var parameterName in new [] {"UserName", "Password", "Resource", "WvdAddress", "TypingSpeed"})
{
var getMethod = GetType().GetMethod($"Get{parameterName}");
var inputValue = getMethod?.Invoke(this, null);
if (parameterName == "Password")
{
inputValue = new string('*', inputValue.ToString().Length + new Random().Next(1, 5));
}
Log($"{parameterName}: {inputValue}");
}
}
private void PerformActions(Dictionary[] actions)
{
var state = 0;
var engineTimer = Stopwatch.StartNew();
while(state < actions.Length && engineTimer.Elapsed.Seconds < EngineTimeoutSeconds) {
Log($"PerformActions: Starting to handle state {state}");
var action = actions[state];
if (action[ActionParamSkip] == bool.TrueString) {
Log($"PerformActions: Skipping state {state}");
state++;
continue;
}
var className = action[ActionParamClassName];
var title = action[ActionParamTitle];
var text = action[ActionParamText];
var operation = action[ActionParamOperation];
var postOpWait = int.Parse(action[ActionParamPostOpWait]);
var windows = new IWindow[] {MainWindow};
if (!action.TryGetValue(ActionParamUseMainWindow, out _))
{
windows = FindWindows(className: action[ActionParamWindowClass], processName: action[ActionParamProcess]);
}
foreach(var w in windows) {
try {
var targetCtrl = w.FindControl(className: className, title: title, text: text);
if (targetCtrl == null) continue;
Log($"PerformActions: Found control with className: {className}, title: {title}, text: {text}");
w.Focus();
switch (operation)
{
case OperationNameType:
targetCtrl.Type(action[ActionParamData], cpm: GetTypingSpeed());
break;
case OperationNameClick:
targetCtrl.Click();
break;
default:
Log($"PerformActions: Unknown operation: {operation}");
break;
}
Wait(postOpWait);
Log($"PerformActions: Finished handling state {state}");
state++;
break;
} catch {
Log($"PerformActions: Searching for control with className: {className}, title: {title}, text: {text}");
}
}
} // END while
} // END: ExecuteAvdLogin
private new void Log(string message)
{
var logMessage = $"[{DateTime.Now:HH:mm:ss}] {message}";
base.Log(logMessage);
using (StreamWriter sw = File.AppendText(GetConnectorLogFile()))
{
sw.WriteLine(logMessage);
}
}
}
}