Last active
April 27, 2025 14:35
-
-
Save mmotti/bfc697d03c5c5b03d09806abdc6c107f to your computer and use it in GitHub Desktop.
Simple script to automatically shut down your PC after Steam downloads are complete.
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
<# | |
.SYNOPSIS | |
Monitors Steam downloads and initiates a system shutdown when downloads have completed. | |
.DESCRIPTION | |
This script will check for active Steam downloads and monitor their progress. Other scripts rely on | |
network / disk activity however they are somewhat flawed as they don't accommodate for user intervention | |
or drop in network connectivity. | |
Providing Steam doesn't change how the registry settings work, this script should correctly identify when | |
downloads are active, when they have completed and when there has been user intervention. | |
.PARAMETER loopSleepSeconds | |
This is the interval timer for the loop that monitors the active download. | |
.PARAMETER shutdownDelaySeconds | |
This is the delay for system shutdown, AFTER the threshold for no download activity has been exceeded. | |
.PARAMETER noDownloadActivityThreshold | |
This is used to specify how many loop iterations of an inactive download are completed before the script is to | |
initiate the shutdown call. | |
#> | |
param ( | |
[int] $loopSleepSeconds = 60, | |
[int] $shutdownDelaySeconds = 900, # 15 minutes | |
[int] $noDownloadActivityThreshold = 5 | |
) | |
$STEAM_REG_PATHS = @( | |
'HKLM:\SOFTWARE\WOW6432Node\Valve\Steam', | |
'HKLM:\SOFTWARE\Valve\Steam' | |
) | |
$STEAM_APPS_REG_PATH = "HKCU:\Software\Valve\Steam\Apps\*" | |
$STEAM_PROCESS_NAME = "steam" | |
$ACF_FILE_PATTERN = "appmanifest_{0}.acf" | |
function Get-SteamPath { | |
# There should only be one return here but just in case... | |
foreach ($path in $STEAM_REG_PATHS | Where-Object {Test-Path $_}) { | |
$installPath = (Get-ItemProperty -Path $path -Name "InstallPath" -ErrorAction SilentlyContinue).InstallPath | |
if (Test-Path -Path $installPath) { | |
return $installPath | |
} else { | |
throw "No Steam path was detected." | |
} | |
} | |
} | |
function Get-ActiveDownload { | |
return Get-ItemProperty -Path $STEAM_APPS_REG_PATH -Name "Updating" -ErrorAction SilentlyContinue | Where-Object {$_.Updating -eq 1} | |
} | |
function Confirm-DownloadComplete { | |
param ( | |
[Parameter(Mandatory=$true)] | |
[string] $acfPath | |
) | |
# User has cancelled the download and uninstalled. | |
if (!(Test-Path -Path $acfPath)) { | |
return $false | |
} | |
$acfContent = Get-Content -Path $acfPath | |
$isDownloadCompleted = $false | |
if ($acfContent) { | |
$bytesToDownloadMatch = $acfContent | Select-String -Pattern "`"BytesToDownload`"\s+`"(\d+)`"" | |
$bytesDownloadedMatch = $acfContent | Select-String -Pattern "`"BytesDownloaded`"\s+`"(\d+)\`"" | |
if($bytesToDownloadMatch -and $bytesDownloadedMatch) { | |
$bytesToDownload = [int64] $bytesToDownloadMatch.Matches[0].Groups[1].Value | |
$bytesDownloaded = [int64] $bytesDownloadedMatch.Matches[0].Groups[1].Value | |
if ($bytesToDownload -eq $bytesDownloaded) { | |
$isDownloadCompleted = $true | |
} | |
} | |
} | |
return $isDownloadCompleted | |
} | |
$steamPath = Get-SteamPath | |
$downloadAppID = $null | |
$loopState = "WaitingForSteam" | |
$i = 0 | |
while ($true) { | |
Clear-Host | |
switch ($loopState) { | |
"WaitingForSteam" { | |
Write-Output "Looking for Steam process..." | |
if (Get-Process $STEAM_PROCESS_NAME -ErrorAction SilentlyContinue) { | |
Write-Output "Steam process detected!" | |
$loopState = "WaitingForDownloads" | |
} else { | |
Start-Sleep -Seconds 3 | |
} | |
} | |
"WaitingForDownloads" { | |
$activeDownload = Get-ActiveDownload | |
if ($activeDownload) { | |
$downloadAppID = $activeDownload.PSChildName | |
$loopState = "MonitoringDownloads" | |
$i = 0 | |
} else { | |
Write-Output "Waiting for downloads to begin..." | |
Start-Sleep -Seconds 3 | |
} | |
} | |
"MonitoringDownloads" { | |
$activeDownload = Get-ActiveDownload | |
if ($activeDownload) { | |
Write-Output "Steam client is downloading." | |
$downloadAppID = $activeDownload.PSChildName | |
$i = 0 | |
} else { | |
if(Confirm-DownloadComplete -acfPath ("${steamPath}\steamapps\$ACF_FILE_PATTERN" -f $downloadAppID)) { | |
Write-Warning "There is no active download." | |
Write-Warning "$($noDownloadActivityThreshold - $i) checks remain until shutdown." | |
if ($i -ge $noDownloadActivityThreshold) { | |
Write-Output "Sending shutdown signal..." | |
shutdown /s /f /t $shutdownDelaySeconds | |
Write-Output "To cancel the shutdown, run `"shutdown /a`"." | |
$null = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown') | |
exit | |
} | |
$i++ | |
} else { | |
Write-Output "User intervention has been detected." | |
$loopState = "WaitingForDownloads" | |
continue | |
} | |
} | |
Write-Output "Next check: ~$((Get-Date).AddSeconds($loopSleepSeconds).ToString('HH:mm:ss'))" | |
Start-Sleep -Seconds $loopSleepSeconds | |
} | |
} | |
} |
About Toast notification. Do you know the BurntToast module (https://github.com/Windos/BurntToast)? Look at the images of the examples provided on the Github, it's very interesting and practical.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
@Rapidhands I could indeed change the shutdown command out. I used
shutdown
primarily because it gives a toast notification that the system will shutdown, and subsequent warnings when it gets closer. I thought that may avoid any instances where the user may have sat back down at their PC, cancelled the downloads but forgotten about the script running in the background.Also I could expand this to use params etc. I might make a repo for it if it's useful enough to people and expand it properly there.
I'm a little concerned at the moment about Steam adjusting their logs. With the current logic of the script, for it to work properly, it needs to be able to evaluate for what reason the downloads aren't active and to do that it needs for all of the regexp patterns to match and find which one occurred most recently. Otherwise it could get confused and think that the download is in the wrong state.
E.g. If I only match for "Pause" but actually the user has moved the download out of the schedule (so it's not running), but the unscheduled regex match failed and paused is still the most recent match it could find, it could get stuck in a "pause" state instead of terminating.