In this guide, I am going to show you how to put a workstation out of service remotely using Active Directory and PowerShell.

If you work in an environment that has public computing sites (particularly in the higher education field), the workstations deployed to those sites tend to have issues quickly because of high foot traffic. While many IT departments use some sort of signage to deter patrons from using problematic workstations, patrons often ignore those signs. Thus, it makes sense to put a workstation out of service, which ensures patrons are not using an unstable workstation.

Launch the Active Directory MMC snap-in and navigate to the organization unit (OU) for your public workstations. For this guide, I am going to navigate to ad.home.net/AD/Workstations/FA17/Public/Library. Create a new OU called DFM (down for maintenance) and set your desired security permissions. If you have sub-OUs such as for buildings or floors, create a DFM OU for each building sub-OU to keep track of which workstations are down for maintenance in which building.

DFM organizational unit

DFM organizational unit

On your workstation, download and install Remote Server Administration Tools (RSAT) for Windows 10. Navigate to C:\Windows\System32\WindowsPowerShell\v1.0\Modules and copy the folder ActiveDirectory to a network share accessible by Group Policy. For this guide, I am going to create a folder on my NETLOGON share called PowerShell and a subfolder called Modules. I will copy the folder there.

Active Directory module

Active Directory module

To use this module on a workstation without RSAT installed, we will need to copy the Active Directory module assemblies down to remote computers using Group Policy. On your workstation, navigate to C:\Windows\Microsoft.NET\assembly\GAC_64 and copy the folders Microsoft.ActiveDirectory.Management and Microsoft.ActiveDirectory.Management.Resources to a location accessible by Group Policy. I will be copying these two folders to the PowerShell folder created earlier in a subfolder called Assemblies.

Active Directory assemblies

Active Directory assemblies

Launch the Group Policy MMC snap-in and open the Group Policy Object that contains the computer settings for the workstations you wish to apply this guide to. At this point we need to specify the policy settings that are going to copy down the PowerShell Active Directory modules and assemblies. Since standard users cannot import modules or assemblies to the global assembly cache, we must copy the files down with Group Policy.

Navigate to Computer Configuration\Preferences\Files and add a new file. For the source file, enter the folder path to the Active Directory Management DLL assembly you saved on the network share accessible by Group Policy. To ensure all contents get copied over, append the text \*.* to the end of the folder path.

For the destination file, enter C:\Windows\Microsoft.NET\assembly\MSIL\Microsoft.ActiveDirectory.Management\v4.0_10.0.0.0__31bf3856ad364e35. Add another new file with the source file path to the Active Directory Management Resources DLL assembly you saved on the network share accessible by Group Policy. Again, to ensure all contents get copied over, append the text \*.* to the end of the folder path. For the destination folder, enter C:\Windows\Microsoft.NET\assembly\MSIL\Microsoft.ActiveDirectory.Management.Resources\v4.0_10.0.0.0_en_31bf3856ad364e35.

Add another new file with the source file path to the Active Directory module you saved on the network share accessible by Group Policy. To ensure all contents get copied over, append the text \*.* to the end of the folder path. For the destination folder, enter C:\Windows\System32\WindowsPowerShell\v1.0\Modules\ActiveDirectory.

Once more, add a new file and for the source file, enter the folder path to the Active Directory module sub-folder en-US you saved on the network share accessible by Group Policy. To ensure all contents get copied over, append the text \*.* to the end of the folder path. For the destination folder, enter C:\Windows\System32\WindowsPowerShell\v1.0\Modules\ActiveDirectory\en-US.

Rename both assembly files to something a little friendlier, such as Active Directory Management assembly and Active Directory Management Resources assembly, respectively. Likewise, rename both module files to something a little friendlier, such as Active Directory PowerShell module and Active Directory PowerShell module en-US.

Group Policy files

Group Policy files

Now we are going to create the PowerShell script that will check the workstation OU upon logon, determining whether the workstation is in or out of service.

Launch PowerShell ISE and create a new .PS1 file. First things first—we need to tell PowerShell to add the Windows Forms assemblies. Copy and paste the following into your PowerShell script:

# Add form object
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

We also need to define the variables used throughout the script.

# Add form object
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

We also need to define the variables used throughout the script.
# Define variables
## Script
$Workstation = Get-ADObject -Filter "Name -like '$env:ComputerName'"
$StartTime = (Get-Date).AddSeconds(45)

## Form
$Form = New-Object System.Windows.Forms.Form
$PictureBox = New-Object System.Windows.Forms.PictureBox
$Label = New-Object System.Windows.Forms.Label
$Button = New-Object System.Windows.Forms.Button
$Timer = New-Object System.Windows.Forms.Timer
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState

Two actions are going to take place when the script runs: a timer ticking and possibly a button clicking. For the timer action, we need to tell the script to check its start time against the current time, and if 45 seconds have passed, close the form and sign out. I have put 45 seconds instead of 15 seconds to account for the time it will take to show the desktop environment.

Since we will not be running logon scripts synchronously, Windows will likely show the form behind the "We're getting things ready" screen before showing the desktop. For the button action, we need to tell the script to close the form and sign out if the OK button is clicked.

# Form actions
$Timer_onTick = {
    if($StartTime -LT (Get-Date))
    {
        $Timer.Stop();
        $Form.Close();
        $Form.Dispose();
        $Timer.Dispose();
        $Label.Dispose();
        $Button.Dispose();
        cmd.exe /c "logoff.exe"
    }
}

$Button_onClick = {
    $Timer.Stop();
    $Form.Close();
    $Form.Dispose();
    $Timer.Dispose();
    $Label.Dispose();
    $Button.Dispose();
    cmd.exe /c "logoff.exe"
}

The most important part of this script is the form that notifies users the workstation is out of service. With this form, you can change a few things depending on your environment.

# Form Content
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 261
$System_Drawing_Size.Width = 450
$Form.FormBorderStyle = 2
$Form.ClientSize = $System_Drawing_Size
$Form.ControlBox = $False
$Form.DataBindings.DefaultDataSourceUpdateMode = 0
$Form.Font = New-Object System.Drawing.Font("Microsoft Sans Serif",14.25,0,3,0)
$Form.Name = "form1"
$Form.ShowIcon = $False
$Form.ShowInTaskbar = $False
$Form.Text = "Information Technology Department" # Replace with your IT department name
$Form.TopMost = $True


$PictureBox.DataBindings.DefaultDataSourceUpdateMode = 0

$PictureBox.Image = [System.Drawing.Image]::FromFile('\\ad.home.net\NETLOGON\OOS_Banner.jpg')

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 0
$System_Drawing_Point.Y = 0
$PictureBox.Location = $System_Drawing_Point
$PictureBox.Name = "pictureBox1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 99
$System_Drawing_Size.Width = 450
$PictureBox.Size = $System_Drawing_Size
$PictureBox.TabIndex = 2
$PictureBox.TabStop = $False

$Form.Controls.Add($PictureBox)

$Label.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 12
$System_Drawing_Point.Y = 102
$Label.Location = $System_Drawing_Point
$Label.Name = "label1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 98
$System_Drawing_Size.Width = 426
$Label.Size = $System_Drawing_Size
$Label.TabIndex = 1
$Label.Text = "This workstation is not in service. You will be automatically signed out in 15 seconds.

We apologize for any inconvenience. " # You can edit this message as needed
$Label.TextAlign = 32

$Form.Controls.Add($Label)

$Button.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 95
$System_Drawing_Point.Y = 213
$Button.Location = $System_Drawing_Point
$Button.Name = "button1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 36
$System_Drawing_Size.Width = 260
$Button.Size = $System_Drawing_Size
$Button.TabIndex = 0
$Button.Text = "OK"
$Button.UseVisualStyleBackColor = $True
$Button.add_Click($Button_onClick)

$Form.Controls.Add($Button)

$Timer = New-Object System.Windows.Forms.Timer
$Timer.Interval = 1000
$Timer.add_Tick($Timer_onTick)

The first thing you can change is the path to the $PictureBox.Image. For this guide, I am using a simple banner with the text "Out of Service." However, you can replace it with a banner of a corporate logo if you wish. You may also download the image below.

Out of Serivce banner

Out of Serivce banner

The second thing you can change is the $Form.Text from "Information Technology Department" to something less generic. If you wish to change the time, you can do that also by modifying the $StartTime variable. The last part of the script we need to add is the part that checks to see if the workstation is in the DFM OU, and if it is, start the 45-second timer and display the out-of-service banner.

$Workstation | ForEach-Object{

    # Check if workstation is out of service
    If($_.DistinguishedName -like "*DFM*"){

        # Save the initial state of the form
        $InitialFormWindowState = $Form.WindowState
        # Init the OnLoad event to correct the initial state of the form
        $Form.add_Load($OnLoadForm_StateCorrection)
        # Enable timer
        $Timer.Enabled = $True
        # Start timer
        $Timer.Start()
        # Show the Form
        $Form.ShowDialog() |Out-Null

    } else {

        # Do nothing; the workstation is not out of service.

    }
}

That's all the code we are going to need for this script; your final code should look like the following.

# Add Form Object
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing

# Define Variables
## Script
$Workstation = Get-ADObject -Filter "Name -like '$env:ComputerName'"
$StartTime = (Get-Date).AddSeconds(45)

## Form
$Form = New-Object System.Windows.Forms.Form
$PictureBox = New-Object System.Windows.Forms.PictureBox
$Label = New-Object System.Windows.Forms.Label
$Button = New-Object System.Windows.Forms.Button
$Timer = New-Object System.Windows.Forms.Timer
$InitialFormWindowState = New-Object System.Windows.Forms.FormWindowState

# Form Actions
$Timer_onTick = {
    if($StartTime -LT (Get-Date))
    {
        $Timer.Stop(); 
        $Form.Close(); 
        $Form.Dispose();
        $Timer.Dispose();
        $Label.Dispose();
        $Button.Dispose();
        cmd.exe /c "logoff.exe"
    }
}

$Button_onClick = {
    $Timer.Stop(); 
    $Form.Close(); 
    $Form.Dispose();
    $Timer.Dispose();
    $Label.Dispose();
    $Button.Dispose();
    cmd.exe /c "logoff.exe"
}

# Form Content
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 261
$System_Drawing_Size.Width = 450
$Form.FormBorderStyle = 2
$Form.ClientSize = $System_Drawing_Size
$Form.ControlBox = $False
$Form.DataBindings.DefaultDataSourceUpdateMode = 0
$Form.Font = New-Object System.Drawing.Font("Microsoft Sans Serif",14.25,0,3,0)
$Form.Name = "form1"
$Form.ShowIcon = $False
$Form.ShowInTaskbar = $False
$Form.Text = "Information Technology Department" # Replace with your IT department name
$Form.TopMost = $True


$PictureBox.DataBindings.DefaultDataSourceUpdateMode = 0

$PictureBox.Image = [System.Drawing.Image]::FromFile('\\ad.home.net\NETLOGON\OOS_Banner.jpg')

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 0
$System_Drawing_Point.Y = 0
$PictureBox.Location = $System_Drawing_Point
$PictureBox.Name = "pictureBox1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 99
$System_Drawing_Size.Width = 450
$PictureBox.Size = $System_Drawing_Size
$PictureBox.TabIndex = 2
$PictureBox.TabStop = $False

$Form.Controls.Add($PictureBox)

$Label.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 12
$System_Drawing_Point.Y = 102
$Label.Location = $System_Drawing_Point
$Label.Name = "label1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 98
$System_Drawing_Size.Width = 426
$Label.Size = $System_Drawing_Size
$Label.TabIndex = 1
$Label.Text = "This workstation is not in service. You will be automatically signed out in 15 seconds.

We apologize for any inconvenience. " # You can edit this message as needed
$Label.TextAlign = 32

$Form.Controls.Add($Label)


$Button.DataBindings.DefaultDataSourceUpdateMode = 0

$System_Drawing_Point = New-Object System.Drawing.Point
$System_Drawing_Point.X = 95
$System_Drawing_Point.Y = 213
$Button.Location = $System_Drawing_Point
$Button.Name = "button1"
$System_Drawing_Size = New-Object System.Drawing.Size
$System_Drawing_Size.Height = 36
$System_Drawing_Size.Width = 260
$Button.Size = $System_Drawing_Size
$Button.TabIndex = 0
$Button.Text = "OK"
$Button.UseVisualStyleBackColor = $True
$Button.add_Click($Button_onClick)

$Form.Controls.Add($Button)

$Timer = New-Object System.Windows.Forms.Timer
$Timer.Interval = 1000
$Timer.add_Tick($Timer_onTick)

$Workstation | ForEach-Object{

    # Check if workstation is out of service
    If($_.DistinguishedName -like "*DFM*"){

        # Save the initial state of the form
        $InitialFormWindowState = $Form.WindowState
        # Init the OnLoad event to correct the initial state of the form
        $Form.add_Load($OnLoadForm_StateCorrection)
        # Enable timer
        $Timer.Enabled = $True
        # Start timer
        $Timer.Start()
        # Show the Form
        $Form.ShowDialog() |Out-Null
        
    } else {

        # Do nothing; the workstation is not out of service.

    }
       
}

Save your script to a network share accessible by Group Policy. For this guide, I am going to save my script as Is_Out_of_Service.ps1 to my NETLOGON share, as well as the JPEG for the banner. Finally, we are going to configure our group policy to run this script at logon.

Launch the Group Policy MMC snap-in and open the Group Policy Object that contains the logon scripts for the workstations you wish to apply this guide to. Navigate to User Configuration\Windows Settings\Scripts (Logon/Logoff) and add the script to the Logon item. If you have any other scripts to apply as well, apply this script last.

Add logon script

Add logon script

Lastly, navigate to Computer Configuration\Administrative Templates\System\Scripts and disable the setting Run logon scripts synchronously. This will allow Windows to display the form after loading the desktop.

Subscribe to 4sysops newsletter!

Out of Service pop up

Out of Service pop up

When you move the computer object to the DFM OU, all users will receive the pop-up above notifying them the workstation is out of service and that the system will sign them out in 15 seconds.

2 Comments
  1. Andrew Hilborne 6 years ago

    This is interest – thank you.

    Please will you explain why the assemblies have to be copied to a folder with that particular name, and how we can find out what that name is in the future? Supplementary question: is it possible these days to change the background to the logon screen? This could work quite well, I feel.

    I also note that changing from synchronous scripts may give some people problems, maybe those who are attaching network drives which are used immediately?

    • 1) I have not tried renaming the folders the assemblies are copied to. I assumed the Active Directory PS module searched for the required assemblies in a specific location, which is why I pushed the folder down with exact names.

      2) You can change the logon background — either through group policy or the Windows registry. Note the group policy option will work with Windows 10 Enterprise and Education, only. Support for Windows 10 Pro has been deprecated (I believe) as of the Windows 10 Build 1511 ADMX update.

      3) That is something to take into consideration, for sure. Synchronous scripts have never given me any problems just because I use a master logon script that maps network drives and then performs any actions that require usage of those drives.

Leave a reply

Please enclose code in pre tags

Your email address will not be published. Required fields are marked *

*

© 4sysops 2006 - 2023

CONTACT US

Please ask IT administration questions in the forums. Any other messages are welcome.

Sending

Log in with your credentials

or    

Forgot your details?

Create Account