Skip to content

Instantly share code, notes, and snippets.

@Jaykul
Last active January 25, 2025 22:54
Show Gist options
  • Save Jaykul/a82e20e91a4ff42acd1be868f3d58a58 to your computer and use it in GitHub Desktop.
Save Jaykul/a82e20e91a4ff42acd1be868f3d58a58 to your computer and use it in GitHub Desktop.
This is how you can query the active user from a script running under other credentials

Someone on the PowerShell Discord asked about sending a message box to the current user on a computer.

This method should work to query the user for a choice (with an optional timeout) or simply notify them of something, whether the script is being run by the user or by an administrator in a remote PowerShell session, or from system, etc.

It's not thoroughly tested (as with most of what I put on gists), but it does work.

Here's some examples:

Show-RemoteDesktopMessage -Title "Test Message" -Message "This is a test message."

switch (Get-RemoteDesktopResponse -Title "Problem found" -Message "Should we delete the problem file?" -Style "BUTTON_YESNO,ICON_EXCLAMATION") {

    "Yes" { "Deleting the file" }
    "No" { "Leaving the problem for the user to deal with" }
    default { "User did not respond" }
}

Maybe I'll do more later.

# Send-MessageBox.ps1 with Logging
param(
# Set up logging
$logFile = "$($PSScriptRoot)\ShowMessage.log"
)
filter Write-LogMessage {
param(
[Parameter(ValueFromRemainingArguments, ValueFromPipeline, Position = 0)]
[string[]]$Message,
[switch]$WriteHost
)
$Message = "$([DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss zzz")) $Message"
$Message | Out-File -Append -FilePath $logFile
if ($WriteHost) {
Write-Host $Message
}
}
# TODO: Should this use WTSEnumerateSessions instead?
function Get-RDSession {
[CmdletBinding()]
param(
[string]$Username
)
# Find the session id of the active session by parsing the output of `qwinsta`
$sessions = (qwinsta) -replace '\s+', ' '
| Select-String -Pattern "^\s*(?<name>\S+)\s+(?<user>\S+)?\s*(?<id>\d+)\s+(?<state>\S+)"
| ForEach-Object {
$props = @{}
$_.Matches.Groups
| Where-Object Name -NE 0
| ForEach-Object { $props[$_.Name] = $_.Value }
[PSCustomObject]$props
}
Write-LogMessage "Qwinsta sessions:"
$sessions | Out-String -Stream | Write-LogMessage
if ($Username) {
$sessions | Where-Object { $_.User -eq $Username }
} else {
$sessions | Where-Object { $_.State -eq "Active" }
}
}
# Start logging
Write-LogMessage "Script started. Logging to $logFile"
# Define the WTS API using C# within PowerShell
if ("RemoteDesktop" -as [type]) {
Write-LogMessage "C# type already exists in PowerShell."
} else {
Add-Type @"
using System;
using System.Runtime.InteropServices;
using System.Text;
[Flags]
public enum MessageBoxType : uint {
// The message box contains three push buttons: Abort, Retry, and Ignore.
BUTTON_ABORTRETRYIGNORE = (uint)0x00000002L,
// The message box contains three push buttons: Cancel, Try Again, Continue. Use this message box type instead of MB_ABORTRETRYIGNORE. =
BUTTON_CANCELTRYCONTINUE = (uint)0x00000006L,
// Adds a Help button to the message box. When the user clicks the Help button or presses F1, the system sends a WM_HELP message to the owner.
BUTTON_HELP = (uint)0x00004000L,
// The message box contains one push button: OK. This is the default.
BUTTON_OK = (uint)0x00000000L,
// The message box contains two push buttons: OK and Cancel.
BUTTON_OKCANCEL = (uint)0x00000001L,
// The message box contains two push buttons: Retry and Cancel.
BUTTON_RETRYCANCEL = (uint)0x00000005L,
// The message box contains two push buttons: Yes and No.
BUTTON_YESNO = (uint)0x00000004L,
// The message box contains three push buttons: Yes, No, and Cancel.
BUTTON_YESNOCANCEL = (uint)0x00000003L,
// ### To add an icon, specify one of the following values.
// An exclamation-point icon appears in the message box.
ICON_EXCLAMATION = (uint)0x00000030L,
// An icon consisting of a lowercase letter i in a circle appears in the message box.
ICON_INFORMATION = (uint)0x00000040L,
// A question-mark icon appears in the message box.
// The question-mark message icon is no longer recommended
// because it does not clearly represent a specific type of message
// and because the phrasing of a message as a question could apply to any message type.
// additionally, users can confuse the message symbol question mark with Help information.
// Therefore, do not use this question mark message symbol in your message boxes.
// The system continues to support its inclusion only for backward compatibility.
ICON_QUESTION = (uint)0x00000020L,
// A stop-sign icon appears in the message box.
ICON_STOP = (uint)0x00000010L,
// ### To indicate the default button, specify one of the following values.
// The first button is the default button. This is the default.
DEFAULT_BUTTON1 = (uint)0x00000000L,
// The second button is the default button.
DEFAULT_BUTTON2 = (uint)0x00000100L,
// The third button is the default button.
DEFAULT_BUTTON3 = (uint)0x00000200L,
// The fourth button is the default button.
DEFAULT_BUTTON4 = (uint)0x00000300L,
// ## Miscaellaneous Options
// Same as desktop of the interactive window station. For more information, see Window Stations.
// if the current input desktop is not the default desktop,
// MessageBox does not return until the user switches to the default desktop.
MB_DEFAULT_DESKTOP_ONLY = (uint)0x00020000L,
// The text is right-justified.
MB_RIGHT = (uint)0x00080000L,
// Displays message and caption text using right-to-left reading order on Hebrew and Arabic systems.
MB_RTLREADING = (uint)0x00100000L,
// The message box becomes the foreground window. Internally, the system calls the SetForegroundWindow function for the message box.
MB_SETFOREGROUND = (uint)0x00010000L,
// The message box is created with the WS_EX_TOPMOST window style.
MB_TOPMOST = (uint)0x00040000L,
// The caller is a service notifying the user of an event. The function displays a message box on the current active desktop, even if there is no user logged on to the computer.
MB_SERVICE_NOTIFICATION = (uint)0x00200000L
}
public enum MessageBoxResult : int {
// The OK button was selected.
Ok = 1,
// The Cancel button was selected.
Cancel = 2,
// The Abort button was selected.
Abort = 3,
// The Retry button was selected.
Retry = 4,
// The Ignore button was selected.
Ignore = 5,
// The Yes button was selected.
Yes = 6,
// The No button was selected.
No = 7,
// The Try Again button was selected.
TryAgain = 10,
// The Continue button was selected.
Continue = 11,
// The wait flag wasn't specified so we don't know the result.
NoWait = 32001,
// The message box timed out without a response from the user
Timeout = 32000,
// The message box call failed
Failed = 64000
}
public class RemoteDesktop
{
public static MessageBoxResult QueryMessage(int sessionId, string title, string message, MessageBoxType style = MessageBoxType.BUTTON_YESNO, int timeout = 0)
{
MessageBoxResult Response = MessageBoxResult.Failed;
if (WTSSendMessage(IntPtr.Zero, sessionId, title, Encoding.Unicode.GetByteCount(title), message, Encoding.Unicode.GetByteCount(message), style, timeout, out Response, true)) {
return (MessageBoxResult)Response;
} else {
return MessageBoxResult.Failed;
}
}
public static bool SendMessage(int sessionId, string title, string message, MessageBoxType style = MessageBoxType.BUTTON_OK, int timeout = 0)
{
MessageBoxResult Response = MessageBoxResult.Failed;
return WTSSendMessage(IntPtr.Zero, sessionId, title, Encoding.Unicode.GetByteCount(title), message, Encoding.Unicode.GetByteCount(message), style, timeout, out Response, false);
}
[DllImport("wtsapi32.dll", EntryPoint = "WTSSendMessageW", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool WTSSendMessage(
// Handle to the server (null for current server)
IntPtr Server,
// The desktop session ID (e.g. from qwinsta output)
// [MarshalAs(UnmanagedType.U4)]
int SessionId,
// The title of the message box
string Title,
// The length of the title string
// [MarshalAs(UnmanagedType.U4)]
int TitleLengthInBytes,
// The message to display
string Message,
// The length of the message string
// [MarshalAs(UnmanagedType.U4)]
int MessageLengthInBytes,
// The style of the message box
// [MarshalAs(UnmanagedType.U4)]
MessageBoxType Style,
// The timeout in seconds
// [MarshalAs(UnmanagedType.U4)]
int Timeout,
// The user's response (as an output), if bWait is set to true
// [MarshalAs(UnmanagedType.U4)]
out MessageBoxResult Response,
// Whether to wait for the user's response
bool Wait);
}
"@
}
# Import the defined C# class into PowerShell
Write-LogMessage "C# type added to PowerShell."
function Show-RemoteDesktopMessage {
param(
[Parameter(Mandatory)]
[string]$Title,
[Parameter(Mandatory)]
[string]$Message,
# No timeout by default
[int]$Timeout = 0,
# OK button by default
[MessageBoxType]$Style = "BUTTON_OK,ICON_INFORMATION",
# The Windows login session to send to (see `qwinsta` command)
[int]$sessionId = ((Get-RDSession).Id ?? 1)
)
if (-not $sessionId) {
Write-Error "Active Session ID not found. Cannot display message box."
exit 1
}
Write-LogMessage "Calling WTSSendMessage to display message box."
Write-LogMessage "Session: $SessionId"
Write-LogMessage "Title: $Title"
Write-LogMessage "Message: $Message"
Write-LogMessage "Style: $Style"
$result = [RemoteDesktop]::SendMessage($sessionId, $title, $message, $style)
# Log the result
if ($result) {
Write-LogMessage "Message box displayed successfully in session ID $sessionId."
Write-Output "Message box displayed successfully."
} else {
$errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-LogMessage "Failed to send message box. Error code: $errorCode"
Write-Error "Failed to send message box. Error code: $errorCode"
}
}
function Get-RemoteDesktopResponse {
param(
[Parameter(Mandatory)]
[string]$Title,
[Parameter(Mandatory)]
[string]$Message,
# OK button by default
[Parameter(Mandatory)]
[MessageBoxType]$Style,
# No timeout by default
[int]$Timeout = 0,
# The Windows login session to send to (see `qwinsta` command)
[int]$sessionId = ((Get-RDSession).Id ?? 1)
)
if (-not $sessionId) {
Write-Error "Active Session ID not found. Cannot display message box."
exit 1
}
Write-LogMessage "Calling WTSSendMessage to display message box."
Write-LogMessage "Session: $SessionId"
Write-LogMessage "Title: $Title"
Write-LogMessage "Message: $Message"
Write-LogMessage "Style: $Style"
$result = [RemoteDesktop]::QueryMessage($sessionId, $title, $message, $style)
# Log the result
if ($result -ne "Failed") {
Write-LogMessage "Message box displayed successfully in session ID $sessionId and they responded with $result."
} else {
$errorCode = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
Write-LogMessage "Failed to send message box. Error code: $errorCode"
Write-Error "Failed to send message box. Error code: $errorCode"
}
$result
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment