Skip to content

Instantly share code, notes, and snippets.

@ClassicOldSong
Last active February 16, 2025 18:48
Show Gist options
  • Save ClassicOldSong/0a088d2a92a06f86e48fe328b67ec995 to your computer and use it in GitHub Desktop.
Save ClassicOldSong/0a088d2a92a06f86e48fe328b67ec995 to your computer and use it in GitHub Desktop.
Auto Pause/Resume script for providing console grade experience with Apollo
#Requires AutoHotkey v2.0
#SingleInstance Force
DetectHiddenWindows true
; Globals to manage the suspend/resume state
global IsProcessSuspended := false
global IsProcessResumed := false
LockFile := A_Temp "\AutoPauseResume.lock"
; Ensure we clean up on script exit:
OnExit(Cleanup)
; Check if the lock file exists before deleting it
if FileExist(LockFile)
FileDelete(LockFile)
; --------------------------------------------------------------------------
; 1) Define excluded processes as a map (key-value)
; - The key is the process name in lowercase, mapped to `true`.
; --------------------------------------------------------------------------
excludedProcesses := Map()
excludedProcesses["explorer.exe"] := true
excludedProcesses["csrss.exe"] := true
excludedProcesses["winlogon.exe"] := true
excludedProcesses["svchost.exe"] := true
excludedProcesses["dwm.exe"] := true
excludedProcesses["cmd.exe"] := true
excludedProcesses["chrome.exe"] := true
excludedProcesses["powershell.exe"] := true
excludedProcesses["sublime_text.exe"] := true
excludedProcesses["sublime_merge.exe"] := true
excludedProcesses["steamwebhelper.exe"] := true
excludedProcesses["openconsole.exe"] := true
excludedProcesses["windowsterminal.exe"] := true
excludedProcesses["playnite.desktopapp.exe"] := true
excludedProcesses["playnite.fullscreenapp.exe"] := true
; --------------------------------------------------------------------------
; 2) Get the active window's PID & Process Name, confirm it's not excluded
; --------------------------------------------------------------------------
if !WinExist("A")
{
MsgBox "No active window detected. Please focus a window to suspend."
ExitApp
}
pid := WinGetPID("A")
if !pid
{
MsgBox "Failed to get the active window's PID."
ExitApp
}
processName := WinGetProcessName()
if !processName
{
MsgBox "Failed to retrieve the active window's process name."
ExitApp
}
; Convert the process name to lowercase for case-insensitive matching
procLower := StrLower(processName)
; Check if `excludedProcesses` has this process in its keys, and if the value is true
if excludedProcesses.Has(procLower) && excludedProcesses[procLower]
{
; Excluded process, so do nothing and exit.
ExitApp
}
; --------------------------------------------------------------------------
; 3) Suspend the process & start checking for the lock file in a timer
; --------------------------------------------------------------------------
SuspendProcess(pid)
IsProcessSuspended := true ; Mark that we have indeed suspended a process
; Optional: show a short tooltip or do a quick beep instead of a MsgBox
; MsgBox "Process '" processName "' (PID: " pid ") suspended. Waiting for lock file..."
SetTimer(CheckLockFile, 500) ; check the lock file every 500 ms
return
; --------------------------------------------------------------------------
CheckLockFile(*)
{
global LockFile, pid, processName, IsProcessResumed
if FileExist(LockFile)
{
ResumeProcess(pid)
IsProcessResumed := true
; MsgBox "Process '" processName "' (PID: " pid ") resumed. Exiting."
FileDelete(LockFile)
ExitApp
}
}
; --------------------------------------------------------------------------
SuspendProcess(pid)
{
PROCESS_ALL_ACCESS := 0x1F0FFF
hProc := DllCall("OpenProcess", "UInt", PROCESS_ALL_ACCESS, "Int", 0, "UInt", pid, "Ptr")
if !hProc
{
MsgBox "Failed to open process (PID: " pid ")."
ExitApp
}
hNtdll := DllCall("GetModuleHandle", "Str", "ntdll.dll", "Ptr")
if !hNtdll
{
MsgBox "Failed to get module handle for ntdll.dll."
ExitApp
}
NtSuspendProcess := DllCall("GetProcAddress", "Ptr", hNtdll, "AStr", "NtSuspendProcess", "Ptr")
if !NtSuspendProcess
{
MsgBox "Failed to locate NtSuspendProcess in ntdll.dll."
ExitApp
}
DllCall(NtSuspendProcess, "Ptr", hProc)
DllCall("CloseHandle", "Ptr", hProc)
}
; --------------------------------------------------------------------------
ResumeProcess(pid)
{
PROCESS_ALL_ACCESS := 0x1F0FFF
hProc := DllCall("OpenProcess", "UInt", PROCESS_ALL_ACCESS, "Int", 0, "UInt", pid, "Ptr")
if !hProc
{
; Process may no longer exist, don't do anything.
; MsgBox "Failed to open process (PID: " pid ")."
ExitApp
}
hNtdll := DllCall("GetModuleHandle", "Str", "ntdll.dll", "Ptr")
if !hNtdll
{
MsgBox "Failed to get module handle for ntdll.dll."
ExitApp
}
NtResumeProcess := DllCall("GetProcAddress", "Ptr", hNtdll, "AStr", "NtResumeProcess", "Ptr")
if !NtResumeProcess
{
MsgBox "Failed to locate NtResumeProcess in ntdll.dll."
ExitApp
}
DllCall(NtResumeProcess, "Ptr", hProc)
DllCall("CloseHandle", "Ptr", hProc)
}
; --------------------------------------------------------------------------
; Automatically resume the process if the user manually exits the script
; or the script ends unexpectedly.
; --------------------------------------------------------------------------
Cleanup(exitReason, exitCode)
{
global pid, IsProcessSuspended, IsProcessResumed
; If the process was suspended and hasn't been resumed via the lock file,
; auto-resume it here.
if IsProcessSuspended && !IsProcessResumed
{
ResumeProcess(pid)
}
}
#Requires AutoHotkey v2.0
#SingleInstance Force
LockFile := A_Temp "\AutoPauseResume.lock"
; Simply create the lock file and exit
FileAppend("", LockFile)
ExitApp
@zjamnik
Copy link

zjamnik commented Feb 13, 2025

For some reason, when quitting the game global undo command gets executed before client disconnect command. So my global resume runs, doing nothing and then client disconnect pause runs, pausing the foreground app. Any way around this?

@ClassicOldSong
Copy link
Author

If this happens occasionally, go to the system tray you’ll see a running AHK icon, click on it and exit the script will unfreeze the app.

If this always happens, then you can add a delay to the execution of resume.

@zjamnik
Copy link

zjamnik commented Feb 13, 2025

Tried with the delay before even writing my comment. AHK sleep in resume just delayed the stream from closing.
I just finished writing my own version of the scripts. I'm using environmental variables to store the game process when starting it, to avoid guessing the window to suspend. For some reason your suspend method stopped working for me so I moved to PSTools PSSuspend from sysinternals to suspend it. In the end they are shorter and easier to read for the price of some dependencies.
I'm also using a script to launch the game itself instead of using a detached command. WinWaitClose in ahk allows for tracking when game closes so the stream ends with it.
Appreciate the response and a very quick one!

@ClassicOldSong
Copy link
Author

Well usually you don’t put the game start command in detached command, but directly in command. So when the game quits, the stream will terminate automatically.

Envvars can be unreliable in some cases that’s why I didn’t use the method.

@mysteriousHerb
Copy link

Where in global undo i should put the resume,ahk?
Is it for example the detach command for steam big picutre? I got steam big picture frozen when i exit stream.

@zjamnik
Copy link

zjamnik commented Feb 16, 2025

In Configuration, first tab General, you'll find a section Command Preparations. Add a new one there and only fill out the Undo Command, put the path to the resume script there.

@ClassicOldSong
Copy link
Author

Or you can add steam.exe to the ignore list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment