Hey, Scripting Guy! How Can I Log Out a User if They Launch a Particular Application?

ScriptingGuy1

 

Bookmark and Share

 

Hey, Scripting Guy! Question

Hey, Scripting Guy! I need to be able to use Windows PowerShell 2.0 to log a user out of their desktop machine if they launch a particular application. This is not for security reasons or to keep people from playing games. This is because we have a really dumb application that requires a specific user name and profile settings. Therefore, what I would like to do is have a shortcut so that when the person clicks it, they will be logged out so they can log into the new program. I have hidden the logout/shutdown icon from their Start menu because they kept accidently logging out of the machine and losing information. I know it sounds like a training issue, but our management does not believe in training users. Can you help me?

— GS

 

Hey, Scripting Guy! AnswerHello GS,

Microsoft Scripting Guy Ed Wilson here. Have I got a beautiful script for you! I did not write it, because it was submitted for the 2010 Scripting Games. Nevertheless, it is a really cool script. The ADV9_GlennSizemore.ps1 script was written by the winner of the 2010 Scripting Games (Glenn Sizemore), and it has a number of useful features. For one thing, it allows you to log a user out if he or she launches a particular application. By default it looks for Calculator and Notepad. In addition, if Microsoft Word or Excel is running, it will not log the user out. A force switch is available to override this behavior if you desire. The script gives the user 60 seconds to save their work and close any applications they may have running before the machine will log them out. A popup notification (sometimes called “toast”) is used to let them know about this fact. It refreshes every second as it counts down.

After creating the help system for the script, Glenn begins by creating the command-line parameters. He uses a number of tags to allow the script to receive pipelined input, and to specify the parameters as not mandatory. The not mandatory portion is not required. The SupportsShouldProcess is also set to $false and is therefore not required. ShouldProcess tells the Windows PowerShell script to support the –whatif parameter—something this script would benefit from because it makes changes to system state by logging the user off. The parameter section is shown here:

[CmdletBinding(SupportsShouldProcess=$false,ConfirmImpact=”low”)]
Param(
[Parameter(Mandatory=$False, ValueFromPipelinebyPropertyName=$True)]
[String]
$Program = “calc.exe”,
[Parameter(Mandatory=$False, ValueFromPipelinebyPropertyName=$True)]
[Int]
$Timeout = 60,
[Parameter(Mandatory=$False)]
[Switch]
$Force
)

The next section of the script removes and unregisters any events called LogoffTrigger. This is a good idea because you cannot have more than one event registered with the same name. Next, Glenn uses a Try/Catch block to create his WMI event registration. I have written several Hey, Scripting Guy! posts that talk about using WMI events inside a Windows PowerShell script. Glenn uses the generic WMI event class __InstanceCreationEvent and looks for new instances of Win32_Process. An easier WMI class to use would be the Win32_ProcessStartTrace class. There are several Hey, Scripting Guy! posts that illustrate using this WMI class in event-driven scripts. If an error occurs while the script attempts to create the WMI event, the catch block catches the error and displays the exception message. I wrote a series of Hey, Scripting Guy! posts that talk about error handling, too. The complete WMI event registration portion of the script is shown here:

Remove-Event -SourceIdentifier LogoffTrigger -ea SilentlyContinue
Unregister-Event -SourceIdentifier LogoffTrigger -ea SilentlyContinue
Try
{
Register-WmiEvent -SourceIdentifier LogoffTrigger -Query @”
SELECT *
FROM __InstanceCreationEvent
WITHIN 5
WHERE TargetInstance ISA ‘Win32_Process’
AND TargetInstance.Name = ‘$Program’
“@
}
Catch
{
Write-Warning $_.Exception.Message
exit;
}

The Wait-NotifyUser function is used to create the popup notification that provides the countdown timer before the user is logged off. The function unregisters and removes any event named notification_event_close for the same reason the previous code removed the WMI event: You cannot have two events with the same name. Next the script loads a couple of .NET Framework assemblies. The first provides support for Windows Forms, and the second for timers. To display the popup toast, a new instance of the NotifyIcon .NET Framework class is created. The static Exclamation property provides an exclamation point icon for the balloon, and the ballonTipTitle property is used to specify your message. The complete Wait-NotifyUser function is shown here:

Function Wait-NotifyUser([int]$Timeout)
{
# remove any previous events
Unregister-Event notification_event_close -ea SilentlyContinue
Remove-Event notification_event_close -ea SilentlyContinue
#Initialize our balloon tip
[void][System.Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”) | out-null
[void][System.Reflection.Assembly]::LoadWithPartialName(“System.Timers”)
$notification = new-object System.Windows.Forms.NotifyIcon
$notification.Icon = [System.Drawing.SystemIcons]::Exclamation
$notification.BalloonTipTitle = “Pending logout notification!”
$notification.Visible = $True
# register a new event on the balloon Tip Clicked Closed
Register-ObjectEvent $notification BalloonTipClicked notification_event_close
$global:loop = $true
while ($Timeout -gt 0)
{
$notification.BalloonTipText = “You will be logged out in $($Timeout) Seconds. Please save all work imediatly”
$notification.ShowBalloonTip(150000)
# sleep until either the balloon tip is clicked or 1 second elapses.
Wait-Event -Timeout 1 -SourceIdentifier notification_event_close | out-null
Remove-Event notification_event_close -ErrorAction SilentlyContinue
$Timeout–
}
Remove-Event notification_event_close -ErrorAction SilentlyContinue
Unregister-Event notification_event_close
$notification.Dispose()
}

Glenn uses PInvoke to log the user off from the machine. He was just playing around. In his comments, he states he could have used WMI or the shutdown command to do this. There are times you may need to use PInvoke to execute native code. An excellent recourse for this, in addition to MSDN, is the PInvoke community wiki because it has sample signatures for various methods.

Function Exit-UserSession([switch]$Force) {
$Win32ExitWindowsEx = Add-Type -Name ‘Win32ExitWindowsEx’ `
-namespace ‘Win32Functions’ `
-memberDefinition @”
[DllImport(“user32.dll”)]
public static extern int ExitWindowsEx(int uFlags, int dwReason);
“@ -passThru
IF ($Force)
{
$Win32ExitWindowsEx::ExitWindowsEx(10,0)
}
Else
{
$Win32ExitWindowsEx::ExitWindowsEx(0,0)
}
}
}

The Wait-Event cmdlet is used to wait for the LogoffTrigger event to be generated. When it is, the Write-Verbose cmdlet is used to display a status message. This will only happen if the $VerbosePreference variable is set to Continue. By default, it is set to SilentlyContinue as shown here:

PS C:\> $VerbosePreference

SilentlyContinue

PS C:\>

If the Force switched parameter was used when the script was launched, another WMI query is used to see if Microsoft Word or Excel are running. If they are, the script will sleep for five seconds and then take another look to see if they are still running. If they are not, then the Wait-NotifyUser function is called, which generates the popup notification, and finally the Exit-UserSession function is called to exit the user. This section of the script is seen here:

Wait-Event -SourceIdentifier LogoffTrigger | out-null
Write-Verbose “$($Program) Launch detected!”
IF (!$Force)
{
While (Get-WmiObject -Class Win32_Process -Filter “Name = ‘winword.exe’ OR Name = ‘excel.exe'”)
{
Start-Sleep -Seconds 5
}
}
Wait-NotifyUser -timeout $Timeout
Exit-UserSession
}

The complete ADV9_GlennSizemore.ps1 script is seen here.

ADV9_GlennSizemore.ps1

<#
.Synopsis
Log out users from their workstations when a particular program launches.
.Description
Log out users from their workstations when a particular program launches.
.Parameter Program
The program that will trigger the user to log out. The default is calc.exe
.Parameter Timeout
Time in seconds the user will have to save their work before being logged off
The default is 60 seconds.
.Parameter Force
By default we will not log off any user that has a Word document or Excel spreadsheet open. To
Override this setting and log off regardless of any open application, apply
the force parameter.
.Example
./Start-Logout -Program “calc.exe” -Timeout 90
.Example
./Start-Logout -Program “notepad.exe” -Timeout 15 -force
#>
[CmdletBinding(SupportsShouldProcess=$false,ConfirmImpact=”low”)]
Param(
[Parameter(Mandatory=$False, ValueFromPipelinebyPropertyName=$True)]
[String]
$Program = “calc.exe”,
[Parameter(Mandatory=$False, ValueFromPipelinebyPropertyName=$True)]
[Int]
$Timeout = 60,
[Parameter(Mandatory=$False)]
[Switch]
$Force
)
Begin
{
# Create our event trigger
Remove-Event -SourceIdentifier LogoffTrigger -ea SilentlyContinue
Unregister-Event -SourceIdentifier LogoffTrigger -ea SilentlyContinue
Try
{
Register-WmiEvent -SourceIdentifier LogoffTrigger -Query @”
SELECT *
FROM __InstanceCreationEvent
WITHIN 5
WHERE TargetInstance ISA ‘Win32_Process’
AND TargetInstance.Name = ‘$Program’
“@
}
Catch
{
Write-Warning $_.Exception.Message
exit;
}
#Helper function that will handle the system tray notification to the user.
Function Wait-NotifyUser([int]$Timeout)
{
# remove any previous events
Unregister-Event notification_event_close -ea SilentlyContinue
Remove-Event notification_event_close -ea SilentlyContinue
#Initialize our balloon tip
[void][System.Reflection.Assembly]::LoadWithPartialName(“System.Windows.Forms”) | out-null
[void][System.Reflection.Assembly]::LoadWithPartialName(“System.Timers”)
$notification = new-object System.Windows.Forms.NotifyIcon
$notification.Icon = [System.Drawing.SystemIcons]::Exclamation
$notification.BalloonTipTitle = “Pending logout notification!”
$notification.Visible = $True
# register a new event on the balloon Tip Clicked Closed
Register-ObjectEvent $notification BalloonTipClicked notification_event_close
$global:loop = $true
while ($Timeout -gt 0)
{
$notification.BalloonTipText = “You will be logged out in $($Timeout) Seconds. Please save all work immediately”
$notification.ShowBalloonTip(150000)
# sleep until either the balloon tip is clicked or 1 second elapses.
Wait-Event -Timeout 1 -SourceIdentifier notification_event_close | out-null
Remove-Event notification_event_close -ErrorAction SilentlyContinue
$Timeout–
}
Remove-Event notification_event_close -ErrorAction SilentlyContinue
Unregister-Event notification_event_close
$notification.Dispose()
}
# Converted some C# to Windows PowerShell to use PInvoke to log off the user.
# There is no advantage to this over using WMI/Shutdown.exe…
# Just wanted to highlight the trick, and show off add-type’s power.
Function Exit-UserSession([switch]$Force) {
$Win32ExitWindowsEx = Add-Type -Name ‘Win32ExitWindowsEx’ `
-namespace ‘Win32Functions’ `
-memberDefinition @”
[DllImport(“user32.dll”)]
public static extern int ExitWindowsEx(int uFlags, int dwReason);
“@ -passThru
IF ($Force)
{
$Win32ExitWindowsEx::ExitWindowsEx(10,0)
}
Else
{
$Win32ExitWindowsEx::ExitWindowsEx(0,0)
}
}
}
End
{
# wait here until the specified app is launched
Wait-Event -SourceIdentifier LogoffTrigger | out-null
Write-Verbose “$($Program) Launch detected!”
IF (!$Force)
{
While (Get-WmiObject -Class Win32_Process -Filter “Name = ‘winword.exe’ OR Name = ‘excel.exe'”)
{
Start-Sleep -Seconds 5
}
}
Wait-NotifyUser -timeout $Timeout
Exit-UserSession
}

When the script runs, nothing is displayed in the Windows PowerShell console. The only notification is the popup notification, which is seen in the following image.

Image of popup notification that appears when script is run

 

GS, that is all there is to using Windows PowerShell to log off a user. The 2010 Scripting Games Wrap-Up Week 2 will continue tomorrow.

If you want to know exactly what we will be looking at tomorrow, follow us on Twitter or Facebook. If you have any questions, send e-mail to us at scripter@microsoft.com or post your questions on the Official Scripting Guys Forum. See you tomorrow. Until then, peace.

Ed Wilson and Craig Liebendorfer, Scripting Guys