├── .gitattributes ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE.md ├── Main.c ├── Main.h ├── NiceLogo.png ├── README.md ├── Server.c ├── Server.h ├── UniversalPauseButton.rc ├── UniversalPauseButton.sln ├── UniversalPauseButton.vcxproj ├── UniversalPauseButton.vcxproj.filters ├── debugconsole.png ├── main_page.htm ├── pause.ico ├── registry.png ├── resource.h └── welcome.htm /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ryanries 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # Build results 11 | .vs/ 12 | [Dd]ebug/ 13 | [Dd]ebugPublic/ 14 | [Rr]elease/ 15 | [Rr]eleases/ 16 | x64/ 17 | x86/ 18 | build/ 19 | bld/ 20 | [Bb]in/ 21 | [Oo]bj/ 22 | 23 | # Roslyn cache directories 24 | *.ide/ 25 | 26 | # MSTest test Results 27 | [Tt]est[Rr]esult*/ 28 | [Bb]uild[Ll]og.* 29 | 30 | #NUNIT 31 | *.VisualState.xml 32 | TestResult.xml 33 | 34 | # Build Results of an ATL Project 35 | [Dd]ebugPS/ 36 | [Rr]eleasePS/ 37 | dlldata.c 38 | 39 | *_i.c 40 | *_p.c 41 | *_i.h 42 | *.ilk 43 | *.meta 44 | *.obj 45 | *.pch 46 | *.pdb 47 | *.pgc 48 | *.pgd 49 | *.rsp 50 | *.sbr 51 | *.tlb 52 | *.tli 53 | *.tlh 54 | *.tmp 55 | *.tmp_proj 56 | *.log 57 | *.vspscc 58 | *.vssscc 59 | .builds 60 | *.pidb 61 | *.svclog 62 | *.scc 63 | 64 | # Chutzpah Test files 65 | _Chutzpah* 66 | 67 | # Visual C++ cache files 68 | ipch/ 69 | *.aps 70 | *.ncb 71 | *.opensdf 72 | *.sdf 73 | *.cachefile 74 | 75 | # Visual Studio profiler 76 | *.psess 77 | *.vsp 78 | *.vspx 79 | 80 | # TFS 2012 Local Workspace 81 | $tf/ 82 | 83 | # Guidance Automation Toolkit 84 | *.gpState 85 | 86 | # ReSharper is a .NET coding add-in 87 | _ReSharper*/ 88 | *.[Rr]e[Ss]harper 89 | *.DotSettings.user 90 | 91 | # JustCode is a .NET coding addin-in 92 | .JustCode 93 | 94 | # TeamCity is a build add-in 95 | _TeamCity* 96 | 97 | # DotCover is a Code Coverage Tool 98 | *.dotCover 99 | 100 | # NCrunch 101 | _NCrunch_* 102 | .*crunch*.local.xml 103 | 104 | # MightyMoose 105 | *.mm.* 106 | AutoTest.Net/ 107 | 108 | # Web workbench (sass) 109 | .sass-cache/ 110 | 111 | # Installshield output folder 112 | [Ee]xpress/ 113 | 114 | # DocProject is a documentation generator add-in 115 | DocProject/buildhelp/ 116 | DocProject/Help/*.HxT 117 | DocProject/Help/*.HxC 118 | DocProject/Help/*.hhc 119 | DocProject/Help/*.hhk 120 | DocProject/Help/*.hhp 121 | DocProject/Help/Html2 122 | DocProject/Help/html 123 | 124 | # Click-Once directory 125 | publish/ 126 | 127 | # Publish Web Output 128 | *.[Pp]ublish.xml 129 | *.azurePubxml 130 | # TODO: Comment the next line if you want to checkin your web deploy settings 131 | # but database connection strings (with potential passwords) will be unencrypted 132 | *.pubxml 133 | *.publishproj 134 | 135 | # NuGet Packages 136 | *.nupkg 137 | # The packages folder can be ignored because of Package Restore 138 | **/packages/* 139 | # except build/, which is used as an MSBuild target. 140 | !**/packages/build/ 141 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 142 | #!**/packages/repositories.config 143 | 144 | # Windows Azure Build Output 145 | csx/ 146 | *.build.csdef 147 | 148 | # Windows Store app package directory 149 | AppPackages/ 150 | 151 | # Others 152 | sql/ 153 | *.Cache 154 | ClientBin/ 155 | [Ss]tyle[Cc]op.* 156 | ~$* 157 | *~ 158 | *.dbmdl 159 | *.dbproj.schemaview 160 | *.pfx 161 | *.publishsettings 162 | node_modules/ 163 | 164 | # RIA/Silverlight projects 165 | Generated_Code/ 166 | 167 | # Backup & report files from converting an old project file 168 | # to a newer Visual Studio version. Backup files are not needed, 169 | # because we have git ;-) 170 | _UpgradeReport_Files/ 171 | Backup*/ 172 | UpgradeLog*.XML 173 | UpgradeLog*.htm 174 | 175 | # SQL Server files 176 | *.mdf 177 | *.ldf 178 | 179 | # Business Intelligence projects 180 | *.rdl.data 181 | *.bim.layout 182 | *.bim_*.settings 183 | 184 | # Microsoft Fakes 185 | FakesAssemblies/ 186 | 187 | # ========================= 188 | # Operating System Files 189 | # ========================= 190 | 191 | # OSX 192 | # ========================= 193 | 194 | .DS_Store 195 | .AppleDouble 196 | .LSOverride 197 | 198 | # Thumbnails 199 | ._* 200 | 201 | # Files that might appear on external disk 202 | .Spotlight-V100 203 | .Trashes 204 | 205 | # Directories potentially created on remote AFP share 206 | .AppleDB 207 | .AppleDesktop 208 | Network Trash Folder 209 | Temporary Items 210 | .apdisk 211 | 212 | # Windows 213 | # ========================= 214 | 215 | # Windows image file caches 216 | Thumbs.db 217 | ehthumbs.db 218 | 219 | # Folder config file 220 | Desktop.ini 221 | 222 | # Recycle Bin used on file shares 223 | $RECYCLE.BIN/ 224 | 225 | # Windows Installer files 226 | *.cab 227 | *.msi 228 | *.msm 229 | *.msp 230 | 231 | # Windows shortcuts 232 | *.lnk 233 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | ===================== 3 | 4 | Copyright (c) 2015 Joseph Ryan Ries 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Main.c: -------------------------------------------------------------------------------- 1 | // Main.c 2 | // UniversalPauseButton 3 | // Joseph Ryan Ries, 2015-2023 4 | // ryanries09@gmail.com 5 | // https://github.com/ryanries/UniversalPauseButton/ 6 | 7 | #ifndef UNICODE 8 | #define UNICODE 9 | #endif 10 | #ifndef _UNICODE 11 | #define _UNICODE 12 | #endif 13 | 14 | #define WM_TRAYICON (WM_USER + 1) 15 | 16 | #include 17 | #include 18 | #include 19 | #include "Main.h" 20 | #include "resource.h" 21 | #include "Server.h" 22 | 23 | CONFIG gConfig; 24 | HANDLE gDbgConsole = INVALID_HANDLE_VALUE; 25 | BOOL gIsRunning = TRUE; 26 | HANDLE gMutex; 27 | NOTIFYICONDATA gTrayNotifyIconData; 28 | BOOL gIsPaused; 29 | u32 gPreviouslyPausedProcessId; 30 | 31 | 32 | int WINAPI wWinMain(_In_ HINSTANCE Instance, _In_opt_ HINSTANCE PrevInstance, _In_ PWSTR CmdLine, _In_ int CmdShow) 33 | { 34 | UNREFERENCED_PARAMETER(PrevInstance); 35 | UNREFERENCED_PARAMETER(CmdLine); 36 | UNREFERENCED_PARAMETER(CmdShow); 37 | 38 | HMODULE NtDll = NULL; 39 | MSG WndMsg = { 0 }; 40 | //HHOOK KeyboardHook = NULL; 41 | 42 | bool runningServer = false; 43 | 44 | if (LoadRegistrySettings() != ERROR_SUCCESS) 45 | { 46 | goto Exit; 47 | } 48 | 49 | gMutex = CreateMutexW(NULL, FALSE, APPNAME); 50 | 51 | if (GetLastError() == ERROR_ALREADY_EXISTS) 52 | { 53 | MsgBox(L"An instance of the program is already running.", APPNAME L" Error", MB_OK | MB_ICONERROR); 54 | goto Exit; 55 | } 56 | if ((NtDll = GetModuleHandleW(L"ntdll.dll")) == NULL) 57 | { 58 | MsgBox(L"Unable to locate ntdll.dll!\nError: 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, GetLastError()); 59 | goto Exit; 60 | } 61 | if ((NtSuspendProcess = (_NtSuspendProcess)((void*)GetProcAddress(NtDll, "NtSuspendProcess"))) == NULL) 62 | { 63 | MsgBox(L"Unable to locate the NtSuspendProcess procedure in the ntdll.dll module!", APPNAME L" Error", MB_OK | MB_ICONERROR); 64 | goto Exit; 65 | } 66 | if ((NtResumeProcess = (_NtResumeProcess)((void*)GetProcAddress(NtDll, "NtResumeProcess"))) == NULL) 67 | { 68 | MsgBox(L"Unable to locate the NtResumeProcess procedure in the ntdll.dll module!", APPNAME L" Error", MB_OK | MB_ICONERROR); 69 | goto Exit; 70 | } 71 | 72 | // There will be no visible window either way, but in one case, there will be a system tray icon, 73 | // and in the other case there will be no icon. This is because someone requested that I make this 74 | // app work even when the user has no shell (explorer.exe) or has replaced their shell with an alternative shell. 75 | // The Windows system tray API obviously won't work if there is no Windows system tray. I don't know if the user's 76 | // shell even has a taskbar so I'm skipping that too. 77 | 78 | if (gConfig.TrayIcon) 79 | { 80 | WNDCLASSW WndClass = { 0 }; 81 | HWND HWnd = NULL; 82 | 83 | WndClass.hInstance = Instance; 84 | WndClass.lpszClassName = APPNAME L"_WndClass"; 85 | WndClass.lpfnWndProc = SysTrayCallback; 86 | 87 | if (RegisterClassW(&WndClass) == 0) 88 | { 89 | MsgBox(L"Failed to register WindowClass! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, GetLastError()); 90 | goto Exit; 91 | } 92 | 93 | HWnd = CreateWindowExW( 94 | WS_EX_TOOLWINDOW, 95 | WndClass.lpszClassName, 96 | APPNAME L"_Systray_Window", 97 | WS_ICONIC, 98 | CW_USEDEFAULT, 99 | CW_USEDEFAULT, 100 | CW_USEDEFAULT, 101 | CW_USEDEFAULT, 102 | 0, 103 | 0, 104 | Instance, 105 | 0); 106 | 107 | if (HWnd == NULL) 108 | { 109 | MsgBox(L"Failed to create window! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, GetLastError()); 110 | goto Exit; 111 | } 112 | 113 | gTrayNotifyIconData.cbSize = sizeof(NOTIFYICONDATA); 114 | gTrayNotifyIconData.hWnd = HWnd; 115 | gTrayNotifyIconData.uID = 1982; 116 | gTrayNotifyIconData.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP; 117 | gTrayNotifyIconData.uCallbackMessage = WM_TRAYICON; 118 | wcscpy_s(gTrayNotifyIconData.szTip, _countof(gTrayNotifyIconData.szTip), APPNAME L" v" VERSION); 119 | gTrayNotifyIconData.hIcon = (HICON)LoadImageW(GetModuleHandleW(NULL), MAKEINTRESOURCEW(IDI_ICON1), IMAGE_ICON, 0, 0, 0); 120 | 121 | if (gTrayNotifyIconData.hIcon == NULL) 122 | { 123 | MsgBox(L"Failed to load systray icon resource!", APPNAME L" Error", MB_OK | MB_ICONERROR); 124 | goto Exit; 125 | } 126 | 127 | if (Shell_NotifyIconW(NIM_ADD, &gTrayNotifyIconData) == FALSE) 128 | { 129 | MsgBox(L"Failed to register systray icon!", APPNAME L" Error", MB_OK | MB_ICONERROR); 130 | goto Exit; 131 | } 132 | } 133 | 134 | if (RegisterHotKey(NULL, 1, MOD_NOREPEAT, gConfig.PauseKey) == 0) 135 | { 136 | MsgBox(L"Failed to register hotkey! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, GetLastError()); 137 | goto Exit; 138 | } 139 | 140 | DbgPrint(L"Registered hotkey 0x%x.", gConfig.PauseKey); 141 | 142 | runningServer = (gConfig.WebPort != 0); 143 | 144 | if (runningServer) 145 | { 146 | DbgPrint(L"Starting Server!"); 147 | 148 | if (serve_start(gConfig.WebPort)) 149 | { 150 | MsgBox(L"Failed to start webserver!", APPNAME L" Error", MB_OK | MB_ICONERROR); 151 | runningServer = false; 152 | } 153 | 154 | openWelcomePageInBrowser(gConfig.WebPort); 155 | } 156 | 157 | 158 | while (gIsRunning) 159 | { 160 | while (PeekMessageW(&WndMsg, NULL, 0, 0, PM_REMOVE)) 161 | { 162 | if (WndMsg.message == WM_HOTKEY) 163 | { 164 | HandlePauseKeyPress(); 165 | } 166 | 167 | DispatchMessageW(&WndMsg); 168 | } 169 | 170 | if (runningServer && serve_request(gIsPaused)) 171 | HandlePauseKeyPress(); 172 | 173 | Sleep(5); 174 | } 175 | 176 | Exit: 177 | if(runningServer) 178 | serve_stop(); 179 | 180 | return(0); 181 | } 182 | 183 | void HandlePauseKeyPress(void) 184 | { 185 | HANDLE ProcessHandle = NULL; 186 | HANDLE ProcessSnapshot = NULL; 187 | PROCESSENTRY32W ProcessEntry = { sizeof(PROCESSENTRY32W) }; 188 | u32 ProcessId = 0; 189 | 190 | // Either we configured it to pause/un-pause a specified process, or we will pause/un-pause 191 | // the currently in-focus foreground window. 192 | if (wcslen(gConfig.ProcessNameToPause) > 0) 193 | { 194 | if (gIsPaused) 195 | { 196 | // Process currently paused, need to un-pause it. 197 | UnpausePreviouslyPausedProcess(); 198 | } 199 | else 200 | { 201 | // Process needs to be paused. 202 | DbgPrint(L"Pause key pressed. Attempting to pause named process %s...", gConfig.ProcessNameToPause); 203 | ProcessSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); 204 | if (ProcessSnapshot == INVALID_HANDLE_VALUE) 205 | { 206 | MsgBox(L"Failed to create snapshot of running processes! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, GetLastError()); 207 | goto Exit; 208 | } 209 | 210 | if (Process32FirstW(ProcessSnapshot, &ProcessEntry) == FALSE) 211 | { 212 | MsgBox(L"Failed to retrieve list of running processes! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, GetLastError()); 213 | goto Exit; 214 | } 215 | 216 | do 217 | { 218 | if (_wcsicmp(ProcessEntry.szExeFile, gConfig.ProcessNameToPause) == 0) 219 | { 220 | ProcessId = ProcessEntry.th32ProcessID; 221 | DbgPrint(L"Found process %s with PID %d.", ProcessEntry.szExeFile, ProcessId); 222 | break; 223 | } 224 | } while (Process32NextW(ProcessSnapshot, &ProcessEntry)); 225 | 226 | if (ProcessId == 0) 227 | { 228 | DbgPrint(L"Unable to locate any process with the name %s!", gConfig.ProcessNameToPause); 229 | goto Exit; 230 | } 231 | ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); 232 | if (ProcessHandle == NULL) 233 | { 234 | MsgBox(L"Failed to open process %d! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, ProcessId, GetLastError()); 235 | goto Exit; 236 | } 237 | NtSuspendProcess(ProcessHandle); 238 | gIsPaused = TRUE; 239 | gPreviouslyPausedProcessId = ProcessId; 240 | DbgPrint(L"Process paused!"); 241 | } 242 | } 243 | else 244 | { 245 | if (gIsPaused) 246 | { 247 | // Process currently paused, need to un-pause it. 248 | UnpausePreviouslyPausedProcess(); 249 | } 250 | else 251 | { 252 | // Process needs to be paused. 253 | DbgPrint(L"Pause key pressed. Attempting to pause current foreground window..."); 254 | HWND ForegroundWindow = GetForegroundWindow(); 255 | if (ForegroundWindow == NULL) 256 | { 257 | MsgBox(L"Failed to detect foreground window!", APPNAME L" Error", MB_OK | MB_ICONERROR); 258 | goto Exit; 259 | } 260 | GetWindowThreadProcessId(ForegroundWindow, &ProcessId); 261 | if (ProcessId == 0) 262 | { 263 | MsgBox(L"Failed to get PID from foreground window! Error code 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, GetLastError()); 264 | goto Exit; 265 | } 266 | ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, ProcessId); 267 | if (ProcessHandle == NULL) 268 | { 269 | MsgBox(L"Failed to open process %d! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, ProcessId, GetLastError()); 270 | goto Exit; 271 | } 272 | NtSuspendProcess(ProcessHandle); 273 | gIsPaused = TRUE; 274 | gPreviouslyPausedProcessId = ProcessId; 275 | DbgPrint(L"Process paused!"); 276 | } 277 | } 278 | 279 | Exit: 280 | if (ProcessSnapshot && ProcessSnapshot != INVALID_HANDLE_VALUE) 281 | { 282 | CloseHandle(ProcessSnapshot); 283 | } 284 | if (ProcessHandle) 285 | { 286 | CloseHandle(ProcessHandle); 287 | } 288 | } 289 | 290 | void UnpausePreviouslyPausedProcess(void) 291 | { 292 | HANDLE ProcessHandle = NULL; 293 | DbgPrint(L"Pause key pressed. Attempting to un-pause previously paused PID %d.", gPreviouslyPausedProcessId); 294 | 295 | ProcessHandle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, gPreviouslyPausedProcessId); 296 | if (ProcessHandle == NULL) 297 | { 298 | // Maybe the previously paused process was killed, no longer exists? 299 | MsgBox(L"Failed to open process %d! Error 0x%08lx", APPNAME L" Error", MB_OK | MB_ICONERROR, gPreviouslyPausedProcessId, GetLastError()); 300 | } 301 | else 302 | { 303 | NtResumeProcess(ProcessHandle); 304 | DbgPrint(L"Un-pause successful."); 305 | } 306 | gIsPaused = FALSE; 307 | gPreviouslyPausedProcessId = 0; 308 | if (ProcessHandle) 309 | { 310 | CloseHandle(ProcessHandle); 311 | } 312 | } 313 | 314 | u32 LoadRegistrySettings(void) 315 | { 316 | u32 Result = ERROR_SUCCESS; 317 | HKEY RegKey = NULL; 318 | 319 | typedef struct _REG_SETTING 320 | { 321 | wchar_t* Name; 322 | u32 DataType; 323 | void* DefaultValue; 324 | void* MinValue; 325 | void* MaxValue; 326 | void* Destination; 327 | } REG_SETTING; 328 | 329 | REG_SETTING Settings[] = { 330 | { // Debug should always be the first setting loaded. 331 | .Name = L"Debug", 332 | .DataType = REG_DWORD, 333 | .DefaultValue = &(u32) { 0 }, 334 | .MinValue = &(u32) { 0 }, 335 | .MaxValue = &(u32) { 1 }, 336 | .Destination = &gConfig.Debug 337 | }, 338 | { 339 | .Name = L"TrayIcon", 340 | .DataType = REG_DWORD, 341 | .DefaultValue = &(u32) { 1 }, 342 | .MinValue = &(u32) { 0 }, 343 | .MaxValue = &(u32) { 1 }, 344 | .Destination = &gConfig.TrayIcon 345 | }, 346 | { 347 | .Name = L"PauseKey", 348 | .DataType = REG_DWORD, 349 | .DefaultValue = &(u32) { VK_PAUSE }, 350 | .MinValue = &(u32) { 1 }, 351 | .MaxValue = &(u32) { 0xFE }, 352 | .Destination = &gConfig.PauseKey 353 | }, 354 | { 355 | .Name = L"ProcessNameToPause", 356 | .DataType = REG_SZ, 357 | .DefaultValue = &(wchar_t[128]) { L"" }, 358 | .MinValue = NULL, 359 | .MaxValue = NULL, 360 | .Destination = &gConfig.ProcessNameToPause 361 | }, 362 | { 363 | .Name = L"WebPort", 364 | .DataType = REG_DWORD, 365 | .DefaultValue = &(u32) { 0 }, 366 | .MinValue = &(u32) { 0 }, 367 | .MaxValue = &(u32) { 65535 }, 368 | .Destination = &gConfig.WebPort 369 | }, 370 | }; 371 | 372 | Result = RegCreateKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\" APPNAME, 0, NULL, 0, KEY_ALL_ACCESS, NULL, &RegKey, NULL); 373 | if (Result != ERROR_SUCCESS) 374 | { 375 | MsgBox(L"Failed to load registry settings!\nError: 0x%08lx", L"Error", MB_OK | MB_ICONERROR, Result); 376 | goto Exit; 377 | } 378 | 379 | for (u32 s = 0; s < _countof(Settings); s++) 380 | { 381 | switch (Settings[s].DataType) 382 | { 383 | case REG_DWORD: 384 | { 385 | u32 BytesRead = sizeof(u32); 386 | Result = RegGetValueW( 387 | RegKey, 388 | NULL, 389 | Settings[s].Name, 390 | RRF_RT_DWORD, 391 | NULL, 392 | Settings[s].Destination, 393 | &BytesRead); 394 | if (Result != ERROR_SUCCESS) 395 | { 396 | if (Result == ERROR_FILE_NOT_FOUND) 397 | { 398 | Result = ERROR_SUCCESS; 399 | *(u32*)Settings[s].Destination = *(u32*)Settings[s].DefaultValue; 400 | } 401 | else 402 | { 403 | MsgBox(L"Failed to load registry value '%s'!\nError: 0x%08lx", L"Error", MB_OK | MB_ICONERROR, Settings[s].Name, Result); 404 | goto Exit; 405 | } 406 | } 407 | else 408 | { 409 | if (*(u32*)Settings[s].Destination < *(u32*)Settings[s].MinValue || *(u32*)Settings[s].Destination > *(u32*)Settings[s].MaxValue) 410 | { 411 | MsgBox(L"Registry value '%s' was out of range! Using default of %d.", L"Error", MB_OK | MB_ICONWARNING, Settings[s].Name, *(u32*)Settings[s].DefaultValue); 412 | *(u32*)Settings[s].Destination = *(u32*)Settings[s].DefaultValue; 413 | } 414 | } 415 | 416 | // Enable the debug console as early as possible if configured. 417 | // This is so the debug console can report on the other registry settings. 418 | if (Settings[s].Destination == &gConfig.Debug) 419 | { 420 | if (gConfig.Debug) 421 | { 422 | if (AllocConsole() == FALSE) 423 | { 424 | MsgBox(L"Failed to allocate debug console!\nError: 0x%08lx", L"Error", MB_OK | MB_ICONERROR, GetLastError()); 425 | goto Exit; 426 | } 427 | gDbgConsole = GetStdHandle(STD_OUTPUT_HANDLE); 428 | if (gDbgConsole == INVALID_HANDLE_VALUE) 429 | { 430 | MsgBox(L"Failed to get stdout debug console handle!\nError: 0x%08lx", L"Error", MB_OK | MB_ICONERROR, GetLastError()); 431 | goto Exit; 432 | } 433 | DbgPrint(L"%s version %s.", APPNAME, VERSION); 434 | DbgPrint(L"To disable this debug console, delete the 'Debug' reg setting at HKCU\\SOFTWARE\\%s", APPNAME); 435 | } 436 | } 437 | 438 | DbgPrint(L"Using value 0n%d (0x%x) for registry setting '%s'.", *(u32*)Settings[s].Destination, *(u32*)Settings[s].Destination, Settings[s].Name); 439 | 440 | break; 441 | } 442 | case REG_SZ: 443 | { 444 | u32 BytesRead = 128 * sizeof(wchar_t); 445 | Result = RegGetValueW( 446 | RegKey, 447 | NULL, 448 | Settings[s].Name, 449 | RRF_RT_REG_SZ, 450 | NULL, 451 | Settings[s].Destination, 452 | &BytesRead); 453 | if (Result != ERROR_SUCCESS) 454 | { 455 | if (Result == ERROR_FILE_NOT_FOUND) 456 | { 457 | Result = ERROR_SUCCESS; 458 | } 459 | else 460 | { 461 | MsgBox(L"Failed to load registry value '%s'!\nError: 0x%08lx", L"Error", MB_OK | MB_ICONERROR, Settings[s].Name, Result); 462 | goto Exit; 463 | } 464 | } 465 | 466 | DbgPrint(L"Using value '%s' for registry setting '%s'.", Settings[s].Destination, Settings[s].Name); 467 | 468 | break; 469 | } 470 | default: 471 | { 472 | MsgBox(L"Registry value '%s' was not of the expected data type!", L"Error", MB_OK | MB_ICONERROR, Settings[s].Name); 473 | goto Exit; 474 | } 475 | } 476 | } 477 | 478 | Exit: 479 | if (RegKey != NULL) 480 | { 481 | RegCloseKey(RegKey); 482 | } 483 | return(Result); 484 | } 485 | 486 | void MsgBox(const wchar_t* Message, const wchar_t* Caption, u32 Flags, ...) 487 | { 488 | wchar_t FormattedMessage[1024] = { 0 }; 489 | va_list Args = NULL; 490 | 491 | va_start(Args, Flags); 492 | _vsnwprintf_s(FormattedMessage, _countof(FormattedMessage), _TRUNCATE, Message, Args); 493 | va_end(Args); 494 | DbgPrint(FormattedMessage); 495 | MessageBoxW(NULL, FormattedMessage, Caption, Flags); 496 | } 497 | 498 | void DbgPrint(const wchar_t* Message, ...) 499 | { 500 | if (gConfig.Debug == FALSE || gDbgConsole == INVALID_HANDLE_VALUE) 501 | { 502 | return; 503 | } 504 | 505 | wchar_t FormattedMessage[1024] = { 0 }; 506 | u32 MsgLen = 0; 507 | wchar_t TimeString[64] = { 0 }; 508 | SYSTEMTIME Time; 509 | va_list Args = NULL; 510 | 511 | va_start(Args, Message); 512 | _vsnwprintf_s(FormattedMessage, _countof(FormattedMessage), _TRUNCATE, Message, Args); 513 | va_end(Args); 514 | 515 | MsgLen = (u32)wcslen(FormattedMessage); 516 | FormattedMessage[MsgLen] = '\n'; 517 | FormattedMessage[MsgLen + 1] = '\0'; 518 | 519 | GetLocalTime(&Time); 520 | _snwprintf_s(TimeString, _countof(TimeString), _TRUNCATE, L"[%02d.%02d.%04d %02d.%02d.%02d.%03d] ", Time.wMonth, Time.wDay, Time.wYear, Time.wHour, Time.wMinute, Time.wSecond, Time.wMilliseconds); 521 | WriteConsoleW(gDbgConsole, TimeString, (u32)wcslen(TimeString), NULL, NULL); 522 | WriteConsoleW(gDbgConsole, FormattedMessage, (u32)wcslen(FormattedMessage), NULL, NULL); 523 | } 524 | 525 | LRESULT CALLBACK SysTrayCallback(_In_ HWND Window, _In_ UINT Message, _In_ WPARAM WParam, _In_ LPARAM LParam) 526 | { 527 | LRESULT Result = 0; 528 | static BOOL QuitMessageBoxIsShowing = FALSE; 529 | 530 | switch (Message) 531 | { 532 | case WM_TRAYICON: 533 | { 534 | if (!QuitMessageBoxIsShowing && (LParam == WM_LBUTTONDOWN || LParam == WM_RBUTTONDOWN || LParam == WM_MBUTTONDOWN)) 535 | { 536 | QuitMessageBoxIsShowing = TRUE; 537 | if (MessageBox(Window, L"Quit " APPNAME L"?", L"Are you sure?", MB_YESNO | MB_ICONQUESTION | MB_SYSTEMMODAL) == IDYES) 538 | { 539 | Shell_NotifyIconW(NIM_DELETE, &gTrayNotifyIconData); 540 | gIsRunning = FALSE; 541 | PostQuitMessage(0); 542 | } 543 | else 544 | { 545 | QuitMessageBoxIsShowing = FALSE; 546 | } 547 | } 548 | break; 549 | } 550 | default: 551 | { 552 | Result = DefWindowProcW(Window, Message, WParam, LParam); 553 | break; 554 | } 555 | } 556 | return(Result); 557 | } -------------------------------------------------------------------------------- /Main.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #pragma warning(disable:4820) // padding in structures 4 | 5 | #define VERSION L"1.1.5" 6 | #define APPNAME L"UniversalPauseButton" 7 | 8 | // The Lord's data types. 9 | typedef unsigned char u8; 10 | typedef unsigned short u16; 11 | typedef unsigned long u32; 12 | typedef unsigned long long u64; 13 | typedef signed char i8; 14 | typedef signed short i16; 15 | typedef signed long i32; 16 | typedef signed long long i64; 17 | typedef float f32; 18 | typedef double f64; 19 | 20 | // WARNING: Undocumented Win32 API functions! 21 | typedef LONG(NTAPI* _NtSuspendProcess) (IN HANDLE ProcessHandle); 22 | typedef LONG(NTAPI* _NtResumeProcess) (IN HANDLE ProcessHandle); 23 | typedef HWND(NTAPI* _HungWindowFromGhostWindow) (IN HWND GhostWindowHandle); 24 | 25 | _NtSuspendProcess NtSuspendProcess; 26 | _NtResumeProcess NtResumeProcess; 27 | _HungWindowFromGhostWindow HungWindowFromGhostWindow; 28 | 29 | // Configurable registry settings. 30 | typedef struct _CONFIG 31 | { 32 | u32 Debug; 33 | u32 TrayIcon; 34 | u32 PauseKey; 35 | wchar_t ProcessNameToPause[128]; 36 | u32 WebPort; 37 | } CONFIG; 38 | 39 | // Function declarations. 40 | u32 LoadRegistrySettings(void); 41 | void MsgBox(const wchar_t* Message, const wchar_t* Caption, u32 Flags, ...); 42 | void DbgPrint(const wchar_t* Message, ...); 43 | LRESULT CALLBACK SysTrayCallback(_In_ HWND Window, _In_ UINT Message, _In_ WPARAM WParam, _In_ LPARAM LParam); 44 | void HandlePauseKeyPress(void); 45 | void UnpausePreviouslyPausedProcess(void); -------------------------------------------------------------------------------- /NiceLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanries/UniversalPauseButton/469276b4c58a1193db0c793426ac148244f1dbd2/NiceLogo.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Pause Button 2 | ------------------------ 3 | 4 | Universal Pause Button Pause. 5 | 6 | GitHub all releases 7 | 8 | I like to play video games. I also have a significant other, and she often walks into the room to talk to me while I'm playing video games. I would like to pause the game so that I can give her my undivided attention while she's talking to me, but a lot of games (particularly single-player ones) have these "un-pausable" cut scenes or other areas of the game where the normal pause functionality doesn't work. This annoys both me and her, because I'm supposed to be the computer expert, and it looks like I don't even know how to pause my stupid video game. So usually what ends up happening is either I skip the cut scene and miss the story, or upset my SO by not paying attention to her as well as I should. 9 | 10 | So that is why I wrote Universal Pause Button. It's a very simple Windows desktop app that sits in the system tray. Its icon resembles a pause button. When you hit the actual Pause key (also known as Break) on your keyboard, (you can also define your own custom pause key in the registry,) the program determines which window is currently in the foreground (i.e. your game's window,) and pauses it. No matter where you are in the game. Even in the middle of one of those pesky cutscenes that would otherwise be un-pausable. When you press the key again, the game will un-pause. You can also specify a process by name in the registry to pause and un-pause that process, regardless of whatever the foreground window may be. 11 | 12 | I first wrote this app in 2015, but mostly forgot about it since then. But it has always been my most popular app on Github, so today in 2023 I've come back and rewritten the app from scratch, 8 years later, adding several improvements and requested features along the way. 13 | 14 | Back in 2015, I used this app with The Witcher 3, and it worked great. 15 | 16 | Today in 2023, I used this app with Baldur's Gate 3 and it is still working great. 17 | 18 | However, your mileage may vary. It does not work with every app/game. "Pausing" processes is something that usually only debuggers do, and not every process will react the same way to being paused. Pausing processes may lead to race conditions among the threads of that process, but like I said, testing has been very positive for me so far. I've already gotten great value out of the program, as there are lots of cut scenes in The Witcher 3 that I don't want to skip. The main use case for this app is single player games, as pausing your multi-player game will undoubtedly just get you kicked from the session, as if your computer had just crashed or hung. So don't use it in multi-player games. It also works on applications that are not games at all. 19 | 20 | There are some new registry settings you should be aware of: 21 | 22 | ![Registry](https://github.com/ryanries/UniversalPauseButton/blob/master/registry.png) 23 | 24 | All registry settings are in HKCU\Software\UniversalPauseButton. 25 | 26 | **Debug** 27 | 28 | Type: DWORD 29 | 30 | Default: 0 31 | 32 | Minimum: 0 33 | 34 | Maximum: 1 35 | 36 | 37 | By enabling this Debug setting, the app spawns a debug console that allows you to see all the internal messages generated by UniversalPauseButton. It is mostly intended just for use by me during debugging. But you can turn it on if you are having issues with the app and want to troubleshoot. 38 | 39 | ![Debug Console](https://github.com/ryanries/UniversalPauseButton/blob/master/debugconsole.png) 40 | 41 | **PauseKey** 42 | 43 | Type: DWORD 44 | 45 | Default: 0x13 46 | 47 | Minimum: 0x1 48 | 49 | Maximum: 0xFE 50 | 51 | 52 | This is the customizable Pause key. By default, it is 0x13, which is the virtual key code for the Pause/Break key on your keyboard. For a list of virtual key codes so that you can map this to another key, use the virtual key codes defined in the Windows documentation here: https://learn.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes 53 | 54 | **TrayIcon** 55 | 56 | Type: DWORD 57 | 58 | Default: 1 59 | 60 | Minimum: 0 61 | 62 | Maximum: 1 63 | 64 | 65 | If TrayIcon is enabled, which it is by default, there will be a small system tray icon that looks like a pause button. When you click that icon it will give you a dialog box asking if you want to exit the app. If you turn TrayIcon off, there will be no tray icon, no taskbar icon, no nothing. The app will be completely invisible and in the background. I added this feature because someone requested it, saying that they use a custom shell other than Explorer.exe, and since they didn't have a system tray in their shell, the app wouldn't work. Well, here you go, Mr. Alternative Shell. 66 | 67 | **ProcessNameToPause** 68 | 69 | Type: REG_SZ (String) 70 | 71 | Default: "" (Empty string) 72 | 73 | Minimum: n/a 74 | 75 | Maximum: n/a 76 | 77 | 78 | If ProcessNameToPause is defined, then the app will only pause that process by name, regardless of foreground window. E.g., notepad.exe or mycoolgame.exe. Include the .exe file extension. It expects process name, not Window text. WARNING: In case there are multiple processes with the same name, only the first instance found will be paused. Please don't try dumb things like trying to pause svchost.exe or lsass.exe or csrss.exe... 79 | 80 | **WebPort** 81 | 82 | Type: REG_SZ (String) 83 | 84 | Default: "" (Empty string) 85 | 86 | Minimum: n/a 87 | 88 | Maximum: n/a 89 | 90 | 91 | If WebPort is defined, then the app will open up a port for which another device, such as a phone can connect to. This can be useful for games which take control of the keyboard and do not allow key presses to flow to the Universal Pause Button app. 92 | 93 | Type: DWORD 94 | 95 | Default: 0x0 (Disabled) 96 | 97 | Minimum: 0x0 98 | 99 | Maximum: 0xFFFF 100 | 101 | As always, please try it out, and let me know if you find any bugs or have any feature requests. 102 | 103 | Thanks! -------------------------------------------------------------------------------- /Server.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "Server.h" 11 | #include "resource.h" 12 | 13 | //for debugging msgs 14 | #include "Main.h" 15 | 16 | #pragma comment(lib, "iphlpapi.lib") 17 | #pragma comment(lib, "ws2_32.lib") 18 | 19 | 20 | 21 | #define BUFFER_SIZE 4096 22 | 23 | // Global variables 24 | bool serverRunning = true; 25 | HANDLE serverThreadHandle; 26 | 27 | static SOCKET serverSocket = INVALID_SOCKET; 28 | 29 | // Response when app is paused 30 | const char* pauseResponse = "1"; 31 | 32 | // Response when app is unpaused 33 | const char* unpauseResponse = "0"; 34 | 35 | const char* htmlResponseHeader = 36 | "HTTP/1.1 200 OK\r\n" 37 | "Content-Type: text/html\r\n" 38 | "Connection: close\r\n" 39 | "\r\n"; 40 | 41 | const char* plainResponseHeader = 42 | "HTTP/1.1 200 OK\r\n" 43 | "Content-Type: text/plain\r\n" 44 | "Connection: close\r\n" 45 | "\r\n"; 46 | 47 | //main web page 48 | char* mainPage = NULL; 49 | 50 | //welcome web page 51 | char* welcomePage = NULL; 52 | 53 | int webPort; 54 | 55 | // Global buffer for storing the Unicode long string (WCHAR array) 56 | #define MAX_BUFFER_SIZE 256 57 | WCHAR g_unicodeBuffer[MAX_BUFFER_SIZE]; 58 | 59 | // Default Unicode string to return on failure (empty string) 60 | WCHAR g_defaultUnicodeString[] = L""; 61 | 62 | // Function to convert ASCII to Unicode long (WCHAR) format using the global buffer. 63 | // Returns: 64 | // Pointer to the converted Unicode string (g_unicodeBuffer) on success. 65 | // Pointer to the default Unicode string (g_defaultUnicodeString) on failure. 66 | WCHAR* AsciiToUnicodeLong(const char* asciiString) { 67 | size_t asciiLength = 0; 68 | 69 | // Check if the input string is NULL 70 | if (asciiString == NULL) { 71 | return g_defaultUnicodeString; 72 | } 73 | 74 | // Calculate the length of the ASCII string safely 75 | while (asciiString[asciiLength] != '\0') { 76 | asciiLength++; 77 | if (asciiLength >= MAX_BUFFER_SIZE) { // Prevent buffer overflow during length calculation 78 | return g_defaultUnicodeString; // String is too long 79 | } 80 | } 81 | 82 | // Check if the ASCII string is too long to fit in the Unicode buffer 83 | if (asciiLength >= MAX_BUFFER_SIZE) { 84 | return g_defaultUnicodeString; // String is too long to convert safely 85 | } 86 | 87 | // Clear the global buffer before use 88 | memset(g_unicodeBuffer, 0, sizeof(WCHAR) * MAX_BUFFER_SIZE); 89 | 90 | // Perform the conversion using MultiByteToWideChar 91 | int result = MultiByteToWideChar(CP_ACP, 0, asciiString, -1, g_unicodeBuffer, (int)MAX_BUFFER_SIZE); 92 | 93 | if (result == 0) { 94 | // Conversion failed. 95 | DWORD error = GetLastError(); 96 | fprintf(stderr, "Error in MultiByteToWideChar: %lu\n", error); 97 | return g_defaultUnicodeString; 98 | } 99 | 100 | return g_unicodeBuffer; // Return pointer to the global buffer 101 | } 102 | 103 | 104 | /** 105 | * Writes formatted text to a buffer, updating the buffer pointer and remaining character count. 106 | * Similar to sprintf, but prevents buffer overflow. 107 | * 108 | * @param buf Pointer to the buffer pointer. Will be updated to point to the end of written text. 109 | * @param rem_chars Pointer to the remaining character count. Will be decreased by chars written. 110 | * @param format Format string, similar to printf format. 111 | * @param ... Additional arguments for the format string. 112 | * 113 | * @return Number of characters written (not including null terminator), or -1 on error. 114 | */ 115 | static int write_to_buf(char** buf, int* rem_chars, const char* format, ...) { 116 | if (!buf || !*buf || !rem_chars || *rem_chars <= 0 || !format) { 117 | return -1; // Invalid parameters 118 | } 119 | 120 | va_list args; 121 | va_start(args, format); 122 | 123 | // Use vsnprintf to safely format and determine length 124 | int chars_written = vsnprintf(NULL, 0, format, args); 125 | va_end(args); 126 | 127 | if (chars_written < 0) { 128 | return -1; // Format error 129 | } 130 | 131 | // If not enough space, limit to available space (minus 1 for null terminator) 132 | int space_to_use = (chars_written < *rem_chars - 1) ? chars_written : *rem_chars - 1; 133 | 134 | // Reset va_list for actual writing 135 | va_start(args, format); 136 | int actual_written = vsnprintf(*buf, space_to_use + 1, format, args); 137 | va_end(args); 138 | 139 | if (actual_written < 0) { 140 | return -1; // Formatting error 141 | } 142 | 143 | // Update the buffer pointer and remaining characters 144 | *buf += actual_written; 145 | *rem_chars -= actual_written; 146 | 147 | return actual_written; 148 | } 149 | 150 | #define MAX_CONNECTION_INFO_BUFFER 2048 151 | 152 | char conn_info_buffer[MAX_CONNECTION_INFO_BUFFER]; 153 | 154 | char* get_connection_info(int port) { 155 | // Initialize Winsock 156 | WSADATA wsaData; 157 | if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { 158 | DbgPrint(L"WSAStartup failed\n"); 159 | return NULL; 160 | } 161 | 162 | ULONG outBufLen = 15000; // Initial buffer size 163 | PIP_ADAPTER_ADDRESSES pAddresses = (IP_ADAPTER_ADDRESSES*)malloc(outBufLen); 164 | if (pAddresses == NULL) { 165 | DbgPrint(L"Memory allocation failed\n"); 166 | WSACleanup(); 167 | return NULL; 168 | } 169 | 170 | // Make the first call to GetAdaptersAddresses 171 | ULONG result = GetAdaptersAddresses( 172 | AF_UNSPEC, // Both IPv4 and IPv6 173 | GAA_FLAG_INCLUDE_PREFIX, // Include prefixes 174 | NULL, 175 | pAddresses, 176 | &outBufLen 177 | ); 178 | 179 | // If buffer was too small, reallocate with the required size 180 | if (result == ERROR_BUFFER_OVERFLOW) { 181 | free(pAddresses); 182 | pAddresses = (IP_ADAPTER_ADDRESSES*)malloc(outBufLen); 183 | if (pAddresses == NULL) { 184 | DbgPrint(L"Memory allocation failed\n"); 185 | WSACleanup(); 186 | return NULL; 187 | } 188 | 189 | result = GetAdaptersAddresses( 190 | AF_UNSPEC, 191 | GAA_FLAG_INCLUDE_PREFIX, 192 | NULL, 193 | pAddresses, 194 | &outBufLen 195 | ); 196 | } 197 | 198 | if (result != NO_ERROR) { 199 | DbgPrint(L"GetAdaptersAddresses failed with error code %lu\n", result); 200 | free(pAddresses); 201 | WSACleanup(); 202 | return NULL; 203 | } 204 | 205 | int rem = MAX_CONNECTION_INFO_BUFFER; 206 | char* buf = conn_info_buffer; 207 | write_to_buf(&buf, &rem, "%d", port); 208 | 209 | // Iterate through all adapters 210 | PIP_ADAPTER_ADDRESSES pCurrAddresses = pAddresses; 211 | while (pCurrAddresses) { 212 | // Skip virtual adapters and adapters that are not operational 213 | if (pCurrAddresses->IfType == IF_TYPE_SOFTWARE_LOOPBACK || // Loopback interface 214 | pCurrAddresses->OperStatus != IfOperStatusUp || // Interface not up 215 | //pCurrAddresses->ConnectionType == NET_IF_CONNECTION_DEDICATED || // Check for certain virtual types 216 | wcsstr(pCurrAddresses->Description, L"VPN") || // VPN adapters 217 | wcsstr(pCurrAddresses->Description, L"Hyper-V") || // Hyper-V adapters 218 | wcsstr(pCurrAddresses->Description, L"VMware") || // VMware adapters 219 | wcsstr(pCurrAddresses->Description, L"VirtualBox")) { // VirtualBox adapters 220 | 221 | DbgPrint(L"Skipping adapter: %s\n", pCurrAddresses->FriendlyName); 222 | pCurrAddresses = pCurrAddresses->Next; 223 | continue; 224 | } 225 | 226 | DbgPrint(L"\nAdapter Name: %s\n", pCurrAddresses->FriendlyName); 227 | DbgPrint(L"Description: %s\n", pCurrAddresses->Description); 228 | 229 | bool not_added_ip4 = true; 230 | for (int i = 0; i < 2; i++) { 231 | bool is_adding_ip4 = (i == 0); 232 | 233 | // Print all unicast addresses (IPv4 and IPv6) 234 | PIP_ADAPTER_UNICAST_ADDRESS pUnicast = pCurrAddresses->FirstUnicastAddress; 235 | while (pUnicast != NULL) { 236 | char ipStr[INET6_ADDRSTRLEN] = { 0 }; 237 | 238 | // Get the address family 239 | if (is_adding_ip4 && pUnicast->Address.lpSockaddr->sa_family == AF_INET) { 240 | // IPv4 241 | struct sockaddr_in* sa_in = (struct sockaddr_in*)pUnicast->Address.lpSockaddr; 242 | inet_ntop(AF_INET, &(sa_in->sin_addr), ipStr, INET_ADDRSTRLEN); 243 | 244 | // Skip localhost (127.0.0.1) and APIPA addresses (169.254.x.x) 245 | if (strncmp(ipStr, "127.", 4) != 0 && strncmp(ipStr, "169.254.", 8) != 0) { 246 | DbgPrint(L" IPv4 Address: %s\n", AsciiToUnicodeLong(ipStr)); 247 | write_to_buf(&buf, &rem, ",%s", ipStr); 248 | 249 | not_added_ip4 = false; 250 | } 251 | else { 252 | DbgPrint(L" Skipping IPv4 Address: %s\n", AsciiToUnicodeLong(ipStr)); 253 | } 254 | } 255 | else if (!is_adding_ip4 && pUnicast->Address.lpSockaddr->sa_family == AF_INET6 && not_added_ip4) { 256 | // IPv6 257 | struct sockaddr_in6* sa_in6 = (struct sockaddr_in6*)pUnicast->Address.lpSockaddr; 258 | inet_ntop(AF_INET6, &(sa_in6->sin6_addr), ipStr, INET6_ADDRSTRLEN); 259 | 260 | // Skip localhost (::1) and link-local addresses (fe80::) 261 | if (strcmp(ipStr, "::1") != 0 && strncmp(ipStr, "fe80:", 5) != 0) { 262 | DbgPrint(L" IPv6 Address: %s\n", AsciiToUnicodeLong(ipStr)); 263 | write_to_buf(&buf, &rem, ",[%s]", ipStr); 264 | } 265 | else { 266 | DbgPrint(L" Skipping IPv6 Address: %s\n", AsciiToUnicodeLong(ipStr)); 267 | } 268 | } 269 | 270 | pUnicast = pUnicast->Next; 271 | } 272 | 273 | pCurrAddresses = pCurrAddresses->Next; 274 | } 275 | } 276 | 277 | free(pAddresses); 278 | WSACleanup(); 279 | return conn_info_buffer; 280 | } 281 | 282 | 283 | static void send_str(SOCKET clientSocket, const char* txt) 284 | { 285 | send(clientSocket, txt, (int)strlen(txt), 0); 286 | } 287 | 288 | //#define send_str(clientSocket, txt) {send(clientSocket, txt, (int)strlen(txt), 0);} 289 | 290 | //run this in main loop to accept requests from a client 291 | //isPaused -- status of application, true if already paused 292 | //return -- true if paused status *changed* (ie. went from unpause to pause or pause to unpause 293 | bool serve_request(bool isPaused) { 294 | SOCKET clientSocket; 295 | struct sockaddr_in clientAddr; 296 | int clientAddrSize = sizeof(clientAddr); 297 | 298 | bool pause_status_changed = false; 299 | 300 | // Accept a client socket (non-blocking) 301 | clientSocket = accept(serverSocket, (struct sockaddr*)&clientAddr, &clientAddrSize); 302 | 303 | if (clientSocket != INVALID_SOCKET) { 304 | char buffer[BUFFER_SIZE]; 305 | int bytesReceived = recv(clientSocket, buffer, BUFFER_SIZE - 1, 0); 306 | 307 | if (bytesReceived > 0) { 308 | buffer[bytesReceived] = '\0'; 309 | 310 | // Check if this is a pause command 311 | if (strstr(buffer, "POST /pause") != NULL) { 312 | send_str(clientSocket, plainResponseHeader); 313 | isPaused = !isPaused; 314 | 315 | send_str(clientSocket, isPaused ? pauseResponse : unpauseResponse); 316 | 317 | pause_status_changed = true; 318 | } 319 | else if (strstr(buffer, "GET /connection-info") != NULL) { 320 | send_str(clientSocket, plainResponseHeader); 321 | isPaused = !isPaused; 322 | 323 | char *connection_info = get_connection_info(webPort); 324 | 325 | if (connection_info == NULL) { 326 | DbgPrint(L"Unable to get connection_info\n"); 327 | send_str(clientSocket, "ERROR"); 328 | } 329 | else 330 | send_str(clientSocket, connection_info); 331 | } 332 | else if (strstr(buffer, "GET /welcome.htm") != NULL) { 333 | send_str(clientSocket, htmlResponseHeader); 334 | send_str(clientSocket, welcomePage); 335 | } 336 | else { 337 | send_str(clientSocket, htmlResponseHeader); 338 | send_str(clientSocket, mainPage); 339 | } 340 | } 341 | 342 | closesocket(clientSocket); 343 | } 344 | else { 345 | // Check if there was a real error or just no connection available 346 | int err = WSAGetLastError(); 347 | if (err != WSAEWOULDBLOCK) { 348 | DbgPrint(L"Accept failed: %d\n", err); 349 | } 350 | } 351 | 352 | return pause_status_changed; 353 | } 354 | 355 | //returns an html page from the rc file 356 | // id - (IDR_HTML1,IDR_HTML2...) 357 | static char* read_html_page(int id) { 358 | HMODULE hModule = GetModuleHandle(NULL); 359 | 360 | // Load the resource 361 | HRSRC hResource = FindResource(hModule, MAKEINTRESOURCE(id), RT_HTML); 362 | if (hResource == NULL) { 363 | DbgPrint(L"Failed to find resource. Error: %d\n", GetLastError()); 364 | return NULL; 365 | } 366 | 367 | // Load the resource data 368 | HGLOBAL hGlobal = LoadResource(hModule, hResource); 369 | if (hGlobal == NULL) { 370 | DbgPrint(L"Failed to load resource. Error: %d\n", GetLastError()); 371 | return NULL; 372 | } 373 | 374 | // Get a pointer to the resource data 375 | // This is read-only access 376 | const void* pData = LockResource(hGlobal); 377 | if (pData == NULL) { 378 | DbgPrint(L"Failed to lock resource. Error: %d\n", GetLastError()); 379 | return NULL; 380 | } 381 | 382 | // Get the size of the resource 383 | DWORD dwSize = SizeofResource(hModule, hResource); 384 | 385 | char* cData = malloc(dwSize + sizeof(char)); //we want to null terminate the html page, so we need an extra char 386 | 387 | if (cData == NULL) { 388 | DbgPrint(L"Failed to allocate memory, wanted %d bytes", dwSize + sizeof(char)); 389 | return NULL; 390 | } 391 | 392 | memcpy(cData, pData, dwSize); 393 | cData[dwSize] = '\0'; 394 | 395 | FreeResource(hGlobal); 396 | 397 | 398 | return cData; 399 | } 400 | 401 | 402 | static int read_resources(void) { 403 | if (!(mainPage = read_html_page(IDR_HTML1))) 404 | return 1; 405 | 406 | if (!(welcomePage = read_html_page(IDR_HTML2))) 407 | return 1; 408 | 409 | return 0; 410 | 411 | } 412 | 413 | int serve_start(int port) { 414 | webPort = port; 415 | 416 | if (read_resources()) { 417 | DbgPrint(L"Read resources failed.\n"); 418 | return 1; 419 | } 420 | 421 | WSADATA wsaData; 422 | struct addrinfo* result = NULL, hints; 423 | int iResult; 424 | 425 | // Initialize Winsock 426 | iResult = WSAStartup(MAKEWORD(2, 2), &wsaData); 427 | if (iResult != 0) { 428 | DbgPrint(L"WSAStartup failed: %d\n", iResult); 429 | return 1; 430 | } 431 | 432 | ZeroMemory(&hints, sizeof(hints)); 433 | hints.ai_family = AF_INET; 434 | hints.ai_socktype = SOCK_STREAM; 435 | hints.ai_protocol = IPPROTO_TCP; 436 | hints.ai_flags = AI_PASSIVE; 437 | 438 | char s_port[10]; 439 | 440 | sprintf_s(s_port, 10, "%d", port); 441 | 442 | // Resolve the server address and port 443 | iResult = getaddrinfo(NULL, s_port, &hints, &result); 444 | if (iResult != 0) { 445 | DbgPrint(L"getaddrinfo failed: %d\n", iResult); 446 | WSACleanup(); 447 | return 1; 448 | } 449 | 450 | // Create a socket for the server 451 | serverSocket = socket(result->ai_family, result->ai_socktype, result->ai_protocol); 452 | if (serverSocket == INVALID_SOCKET) { 453 | DbgPrint(L"Error creating socket: %d\n", WSAGetLastError()); 454 | freeaddrinfo(result); 455 | WSACleanup(); 456 | return 1; 457 | } 458 | 459 | // Bind the socket 460 | iResult = bind(serverSocket, result->ai_addr, (int)result->ai_addrlen); 461 | if (iResult == SOCKET_ERROR) { 462 | DbgPrint(L"Bind failed: %d\n", WSAGetLastError()); 463 | closesocket(serverSocket); 464 | freeaddrinfo(result); 465 | WSACleanup(); 466 | return 1; 467 | } 468 | 469 | freeaddrinfo(result); 470 | 471 | // Start listening for client connections 472 | if (listen(serverSocket, SOMAXCONN) == SOCKET_ERROR) { 473 | DbgPrint(L"Listen failed: %d\n", WSAGetLastError()); 474 | closesocket(serverSocket); 475 | WSACleanup(); 476 | return 1; 477 | } 478 | 479 | DbgPrint(L"Web server started. Open your browser and navigate to http://localhost:%d\n", port); 480 | 481 | // Set up non-blocking socket mode for accept 482 | u_long mode = 1; // 1 = non-blocking 483 | ioctlsocket(serverSocket, FIONBIO, &mode); 484 | 485 | return 0; 486 | } 487 | 488 | int serve_stop(void) { 489 | if (serverRunning) { 490 | // Shutdown the server thread 491 | serverRunning = false; 492 | 493 | // Wait for server thread to finish (with timeout) 494 | WaitForSingleObject(serverThreadHandle, 5000); 495 | CloseHandle(serverThreadHandle); 496 | 497 | // Clean up 498 | closesocket(serverSocket); 499 | WSACleanup(); 500 | DbgPrint(L"Web server thread exiting cleanly\n"); 501 | return 0; 502 | } 503 | 504 | return 0; 505 | } 506 | 507 | #include 508 | #include 509 | #include 510 | 511 | /** 512 | * Opens the default web browser and navigates to http://localhost:/welcome 513 | * 514 | * @param port The port number to connect to 515 | * @return 0 on success, non-zero on failure 516 | */ 517 | int openWelcomePageInBrowser() { 518 | char url[100]; 519 | 520 | // Format the URL with the provided port 521 | snprintf(url, sizeof(url), "http://localhost:%d/welcome.htm", webPort); 522 | 523 | // ShellExecute returns a value greater than 32 if successful 524 | HINSTANCE result = ShellExecuteA(NULL, "open", url, NULL, NULL, SW_SHOWNORMAL); 525 | 526 | // Check if operation was successful 527 | if ((int)result <= 32) { 528 | // Error occurred 529 | switch ((int)result) { 530 | case 0: 531 | DbgPrint(L"openWelcomePageInBrowser Error: The operating system is out of memory or resources.\n"); 532 | break; 533 | case ERROR_FILE_NOT_FOUND: 534 | DbgPrint(L"openWelcomePageInBrowser Error: The specified file was not found.\n"); 535 | break; 536 | case ERROR_PATH_NOT_FOUND: 537 | DbgPrint(L"openWelcomePageInBrowser Error: The specified path was not found.\n"); 538 | break; 539 | case ERROR_BAD_FORMAT: 540 | DbgPrint(L"openWelcomePageInBrowser Error: The .exe file is invalid.\n"); 541 | break; 542 | case SE_ERR_ACCESSDENIED: 543 | DbgPrint(L"openWelcomePageInBrowser Error: Access denied.\n"); 544 | break; 545 | case SE_ERR_ASSOCINCOMPLETE: 546 | DbgPrint(L"openWelcomePageInBrowser Error: The file name association is incomplete or invalid.\n"); 547 | break; 548 | case SE_ERR_NOASSOC: 549 | DbgPrint(L"openWelcomePageInBrowser Error: No application is associated with the given file type.\n"); 550 | break; 551 | default: 552 | DbgPrint(L"openWelcomePageInBrowser Error: Unknown error occurred (code: %d).\n", (int)result); 553 | break; 554 | } 555 | return 1; 556 | } 557 | 558 | return 0; 559 | } 560 | -------------------------------------------------------------------------------- /Server.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | #include 3 | 4 | int serve_start(int port); 5 | 6 | bool serve_request(bool isPaused); 7 | 8 | int serve_stop(void); 9 | 10 | int openWelcomePageInBrowser(); 11 | -------------------------------------------------------------------------------- /UniversalPauseButton.rc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanries/UniversalPauseButton/469276b4c58a1193db0c793426ac148244f1dbd2/UniversalPauseButton.rc -------------------------------------------------------------------------------- /UniversalPauseButton.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 2013 4 | VisualStudioVersion = 12.0.31101.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "UniversalPauseButton", "UniversalPauseButton.vcxproj", "{229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Win32 = Debug|Win32 11 | Debug|x64 = Debug|x64 12 | Release|Win32 = Release|Win32 13 | Release|x64 = Release|x64 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Debug|Win32.ActiveCfg = Debug|Win32 17 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Debug|Win32.Build.0 = Debug|Win32 18 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Debug|x64.ActiveCfg = Debug|x64 19 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Debug|x64.Build.0 = Debug|x64 20 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Release|Win32.ActiveCfg = Release|Win32 21 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Release|Win32.Build.0 = Release|Win32 22 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Release|x64.ActiveCfg = Release|x64 23 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C}.Release|x64.Build.0 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | EndGlobal 29 | -------------------------------------------------------------------------------- /UniversalPauseButton.vcxproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | Win32 7 | 8 | 9 | Debug 10 | x64 11 | 12 | 13 | Release 14 | Win32 15 | 16 | 17 | Release 18 | x64 19 | 20 | 21 | 22 | {229438CB-2B5E-4C34-B1CE-F9AC14B0C47C} 23 | UniversalPauseButton 24 | 25 | 26 | 27 | Application 28 | true 29 | v143 30 | Unicode 31 | 32 | 33 | Application 34 | true 35 | v143 36 | Unicode 37 | 38 | 39 | Application 40 | false 41 | v143 42 | true 43 | Unicode 44 | 45 | 46 | Application 47 | false 48 | v143 49 | true 50 | Unicode 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | $(SolutionDir)$(Platform)\$(Configuration)\ 70 | $(SolutionDir)$(Platform)\$(Configuration)\Temp\ 71 | 72 | 73 | $(SolutionDir)$(Platform)\$(Configuration)\ 74 | $(SolutionDir)$(Platform)\$(Configuration)\Temp\ 75 | 76 | 77 | $(SolutionDir)$(Platform)\$(Configuration)\Temp\ 78 | 79 | 80 | $(SolutionDir)$(Platform)\$(Configuration)\Temp\ 81 | 82 | 83 | 84 | EnableAllWarnings 85 | Disabled 86 | true 87 | MultiThreadedDebug 88 | true 89 | Level3 90 | 91 | 92 | true 93 | Windows 94 | 95 | 96 | 97 | 98 | EnableAllWarnings 99 | Disabled 100 | true 101 | MultiThreadedDebug 102 | true 103 | Level3 104 | 105 | 106 | true 107 | Windows 108 | 109 | 110 | 111 | 112 | EnableAllWarnings 113 | MaxSpeed 114 | true 115 | true 116 | true 117 | MultiThreaded 118 | true 119 | Level3 120 | 121 | 122 | true 123 | true 124 | true 125 | Windows 126 | 127 | 128 | 129 | 130 | EnableAllWarnings 131 | MaxSpeed 132 | true 133 | true 134 | true 135 | MultiThreaded 136 | true 137 | Level3 138 | 139 | 140 | true 141 | true 142 | true 143 | Windows 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | true 164 | 165 | 166 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /UniversalPauseButton.vcxproj.filters: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | {4FC737F1-C7A5-4376-A066-2A32D752A2FF} 6 | cpp;c;cc;cxx;def;odl;idl;hpj;bat;asm;asmx 7 | 8 | 9 | {93995380-89BD-4b04-88EB-625FBE52EBFB} 10 | h;hh;hpp;hxx;hm;inl;inc;xsd 11 | 12 | 13 | {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} 14 | rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms 15 | 16 | 17 | 18 | 19 | Header Files 20 | 21 | 22 | Header Files 23 | 24 | 25 | Header Files 26 | 27 | 28 | 29 | 30 | Resource Files 31 | 32 | 33 | 34 | 35 | Resource Files 36 | 37 | 38 | 39 | 40 | Source Files 41 | 42 | 43 | Source Files 44 | 45 | 46 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /debugconsole.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanries/UniversalPauseButton/469276b4c58a1193db0c793426ac148244f1dbd2/debugconsole.png -------------------------------------------------------------------------------- /main_page.htm: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Universal Pause Button 7 | 287 | 288 | 289 |
290 | 291 | 292 |
293 |

Open the game you want to pause.

294 | 295 |
296 | 297 | 298 | 309 | 310 | 311 | 417 | 418 | -------------------------------------------------------------------------------- /pause.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanries/UniversalPauseButton/469276b4c58a1193db0c793426ac148244f1dbd2/pause.ico -------------------------------------------------------------------------------- /registry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanries/UniversalPauseButton/469276b4c58a1193db0c793426ac148244f1dbd2/registry.png -------------------------------------------------------------------------------- /resource.h: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ryanries/UniversalPauseButton/469276b4c58a1193db0c793426ac148244f1dbd2/resource.h -------------------------------------------------------------------------------- /welcome.htm: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Connection Guide 7 | 231 | 232 | 233 |
234 |
235 |
236 |
237 |
Detecting connection options...
238 |
239 | 240 | 290 |
291 | 292 | 428 | 429 | 430 | --------------------------------------------------------------------------------