├── .eslintrc.json ├── .gitignore ├── .gitlab-ci.yml ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── binding.gyp ├── demo ├── electron.js └── index.html ├── install.js ├── module ├── WinShutdownHandler.cpp ├── WinShutdownHandler.h └── main.cpp ├── package-lock.json ├── package.json ├── src ├── index.ts └── types.ts └── tsconfig.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "env": { 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier", 11 | "plugin:import/recommended", 12 | "plugin:import/typescript" 13 | ], 14 | "plugins": ["@typescript-eslint", "prettier", "import"], 15 | "parserOptions": { 16 | "ecmaVersion": 2021, 17 | "project": "./tsconfig.json", 18 | "tsconfigRootDir": "." 19 | }, 20 | "rules": { 21 | "no-console": 1, 22 | "prettier/prettier": 2, 23 | "import/no-dynamic-require": 2, 24 | "import/no-useless-path-segments": 1, 25 | "import/order": [1, { "newlines-between": "always" }], 26 | "import/no-named-as-default": 0 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # node modules 2 | node_modules/ 3 | 4 | # eslint cache 5 | .eslintcache 6 | 7 | # build artifacts 8 | dist/ 9 | **/build/ 10 | prebuilds/ 11 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | GIT_DEPTH: 50 3 | 4 | # Mixins 5 | .only_any_v_tags: 6 | only: 7 | - /^v[0-9]+\.[0-9]+\.[0-9]+/ 8 | except: 9 | - branches 10 | 11 | .only_release_v_tags: 12 | only: 13 | - /^v[0-9]+\.[0-9]+\.[0-9]+$/ 14 | except: 15 | - branches 16 | 17 | .job_test: 18 | stage: test 19 | rules: 20 | - if: $CI_MERGE_REQUEST_ID 21 | - if: $CI_COMMIT_TAG 22 | before_script: 23 | - npm ci --ignore-scripts 24 | 25 | stages: 26 | - test 27 | - build-addon 28 | - publish 29 | 30 | # Test stage 31 | lint: 32 | extends: 33 | - .job_test 34 | script: 35 | - npm run lint 36 | 37 | typecheck: 38 | extends: 39 | - .job_test 40 | script: 41 | - npm run typecheck 42 | 43 | # Build-addon stage 44 | build-addon-windows: 45 | extends: 46 | - .only_any_v_tags 47 | stage: build-addon 48 | tags: 49 | - win 50 | before_script: 51 | - npm ci --ignore-scripts 52 | script: 53 | - npm run prebuild -- --arch x64 --upload $PREBUILD_GH_TOKEN 54 | - npm run prebuild -- --arch ia32 --upload $PREBUILD_GH_TOKEN 55 | needs: 56 | - lint 57 | - typecheck 58 | 59 | # Publish stage 60 | build-library: 61 | extends: 62 | - .only_any_v_tags 63 | stage: publish 64 | before_script: 65 | - npm ci --ignore-scripts 66 | script: 67 | - npm run build:ts 68 | - npm set //registry.npmjs.org/:_authToken $NPM_TOKEN 69 | - npm publish --access public 70 | - LAST_RELEASE="$(git describe --abbrev=0 --tags ${CI_COMMIT_TAG}^)" 71 | - CHANGELOG="$(git diff -U0 $LAST_RELEASE $CI_COMMIT_TAG CHANGELOG.md | grep -E "^\+" | grep -vE "^\+\+\+" | grep -vE "^\+#+ \[" | sed "s/^\+//")" 72 | - > 73 | curl -sSL -X POST -H "JOB-TOKEN: $CI_JOB_TOKEN" -H "Content-type: application/json" -d "{ \"name\": $(echo $CI_COMMIT_TAG | jq -Ra .), \"tag_name\": $(echo $CI_COMMIT_TAG | jq -Ra .), \"description\": $(echo "$CHANGELOG" | jq -Rsa .) }" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/releases 74 | artifacts: 75 | paths: 76 | - dist 77 | expire_in: 12h 78 | needs: 79 | - lint 80 | - typecheck 81 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "useTabs": true, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "trailingComma": "none", 7 | "arrowParens": "avoid" 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [1.1.2](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.1.1...v1.1.2) (2024-05-23) 6 | 7 | 8 | ### Build/CI 9 | 10 | * Removed msvs_version and updated prebuild ([addd125](https://github.com/paymoapp/electron-shutdown-handler/commit/addd12584e5e3b083d71ca83188c7e977f0c1917)) 11 | 12 | ### [1.1.1](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.1.0...v1.1.1) (2024-05-23) 13 | 14 | 15 | ### Build/CI 16 | 17 | * Added x32 prebuilt binaries for windows ([6e89b69](https://github.com/paymoapp/electron-shutdown-handler/commit/6e89b69252ab9de318f88d0ed236b751433197a5)) 18 | 19 | ## [1.1.0](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.15...v1.1.0) (2024-05-23) 20 | 21 | 22 | ### Features 23 | 24 | * Replaced license with MIT ([5f7b843](https://github.com/paymoapp/electron-shutdown-handler/commit/5f7b843efdeb2f7477db943717d192b29bd5d481)) 25 | 26 | ### [1.0.15](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.14...v1.0.15) (2023-10-13) 27 | 28 | 29 | ### Improvements 30 | 31 | * **module:** Changed method exports ([74b181c](https://github.com/paymoapp/electron-shutdown-handler/commit/74b181ca80f4b14400bc063c961bf8f21d0b7750)) 32 | 33 | 34 | ### Build/CI 35 | 36 | * **gitlab:** Use new runners ([8774c87](https://github.com/paymoapp/electron-shutdown-handler/commit/8774c876d01e460f9c73ab5ff0a6677b4cb10c44)) 37 | 38 | ### [1.0.14](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.13...v1.0.14) (2022-12-21) 39 | 40 | 41 | ### Documentation 42 | 43 | * Removed extra dot ([2562739](https://github.com/paymoapp/electron-shutdown-handler/commit/256273962ac720204429d3aaf1fadad2cf971b1c)), closes [#1](https://github.com/paymoapp/electron-shutdown-handler/issues/1) 44 | 45 | ### [1.0.13](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.12...v1.0.13) (2022-09-12) 46 | 47 | ### [1.0.12](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.11...v1.0.12) (2022-09-12) 48 | 49 | ### [1.0.11](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.10...v1.0.11) (2022-09-12) 50 | 51 | 52 | ### Bug Fixes 53 | 54 | * In install script set cwd to dirname ([579e5e3](https://github.com/paymoapp/electron-shutdown-handler/commit/579e5e34a4bbec70c351e1ee027fa72c2f9ae28c)) 55 | 56 | ### [1.0.10](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.9...v1.0.10) (2022-09-07) 57 | 58 | 59 | ### Documentation 60 | 61 | * Fix docs ([1696feb](https://github.com/paymoapp/electron-shutdown-handler/commit/1696feb8d3b93e6042261d8e509e302c9882efb4)) 62 | 63 | ### [1.0.9](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.8...v1.0.9) (2022-09-07) 64 | 65 | 66 | ### Build/CI 67 | 68 | * Only build native module on windows ([fa42a8d](https://github.com/paymoapp/electron-shutdown-handler/commit/fa42a8d9522c148c8fe0687d0ba4a4c499ac067d)) 69 | 70 | ### [1.0.8](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.7...v1.0.8) (2022-09-06) 71 | 72 | ### [1.0.7](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.6...v1.0.7) (2022-09-06) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * **addon:** Shut the main thread down first, so we will not get errors because the renderer process already exited ([0079193](https://github.com/paymoapp/electron-shutdown-handler/commit/007919342d41837ed09f492d81f348be03f0234c)) 78 | 79 | 80 | ### Improvements 81 | 82 | * **demo:** Better demo ([672f687](https://github.com/paymoapp/electron-shutdown-handler/commit/672f6874f48dd3d938ab028feb0c6287267bac77)) 83 | 84 | 85 | ### Documentation 86 | 87 | * Updated docs ([3cd964e](https://github.com/paymoapp/electron-shutdown-handler/commit/3cd964ebe458b962e4d604c8659699318eb12b7e)) 88 | 89 | ### [1.0.6](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.5...v1.0.6) (2022-09-06) 90 | 91 | 92 | ### Bug Fixes 93 | 94 | * **addon:** Handle WM_ENDSESSION where wParam is false ([b94dfb1](https://github.com/paymoapp/electron-shutdown-handler/commit/b94dfb1307b44f52201b7075fb3ebe969ea1b98b)) 95 | 96 | ### [1.0.5](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.4...v1.0.5) (2022-09-06) 97 | 98 | ### [1.0.4](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.3...v1.0.4) (2022-09-06) 99 | 100 | 101 | ### Bug Fixes 102 | 103 | * **addon:** Call javascript callback on QueryEndSession instead of EndSession ([92d435b](https://github.com/paymoapp/electron-shutdown-handler/commit/92d435b37b99b64f80bc29591c3eab9c44e0fe10)) 104 | 105 | ### [1.0.3](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.2...v1.0.3) (2022-09-06) 106 | 107 | 108 | ### Build/CI 109 | 110 | * Use MSVS version 2019 ([4824d58](https://github.com/paymoapp/electron-shutdown-handler/commit/4824d58ac798491862698067c9c793c0b0180c71)) 111 | 112 | ### [1.0.2](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.1...v1.0.2) (2022-09-05) 113 | 114 | 115 | ### Bug Fixes 116 | 117 | * **js:** Fixed import of addon ([adc9ad8](https://github.com/paymoapp/electron-shutdown-handler/commit/adc9ad8643e6ee9526dc558732c79bddbf58bdcf)) 118 | 119 | ### [1.0.1](https://github.com/paymoapp/electron-shutdown-handler/compare/v1.0.0...v1.0.1) (2022-09-05) 120 | 121 | 122 | ### Build/CI 123 | 124 | * Migrate to prebuild from node-pre-gyp ([c268174](https://github.com/paymoapp/electron-shutdown-handler/commit/c268174d20d3fd1db673d4144cc5556784683cbe)) 125 | 126 | ## [1.0.0](https://github.com/paymoapp/electron-shutdown-handler/compare/v0.1.1...v1.0.0) (2022-09-05) 127 | 128 | 129 | ### Documentation 130 | 131 | * Added documentation ([c814279](https://github.com/paymoapp/electron-shutdown-handler/commit/c814279841b863c9fc07f02796a451a1102b746b)) 132 | 133 | ### 0.1.1 (2022-09-05) 134 | 135 | 136 | ### Features 137 | 138 | * Implemented C++ code of addon ([88c0a54](https://github.com/paymoapp/electron-shutdown-handler/commit/88c0a54e7f896f9d3dd52f91994431a9c860f9e4)) 139 | * Implemented driver code ([ef3e54f](https://github.com/paymoapp/electron-shutdown-handler/commit/ef3e54f724e59927235eb890d942cdf3116b41b3)) 140 | 141 | 142 | ### Build/CI 143 | 144 | * Added pipeline ([6940946](https://github.com/paymoapp/electron-shutdown-handler/commit/6940946d9e2038124ef41b1326b3b1fab5c3dea3)) 145 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Paymo LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Electron Shutdown Handler 2 | 3 | [![NPM](https://img.shields.io/npm/v/@paymoapp/electron-shutdown-handler)](https://www.npmjs.com/package/@paymoapp/electron-shutdown-handler) 4 | [![Typescript](https://img.shields.io/npm/types/@paymoapp/electron-shutdown-handler)](https://www.npmjs.com/package/@paymoapp/electron-shutdown-handler) 5 | [![N-API](https://raw.githubusercontent.com/nodejs/abi-stable-node/doc/assets/Node-API%20v6%20Badge.svg)](https://github.com/nodejs/node-addon-api) 6 | [![License](https://img.shields.io/github/license/paymoapp/electron-shutdown-handler)](https://www.gnu.org/licenses/gpl-3.0.txt) 7 | 8 | NodeJS library using native modules to capture the shutdown events on Windows in Electron applications. 9 | 10 | ### Table of Contents 11 | 12 | 13 | 14 | - [Getting started](#getting-started) 15 | - [Installation](#installation) 16 | - [Native addon](#native-addon) 17 | - [Example](#example) 18 | - [Usage](#usage) 19 | - [API](#api) 20 | - [Functions](#functions) 21 | - [Events](#events) 22 | 23 | 24 | 25 | ## Getting started 26 | 27 | #### Installation 28 | 29 | ```bash 30 | npm install --save @paymoapp/electron-shutdown-handler 31 | ``` 32 | 33 | #### Native addon 34 | 35 | This project uses NodeJS Native Addons to function, so you can use this library in any NodeJS or Electron project, there won't be any problem with bundling and code signing. 36 | 37 | The project uses [prebuild](https://github.com/prebuild/prebuild) to supply prebuilt libraries. 38 | 39 | The project uses Node-API version 6, you can check [this table](https://nodejs.org/api/n-api.html#node-api-version-matrix) to see which node versions are supported. 40 | 41 | If there's a compliant prebuilt binary, it will be downloaded during installation, or it will be built. You can also rebuild it anytime by running `npm run build:gyp`. 42 | 43 | The library has native addons for Windows only, but it won't fail during install or during runtime on other platforms. 44 | 45 | #### Example 46 | 47 | You can run a demo application by calling `npm run demo` and use the `rmlogotext.exe` command to emit the shutdown event without actually shutting the system down. 48 | 49 | ```ts 50 | import ElectronShutdownHandler from '@paymoapp/electron-shutdown-handler'; 51 | import { app, BrowserWindow } from 'electron'; 52 | 53 | app.whenReady().then(() => { 54 | const win = new BrowserWindow({ 55 | width: 600, 56 | height: 600 57 | }); 58 | 59 | win.loadFile('index.html'); 60 | 61 | ElectronShutdownHandler.setWindowHandle(win.getNativeWindowHandle()); 62 | ElectronShutdownHandler.blockShutdown('Please wait for some data to be saved'); 63 | 64 | ElectronShutdownHandler.on('shutdown', () => { 65 | console.log('Shutting down!'); 66 | ElectronShutdownHandler.releaseShutdown(); 67 | win.webContents.send('shutdown'); 68 | app.quit(); 69 | }); 70 | }); 71 | ``` 72 | 73 | #### Usage 74 | 75 | First of all you need to create an electron window, after which you can call the `setWindowHandle` method. Calling this method is required, otherwise the rest of the functions will throw. This function will store the HWND of the window in the addon and will set that the window will be the first process to shut down (otherwise the renderer process might exit first which results in the crash of electron). 76 | 77 | To block the shutdown you need to call the `blockShutdown` function before a shutdown event occures, but you also need to set up an event listener, otherwise the hook won't be added to the window message callback. 78 | 79 | You should also call app.quit() in the shutdown handler. 80 | 81 | Please note that this addon is singleton, you can only use it for one window at the moment, so you should always set the main window's handle. 82 | 83 | ## API 84 | 85 | #### Functions 86 | 87 | ###### 𝑓    setWindowHandle 88 | 89 | ```ts 90 | type setWindowHandle = (handle: Buffer) => void 91 | ``` 92 | 93 | Set the window handle of the main window. You __MUST__ call this method before calling any other methods. 94 | 95 | ###### 𝑓    blockShutdown 96 | 97 | ```ts 98 | type blockShutdown = (reason: string) => boolean 99 | ``` 100 | 101 | Block the system from shutting down. You need to set a reason which will be displayed to the user. The shutdown will only be blocked if you also have a shutdown event listener. The response indicates if the operation was successful. 102 | 103 | ###### 𝑓    releaseShutdown 104 | 105 | ```ts 106 | type releaseShutdown = () => boolean 107 | ``` 108 | 109 | Allow the system to shut down. The response indicates if the operation was successful. 110 | 111 | #### Events 112 | 113 | The exported object extends the node [EventEmitter](https://nodejs.org/api/events.html) class. 114 | 115 | ###### ✨    shutdown 116 | 117 | This event is emitted when the system is shutting down. You should avoid calling long running async code here, since as the function finishes, the process will exit. 118 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "PaymoWinShutdownHandler", 5 | "conditions": [ 6 | ["OS=='win'", { 7 | "sources": [ 8 | "module/main.cpp", 9 | "module/WinShutdownHandler.cpp" 10 | ], 11 | "libraries": [ 12 | "User32.lib" 13 | ], 14 | "msvs_settings": { 15 | "VCCLCompilerTool": { 16 | "AdditionalOptions": [ 17 | "/std:c++17" 18 | ] 19 | } 20 | } 21 | }] 22 | ], 23 | "include_dirs": [ 24 | " { 5 | const win = new BrowserWindow({ 6 | width: 600, 7 | height: 300, 8 | title: 'Electron-shutdown-handler demo' 9 | }); 10 | 11 | win.loadFile('index.html'); 12 | 13 | console.log('--- Electron-shutdown-handler demo ---'); 14 | 15 | console.log('PID =', process.pid); 16 | console.log('You can use rmlogotest.exe to "simulate" a shutdown'); 17 | console.log( 18 | 'You can close this app by executing Ctrl+C in the terminal or shutting down your system' 19 | ); 20 | 21 | let allowQuit = false; 22 | 23 | app.on('before-quit', e => { 24 | if (!allowQuit) { 25 | e.preventDefault(); 26 | } 27 | }); 28 | 29 | win.on('close', e => { 30 | if (!allowQuit) { 31 | e.preventDefault(); 32 | } 33 | }); 34 | 35 | ShutdownHandler.setWindowHandle(win.getNativeWindowHandle()); 36 | ShutdownHandler.blockShutdown('Yayy! It works! Shutdown in 10 seconds'); 37 | 38 | ShutdownHandler.on('shutdown', () => { 39 | console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!'); 40 | console.log('! System is shutting down !'); 41 | console.log('!!!!!!!!!!!!!!!!!!!!!!!!!!!'); 42 | 43 | console.log(); 44 | console.log('Setting a 10 second timer'); 45 | 46 | setTimeout(() => { 47 | console.log('Shutting down NOW'); 48 | ShutdownHandler.releaseShutdown(); 49 | allowQuit = true; 50 | app.quit(); 51 | }, 10_000); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Electron-shutdown-handler demo 5 | 6 | 7 | 8 |

Electron-shutdown-handler demo

9 |

Just try to shut down the system and you will see a message and it will prevent shutdown for 10 seconds!

10 |

See the logs to follow up with the events

11 | 12 | 13 | -------------------------------------------------------------------------------- /install.js: -------------------------------------------------------------------------------- 1 | const { spawnSync } = require('child_process'); 2 | 3 | if (process.platform == 'win32') { 4 | console.log('Building addon'); 5 | const status = spawnSync('npm', ['run', 'install:win32'], { 6 | stdio: 'inherit', 7 | cwd: __dirname, 8 | shell: true 9 | }); 10 | 11 | process.exit(status.status || 0); 12 | } 13 | -------------------------------------------------------------------------------- /module/WinShutdownHandler.cpp: -------------------------------------------------------------------------------- 1 | #include "WinShutdownHandler.h" 2 | 3 | namespace PaymoWinShutdownHandler { 4 | // Internal objects 5 | HWND mainWindow = NULL; 6 | WNDPROC prevWndProc = NULL; 7 | Napi::ThreadSafeFunction tsfn; 8 | bool shouldBlockShutdown = false; 9 | 10 | void Init(Napi::Env env, Napi::Object exports) { 11 | exports.Set("setMainWindowHandle", Napi::Function::New(env)); 12 | exports.Set("insertWndProcHook", Napi::Function::New(env)); 13 | exports.Set("removeWndProcHook", Napi::Function::New(env)); 14 | exports.Set("acquireShutdownBlock", Napi::Function::New(env)); 15 | exports.Set("releaseShutdownBlock", Napi::Function::New(env)); 16 | } 17 | 18 | void setMainWindowHandle(const Napi::CallbackInfo& info) { 19 | Napi::Env env = info.Env(); 20 | 21 | if (info.Length() != 1) { 22 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 23 | return; 24 | } 25 | 26 | if (!info[0].IsBuffer()) { 27 | Napi::TypeError::New(env, "Expected first argument to be a buffer").ThrowAsJavaScriptException(); 28 | return; 29 | } 30 | 31 | mainWindow = *info[0].As>().Data(); 32 | SetProcessShutdownParameters(0x3FF, 0); 33 | } 34 | 35 | Napi::Value insertWndProcHook(const Napi::CallbackInfo& info) { 36 | Napi::Env env = info.Env(); 37 | 38 | if (info.Length() != 1) { 39 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 40 | return env.Null(); 41 | } 42 | 43 | if (!info[0].IsFunction()) { 44 | Napi::TypeError::New(env, "Expected first argument to be a function").ThrowAsJavaScriptException(); 45 | return env.Null(); 46 | } 47 | 48 | if (mainWindow == NULL) { 49 | Napi::Error::New(env, "Should call setMainWindowHandle first").ThrowAsJavaScriptException(); 50 | return env.Null(); 51 | } 52 | 53 | if (prevWndProc != NULL) { 54 | // hook already created 55 | return Napi::Boolean::New(env, false); 56 | } 57 | 58 | tsfn = Napi::ThreadSafeFunction::New(env, info[0].As(), "Windows shutdown handler callback", 0, 1); 59 | 60 | prevWndProc = (WNDPROC)SetWindowLongPtr(mainWindow, GWLP_WNDPROC, (LONG_PTR)&WindowProcCb); 61 | 62 | return Napi::Boolean::New(env, true); 63 | } 64 | 65 | Napi::Value removeWndProcHook(const Napi::CallbackInfo& info) { 66 | Napi::Env env = info.Env(); 67 | 68 | if (mainWindow == NULL) { 69 | Napi::Error::New(env, "Should call setMainWindowHandle first").ThrowAsJavaScriptException(); 70 | return env.Null(); 71 | } 72 | 73 | if (prevWndProc == NULL) { 74 | // no hook set 75 | return Napi::Boolean::New(env, false); 76 | } 77 | 78 | SetWindowLongPtr(mainWindow, GWLP_WNDPROC, (LONG_PTR)prevWndProc); 79 | 80 | tsfn.Release(); 81 | prevWndProc = NULL; 82 | 83 | return Napi::Boolean::New(env, true); 84 | } 85 | 86 | Napi::Value acquireShutdownBlock(const Napi::CallbackInfo& info) { 87 | Napi::Env env = info.Env(); 88 | 89 | if (info.Length() != 1) { 90 | Napi::TypeError::New(env, "Expected 1 argument").ThrowAsJavaScriptException(); 91 | return env.Null(); 92 | } 93 | 94 | if (!info[0].IsString()) { 95 | Napi::TypeError::New(env, "Expected first argument to be a string").ThrowAsJavaScriptException(); 96 | return env.Null(); 97 | } 98 | 99 | if (mainWindow == NULL) { 100 | Napi::Error::New(env, "Should call setMainWindowHandle first").ThrowAsJavaScriptException(); 101 | return env.Null(); 102 | } 103 | 104 | std::u16string u16reason = info[0].As().Utf16Value(); 105 | std::wstring wreason = std::wstring(u16reason.begin(), u16reason.end()); 106 | 107 | BOOL result = ShutdownBlockReasonCreate(mainWindow, wreason.c_str()); 108 | shouldBlockShutdown = true; 109 | 110 | return Napi::Boolean::New(env, result); 111 | } 112 | 113 | Napi::Value releaseShutdownBlock(const Napi::CallbackInfo& info) { 114 | Napi::Env env = info.Env(); 115 | 116 | if (mainWindow == NULL) { 117 | Napi::Error::New(env, "Should call setMainWindowHandle first").ThrowAsJavaScriptException(); 118 | return env.Null(); 119 | } 120 | 121 | BOOL result = ShutdownBlockReasonDestroy(mainWindow); 122 | shouldBlockShutdown = false; 123 | 124 | return Napi::Boolean::New(env, result); 125 | } 126 | 127 | LRESULT CALLBACK WindowProcCb(HWND hWindow, UINT event, WPARAM wParam, LPARAM lParam) { 128 | if (event == WM_QUERYENDSESSION) { 129 | tsfn.BlockingCall([](Napi::Env env, Napi::Function jsCallback) { 130 | jsCallback.Call({}); 131 | }); 132 | 133 | if (shouldBlockShutdown) { 134 | return FALSE; 135 | } 136 | 137 | return TRUE; 138 | } 139 | else if (event == WM_ENDSESSION) { 140 | if (wParam == FALSE) { 141 | return 0; 142 | } 143 | } 144 | 145 | return CallWindowProc(prevWndProc, hWindow, event, wParam, lParam); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /module/WinShutdownHandler.h: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #ifndef _PAYMO_WINSHUTDOWNHANDLER_H 6 | #define _PAYMO_WINSHUTDOWNHANDLER_H 7 | 8 | namespace PaymoWinShutdownHandler { 9 | void Init(Napi::Env env, Napi::Object exports); 10 | 11 | // Exported functions 12 | void setMainWindowHandle(const Napi::CallbackInfo& info); 13 | Napi::Value insertWndProcHook(const Napi::CallbackInfo& info); 14 | Napi::Value removeWndProcHook(const Napi::CallbackInfo& info); 15 | Napi::Value acquireShutdownBlock(const Napi::CallbackInfo& info); 16 | Napi::Value releaseShutdownBlock(const Napi::CallbackInfo& info); 17 | 18 | // Internal functions 19 | LRESULT CALLBACK WindowProcCb(HWND hWindow, UINT event, WPARAM wParam, LPARAM lParam); 20 | } 21 | 22 | #endif 23 | -------------------------------------------------------------------------------- /module/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "WinShutdownHandler.h" 3 | 4 | Napi::Object InitAll(Napi::Env env, Napi::Object exports) { 5 | PaymoWinShutdownHandler::Init(env, exports); 6 | 7 | return exports; 8 | } 9 | 10 | NODE_API_MODULE(NODE_GYP_MODULE_NAME, InitAll); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@paymoapp/electron-shutdown-handler", 3 | "version": "1.1.2", 4 | "description": "Handle shutdown messages on windows in your electron app and delay system shutdown", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "install": "node install.js", 9 | "install:win32": "prebuild-install -r napi || node-gyp rebuild", 10 | "demo": "node_modules\\electron\\dist\\electron.exe demo/electron.js --no-sandbox", 11 | "clean": "node-gyp clean && rimraf dist", 12 | "build": "npm run clean && npm run build:ts && npm run build:gyp", 13 | "build:ts": "tsc", 14 | "build:gyp": "node-gyp rebuild", 15 | "prebuild": "prebuild --all -r napi --strip", 16 | "typecheck": "tsc --noEmit", 17 | "lint": "esw --color --ext .ts src", 18 | "lint:fix": "esw --color --fix --ext .ts src", 19 | "lint:watch": "esw --color --fix --watch --cache --ext .ts src", 20 | "format": "prettier --write src", 21 | "generate:readme-toc": "markdown-toc -i --bullets=\"-\" --maxdepth=4 README.md", 22 | "release": "standard-version", 23 | "release:pre": "standard-version --prerelease" 24 | }, 25 | "binary": { 26 | "napi_versions": [ 27 | 6 28 | ] 29 | }, 30 | "files": [ 31 | "binding.gyp", 32 | "install.js", 33 | "dist/", 34 | "module/" 35 | ], 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/paymoapp/electron-shutdown-handler.git" 39 | }, 40 | "keywords": [ 41 | "electron", 42 | "windows", 43 | "shutdown", 44 | "delay" 45 | ], 46 | "author": "Paymo LLC", 47 | "license": "MIT", 48 | "bugs": { 49 | "url": "https://github.com/paymoapp/electron-shutdown-handler/issues" 50 | }, 51 | "homepage": "https://github.com/paymoapp/electron-shutdown-handler#readme", 52 | "devDependencies": { 53 | "@types/node": "^18.7.14", 54 | "@typescript-eslint/eslint-plugin": "^5.36.1", 55 | "@typescript-eslint/parser": "^5.36.1", 56 | "electron": "^20.1.1", 57 | "eslint": "^8.23.0", 58 | "eslint-config-prettier": "^8.5.0", 59 | "eslint-plugin-import": "^2.26.0", 60 | "eslint-plugin-prettier": "^4.2.1", 61 | "eslint-watch": "^8.0.0", 62 | "markdown-toc": "^1.2.0", 63 | "node-gyp": "^9.1.0", 64 | "prebuild": "^13.0.1", 65 | "prettier": "^2.7.1", 66 | "rimraf": "^3.0.2", 67 | "standard-version": "^9.5.0", 68 | "typescript": "^4.8.2" 69 | }, 70 | "dependencies": { 71 | "node-addon-api": "^5.0.0", 72 | "prebuild-install": "^7.1.2" 73 | }, 74 | "standard-version": { 75 | "scripts": { 76 | "prerelease": "git fetch --all --tags" 77 | }, 78 | "types": [ 79 | { 80 | "type": "feat", 81 | "section": "Features" 82 | }, 83 | { 84 | "type": "fix", 85 | "section": "Bug Fixes" 86 | }, 87 | { 88 | "type": "imp", 89 | "section": "Improvements" 90 | }, 91 | { 92 | "type": "ci", 93 | "section": "Build/CI" 94 | }, 95 | { 96 | "type": "chore", 97 | "hidden": true 98 | }, 99 | { 100 | "type": "docs", 101 | "section": "Documentation" 102 | }, 103 | { 104 | "type": "refactor", 105 | "section": "Refactor" 106 | }, 107 | { 108 | "type": "test", 109 | "section": "Testing" 110 | } 111 | ] 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'node:events'; 2 | 3 | import { Addon } from './types'; 4 | 5 | const SUPPORTED_PLATFORMS = ['win32']; 6 | 7 | let addon: Addon | null = null; 8 | 9 | if (SUPPORTED_PLATFORMS.includes(process.platform)) { 10 | addon = require('../build/Release/PaymoWinShutdownHandler.node'); // eslint-disable-line import/no-dynamic-require 11 | } 12 | 13 | class ElectronShutdownHandlerClass extends EventEmitter { 14 | constructor() { 15 | super(); 16 | 17 | this.on('newListener', (event: string) => { 18 | if (event == 'shutdown' && this.listenerCount('shutdown') == 0) { 19 | // create native listener 20 | if (addon) { 21 | addon.insertWndProcHook(() => { 22 | this.emit('shutdown'); 23 | }); 24 | } 25 | } 26 | }); 27 | 28 | this.on('removeListener', (event: string) => { 29 | if (event == 'shutdown' && this.listenerCount('shutdown') == 0) { 30 | // remove native listener 31 | if (addon) { 32 | addon.removeWndProcHook(); 33 | } 34 | } 35 | }); 36 | } 37 | 38 | setWindowHandle(handle: Buffer): void { 39 | if (!addon) { 40 | return; 41 | } 42 | 43 | addon.setMainWindowHandle(handle); 44 | } 45 | 46 | blockShutdown(reason: string): boolean { 47 | if (!addon) { 48 | return false; 49 | } 50 | 51 | return addon.acquireShutdownBlock(reason); 52 | } 53 | 54 | releaseShutdown(): boolean { 55 | if (!addon) { 56 | return false; 57 | } 58 | 59 | return addon.releaseShutdownBlock(); 60 | } 61 | } 62 | 63 | const ElectronShutdownHandler = new ElectronShutdownHandlerClass(); 64 | 65 | export default ElectronShutdownHandler; 66 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface Addon { 2 | setMainWindowHandle: (handle: Buffer) => void; 3 | insertWndProcHook: (cb: () => void) => boolean; 4 | removeWndProcHook: () => boolean; 5 | acquireShutdownBlock: (reason: string) => boolean; 6 | releaseShutdownBlock: () => boolean; 7 | } 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "lib": ["esnext", "DOM"], 6 | "allowJs": true, 7 | "rootDirs": ["src"], 8 | "outDir": "dist", 9 | "resolveJsonModule": true, 10 | "strict": true, 11 | "skipLibCheck": true, 12 | "allowSyntheticDefaultImports": true, 13 | "esModuleInterop": true, 14 | "moduleResolution": "node", 15 | "isolatedModules": true, 16 | "declaration": true, 17 | "typeRoots": ["./typings", "./node_modules/@types"] 18 | }, 19 | "include": ["src"] 20 | } 21 | --------------------------------------------------------------------------------