Skip to content

Instantly share code, notes, and snippets.

@ardnew
Last active August 13, 2025 20:55
Show Gist options
  • Save ardnew/9e4b7f64aae9a5fef6c81738d815ef32 to your computer and use it in GitHub Desktop.
Save ardnew/9e4b7f64aae9a5fef6c81738d815ef32 to your computer and use it in GitHub Desktop.
# keepawake.ps1
#
# Keep screen unlocked indefinitely after local or remote login
#
# _____________
# OPERATION
#
# - After running this script, a scheduled task "Keepawake" is registered and
# runs in the background upon all future logins (local or remote).
#
# - The Keepawake process runs in the context of a Powershell script invoked by
# Windows Task Scheduler. The task is configured to run in the background
# without an associated console so that direct I/O is impossible.
#
# - The script uses syscall (*void)SetThreadExecutionState(uint) from the
# Windows API (kernel32.dll) to raise its own priority equivalent to what is
# used for full-screen applications.
#
# - The screen will not auto-lock as long as any process is running with this
# priority. The user can still lock the screen manually with, e.g., keyboard
# shortcut Win+L or via the Start menu.
#
# - The script then waits for user input. But because it is not attached to a
# console, the user cannot provide input. The script thus waits indefinitely
# until the process is killed (via Windows Task Manager or Task Scheduler).
#
# - Windows Task Scheduler automatically kills this process upon logout, and
# it will restart it upon the next login (local or remote).
#
# _____________
# UNINSTALL
#
# - Use Windows Task Scheduler (found in the Start menu) to uninstall or
# disable the task temporarily.
#
# __________________
# TASK SCHEDULER
#
# - From Windows Task Scheduler, you can customize all options for the task
# including trigger event(s), run duration, date/time ranges it is allowed
# to run, etc.
#
# - Task location:
#
# | Task Scheduler (Local) > Task Scheduler Library > Keepawake
#
# _____________
# REVISIONS
#
# - 2025-07-30 | Andrew Shultzabarger
# + add support for triggering on local logins
# - 2025-07-14 | Andrew Shultzabarger
# + document internal operations in file header comments
# - 2025-06-02 | Andrew Shultzabarger
# + automatically install task (if missing) via Windows Task Scheduler
# - 2025-04-01 | Andrew Shultzabarger
# + init
#
# --
# Configure task using attributes from the invoking user's environment
$TaskName = "Keepawake"
$TaskOwner = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
$TaskDateTime = Get-Date -Format "yyyy-MM-ddTHH:mm:ss.fffffff"
$ShellPath = Get-Command powershell | Select-Object -ExpandProperty Source
# Compatibility shim for versions of Powershell without $PSCommandPath
if ($PSCommandPath -eq $null) {
function GetPSCommandPath() {
return $MyInvocation.PSCommandPath
}
$PSCommandPath = GetPSCommandPath
}
$TaskSpecXML = @"
<?xml version="1.0" encoding="UTF-16"?>
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
<RegistrationInfo>
<Date>${TaskDateTime}</Date>
<Author>${TaskOwner}</Author>
<Description>Keep screen active during Remote Desktop connections</Description>
<URI>\${TaskName}</URI>
</RegistrationInfo>
<Principals>
<Principal id="Author">
<UserId>${TaskOwner}</UserId>
<LogonType>InteractiveToken</LogonType>
<RunLevel>HighestAvailable</RunLevel>
</Principal>
</Principals>
<Settings>
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
<DisallowStartIfOnBatteries>true</DisallowStartIfOnBatteries>
<StopIfGoingOnBatteries>true</StopIfGoingOnBatteries>
<AllowHardTerminate>true</AllowHardTerminate>
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
<StartWhenAvailable>true</StartWhenAvailable>
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
<IdleSettings>
<StopOnIdleEnd>true</StopOnIdleEnd>
<RestartOnIdle>false</RestartOnIdle>
</IdleSettings>
<AllowStartOnDemand>true</AllowStartOnDemand>
<Hidden>false</Hidden>
<RunOnlyIfIdle>false</RunOnlyIfIdle>
<WakeToRun>false</WakeToRun>
</Settings>
<Triggers>
<SessionStateChangeTrigger>
<StateChange>RemoteConnect</StateChange>
<UserId>${TaskOwner}</UserId>
</SessionStateChangeTrigger>
<SessionStateChangeTrigger>
<StateChange>ConsoleConnect</StateChange>
<UserId>${TaskOwner}</UserId>
</SessionStateChangeTrigger>
</Triggers>
<Actions Context="Author">
<Exec>
<Command>${ShellPath}</Command>
<Arguments>-WindowStyle Hidden -File "${PSCommandPath}"</Arguments>
</Exec>
</Actions>
</Task>
"@
try {
$TaskInfo = SCHTASKS /QUERY /TN "${TaskName}" /FO "CSV" /V 2>$null
$TaskCSV = ${TaskInfo} | ConvertFrom-Csv 2>$null
$TaskStatus = ${TaskCSV} | Select-Object -ExpandProperty "Status"
$SetState = ${TaskStatus} -eq "Running"
if (-not ${SetState}) { throw }
} catch {
try {
$TempXML = New-TemporaryFile
$TaskSpecXML | Out-File -FilePath ${TempXML}.FullName
SCHTASKS /CREATE /F /XML ${TempXML}.FullName /TN "${TaskName}" /HRESULT
Write-Host "SUCCESS: The scheduled task `"${TaskName}`" will run on all future logins from `"${TaskOwner}`"."
SCHTASKS /RUN /TN "${TaskName}" /HRESULT
Write-Host "SUCCESS: The scheduled task `"${TaskName}`" is running."
} finally {
Remove-Item -Path ${TempXML}.FullName -Force -ErrorAction "SilentlyContinue"
}
}
if (${SetState}) {
# Load the Win32 API system call:
#
# void SetThreadExecutionState(uint)
#
# This is the magic syscall that forces Windows to remain in the foreground
# for as long as the user remains logged in locally or remotely.
#
# As soon as the user disconnects or logs out, the system call is invoked
# again to stop forcing the foregrounded state.
#
# We use the Windows Task Scheduler as a proxy to associate session
# login/logout events with appropriate system calls via this API.
$DeclSetTXS = @'
[DllImport("kernel32.dll", CharSet = CharSet.Auto,SetLastError = true)]
public static extern void SetThreadExecutionState(uint esFlags);
'@
$ExecContext = Add-Type -memberDefinition $DeclSetTXS -name System -namespace Win32 -passThru
# Requests that the other EXECUTION_STATE flags set remain in effect until
# SetThreadExecutionState is called again with the TXSContinuous flag set and
# one of the other EXECUTION_STATE flags cleared.
$TXSContinuous = [uint32]"0x80000000"
$TXSAwaymodeRequired = [uint32]"0x00000040"
$TXSDisplayRequired = [uint32]"0x00000002"
$TXSSystemRequired = [uint32]"0x00000001"
$TXSAcquire = $TXSSystemRequired -bor $TXSDisplayRequired -bor $TXSContinuous
$TXSRelease = $TXSContinuous
try {
Write-Host "start: keepawake"
$ExecContext::SetThreadExecutionState( $TXSAcquire )
Read-Host "press [Enter] to quit"
} finally {
Write-Host "stop: keepawake"
$ExecContext::SetThreadExecutionState( $TXSRelease )
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment