Last active
August 13, 2025 20:55
-
-
Save ardnew/9e4b7f64aae9a5fef6c81738d815ef32 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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