Screensaver with a Shotgun: Kicking Out Stale Kiosk Sessions Before Horizon Does


Disclaimer :
This tool was built for a specific use case in a Horizon environment with kiosk-style endpoints.
It’s a quick, configurable workaround not a polished enterprise product.
Use it at your own risk, test before deploying, and adapt it to your needs.
If it deletes your lunch or logs off your CEO mid-call... that’s on you.
Let’s talk about a problem you’ve probably already tripped over if you manage shared Horizon setups. This is especially true in hospital meeting rooms with badge-based logins.
Picture this:
A clinician swipes their badge on a kiosk PC.
Their personal VDI session launches instantly seamless roaming magic.
They join a Teams call. Maybe open a few files. Maybe they’re presenting.
Then... they leave.
No lock. No logoff.
Just an active, unlocked session quietly waiting for the next person to stroll in.
What Could Possibly Go Wrong?
• The next person walks in and sees everything.
• Horizon will eventually disconnect the session, but not before 30 to 60 minutes, depending on your pod-level timeout settings.
• Meanwhile, the kiosk sits there exposing personal data, chats, and open applications.
The “Proper” Way?
You could go the clean architecture route and set up a separate Horizon pool just for meeting rooms, with a short idle timer.
But then:
• Users no longer roam seamlessly between their office and the meeting room.
• Instead of resuming their session, they get a completely new desktop losing context, open apps, and unsaved data.
• And if you do this across multiple shared locations? You end up with pool sprawl.
But Can’t We Warn the User?”
Yes. Horizon supports a forced logoff warning using these global GPOs:
• DisplayWarningBeforeForcedLogoff
• ForcedLogoffTimeout
This displays a warning message like:
“You will be logged off in X seconds...” before disconnecting or logging off the session due to inactivity.
Sounds like a clean solution?
Not quite.
These settings apply to the entire Horizon pod. Not just one pool or use case.
So if you enable them to protect a few kiosk PCs in meeting rooms, every user across every pool in that pod including persistent desktop users, radiologists, or anyone with open apps gets the same global countdown and forced logoff behavior.
This can lead to:
• Disrupted workflows
• Unexpected logouts
• Angry users who just lost their work
Once again, it’s a good idea with the wrong blast radius.
Or You Let Horizon Handle It…
...and that’s when someone gets booted mid-Teams-call without warning.
Because here's the other kicker:
Horizon’s idle timeout doesn’t natively warn the user unless you configure the above GPOs.
And even then, it only works at the pod level not per machine or per user context.
Imagine presenting to the board, and the Horizon session silently pulls the plug because you haven’t moved your mouse enough.
The Fix? A Custom Screensaver With Teeth
This tool does what Horizon doesn’t:
• Detects inactivity after a configurable number of seconds (you generic screensaver behavior)
• Pops up a countdown message across all screens:
“No input detected. Closing in 30 seconds.” (or whatever you like it to be)
• If no one clicks, it forcefully kills Horizon (or any process you configure)
• Session gone. Machine clean. Meeting room safe
It’s:
• Brandable
• Policy-deployable
• Silent but effective
And most importantly, it puts you in control of when kiosk sessions die.
No pool gymnastics.
No surprise Teams drop-offs.
No global settings that backfire on your normal desktops.
Just a silent enforcer that cleans up behind forgetful users.
Sounds interesting?
Below is the full code explained, as usual.
Setup: Simple by Design
Let’s start with the basics.
This tool consists of just two forms:
frmCheck
the hidden watchdog that decides whether to show the warning screenfrmMessage
the fullscreen message with the countdown timer and kill switch
No complicated dependencies.
No external libraries.
No weird NuGet voodoo.
Just straightforward VB.NET, and yes it works without admin rights.
But here’s the important part:
📌 Target .NET Framework 4.0
Why? Because this thing is going in C:\Windows\System32
as a .scr
.
And if you target anything higher, Windows might silently refuse to launch it in screensaver mode. Don’t ask. Just trust me.
Next up: let’s walk through what frmCheck
actually does.
Private Sub Check_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim configPath As String = IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "kiosksaver\config.ini")
Me.Visible = False
Dim processName As String = ReadINIValue(configPath, "KillProcess", "vmware-view")
Dim p() As Process
p = Process.GetProcessesByName(processName)
If p.Length >= 1 Then
Dim ScreenMessage As New frmMessage(configPath)
ScreenMessage.ShowDialog()
End If
Me.Close()
End Sub
Let’s break it down:
configPath
points to your shared INI file inC:\ProgramData\kiosksaver
.
No hardcoded values it works everywhere.It reads the process name to monitor (typically
vmware-view
) from the INI:
; Process to terminate when timer expires
KillProcess=vmware-view
Then it checks if that process is running using
Process.GetProcessesByName
.If found, it quietly launches
frmMessage
, your fullscreen warning UI.Whether it finds anything or not, it always ends by calling
Me.Close()
.
So what does that mean in plain English?
The screensaver only activates if the process you’re targeting is actually running
If the process isn’t found (e.g. Horizon isn’t open, or it already closed), it does nothing and exits silently
This prevents:
False alarms on idle desktops that aren’t using Horizon
Countdown screens appearing when there’s nothing to clean up
Accidental kills of unrelated processes or idle machines doing nothing wrong
So If vmware-view.exe
Isn’t Running?
No warning.
No countdown.
Just Me.Close()
and done.
You can adjust this behavior if you want it to always show the message even if no process is found but the current design keeps things minimal and purposeful.
This form should never be seen hence the Me.Visible = False
The ReadINIValue Helper
Same form, utility function:
Public Function ReadINIValue(filePath As String, key As String, Optional defaultValue As String = "") As String
If Not IO.File.Exists(filePath) Then Return defaultValue
For Each line In IO.File.ReadAllLines(filePath)
If line.TrimStart().StartsWith(";") Then Continue For ' Skip comments
Dim parts = line.Split({"="c}, 2)
If parts.Length = 2 AndAlso parts(0).Trim().Equals(key, StringComparison.OrdinalIgnoreCase) Then
Return parts(1).Trim()
End If
Next
Return defaultValue
End Function
No external config libraries here just old-school INI parsing:
Skips commented lines (
;
)Splits each line into key/value
Trims whitespace
Returns a default if the key’s not found or the file’s missing
frmMessage: The Countdown Enforcer
Here’s where the real work happens.
This form goes fullscreen, paints every screen in your setup, shows your message and logo, starts a countdown, and if nobody clicks boom, it kills the Horizon process (or whatever you configure).
New()
Startup Prep
Public Sub New(iniPath As String)
InitializeComponent()
SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.UserPaint Or ControlStyles.DoubleBuffer, True)
UpdateStyles()
configPath = iniPath
Me.Visible = False
End Sub
This constructor:
Stores the INI path
Enables double-buffered drawing (smoother visuals)
Hides the form initially to avoid flicker on load
Message_Load
Config, Layout, and UI Generation
This is the main routine. Here's what happens:
1. First: Core Functionality
processName = ReadINIValue(configPath, "KillProcess", "vmware-view")
timeout = CInt(ReadINIValue(configPath, "Timeout", "300"))
Dim messageText As String = ReadINIValue(configPath, "Message", "Closing in %s seconds")
KillProcess
is the name of the executable to terminate at the end of the countdown. Typically vmware-view
, but you can set it to anything even notepad
for testing.
Timeout
is the countdown duration in seconds.Message
is the string shown to users. It supports placeholders:%s
→ current countdown value
Visuals and Branding
Dim Bgcolor As String = ReadINIValue(configPath, "BackColor", "#000000")
Dim BgImage As String = ReadINIValue(configPath, "BgImage", "")
Dim Fgcolor As String = ReadINIValue(configPath, "TextColor", "#FFFFFF")
Dim TextSize As String = ReadINIValue(configPath, "TextSize", "#FFFFFF")
Dim Font As String = ReadINIValue(configPath, "Font", "Segoe UI")
Dim FontSize As String = ReadINIValue(configPath, "FontSize", "28")
All colors use hex notation (e.g.
#000000
).Font and font size can be fully customized.
You can show a branded image behind the message if desired (
BgImage
).
Logo Support
Dim Logo As String = ReadINIValue(configPath, "Logo", "C:\ProgramData\Kiosksaver\logo.png")
Dim LogoWidth As String = ReadINIValue(configPath, "LogoWidth", "128")
Dim LogoHeight As String = ReadINIValue(configPath, "LogoHeight", "64")
The logo appears in the top-left corner of each screen.
You control the size in pixels.
System Screensaver Timeout (for %sc
)
Dim timeoutSecScr As Integer = GetScreenSaverTimeoutSeconds()
This reads the system value from the registry (
HKCU\Control Panel\Desktop\ScreenSaveTimeOut
)If you include
%sc
in your message string, it gets replaced with this timeout (in minutes)
Form Setup
Dim virtualBounds As Rectangle = SystemInformation.VirtualScreen
Me.FormBorderStyle = FormBorderStyle.None
Me.Bounds = virtualBounds
Me.StartPosition = FormStartPosition.Manual
Me.TopMost = True
The form stretches over all connected monitors
No border, no taskbar icon, and always on top
Safe Fallbacks
Try
Me.BackColor = ColorTranslator.FromHtml(...)
Catch
Me.BackColor = Color.Black
End Try
Try
Me.BackgroundImage = Image.FromFile(BgImage)
Catch
Me.BackgroundImage = Nothing
End Try
If someone messes up the INI (wrong hex color, missing file), it won’t crash.
The app uses hardcoded defaults if needed.
Final Touch: Message Template Cleanup
messageTemplate = ReadINIValue(configPath, "Message", "Closing in %s seconds")
messageTemplate = messageTemplate.Replace("%sc", (timeoutSecScr / 60))
messageTemplate = messageTemplate.Replace("\n", vbCrLf)
%sc
gets swapped for the system timeout (in minutes)\n
becomes a real line break
This lets you write messages like:
Message=No input received for %sc minute(s).\nClosing in %s seconds.
…and see something like:
No input received for 5 minute(s).
Closing in 30 seconds.
Fully dynamic. Fully controlled by the admin.
Zero hardcoding.
2.Dynamic UI per Screen
This block builds the message and logo on each connected monitor. Yes, it works on dual, triple, vertical, mirrored, whatever. If Windows sees it, so does the saver.
For Each scr As Screen In Screen.AllScreens
Will loop trough each screen to add the things you want your end user to see
Label: The Countdown Message
Dim label As New Label With {
.Text = messageTemplate.Replace("%s", timeout.ToString()),
.ForeColor = textColor,
.BackColor = Color.Transparent,
.Font = New Font(Font, CInt(FontSize), FontStyle.Bold),
.AutoSize = True,
.TextAlign = ContentAlignment.MiddleCenter
}
Text gets the current timeout value baked in (
%s
).Color, font, size, and style come from the config file.
Background is transparent no ugly blocks or overlaps.
AutoSize = True
means it fits the text no matter what you put in the INI.
Centered Placement (Like It Should Be)
Dim centerX = scr.Bounds.Left + (scr.Bounds.Width \ 2) - (label.PreferredWidth \ 2)
Dim centerY = scr.Bounds.Top + (scr.Bounds.Height \ 2) - (label.PreferredHeight \ 2)
label.Location = New Point(centerX, centerY)
No hardcoded values. It auto-centers on each screen individually.
Add click-to-cancel behavior:
AddHandler label.Click, AddressOf FormClickClose
Then slap it onto the form:
Me.Controls.Add(label)
textLabels.Add(label)
Note: textLabels
is a list so you can easily update all labels during the countdown.
Logo: Optional, Brandable, and Clickable
Dim logobox As New PictureBox With {
.Size = New Size(CInt(LogoWidth), CInt(LogoHeight)),
.SizeMode = PictureBoxSizeMode.StretchImage,
.Location = New Point(scr.Bounds.Left + 20, scr.Bounds.Top + 20),
.BackColor = Color.Transparent
}
Pulled from
Logo
,LogoWidth
, andLogoHeight
in your INI.Top-left aligned with a 20px margin.
Transparent background.
Respects your size but stretches image to fit cleanly.
Try
logobox.Image = Image.FromFile(Logo)
Catch
' Ignore if logo not found
End Try
If the image file is missing or broken? No crash, no complaint. Just skips the logo.
Also clickable to dismiss:
AddHandler logobox.Click, AddressOf FormClickClose
Start Timer and Go Visible
Timer1.Interval = 1000
Timer1.Start()
Me.Visible = True
The countdown starts ticking as soon as everything is drawn.
1000 ms so you count down per second
If the user interacts? It closes.
If not? It kills.
3.Countdown, Kill, and Cleanup
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
timeout -= 1
Every second, the Timer1_Tick
event fires and subtracts 1 from the countdown.
Live Label Updates
For Each lbl As Label In textLabels
lbl.Text = messageTemplate.Replace("%s", timeout.ToString())
Next
Updates all labels on all screens in real time.
No flicker, no redraws, just smooth countdown.
Timeout Reached = Process Killed
If timeout <= 0 Then
Try
For Each p In Process.GetProcessesByName(processName)
p.Kill()
Next
Catch
' suppress
End Try
Me.Close()
End If
Once the timer hits zero, it searches for all processes by name.
Each one gets
Kill()
called on it.If something goes wrong (e.g. access denied), it fails silently.
No popups. No error messages. It just tries, then closes itself.
Dismiss Behavior
Click Anywhere = Cancel
Private Sub FormClickClose(sender As Object, e As EventArgs)
Me.Close()
End Sub
Private Sub Message_Click(sender As Object, e As EventArgs) Handles Me.Click
Me.Close()
End Sub
Every Label
, every PictureBox
, and the form itself are wired to FormClickClose
.
So what happens when someone clicks?
The countdown stops
The screensaver exits
The user keeps working no questions asked
This could be extended with:
Multi-click verification
A password
A puzzle to solve within 10 seconds or you still get disconnected
Depending on how much you love your end users you could do anything.
…but by default, it’s one click to cancel because simplicity wins.
Helpers (Again)
ReadINIValue()
Already explained earlier pulls values from your INI config safely.
GetScreenSaverTimeoutSeconds()
Using regKey = Registry.CurrentUser.OpenSubKey("Control Panel\Desktop")
Dim value As String = regKey.GetValue("ScreenSaveTimeOut", "600").ToString()
Return Integer.Parse(valueGrabs the current Windows screensaver timeout (in seconds).
regKey.GetValue("ScreenSaveTimeOut", "600").ToString()
This reads the system's screensaver timeout value from the registry:
HKCU\Control Panel\Desktop\ScreenSaveTimeOut
The value is in seconds
It tells Windows how long to wait before launching the system screensaver
If the key doesn’t exist (which can happen on fresh profiles or locked-down environments), it returns "600" that’s 10 minutes
So "600" is just the default fallback:
10 minutes of inactivity = screensaver starts.
We use this value to replace %sc in your message string, so users get feedback like:
"No input detected for 10 minutes. Closing in 30 seconds."
It makes the message reflect their actual system behavior, not just a hardcoded guess.
- Used to dynamically insert
%sc
into your message string.
Fallback is -1
if anything fails no crash, no popup.
you can also leave this out of the message and fill in a hard coded time which you then define in your GPO as idle time tor start the screensaver.
That’s it the full screensaver loop.
From idle detection, to visual feedback, to process termination all wrapped in a neat, self-contained .scr
. after you manually rename the exe
Example image :
Full code :
frmCheck:
Public Class frmCheck
Private Sub Check_Load(sender As Object, e As EventArgs) Handles MyBase.Load
Dim configPath As String = IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), "kiosksaver\config.ini")
Me.Visible = False
Dim processName As String = ReadINIValue(configPath, "KillProcess", "vmware-view")
Dim p() As Process
p = Process.GetProcessesByName(processName)
If p.Length >= 1 Then
Dim ScreenMessage As New frmMessage(configPath)
ScreenMessage.ShowDialog()
End If
Me.Close()
End Sub
Public Function ReadINIValue(filePath As String, key As String, Optional defaultValue As String = "") As String
If Not IO.File.Exists(filePath) Then Return defaultValue
For Each line In IO.File.ReadAllLines(filePath)
If line.TrimStart().StartsWith(";") Then Continue For ' Skip comments
Dim parts = line.Split({"="c}, 2)
If parts.Length = 2 AndAlso parts(0).Trim().Equals(key, StringComparison.OrdinalIgnoreCase) Then
Return parts(1).Trim()
End If
Next
Return defaultValue
End Function
End Class
frmMessage
Imports Microsoft.Win32
Public Class frmMessage
'some varaibles used in multiple subs/functions so needed more top declaration
Private configPath As String
Private timeout As Integer = 60
Private killProcess As String = "vmware-view"
Private messageTemplate As String = "Closing in %s seconds"
Private textLabels As New List(Of Label)
Private processName As String = "vmware-view"
Public Sub New(iniPath As String)
InitializeComponent()
SetStyle(ControlStyles.AllPaintingInWmPaint Or ControlStyles.UserPaint Or ControlStyles.DoubleBuffer, True)
UpdateStyles()
configPath = iniPath
Me.Visible = False
End Sub
Private Sub Message_Load(sender As Object, e As EventArgs) Handles MyBase.Load
processName = ReadINIValue(configPath, "KillProcess", "vmware-view")
timeout = CInt(ReadINIValue(configPath, "Timeout", "300"))
Dim messageText As String = ReadINIValue(configPath, "Message", "Closing in %s seconds")
Dim Bgcolor As String = ReadINIValue(configPath, "BackColor", "#000000")
Dim BgImage As String = ReadINIValue(configPath, "BgImage", "")
Dim Fgcolor As String = ReadINIValue(configPath, "TextColor", "#FFFFFF")
Dim TextSize As String = ReadINIValue(configPath, "TextSize", "#FFFFFF")
Dim Font As String = ReadINIValue(configPath, "Font", "Segoe UI")
Dim FontSize As String = ReadINIValue(configPath, "FontSize", "28")
Dim Logo As String = ReadINIValue(configPath, "Logo", "C:\ProgramData\Kiosksaver\logo.png")
Dim LogoWidth As String = ReadINIValue(configPath, "LogoWidth", "128")
Dim LogoHeight As String = ReadINIValue(configPath, "LogoHeight", "64")
Dim timeoutSecScr As Integer = GetScreenSaverTimeoutSeconds()
Dim virtualBounds As Rectangle = SystemInformation.VirtualScreen
Me.FormBorderStyle = FormBorderStyle.None
Me.Bounds = virtualBounds
Me.StartPosition = FormStartPosition.Manual
Me.TopMost = True
' Load config
Try
Me.BackColor = ColorTranslator.FromHtml(ReadINIValue(configPath, "BackColor", "#000000"))
Catch
Me.BackColor = Color.Black
End Try
Try
Me.BackgroundImage = Image.FromFile(BgImage)
Catch
Me.BackgroundImage = Nothing
End Try
Dim textColor As Color
Try
textColor = ColorTranslator.FromHtml(ReadINIValue(configPath, "TextColor", "#FFFFFF"))
Catch
textColor = Color.White
End Try
messageTemplate = ReadINIValue(configPath, "Message", "Closing in %s seconds")
messageTemplate = messageTemplate.Replace("%sc", (timeoutSecScr / 60))
messageTemplate = messageTemplate.Replace("\n", vbCrLf)
' Generate UI per screen
For Each scr As Screen In Screen.AllScreens
' Label
Dim label As New Label With {
.Text = messageTemplate.Replace("%s", timeout.ToString()),
.ForeColor = textColor,
.BackColor = Color.Transparent,
.Font = New Font(Font, CInt(FontSize), FontStyle.Bold),
.AutoSize = True,
.TextAlign = ContentAlignment.MiddleCenter
}
Dim centerX = scr.Bounds.Left + (scr.Bounds.Width \ 2) - (label.PreferredWidth \ 2)
Dim centerY = scr.Bounds.Top + (scr.Bounds.Height \ 2) - (label.PreferredHeight \ 2)
label.Location = New Point(centerX, centerY)
AddHandler label.Click, AddressOf FormClickClose
Me.Controls.Add(label)
textLabels.Add(label)
' Logo
Dim logobox As New PictureBox With {
.Size = New Size(CInt(LogoWidth), CInt(LogoHeight)), ' Adjust as needed
.SizeMode = PictureBoxSizeMode.StretchImage,
.Location = New Point(scr.Bounds.Left + 20, scr.Bounds.Top + 20),
.BackColor = Color.Transparent
}
Try
logobox.Image = Image.FromFile(Logo)
Catch
' Ignore if logo not found
End Try
AddHandler logobox.Click, AddressOf FormClickClose
Me.Controls.Add(logobox)
Next
Timer1.Interval = 1000
Timer1.Start()
Me.Visible = True
End Sub
Private Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
timeout -= 1
' Update all labels
For Each lbl As Label In textLabels
lbl.Text = messageTemplate.Replace("%s", timeout.ToString())
Next
If timeout <= 0 Then
Try
For Each p In Process.GetProcessesByName(processName)
p.Kill()
Next
Catch
' suppress
End Try
Me.Close()
End If
End Sub
Private Sub FormClickClose(sender As Object, e As EventArgs)
Me.Close()
End Sub
Private Sub Message_Click(sender As Object, e As EventArgs) Handles Me.Click
Me.Close()
End Sub
Public Function ReadINIValue(filePath As String, key As String, Optional defaultValue As String = "") As String
If Not IO.File.Exists(filePath) Then Return defaultValue
For Each line In IO.File.ReadAllLines(filePath)
If line.TrimStart().StartsWith(";") Then Continue For ' Skip comments
Dim parts = line.Split({"="c}, 2)
If parts.Length = 2 AndAlso parts(0).Trim().Equals(key, StringComparison.OrdinalIgnoreCase) Then
Return parts(1).Trim()
End If
Next
Return defaultValue
End Function
Public Function GetScreenSaverTimeoutSeconds() As Integer
Try
Using regKey = Registry.CurrentUser.OpenSubKey("Control Panel\Desktop")
If regKey IsNot Nothing Then
Dim value As String = regKey.GetValue("ScreenSaveTimeOut", "600").ToString()
Return Integer.Parse(value)
End If
End Using
Catch ex As Exception
' Optionally log or handle error
End Try
Return -1 ' fallback if error
End Function
End Class
Example ini:
;config.ini
;Screensaver Configuration - Kiosksaver
;This inifile should be locted in OSdisk/Programdata/kiosksaver/config.ini
;This path can be changed in the code but this is the default
[Settings]
; Countdown duration in seconds
Timeout=10
; Process to terminate when timer expires
KillProcess=vmware-view
; Message shown during countdown, %s will be replaced with seconds.
;If you use %sc somewhere it takes the time it takes before starting the screensaver from the system in minutes
;use \n for new lines
Message=No input received for %sc minute(s).\nClosing in %s seconds.\nClick anywhere twice to continue working.
; Background color of the form
BackColor=#000000
; Background Image of the form (path must exist on disk)
BgImage=c:\ProgramData\Kiosksaver\BG.png
; Text color of the countdown/message
TextColor=#FFFFFF
; Font family for the message
Font=Arial
; Font size in points
FontSize=28
; Logo path (must exist on disk)
Logo=C:\ProgramData\Kiosksaver\logo.png
; Logo dimensions in pixels
LogoWidth=380
LogoHeight=380
**Now how to deploy
Deployment: Kiosk PCs & Zero Clients (Horizon-Connected)**
This tool was built for exactly this scenario:
Zero clients or kiosk-mode Windows endpoints that launch a Horizon session automatically and never get logged off.
Think:
Shared meeting room PCs
Badge-swipe kiosk stations
These machines might not run DEM, don’t have fat-client user profiles, and don't benefit from user-scoped logoff policies so you need a device-level screensaver policy to clean up for them.
Make sure your compiled .scr
file is:
Targeted for .NET Framework 4.0
Named
Kiosk_screensaver.scr
! Rename the EXE !Tested locally from
C:\Windows\System32
(screensavers must reside here to load)
! It might be needed to exclude the file from AV !
Also prepare:
config.ini
logo.png
(and optionallyBG.png
)
All referenced in the INI with absolute paths like:
Logo=C:\ProgramData\Kiosksaver\logo.png
BgImage=C:\ProgramData\Kiosksaver\BG.png
Copy Files Using GPO
You’ll push these files to each zero client or Horizon endpoint:
File | Destination |
Kiosk_screensaver.scr | C:\Windows\System32\Kiosk_screensaver.scr |
config.ini | C:\ProgramData\Kiosksaver\config.ini |
logo.png | C:\ProgramData\Kiosksaver\logo.png |
BG.png (opt) | C:\ProgramData\Kiosksaver\BG.png |
Use GPO Preferences:
Path:
Computer Configuration >
Preferences >
Windows Settings >
Files
for each file:
Action: Replace
Source: UNC path to your central share (e.g.
\\fileserver\kiosksaver\Kiosk_screensaver.scr
)Destination: One of the above paths
Also add a Folder creation item for C:\ProgramData\Kiosksaver
if it doesn’t exist.
Step 3 Force the Screensaver via GPO
Path:
Computer Configuration >
Administrative Templates >
Control Panel >
Personalization
Set these policies:
Policy | Value |
Enable screen saver | Enabled |
Screen saver timeout | 300 (or any timeout in seconds) |
Force specific screen saver | Kiosk_screensaver.scr |
This will force Windows to activate your screensaver after X seconds of inactivity, even inside Horizon.
Quote
Important: Horizon sessions run in user context, but the
.scr
activates from the shell so it can still detect inactivity at the endpoint level, regardless of Horizon's internal idle policy.
Security & Considerations
Use a read-only share for source files
Ensure users can't write to
C:\ProgramData\Kiosksaver
Make sure the screensaver is not replaced by login scripts or UEM policies
Don’t target fat clients with this GPO unless you want it active there too
Result: Enforced Session Cleanup
On each zero client or kiosk-mode PC:
After X seconds of user inactivity, your custom screensaver launches
The user sees a branded, multi-monitor warning
If no one clicks the Horizon client (
vmware-view.exe
) is forcefully terminatedThe session ends (well it's a hard disconnect actually). No lingering desktops. No exposed data. No more security risk.
No extra agents.
No login scripts.
No reliance on Horizon pool-level policies.
Just a native .scr
that quietly does its job.
Final Thoughts
Yes some of these problems can be solved by making your Horizon configuration smarter.
You could:
Split out separate pools for kiosks
Set aggressive idle timers
Configure global GPOs for forced logoff warnings
Use DEM with dynamic settings per location
…but let’s be real:
That’s a lot of complexity just to make sure someone doesn’t leave a session open in a meeting room.
This screensaver flips that model.
Instead of solving the problem in the datacenter with layered policy spaghetti, you let the client-side enforce cleanup locally, predictably, and without extra infrastructure.
No new tools
No user training
No Horizon changes
No scripting or registry hacks
It’s just a .scr
file with a config and a purpose:
If no one clicks, the session ends.
Fast, silent, and effective.
Whether you deploy it to zero clients, shared fat clients, or hybrid devices you’re fixing a real security gap without creating new complexity.
Is it elegant? Not really.
Is it extremely practical? Absolutely.
Bonus: Not Just for Horizon
Sure, this started as a fix for Horizon kiosk sessions but let’s not overlook the real flexibility here.
The process that gets killed?
Fully configurable.
Which means this tool becomes useful in way more scenarios than just cleaning up vmware-view
.
Here are a few honorable mentions:
Auto-clean shared app sessions
Have a shared workstation with someone leaving a browser or app open all day?
Set KillProcess=chrome
or KillProcess=teams
When they forget to log off, it closes automatically.
Testing Labs / Simulation Rooms
Want to auto-terminate a heavy app like MATLAB, Unity, or radiotherapy software after idle time?
Just set the process name and walk away.
Kiosk-Mode Apps (Standalone, Not VDI)
Some kiosks run full-screen apps without user sessions like Wayfinding, Room Booking, or Survey tools.
If the app freezes or hangs open?
Let the screensaver detect the stall and restart it via a wrapper script.
Fat Clients with Licensed Software
Want to prevent people from "squatting" on a seat with a limited license (e.g. AutoCAD, SPSS, or even Photoshop)?
Configure a short timeout and reclaim the session cleanly.
This tool is simple on purpose but because you define the target, it adapts to whatever cleanup job you need done.
You might need to adapt some code.
Maybe change how processes are monitored.
Maybe make it handle multiple targets or custom logic per process.
In that case you’ve got a solid base to start from.
As always comments, critical remarks, and heads-ups are very welcome.
If it’s useful, let me know.
If it’s terrible, definitely let me know.
And if you broke something with it... well, that’s on you.
Have Fun,
Mark
Files :
Zip containing the program data folder example contents:
Zip containing the full source without compiled EXE :
Subscribe to my newsletter
Read articles from Mark Platte directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by

Mark Platte
Mark Platte
Mark Platte Born in January, 1984.I currently work as an IT architect in a hospital environment, with a career shaped by hands-on experience in application development, storage engineering, and infrastructure design. My roots are in software, but over time I moved deeper into system architecture, working closely with storage platforms, virtualization, and security especially in regulated and research-intensive environments. I have a strong focus on building stable, secure, and manageable IT solutions, particularly in complex environments where clinical systems, research data, and compliance requirements intersect. I’m especially experienced in enterprise storage design, backup strategies, and performance tuning, often acting as the bridge between engineering teams and long-term architectural planning. I enjoy solving difficult problems and still believe most issues in IT can be fixed with enough determination, focus, and sometimes budget. It’s that drive to find solutions that keeps me motivated.