Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/PSCaffeinate/PSCaffeinate.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
PowerShellVersion = '5.1'
CompatiblePSEditions = @('Desktop', 'Core')

FunctionsToExport = @('Invoke-Caffeinate')
AliasesToExport = @('caffeinate')
FunctionsToExport = @('Invoke-Caffeinate', 'caffeinate')
AliasesToExport = @()
CmdletsToExport = @()
VariablesToExport = @()

Expand Down
110 changes: 101 additions & 9 deletions src/PSCaffeinate/PSCaffeinate.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,20 @@ if (-not ('PSCaffeinate.NativeMethods' -as [type])) {
Set-Variable -Scope Script -Name ES_CONTINUOUS -Value ([uint32]2147483648) -Option Constant
Set-Variable -Scope Script -Name ES_SYSTEM_REQUIRED -Value ([uint32]0x00000001) -Option Constant
Set-Variable -Scope Script -Name ES_DISPLAY_REQUIRED -Value ([uint32]0x00000002) -Option Constant
Set-Variable -Scope Script -Name ES_USER_PRESENT -Value ([uint32]0x00000004) -Option Constant
Set-Variable -Scope Script -Name ES_USER_PRESENT -Value ([uint32]0x00000004) -Option Constant # deprecated; not passed to API
#endregion

#region Private helpers
function Set-SleepAssertion {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
param([uint32]$Flags)
$result = [PSCaffeinate.NativeMethods]::SetThreadExecutionState($Flags)
if ($result -eq 0) {
Write-Warning -Message 'caffeinate: SetThreadExecutionState returned 0 -- sleep prevention may not be active.'
}
return $result
}

function Clear-SleepAssertion {
[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')]
param()
Expand Down Expand Up @@ -62,8 +72,8 @@ function Invoke-Caffeinate {
Alias: -s

.PARAMETER UserActive
Assert that the user is active, resetting the idle and screensaver timers
(ES_USER_PRESENT).
Assert that the user is active. ES_USER_PRESENT is deprecated on modern
Windows, so -u is treated as -d -i (display + idle-sleep prevention).
Alias: -u

.PARAMETER Timeout
Expand Down Expand Up @@ -112,7 +122,6 @@ function Invoke-Caffeinate {
#>

[CmdletBinding(DefaultParameterSetName = 'Indefinite', SupportsShouldProcess)]
[Alias('caffeinate')]
param(
[ValidatePattern('^[disuDISU]+$')]
[string]$Flags,
Expand Down Expand Up @@ -163,8 +172,12 @@ function Invoke-Caffeinate {
#region Build execution-state flags
[uint32]$executionFlags = $script:ES_CONTINUOUS

if ($UserActive) {
Write-Verbose -Message 'caffeinate: -u (ES_USER_PRESENT) is deprecated on modern Windows; substituting display + idle-sleep prevention.'
$executionFlags = $executionFlags -bor $script:ES_DISPLAY_REQUIRED -bor $script:ES_SYSTEM_REQUIRED
}

if ($PreventDisplaySleep) { $executionFlags = $executionFlags -bor $script:ES_DISPLAY_REQUIRED }
if ($UserActive) { $executionFlags = $executionFlags -bor $script:ES_USER_PRESENT }

if ($PreventIdleSleep -or $PreventSystemSleep -or
(-not $PreventDisplaySleep -and -not $UserActive)) {
Expand All @@ -174,7 +187,6 @@ function Invoke-Caffeinate {
$assertionNames = [System.Collections.Generic.List[string]]::new()
if ($executionFlags -band $script:ES_DISPLAY_REQUIRED) { $assertionNames.Add('display') }
if ($executionFlags -band $script:ES_SYSTEM_REQUIRED) { $assertionNames.Add('system-idle') }
if ($executionFlags -band $script:ES_USER_PRESENT) { $assertionNames.Add('user-active') }

$assertionLabel = $assertionNames -join ', '
#endregion
Expand All @@ -183,7 +195,7 @@ function Invoke-Caffeinate {
return
}

$null = [PSCaffeinate.NativeMethods]::SetThreadExecutionState([uint32]$executionFlags)
$null = Set-SleepAssertion -Flags $executionFlags
Write-Verbose -Message "caffeinate: holding [$assertionLabel] sleep assertions"

try {
Expand All @@ -192,8 +204,13 @@ function Invoke-Caffeinate {
'Timeout' {
Write-Information -MessageData "caffeinate: awake for $Timeout second(s) -- Ctrl+C to stop early." -InformationAction Continue
$deadline = [datetime]::UtcNow.AddSeconds($Timeout)
$nextReassert = [datetime]::UtcNow.AddSeconds(30)
while ([datetime]::UtcNow -lt $deadline) {
Start-Sleep -Milliseconds 500
if ([datetime]::UtcNow -ge $nextReassert) {
$null = Set-SleepAssertion -Flags $executionFlags
$nextReassert = [datetime]::UtcNow.AddSeconds(30)
}
}
}

Expand All @@ -204,7 +221,9 @@ function Invoke-Caffeinate {
return
}
Write-Information -MessageData "caffeinate: waiting for PID $WaitPid ($($targetProcess.ProcessName)) -- Ctrl+C to stop early." -InformationAction Continue
$targetProcess.WaitForExit()
while (-not $targetProcess.WaitForExit(30000)) {
$null = Set-SleepAssertion -Flags $executionFlags
}
Write-Verbose -Message "caffeinate: PID $WaitPid exited with code $($targetProcess.ExitCode)."
}

Expand All @@ -219,10 +238,83 @@ function Invoke-Caffeinate {

default {
Write-Information -MessageData 'caffeinate: running indefinitely -- Ctrl+C to stop.' -InformationAction Continue
while ($true) { Start-Sleep -Seconds 30 }
while ($true) {
Start-Sleep -Seconds 30
$null = Set-SleepAssertion -Flags $executionFlags
}
}
}
} finally {
Clear-SleepAssertion
}
}

function caffeinate {
<#
.SYNOPSIS
Wrapper for Invoke-Caffeinate that supports POSIX-style bundled flags.

.DESCRIPTION
Expands arguments like -disu into -Flags disu before calling
Invoke-Caffeinate, enabling a macOS caffeinate-like CLI experience.

.EXAMPLE
caffeinate -disu -t 3600
#>
$targetCmd = Get-Command Invoke-Caffeinate -CommandType Function
$switchNames = [System.Collections.Generic.HashSet[string]]::new(
[System.StringComparer]::OrdinalIgnoreCase
)
foreach ($p in $targetCmd.Parameters.Values) {
if ($p.SwitchParameter) {
$null = $switchNames.Add($p.Name)
foreach ($a in $p.Aliases) { $null = $switchNames.Add($a) }
}
}

$splatParams = @{}
$positionalArgs = [System.Collections.Generic.List[object]]::new()

$i = 0
while ($i -lt $args.Count) {
$current = $args[$i]

if ($current -is [string] -and $current -match '^-([disuDISU]{2,})$') {
$splatParams['Flags'] = $Matches[1].ToLower()
}
elseif ($current -is [string] -and $current -match '^-(.+):(.*)$') {
$paramName = $Matches[1]
$rawValue = $Matches[2]
if ($rawValue -eq '$true') { $splatParams[$paramName] = $true }
elseif ($rawValue -eq '$false') { $splatParams[$paramName] = $false }
else { $splatParams[$paramName] = $rawValue }
}
elseif ($current -is [string] -and $current -match '^-(.+)$') {
$paramName = $Matches[1]
if ($switchNames.Contains($paramName)) {
$splatParams[$paramName] = $true
}
elseif (($i + 1) -lt $args.Count) {
$splatParams[$paramName] = $args[$i + 1]
$i++
}
else {
$splatParams[$paramName] = $true
}
}
else {
$positionalArgs.Add($current)
}

$i++
}

if ($positionalArgs.Count -gt 0) {
$splatParams['Command'] = $positionalArgs[0]
if ($positionalArgs.Count -gt 1) {
$splatParams['ArgumentList'] = @($positionalArgs[1..($positionalArgs.Count - 1)])
}
}

Invoke-Caffeinate @splatParams
}
36 changes: 27 additions & 9 deletions tests/PSCaffeinate.Tests.ps1
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#requires -Modules Pester
#requires -Modules Pester

BeforeAll {
$modulePath = Join-Path (Join-Path (Join-Path $PSScriptRoot '..') 'src') 'PSCaffeinate'
Expand All @@ -19,13 +19,13 @@ Describe 'Module: PSCaffeinate' {
$exportedFunctions | Should -Contain 'Invoke-Caffeinate'
}

It 'exports the caffeinate alias' {
$exportedAliases = (Get-Module PSCaffeinate).ExportedAliases.Keys
$exportedAliases | Should -Contain 'caffeinate'
It 'exports the caffeinate wrapper function' {
$exportedFunctions = (Get-Module PSCaffeinate).ExportedFunctions.Keys
$exportedFunctions | Should -Contain 'caffeinate'
}

It 'alias points to Invoke-Caffeinate' {
(Get-Alias caffeinate).ReferencedCommand.Name | Should -Be 'Invoke-Caffeinate'
It 'caffeinate command is available' {
Get-Command caffeinate -ErrorAction Stop | Should -Not -BeNullOrEmpty
}
}

Expand Down Expand Up @@ -169,19 +169,18 @@ Describe 'Module: PSCaffeinate' {

Context 'Bundled -Flags parameter' {

It '-Flags disu sets all four assertions' {
It '-Flags disu sets display and system-idle assertions' {
$verbose = Invoke-Caffeinate -Flags 'disu' -Timeout 1 -Confirm:$false -Verbose 4>&1
$verboseText = ($verbose | Out-String)
$verboseText | Should -Match 'display'
$verboseText | Should -Match 'system-idle'
$verboseText | Should -Match 'user-active'
}

It '-Flags d sets only display assertion' {
$verbose = Invoke-Caffeinate -Flags 'd' -Timeout 1 -Confirm:$false -Verbose 4>&1
$verboseText = ($verbose | Out-String)
$verboseText | Should -Match 'display'
$verboseText | Should -Not -Match 'user-active'
$verboseText | Should -Not -Match 'system-idle'
}

It '-Flags works with -Timeout' {
Expand All @@ -195,6 +194,25 @@ Describe 'Module: PSCaffeinate' {
}
}

Context 'caffeinate wrapper function' {

It 'caffeinate -disu runs without error' {
$elapsed = Measure-Command { caffeinate -disu -t 1 }
$elapsed.TotalSeconds | Should -BeGreaterOrEqual 0.5
}

It 'caffeinate -di -t works with timeout' {
$elapsed = Measure-Command { caffeinate -di -t 2 }
$elapsed.TotalSeconds | Should -BeGreaterOrEqual 1.5
$elapsed.TotalSeconds | Should -BeLessThan 5
}

It 'caffeinate passes through non-flag args' {
$output = caffeinate cmd.exe /c echo wrapper_test_token
$output | Should -Contain 'wrapper_test_token'
}
}

Context 'ShouldProcess / -WhatIf' {

It 'does not assert sleep state when -WhatIf is used' {
Expand Down
Loading