// 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); } } } }