Last active
March 7, 2025 09:56
-
-
Save 1mm0rt41PC/370491b6b312b8b296727cb9e01543c3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
############################################################# | |
# RDP Session Management with LAPS | |
# | |
# Author: Claude | |
# Date: March 6, 2025 | |
# Version: 1.1 | |
# | |
# Description: | |
# This script provides a GUI for managing RDP connections using LAPS passwords. | |
# It retrieves LAPS credentials from Active Directory, establishes RDP connections, | |
# and supports shadow mode for existing sessions. | |
# | |
# Features: | |
# - Retrieves LAPS passwords without additional modules | |
# - Manages credentials securely | |
# - Supports standard RDP and shadow sessions | |
# - Stays open until RDP session ends | |
# - Comprehensive logging with command output | |
# - Proper error handling | |
############################################################# | |
# Import required .NET assemblies | |
Add-Type -AssemblyName System.Windows.Forms | |
Add-Type -AssemblyName System.DirectoryServices | |
Add-Type -AssemblyName System.Drawing | |
#region Functions | |
Function Main-UI { | |
# Create the main form with appropriate properties | |
$global:form = New-Object System.Windows.Forms.Form | |
$global:form.Text = "RDP Session Management with LAPS" | |
$global:form.Size = New-Object System.Drawing.Size(500, 350) | |
$global:form.MinimumSize = New-Object System.Drawing.Size(500, 350) | |
$global:form.StartPosition = "CenterScreen" # Center the window on the screen | |
$global:form.FormBorderStyle = "Sizable" # Allow resizing | |
$global:form.MaximizeBox = $true # Allow maximizing the window | |
$global:form.AcceptButton = $null # Will be set after button is created | |
# Create a table layout panel for organizing controls | |
# This enables responsive resizing of the UI | |
$tableLayoutPanel = New-Object System.Windows.Forms.TableLayoutPanel | |
$tableLayoutPanel.Dock = [System.Windows.Forms.DockStyle]::Fill # Fill the entire form | |
$tableLayoutPanel.ColumnCount = 2 # Two columns (labels and inputs) | |
$tableLayoutPanel.RowCount = 7 # Seven rows for our controls | |
# Define column widths as percentages | |
$tableLayoutPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 30))) # Labels column (30%) | |
$tableLayoutPanel.ColumnStyles.Add((New-Object System.Windows.Forms.ColumnStyle([System.Windows.Forms.SizeType]::Percent, 70))) # Inputs column (70%) | |
# Configure rows with appropriate sizing | |
# The layout is divided into 7 rows with different proportions | |
for ($i = 0; $i -lt 7; $i++) { | |
if ($i -eq 4) { | |
# Status message row (larger to accommodate log) - 25% of form height | |
# This is where the log TextArea will be displayed | |
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 25))) | |
} | |
elseif ($i -eq 5) { | |
# ListBox row for session selection - 25% of form height | |
# This will display multiple sessions when found | |
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 25))) | |
} | |
elseif ($i -eq 6) { | |
# Button row - 10% of form height | |
# This contains the action buttons | |
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 10))) | |
} | |
else { | |
# Standard input rows - 8% of form height each | |
# These contain the form input fields | |
$tableLayoutPanel.RowStyles.Add((New-Object System.Windows.Forms.RowStyle([System.Windows.Forms.SizeType]::Percent, 8))) | |
} | |
} | |
# Add the table layout to the form | |
$global:form.Controls.Add($tableLayoutPanel) | |
#region Form Controls | |
# Target Computer Name Label and TextBox | |
$computerLabel = New-Object System.Windows.Forms.Label | |
$computerLabel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$computerLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight | |
$computerLabel.Text = "Target Computer Name:" | |
$tableLayoutPanel.Controls.Add($computerLabel, 0, 0) | |
$computerTextBox = New-Object System.Windows.Forms.TextBox | |
$computerTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$computerTextBox.Anchor = [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right | |
# Add KeyDown event for Enter key | |
$computerTextBox.Add_KeyDown({ | |
if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) { | |
$connectButton.PerformClick() | |
} | |
}) | |
$tableLayoutPanel.Controls.Add($computerTextBox, 1, 0) | |
# AD Username Label and TextBox | |
$userLabel = New-Object System.Windows.Forms.Label | |
$userLabel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$userLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight | |
$userLabel.Text = "AD Username:" | |
$tableLayoutPanel.Controls.Add($userLabel, 0, 1) | |
$userTextBox = New-Object System.Windows.Forms.TextBox | |
$userTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$userTextBox.Anchor = [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right | |
# Add KeyDown event for Enter key | |
$userTextBox.Add_KeyDown({ | |
if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) { | |
$connectButton.PerformClick() | |
} | |
}) | |
$tableLayoutPanel.Controls.Add($userTextBox, 1, 1) | |
# Password Label and TextBox | |
$passwordLabel = New-Object System.Windows.Forms.Label | |
$passwordLabel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$passwordLabel.TextAlign = [System.Drawing.ContentAlignment]::MiddleRight | |
$passwordLabel.Text = "Password:" | |
$tableLayoutPanel.Controls.Add($passwordLabel, 0, 2) | |
$passwordTextBox = New-Object System.Windows.Forms.MaskedTextBox | |
$passwordTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$passwordTextBox.Anchor = [System.Windows.Forms.AnchorStyles]::Left -bor [System.Windows.Forms.AnchorStyles]::Right | |
$passwordTextBox.PasswordChar = '*' | |
# Add KeyDown event for Enter key | |
$passwordTextBox.Add_KeyDown({ | |
if ($_.KeyCode -eq [System.Windows.Forms.Keys]::Enter) { | |
$connectButton.PerformClick() | |
} | |
}) | |
$tableLayoutPanel.Controls.Add($passwordTextBox, 1, 2) | |
# Empty row for spacing | |
$placeholder = New-Object System.Windows.Forms.Label | |
$placeholder.Text = "" | |
$tableLayoutPanel.Controls.Add($placeholder, 0, 3) | |
# Status TextArea (for logging actions) | |
# This TextBox serves as a log display showing operation history | |
$global:statusTextBox = New-Object System.Windows.Forms.TextBox | |
$global:statusTextBox.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$global:statusTextBox.Multiline = $true # Allow multiple lines | |
$global:statusTextBox.ReadOnly = $true # Prevent user editing | |
$global:statusTextBox.ScrollBars = [System.Windows.Forms.ScrollBars]::Vertical # Add scrollbar | |
$global:statusTextBox.BackColor = [System.Drawing.Color]::FromArgb(240, 240, 240) # Light gray background | |
$global:statusTextBox.Font = New-Object System.Drawing.Font("Consolas", 9) # Monospaced font for better log readability | |
# Initialize with first log message | |
$global:statusTextBox.Text = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] [INFO] Ready. Enter the information above and click 'Connect'" | |
# Make the TextBox span both columns | |
$tableLayoutPanel.SetColumnSpan($global:statusTextBox, 2) | |
$tableLayoutPanel.Controls.Add($global:statusTextBox, 0, 4) | |
# Panel for Sessions ListBox | |
# This panel contains the ListBox and allows it to be shown/hidden as needed | |
$sessionPanel = New-Object System.Windows.Forms.Panel | |
$sessionPanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$tableLayoutPanel.SetColumnSpan($sessionPanel, 2) # Make it span both columns | |
$tableLayoutPanel.Controls.Add($sessionPanel, 0, 5) | |
# Sessions ListBox | |
# This ListBox displays available sessions for selection when multiple are found | |
$sessionListBox = New-Object System.Windows.Forms.ListBox | |
$sessionListBox.Dock = [System.Windows.Forms.DockStyle]::Fill # Fill the panel | |
$sessionListBox.Visible = $false # Initially hidden | |
$sessionListBox.IntegralHeight = $false # Allow for partial items | |
$sessionListBox.Font = New-Object System.Drawing.Font("Segoe UI", 9) | |
$sessionListBox.BorderStyle = [System.Windows.Forms.BorderStyle]::FixedSingle | |
$sessionListBox.SelectionMode = [System.Windows.Forms.SelectionMode]::One | |
$sessionListBox.HorizontalScrollbar = $true # Enable horizontal scrolling for long entries | |
$sessionListBox.ItemHeight = 18 # Set consistent item height | |
$sessionPanel.Controls.Add($sessionListBox) | |
# Button Panel | |
# This panel organizes the buttons at the bottom of the form | |
$buttonPanel = New-Object System.Windows.Forms.FlowLayoutPanel | |
$buttonPanel.Dock = [System.Windows.Forms.DockStyle]::Fill | |
$buttonPanel.FlowDirection = [System.Windows.Forms.FlowDirection]::RightToLeft # Buttons align to right | |
$buttonPanel.WrapContents = $false # No wrapping | |
$buttonPanel.Padding = New-Object System.Windows.Forms.Padding(10) # Add padding | |
$tableLayoutPanel.SetColumnSpan($buttonPanel, 2) # Span both columns | |
$tableLayoutPanel.Controls.Add($buttonPanel, 0, 6) | |
# Connect Button | |
$connectButton = New-Object System.Windows.Forms.Button | |
$connectButton.AutoSize = $true # Size to fit content | |
$connectButton.MinimumSize = New-Object System.Drawing.Size(120, 30) # Minimum size | |
$connectButton.Text = "Connect" | |
$connectButton.BackColor = [System.Drawing.Color]::FromArgb(220, 240, 220) # Light green background | |
$connectButton.FlatStyle = [System.Windows.Forms.FlatStyle]::System # System-style button | |
$connectButton.Cursor = [System.Windows.Forms.Cursors]::Hand # Hand cursor on hover | |
$buttonPanel.Controls.Add($connectButton) | |
# Cancel Button | |
$cancelButton = New-Object System.Windows.Forms.Button | |
$cancelButton.AutoSize = $true # Size to fit content | |
$cancelButton.MinimumSize = New-Object System.Drawing.Size(120, 30) # Minimum size | |
$cancelButton.Text = "Cancel" | |
$cancelButton.BackColor = [System.Drawing.Color]::FromArgb(240, 240, 240) # Light gray background | |
$cancelButton.FlatStyle = [System.Windows.Forms.FlatStyle]::System # System-style button | |
$cancelButton.Cursor = [System.Windows.Forms.Cursors]::Hand # Hand cursor on hover | |
$cancelButton.Add_Click({ | |
# Close the form when cancel is clicked | |
Add-LogEntry "Operation cancelled by user." -Type "INFO" | |
$global:form.Close() | |
}) | |
$buttonPanel.Controls.Add($cancelButton) | |
#endregion Form Controls | |
#endregion UI Setup | |
#region Event Handlers | |
# Connect Button Click Event Handler | |
$connectButton.Add_Click({ | |
# Validate computer name | |
$computerName = $computerTextBox.Text.Trim() | |
if ([string]::IsNullOrEmpty($computerName)) { | |
[System.Windows.Forms.MessageBox]::Show("Please enter a valid computer name.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | |
return | |
} | |
# Validate credentials | |
$userName = $userTextBox.Text.Trim() | |
$password = $passwordTextBox.Text | |
if ([string]::IsNullOrEmpty($userName) -or [string]::IsNullOrEmpty($password)) { | |
[System.Windows.Forms.MessageBox]::Show("Please enter valid credentials.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | |
return | |
} | |
# Create secure credential object | |
$securePassword = ConvertTo-SecureString $password -AsPlainText -Force | |
$credential = New-Object System.Management.Automation.PSCredential($userName, $securePassword) | |
# Update status and retrieve LAPS password | |
Add-LogEntry "Retrieving LAPS password for $computerName..." | |
$lapsPassword = Get-LAPSPassword -ComputerName $computerName -Credential $credential | |
# Check if password was retrieved | |
if ([string]::IsNullOrEmpty($lapsPassword)) { | |
[System.Windows.Forms.MessageBox]::Show("Unable to retrieve LAPS password for this computer.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | |
Add-LogEntry "ERROR: Unable to retrieve LAPS password for $computerName" | |
return | |
} | |
# Update status and set up connection credentials | |
Add-LogEntry "LAPS password retrieved successfully. Setting up connection credentials..." -Type "INFO" | |
# Store credentials securely for the RDP connection | |
# Note: Password is enclosed in quotes to handle special characters | |
$cmdKeyCommand = "cmdkey /add:$computerName /user:$computerName\administrator /pass:`"$lapsPassword`"" | |
try { | |
$cmdKeyResult = Invoke-Expression $cmdKeyCommand | |
Add-CommandResult -Command "cmdkey /add:$computerName /user:.\administrator /pass:********" -Result "$cmdKeyResult" | |
Add-LogEntry "Stored administrator credentials for $computerName" -Type "SUCCESS" | |
} | |
catch { | |
Add-CommandResult -Command "cmdkey /add:$computerName /user:.\administrator /pass:********" -Result "$_" | |
Add-LogEntry "Failed to store credentials: $_" -Type "ERROR" | |
} | |
# Update status and retrieve Winlogon sessions | |
Add-LogEntry "Retrieving Winlogon sessions from $computerName..." -Type "INFO" | |
$sessions = Get-WinlogonSessions -ComputerName $computerName | |
# Handle different session scenarios | |
if ($sessions -eq $null -or $sessions.Count -eq 0) { | |
# No sessions - standard RDP connection | |
Add-LogEntry "No sessions found. Starting standard RDP connection..." | |
Start-MstscAndWait -Arguments "/v:$computerName" -ComputerName $computerName | |
} | |
elseif ($sessions.Count -eq 1) { | |
# Single session - direct shadow | |
$sessionId = $sessions[0].SessionId | |
Add-LogEntry "One session found (ID: $sessionId). Starting shadow connection..." | |
Start-MstscAndWait -Arguments "/shadow:$sessionId /control /v:$computerName" -ComputerName $computerName | |
} | |
else { | |
# Multiple sessions - prompt for selection | |
Add-LogEntry "Multiple sessions found ($($sessions.Count)). Please select a session to target." | |
$sessionListBox.Visible = $true | |
$global:form.Refresh() | |
# Populate session list | |
$sessionListBox.Items.Clear() | |
foreach ($session in $sessions) { | |
$sessionListBox.Items.Add("Session ID: $($session.SessionId) - Process ID: $($session.ProcessId)") | |
} | |
# Create shadow session button | |
$connectSessionButton = New-Object System.Windows.Forms.Button | |
$connectSessionButton.AutoSize = $true | |
$connectSessionButton.MinimumSize = New-Object System.Drawing.Size(120, 30) | |
$connectSessionButton.Text = "Shadow Session" | |
$connectSessionButton.Add_Click({ | |
# Validate selection | |
if ($sessionListBox.SelectedIndex -eq -1) { | |
[System.Windows.Forms.MessageBox]::Show("Please select a session.", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | |
return | |
} | |
# Get selected session ID | |
$selectedSession = $sessions[$sessionListBox.SelectedIndex] | |
$sessionId = $selectedSession.SessionId | |
# Log the selection | |
Add-LogEntry "Selected session ID: $sessionId - Starting shadow connection..." | |
# Start shadow session | |
Start-MstscAndWait -Arguments "/shadow:$sessionId /control /v:$computerName" -ComputerName $computerName | |
# Note: The nettoyage des identifiants is handled by the Start-MstscAndWait function | |
}) | |
$buttonPanel.Controls.Add($connectSessionButton) | |
return | |
} | |
# Note: The credentials cleanup is handled by the Start-MstscAndWait function | |
# The form closing is also handled by that function | |
}) | |
#endregion Event Handlers | |
# Set the default button (triggered by Enter key in the form) | |
$global:form.AcceptButton = $connectButton | |
# Display the form | |
$global:form.Add_Shown({$global:form.Activate()}) | |
[void] $global:form.ShowDialog() | |
} | |
#region Core Functions | |
<# | |
.SYNOPSIS | |
Retrieves the LAPS password for a specified computer from Active Directory. | |
.DESCRIPTION | |
Uses Directory Services to query AD and retrieve the LAPS password | |
for a specified computer using provided credentials. | |
The function connects to Active Directory, searches for the computer object, | |
and extracts the ms-Mcs-AdmPwd attribute which contains the LAPS password. | |
.PARAMETER ComputerName | |
The name of the computer to retrieve the LAPS password for. | |
.PARAMETER Credential | |
The credentials to use when connecting to Active Directory. | |
.RETURNS | |
Returns the LAPS password as a string if successful, or $null if not found or on error. | |
.EXAMPLE | |
$cred = Get-Credential | |
$password = Get-LAPSPassword -ComputerName "PC001" -Credential $cred | |
#> | |
function Get-LAPSPassword { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$ComputerName, | |
[Parameter(Mandatory = $true)] | |
[System.Management.Automation.PSCredential]$Credential | |
) | |
try { | |
# Connect to the domain controller | |
# First get the domain information from RootDSE | |
Write-Verbose "Connecting to domain controller..." | |
$domain = New-Object System.DirectoryServices.DirectoryEntry("LDAP://RootDSE", $Credential.UserName, $Credential.GetNetworkCredential().Password) | |
$domainDN = $domain.DefaultNamingContext | |
Write-Verbose "Connected to domain: $domainDN" | |
# Prepare the search by creating a searcher object | |
$root = New-Object System.DirectoryServices.DirectoryEntry("LDAP://$domainDN", $Credential.UserName, $Credential.GetNetworkCredential().Password) | |
$searcher = New-Object System.DirectoryServices.DirectorySearcher($root) | |
# Set filter to find the specific computer | |
$searcher.Filter = "(&(objectClass=computer)(name=$ComputerName))" | |
Write-Verbose "Searching for computer: $ComputerName" | |
# Request the ms-Mcs-AdmPwd attribute which contains the LAPS password | |
# This is the actual AD attribute that stores the LAPS password | |
$searcher.PropertiesToLoad.Add("ms-Mcs-AdmPwd") | Out-Null | |
# Perform the search | |
$computer = $searcher.FindOne() | |
# Check if the computer was found | |
if ($null -eq $computer) { | |
Write-Verbose "Computer $ComputerName not found in AD." | |
return $null | |
} | |
# Extract the password from the result | |
$password = $computer.Properties["ms-Mcs-AdmPwd"] | |
# Check if the password attribute exists and has a value | |
if ($null -eq $password -or $password.Count -eq 0) { | |
Write-Verbose "No LAPS password found for $ComputerName." | |
return $null | |
} | |
# Return the password value | |
return $password[0] | |
} | |
catch { | |
# Display error message | |
[System.Windows.Forms.MessageBox]::Show("Error retrieving LAPS password: $_", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | |
Write-Error "Error retrieving LAPS password: $_" | |
# Log the error | |
if ($null -ne $global:statusTextBox) { | |
Add-LogEntry "ERROR: Failed to retrieve LAPS password - $($_)" | |
} | |
return $null | |
} | |
} | |
<# | |
.SYNOPSIS | |
Retrieves active Winlogon sessions from a remote computer. | |
.DESCRIPTION | |
Uses WMIC to query a remote computer for Winlogon processes and retrieves | |
their associated session IDs. This is used to determine which sessions can be | |
shadowed via RDP. | |
Winlogon.exe processes are associated with user login sessions on Windows systems. | |
By identifying these processes, we can find active user sessions for shadowing. | |
.PARAMETER ComputerName | |
The name of the remote computer to query. | |
.RETURNS | |
An array of custom objects containing ProcessId and SessionId properties. | |
.EXAMPLE | |
$sessions = Get-WinlogonSessions -ComputerName "PC001" | |
foreach ($session in $sessions) { | |
Write-Host "Session ID: $($session.SessionId)" | |
} | |
#> | |
function Get-WinlogonSessions { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$ComputerName | |
) | |
# Execute WMIC command to retrieve Winlogon processes | |
# Using WMIC to get remote process information (this will be used to identify active sessions) | |
Add-LogEntry "Executing WMIC query on $ComputerName for winlogon.exe processes..." -Type "INFO" | |
$wmicOutput = '' | |
try{ | |
$wmicCommand = "wmic process where name=`"winlogon.exe`" get ProcessId,SessionId /node:$ComputerName" | |
$wmicOutput = Get-WMIObject Win32_Process -Filter 'Name="explorer.exe"' -ComputerName $ComputerName | |
Add-CommandResult -Command "$wmicCommand" -Result "$wmicOutput" | |
}catch{ | |
Add-LogEntry "Failed to Get-WMIObject Win32_Process - $($_)" -Type "ERROR" | |
return $null | |
} | |
try{ | |
$wmicOutput = $wmicOutput | Select SessionId,@{n="User";e={$u=$_.GetOwner(); "$($u.Domain)\$($u.User)"}} | |
# Log the command and its output | |
Add-CommandResult -Command "$wmicCommand" -Result "$($wmicOutput | ft | out-string)" | |
return $wmicOutput | |
}catch{ | |
Add-LogEntry "Failed to Get-WMIObject Win32_Process WITH the Select - $($_)" -Type "ERROR" | |
return $null | |
} | |
} | |
<# | |
.SYNOPSIS | |
Launches mstsc with specified arguments and waits for it to exit. | |
.DESCRIPTION | |
Starts the RDP client (mstsc.exe) with the provided arguments, monitors the process | |
until it exits, then cleans up the stored credentials. | |
This function uses a background job to monitor the process without blocking the UI, | |
and automatically cleans up stored credentials when the RDP session ends. | |
.PARAMETER Arguments | |
The command-line arguments to pass to mstsc.exe. | |
.PARAMETER ComputerName | |
The name of the computer to connect to. Used for cleaning up credentials. | |
.EXAMPLE | |
Start-MstscAndWait -Arguments "/v:SERVER01" -ComputerName "SERVER01" | |
Start-MstscAndWait -Arguments "/shadow:3 /control /v:SERVER01" -ComputerName "SERVER01" | |
#> | |
function Start-MstscAndWait { | |
[CmdletBinding()] | |
param( | |
[Parameter(Mandatory = $true)] | |
[string]$Arguments, | |
[Parameter(Mandatory = $true)] | |
[string]$ComputerName | |
) | |
try { | |
# Update status | |
Add-LogEntry "Launching mstsc with arguments: $Arguments" | |
# Start mstsc process and get the process object | |
Write-Verbose "Starting mstsc.exe with arguments: $Arguments" | |
Add-LogEntry "Starting mstsc.exe with arguments: $Arguments" -Type "CMD" | |
$process = Start-Process "mstsc.exe" -ArgumentList $Arguments -PassThru | |
# Update status | |
Add-LogEntry "MSTSC process started (PID: $($process.Id)). Waiting for session to end..." -Type "INFO" | |
# Create a background job to monitor the process | |
# This allows the UI to remain responsive while waiting for mstsc to exit | |
$job = Start-Job -ScriptBlock { | |
param($processId, $computerName) | |
# Wait for the process to exit | |
try { | |
Write-Output "Monitoring process ID: $processId" | |
# Wait for the mstsc process to exit before continuing | |
Wait-Process -Id $processId -ErrorAction SilentlyContinue | |
Write-Output "Process has exited" | |
} | |
catch { | |
Write-Output "Error waiting for process: $_" | |
} | |
# Clean up credentials | |
try { | |
Write-Output "Removing stored credentials for $computerName" | |
# Remove the credentials that were stored for this session | |
Invoke-Expression "cmdkey /delete:$computerName" | |
Write-Output "Credentials removed" | |
} | |
catch { | |
Write-Output "Error removing credentials: $_" | |
} | |
return "Completed" | |
} -ArgumentList $process.Id, $ComputerName | |
# Set up a timer to periodically check if the job is complete | |
# This is an event-driven approach to prevent UI blocking | |
$timer = New-Object System.Windows.Forms.Timer | |
$timer.Interval = 1000 # Check every second | |
$timer.Add_Tick({ | |
# Check job status | |
if ($job.State -eq "Completed") { | |
# Stop and clean up the timer | |
$timer.Stop() | |
$timer.Dispose() | |
# Get job output and clean up | |
Write-Verbose "Background job completed" | |
Receive-Job -Job $job | Write-Verbose | |
Remove-Job -Job $job -Force | |
# Update UI and close form | |
Add-LogEntry "Session ended. Cleaning up credentials..." -Type "INFO" | |
# Get and log the cmdkey delete command result | |
try { | |
$cmdkeyDeleteResult = Invoke-Expression "cmdkey /delete:$ComputerName" | |
Add-CommandResult -Command "cmdkey /delete:$ComputerName" -Result "$cmdkeyDeleteResult" | |
} | |
catch { | |
Add-LogEntry "Error while deleting credentials: $_" -Type "ERROR" | |
} | |
Add-LogEntry "All operations completed successfully." -Type "SUCCESS" | |
$global:form.Close() | |
} | |
}) | |
# Start the timer | |
$timer.Start() | |
} | |
catch { | |
# Display error message | |
[System.Windows.Forms.MessageBox]::Show("Error launching mstsc: $_", "Error", [System.Windows.Forms.MessageBoxButtons]::OK, [System.Windows.Forms.MessageBoxIcon]::Error) | |
Write-Error "Error launching mstsc: $_" | |
# Log the error | |
Add-LogEntry "ERROR: Failed to launch mstsc - $($_)" | |
# Clean up credentials even if there's an error | |
try { | |
$cmdkeyDeleteResult = Invoke-Expression "cmdkey /delete:$computerName" | |
Add-CommandResult -Command "cmdkey /delete:$computerName" -Result "$cmdkeyDeleteResult" | |
Add-LogEntry "Cleaned up credentials for $computerName" -Type "SUCCESS" | |
} | |
catch { | |
Write-Error "Error cleaning up credentials: $_" | |
Add-LogEntry "ERROR: Failed to clean up credentials - $($_)" -Type "ERROR" | |
} | |
} | |
} | |
#endregion Core Functions | |
#endregion Functions | |
# Function to add command result to log | |
# This function specifically handles command execution results with proper formatting | |
function Add-CommandResult { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$Command, | |
[string]$Result | |
) | |
# Log the command that was executed | |
Add-LogEntry "Command executed: $Command" -Type "CMD" | |
# Log the command output, with each line indented for readability | |
# Split the result into lines and process each one | |
$resultLines = $Result -split "`n" | |
# If there's no output, report that fact | |
if ($resultLines.Count -eq 0 -or ([string]::IsNullOrWhiteSpace($Result))) { | |
Add-LogEntry " (No output returned)" -Type "OUTPUT" | |
return | |
} | |
# Process each line of the output | |
foreach ($line in $resultLines) { | |
if (![string]::IsNullOrWhiteSpace($line)) { | |
# Indent output lines with two spaces for readability | |
Add-LogEntry " $($line.Trim())" -Type "OUTPUT" | |
} | |
} | |
} | |
# Function to add log entry to status TextBox | |
# This function provides consistent formatting for all log entries | |
function Add-LogEntry { | |
[CmdletBinding()] | |
param ( | |
[Parameter(Mandatory = $true)] | |
[string]$Message, | |
[Parameter(Mandatory = $false)] | |
[ValidateSet("INFO", "ERROR", "SUCCESS", "WARNING", "CMD", "OUTPUT")] | |
[string]$Type = "INFO" | |
) | |
# Format the log entry with ISO-format timestamp for consistency | |
$timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' | |
# Format based on message type (provides visual differentiation) | |
$logEntry = "[$timestamp] [$Type] $Message" | |
if( $global:statusTextBox -ne $null ){ | |
# Append the new log entry to the TextBox with a newline | |
$global:statusTextBox.AppendText("`r`n$logEntry") | |
# Ensure the latest entry is visible by scrolling to it | |
$global:statusTextBox.SelectionStart = $global:statusTextBox.Text.Length | |
$global:statusTextBox.ScrollToCaret() | Out-Null | |
$global:form.Refresh() | Out-Null | |
}else{ | |
# Also write to console/verbose stream for debugging and transcript logging | |
Write-Host $logEntry | |
} | |
} | |
Main-UI |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment