├── .npmignore ├── .gitignore ├── test ├── logger.ts └── index.test.ts ├── src ├── lib │ ├── stackback.ts │ └── format.ts └── index.ts ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /.npmignore: -------------------------------------------------------------------------------- 1 | # npm 2 | /node_modules/ 3 | /.npmrc 4 | npm-debug.log 5 | 6 | # Code coverage 7 | /.coverage/ 8 | /.coveralls.yml 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # npm 2 | /node_modules/ 3 | /.npmrc 4 | npm-debug.log 5 | 6 | # Code coverage 7 | /.coverage/ 8 | /.coveralls.yml 9 | 10 | # Build artifacts 11 | /build/ 12 | /index*.js 13 | -------------------------------------------------------------------------------- /test/logger.ts: -------------------------------------------------------------------------------- 1 | export class Logger { 2 | private lines: Array; 3 | 4 | error(...strings: Array): void { 5 | this.lines.push(...strings); 6 | } 7 | 8 | getOutput(): string { 9 | return this.lines.join('\n'); 10 | } 11 | 12 | getLines(): Array { 13 | return [...this.lines]; 14 | } 15 | 16 | constructor() { 17 | this.lines = []; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/lib/stackback.ts: -------------------------------------------------------------------------------- 1 | /*! https://github.com/defunctzombie/node-stackback/blob/master/index.js */ 2 | 3 | import { formatStackTrace } from './format'; 4 | 5 | export function stackback(error: any): any { 6 | const save = Error.prepareStackTrace; 7 | 8 | Error.prepareStackTrace = function(err: any, trace: any): any { 9 | Object.defineProperty(err, '_sb_callsites', { value: trace }); 10 | 11 | /* istanbul ignore next */ 12 | return (save || formatStackTrace)(err, trace); 13 | }; 14 | 15 | error.stack; 16 | 17 | /* istanbul ignore next */ 18 | if (!error._sb_callsites) { 19 | return []; 20 | } 21 | 22 | Error.prepareStackTrace = save; 23 | 24 | return error._sb_callsites; 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["./src/index.ts"], 3 | "compilerOptions": { 4 | "outDir": "./build", 5 | "module": "commonjs", 6 | "target": "es2018", 7 | "lib": ["es2019"], 8 | "moduleResolution": "node", 9 | "strict": true, 10 | "noEmitOnError": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "noUnusedParameters": true, 13 | "downlevelIteration": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "noUnusedLocals": true, 18 | "removeComments": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020-present, cheap glitch 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose 4 | with or without fee is hereby granted, provided that the above copyright notice 5 | and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 8 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 9 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 10 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 11 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 12 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 13 | THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /test/index.test.ts: -------------------------------------------------------------------------------- 1 | import { whyIsNodeStillRunning } from '../src/index'; 2 | 3 | import { createServer } from 'net'; 4 | import { fetch } from 'fetch-h2'; 5 | 6 | import { Logger } from './logger'; 7 | 8 | test("log open handles #1", async () => { // {{{ 9 | 10 | // Setup 11 | const logger = new Logger(); 12 | const server = createServer(); 13 | server.listen(0); 14 | 15 | // Action 16 | whyIsNodeStillRunning(logger); 17 | 18 | // Tests 19 | expect(logger.getLines().shift()).toMatch(/There are [3-9]\d* handle\(s\) keeping the process running/); 20 | expect(logger.getOutput()).toMatch('TCPSERVERWRAP'); 21 | expect(logger.getOutput()).toMatch('TickObject'); 22 | 23 | // Cleanup 24 | await server.close(); 25 | 26 | }); // }}} 27 | 28 | test("log open handles #2", async () => { // {{{ 29 | 30 | // Setup 31 | const logger = new Logger(); 32 | const request = fetch('https://www.google.com'); 33 | 34 | // Action 35 | whyIsNodeStillRunning(logger); 36 | 37 | // Tests 38 | expect(logger.getLines().shift()).toMatch(/There are [3-9]\d* handle\(s\) keeping the process running/); 39 | expect(logger.getOutput()).toMatch('new Promise'); 40 | expect(logger.getOutput()).toMatch('why-is-node-still-running'); 41 | 42 | // Cleanup 43 | await (await request).text(); 44 | 45 | }); // }}} 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "why-is-node-still-running", 3 | "description": "Find out exactly why Node is still running.", 4 | "version": "1.0.0", 5 | "license": "ISC", 6 | "repository": { 7 | "type": "git", 8 | "url": "git+https://github.com/cheap-glitch/why-is-node-still-running.git" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/cheap-glitch/why-is-node-still-running/issues" 12 | }, 13 | "author": "cheap glitch (https://github.com/cheap-glitch)", 14 | "homepage": "https://github.com/cheap-glitch/why-is-node-still-running#readme", 15 | "keywords": [ 16 | "open-handles", 17 | "call-stack", 18 | "node-is-still-running-goddammit" 19 | ], 20 | "main": "./build/index.min.js", 21 | "directories": { 22 | "test": "test" 23 | }, 24 | "scripts": { 25 | "build": "tsc", 26 | "coverage:collect": "jest --collectCoverage", 27 | "coverage:upload": "coveralls < .coverage/lcov.info", 28 | "lint": "eslint . --ext .ts --ignore-path .gitignore", 29 | "test": "jest --useStderr" 30 | }, 31 | "eslintConfig": { 32 | "extends": "@cheap-glitch/typescript", 33 | "env": { 34 | "es6": true, 35 | "node": true 36 | }, 37 | "overrides": [ 38 | { 39 | "files": [ 40 | "test/*.test.ts" 41 | ], 42 | "rules": { 43 | "@typescript-eslint/no-non-null-assertion": "off" 44 | } 45 | } 46 | ] 47 | }, 48 | "jest": { 49 | "preset": "ts-jest", 50 | "coverageDirectory": ".coverage", 51 | "coveragePathIgnorePatterns": [ 52 | "/node_modules/", 53 | "/src/lib/format.ts" 54 | ] 55 | }, 56 | "devDependencies": { 57 | "@cheap-glitch/eslint-config-typescript": "^1.3.0", 58 | "@types/jest": "^26.0.20", 59 | "@types/node": "^14.14.20", 60 | "@typescript-eslint/eslint-plugin": "^4.13.0", 61 | "eslint": "^7.17.0", 62 | "fetch-h2": "^2.5.1", 63 | "jest": "^26.6.3", 64 | "ts-jest": "^26.4.4", 65 | "typescript": "^4.1.3" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/lib/format.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * Copyright 2012 the V8 project authors. All rights reserved. Redistribution 3 | * and use in source and binary forms, with or without modification, are 4 | * permitted provided that the following conditions are met: 5 | * 6 | * * Redistributions of source code must retain the above copyright notice, 7 | * this list of conditions and the following disclaimer. 8 | * 9 | * * Redistributions in binary form must reproduce the above copyright 10 | * notice, this list of conditions and the following disclaimer in the 11 | * documentation and/or other materials provided with the distribution. 12 | * 13 | * * Neither the name of Google Inc. nor the names of its contributors may 14 | * be used to endorse or promote products derived from this software 15 | * without specific prior written permission. 16 | * 17 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 | * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 | * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 | * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 21 | * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 | * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 | * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 | * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 | * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 | * POSSIBILITY OF SUCH DAMAGE. 28 | */ 29 | 30 | export function formatStackTrace(error: any, frames: any): string { 31 | const lines = []; 32 | 33 | try { 34 | lines.push(error.toString()); 35 | } catch (e) { 36 | try { 37 | lines.push('"); 38 | } catch (ee) { 39 | lines.push(''); 40 | } 41 | } 42 | 43 | for (const frame of frames) { 44 | let line; 45 | try { 46 | line = frame.toString(); 47 | } catch (e) { 48 | try { 49 | line = ''; 50 | } catch (ee) { 51 | line = ''; 52 | } 53 | } 54 | lines.push(' at ' + line); 55 | } 56 | 57 | return lines.join('\n'); 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🏃 why-is-node-still-running 2 | 3 | ![License](https://badgen.net/github/license/cheap-glitch/why-is-node-still-running?color=green) 4 | ![Latest release](https://badgen.net/github/release/cheap-glitch/why-is-node-still-running?color=green) 5 | [![Coverage status](https://coveralls.io/repos/github/cheap-glitch/why-is-node-still-running/badge.svg?branch=main)](https://coveralls.io/github/cheap-glitch/why-is-node-still-running?branch=main) 6 | 7 | > This is a port of mafintosh' [why-is-node-running](https://github.com/mafintosh/why-is-node-running) 8 | > module to TypeScript, with modernized syntax and no dependencies. 9 | 10 | ```javascript 11 | const { whyIsNodeStillRunning } = require('why-is-node-still-running'); 12 | const { createServer } = require('net'); 13 | 14 | const server = createServer(); 15 | server.listen(0); 16 | 17 | whyIsNodeStillRunning(); 18 | // There are 2 handle(s) keeping the process running 19 | ``` 20 | 21 | ## Installation 22 | 23 | ```shell 24 | npm i -D why-is-node-still-running 25 | ``` 26 | 27 | ## Usage 28 | 29 | **Always import this module first** so that the asynchronous hook can be setup. 30 | The hook will log to the console by default, but you can provide it with a 31 | custom logger that implements `error()`. 32 | 33 | ### Example of usage with Jest 34 | 35 | Sometimes Jest complains that there are asynchronous operations still hanging 36 | after the tests have been completed. When the `--detectOpenHandles` flag gives 37 | no output, you can try using this module to help pinpoint the cause: 38 | 39 | 40 | ```javascript 41 | import { whyIsNodeStillRunning } from 'why-is-node-still-running'; 42 | 43 | afterAll(async () => { 44 | // Do your actual cleanup here 45 | // [...] 46 | 47 | // Print the handles still opened 48 | await new Promise(resolve => setTimeout(() => { 49 | whyIsNodeStillRunning(); 50 | resolve(); 51 | }, 4000)); 52 | }); 53 | ``` 54 | 55 | Don't forget to run Jest with `--useStderr` to show console output. 56 | 57 | Alternatively, you can use this module to print some information about the stack 58 | regularly while the tests are running (e.g. see [this comment](https://github.com/facebook/jest/issues/9473#issuecomment-675738694)). 59 | 60 | ## License 61 | 62 | ```text 63 | Copyright (c) 2020-present, cheap glitch 64 | 65 | Permission to use, copy, modify, and/or distribute this software for any purpose 66 | with or without fee is hereby granted, provided that the above copyright notice 67 | and this permission notice appear in all copies. 68 | 69 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 70 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 71 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 72 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS 73 | OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER 74 | TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 75 | THIS SOFTWARE. 76 | ``` 77 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * why-is-node-still-running 3 | * 4 | * Find out exactly why Node is still running. 5 | * 6 | * Copyright (c) 2020-present, cheap glitch 7 | * 8 | * Permission to use, copy, modify, and/or distribute this software for any 9 | * purpose with or without fee is hereby granted, provided that the above 10 | * copyright notice and this permission notice appear in all copies. 11 | * 12 | * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 13 | * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 14 | * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 15 | * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 16 | * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 17 | * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 18 | * PERFORMANCE OF THIS SOFTWARE. 19 | */ 20 | 21 | import { sep as pathSeparator } from 'path'; 22 | import { existsSync, readFileSync } from 'fs'; 23 | import { createHook } from 'async_hooks'; 24 | 25 | import { stackback } from './lib/stackback'; 26 | 27 | const active = new Map(); 28 | 29 | const hook = createHook({ 30 | init(id: number, type: any, _: any, resource: any) { 31 | if (type == 'TIMERWRAP' || type == 'PROMISE') { 32 | return; 33 | } 34 | 35 | const error = new Error(); 36 | const stacks = stackback(error); 37 | active.set(id, { type, stacks, resource }); 38 | }, 39 | /* istanbul ignore next */ 40 | destroy(id) { 41 | active.delete(id); 42 | }, 43 | }); 44 | 45 | hook.enable(); 46 | 47 | /* istanbul ignore next */ 48 | export function whyIsNodeStillRunning(logger: any = console): void { 49 | hook.disable(); 50 | 51 | const activeResources = [...active.values()].filter(resource => 52 | resource.type != 'Timeout' 53 | || typeof resource.resource.hasRef != 'function' 54 | || resource.resource.hasRef() 55 | ); 56 | 57 | logger.error(`There are ${activeResources.length} handle(s) keeping the process running:`); 58 | activeResources.forEach(resource => printStacks(logger, resource)); 59 | } 60 | 61 | function printStacks(logger: any, resource: any): void { 62 | const stacks = resource.stacks.slice(1).filter((stack: any) => { 63 | const filename = stack.getFileName(); 64 | 65 | return (filename && filename.includes(pathSeparator) && filename.indexOf('internal' + pathSeparator) != 0); 66 | }); 67 | 68 | logger.error('\n' + resource.type); 69 | 70 | /* istanbul ignore next */ 71 | if (!stacks[0]) { 72 | logger.error('(unknown stack trace)'); 73 | 74 | return; 75 | } 76 | 77 | const padding = ' '.repeat(Math.max(...stacks.map((stack: any) => (stack.getFileName() + ':' + stack.getLineNumber()).length))); 78 | 79 | for (const stack of stacks) { 80 | const prefix = stack.getFileName() + ':' + stack.getLineNumber(); 81 | 82 | if (existsSync(stack.getFileName())) { 83 | const src = readFileSync(stack.getFileName(), 'utf8').split(/\n|\r\n/); 84 | logger.error(prefix + padding.slice(prefix.length) + ' - ' + src[stack.getLineNumber() - 1].trim()); 85 | } else { 86 | logger.error(prefix + padding.slice(prefix.length)); 87 | } 88 | } 89 | } 90 | --------------------------------------------------------------------------------