├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── deemon.js └── main.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | *.js 4 | !deemon.js -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | *.ts 2 | yarn-error.log 3 | tsconfig.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 João Moreno 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deemon 2 | 3 | [![npm version](https://badge.fury.io/js/deemon.svg)](https://badge.fury.io/js/deemon) 4 | 5 | Utility to run a process in the background and attach to it 6 | 7 | ## Usage 8 | 9 | ``` 10 | npx deemon COMMAND [ARGS] 11 | ``` 12 | 13 | ## Example 14 | 15 | [![asciicast](https://asciinema.org/a/oinexj5mqxxqhMieBbUW5WuXV.svg)](https://asciinema.org/a/oinexj5mqxxqhMieBbUW5WuXV) 16 | 17 | ``` 18 | npx deemon /bin/bash -c "while true; do date; sleep 1; done" 19 | ``` 20 | 21 | Ctrl C will stop the current session and leave the process running in the background. Simply run the same command again to attach to it: 22 | 23 | ``` 24 | npx deemon /bin/bash -c "while true; do date; sleep 1; done" 25 | ``` 26 | 27 | Ctrl D will stop the current session and the background process. You can also simply kill the background process with the `--kill` flag: 28 | 29 | ``` 30 | npx deemon --kill /bin/bash -c "while true; do date; sleep 1; done" 31 | ``` 32 | 33 | Or you can force a restart of the background process and attach to that with the `--restart` flag: 34 | 35 | ``` 36 | npx deemon --restart /bin/bash -c "while true; do date; sleep 1; done" 37 | ``` 38 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deemon", 3 | "version": "1.13.4", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "deemon", 9 | "version": "1.13.4", 10 | "license": "MIT", 11 | "dependencies": { 12 | "bl": "^4.0.2", 13 | "tree-kill": "^1.2.2" 14 | }, 15 | "bin": { 16 | "deemon": "src/deemon.js" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "10", 20 | "typescript": "^5.8.2" 21 | }, 22 | "engines": { 23 | "node": ">=10" 24 | } 25 | }, 26 | "node_modules/@types/node": { 27 | "version": "10.17.19", 28 | "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.19.tgz", 29 | "integrity": "sha512-46/xThm3zvvc9t9/7M3AaLEqtOpqlYYYcCZbpYVAQHG20+oMZBkae/VMrn4BTi6AJ8cpack0mEXhGiKmDNbLrQ==", 30 | "dev": true, 31 | "license": "MIT" 32 | }, 33 | "node_modules/base64-js": { 34 | "version": "1.3.1", 35 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", 36 | "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", 37 | "license": "MIT" 38 | }, 39 | "node_modules/bl": { 40 | "version": "4.0.3", 41 | "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", 42 | "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", 43 | "license": "MIT", 44 | "dependencies": { 45 | "buffer": "^5.5.0", 46 | "inherits": "^2.0.4", 47 | "readable-stream": "^3.4.0" 48 | } 49 | }, 50 | "node_modules/buffer": { 51 | "version": "5.6.0", 52 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.6.0.tgz", 53 | "integrity": "sha512-/gDYp/UtU0eA1ys8bOs9J6a+E/KWIY+DZ+Q2WESNUA0jFRsJOc0SNUO6xJ5SGA1xueg3NL65W6s+NY5l9cunuw==", 54 | "license": "MIT", 55 | "dependencies": { 56 | "base64-js": "^1.0.2", 57 | "ieee754": "^1.1.4" 58 | } 59 | }, 60 | "node_modules/ieee754": { 61 | "version": "1.1.13", 62 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", 63 | "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", 64 | "license": "BSD-3-Clause" 65 | }, 66 | "node_modules/inherits": { 67 | "version": "2.0.4", 68 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 69 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 70 | "license": "ISC" 71 | }, 72 | "node_modules/readable-stream": { 73 | "version": "3.6.0", 74 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", 75 | "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", 76 | "license": "MIT", 77 | "dependencies": { 78 | "inherits": "^2.0.3", 79 | "string_decoder": "^1.1.1", 80 | "util-deprecate": "^1.0.1" 81 | }, 82 | "engines": { 83 | "node": ">= 6" 84 | } 85 | }, 86 | "node_modules/safe-buffer": { 87 | "version": "5.2.0", 88 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", 89 | "integrity": "sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==", 90 | "license": "MIT" 91 | }, 92 | "node_modules/string_decoder": { 93 | "version": "1.3.0", 94 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", 95 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", 96 | "license": "MIT", 97 | "dependencies": { 98 | "safe-buffer": "~5.2.0" 99 | } 100 | }, 101 | "node_modules/tree-kill": { 102 | "version": "1.2.2", 103 | "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", 104 | "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", 105 | "license": "MIT", 106 | "bin": { 107 | "tree-kill": "cli.js" 108 | } 109 | }, 110 | "node_modules/typescript": { 111 | "version": "5.8.2", 112 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", 113 | "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", 114 | "dev": true, 115 | "license": "Apache-2.0", 116 | "bin": { 117 | "tsc": "bin/tsc", 118 | "tsserver": "bin/tsserver" 119 | }, 120 | "engines": { 121 | "node": ">=14.17" 122 | } 123 | }, 124 | "node_modules/util-deprecate": { 125 | "version": "1.0.2", 126 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 127 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", 128 | "license": "MIT" 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deemon", 3 | "version": "1.13.4", 4 | "description": "Run a process in the background and attach to it", 5 | "repository": "git@github.com:joaomoreno/deemon.git", 6 | "author": "João Moreno", 7 | "license": "MIT", 8 | "bin": "src/deemon.js", 9 | "engines": { 10 | "node": ">=10" 11 | }, 12 | "scripts": { 13 | "watch": "tsc --watch", 14 | "prepublishOnly": "tsc" 15 | }, 16 | "devDependencies": { 17 | "@types/node": "10", 18 | "typescript": "^5.8.2" 19 | }, 20 | "dependencies": { 21 | "bl": "^4.0.2", 22 | "tree-kill": "^1.2.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/deemon.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('./main.js'); -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as net from "net"; 3 | import * as os from "os"; 4 | import * as fs from "fs"; 5 | import * as cp from "child_process"; 6 | import * as readline from "readline"; 7 | import * as crypto from "crypto"; 8 | import * as treekill from "tree-kill"; 9 | import { BufferListStream } from "bl"; 10 | 11 | const KILL = 0; 12 | const TALK = 1; 13 | 14 | interface Command { 15 | readonly path: string; 16 | readonly args: string[]; 17 | readonly cwd: string; 18 | } 19 | 20 | function getIPCHandle(command: Command): string { 21 | const scope = crypto 22 | .createHash("md5") 23 | .update(command.path) 24 | .update(command.args.toString()) 25 | .update(command.cwd) 26 | .digest("hex"); 27 | 28 | if (process.platform === "win32") { 29 | return `\\\\.\\pipe\\daemon-${scope}`; 30 | } else { 31 | return path.join( 32 | process.env["XDG_RUNTIME_DIR"] || os.tmpdir(), 33 | `daemon-${scope}.sock` 34 | ); 35 | } 36 | } 37 | 38 | export async function createServer(handle: string): Promise { 39 | return new Promise((c, e) => { 40 | const server = net.createServer(); 41 | 42 | server.on("error", e); 43 | server.listen(handle, () => { 44 | server.removeListener("error", e); 45 | c(server); 46 | }); 47 | }); 48 | } 49 | 50 | export function createConnection(handle: string): Promise { 51 | return new Promise((c, e) => { 52 | const socket = net.createConnection(handle, () => { 53 | socket.removeListener("error", e); 54 | c(socket); 55 | }); 56 | 57 | socket.once("error", e); 58 | }); 59 | } 60 | 61 | export function spawnCommand(server: net.Server, command: Command, options: Options): void { 62 | const clients = new Set(); 63 | const bl = new BufferListStream(); 64 | const child = cp.spawn(command.path, command.args, { 65 | shell: process.platform === "win32", 66 | windowsHide: true, 67 | }); 68 | 69 | const onData = (data) => { 70 | bl.append(data); 71 | 72 | if (bl.length > 100_000_000) { 73 | // buffer caps at 100MB 74 | bl.consume(bl.length - 100_000_000); 75 | } 76 | }; 77 | 78 | child.stdout.on("data", onData); 79 | child.stderr.on("data", onData); 80 | 81 | let justSpawned = true; 82 | setTimeout(() => justSpawned = false, 210); 83 | 84 | let childExitCode: number | undefined; 85 | 86 | server.on("connection", (socket) => { 87 | socket.on("data", (buffer) => { 88 | const command = buffer[0]; 89 | 90 | if (command === KILL) { 91 | treekill(child.pid); 92 | } else if (command === TALK) { 93 | const bufferStream = bl.duplicate(); 94 | 95 | bufferStream.pipe(socket, { end: false }); 96 | bufferStream.on("end", () => { 97 | if (childExitCode !== undefined) { 98 | for (const client of clients) { 99 | client.end(new Uint8Array([childExitCode])); 100 | } 101 | 102 | socket.end(new Uint8Array([childExitCode])); 103 | setTimeout(() => { 104 | server.close(); 105 | process.exit(childExitCode); 106 | }, 0); // windows needs a timeout 107 | return; 108 | } 109 | 110 | child.stdout.pipe(socket, { end: false }); 111 | child.stderr.pipe(socket, { end: false }); 112 | clients.add(socket); 113 | 114 | if (justSpawned) { 115 | setTimeout(() => socket.write("[deemon] Spawned build daemon. Press Ctrl-C to detach, Ctrl-D to kill.\n"), 200); 116 | justSpawned = false; 117 | } else { 118 | setTimeout(() => socket.write("[deemon] Attached to running build daemon. Press Ctrl-C to detach, Ctrl-D to kill.\n"), 0); 119 | } 120 | }); 121 | } 122 | }); 123 | 124 | socket.on("close", () => { 125 | if (clients.has(socket)) { 126 | child.stdout.unpipe(socket); 127 | child.stderr.unpipe(socket); 128 | clients.delete(socket); 129 | } 130 | }); 131 | }); 132 | 133 | child.on("close", code => { 134 | if (options.wait && clients.size === 0) { 135 | childExitCode = code; 136 | return; 137 | } 138 | 139 | for (const client of clients) { 140 | client.end(new Uint8Array([code])); 141 | client.destroy(); 142 | } 143 | 144 | server.close(); 145 | process.exit(code); 146 | }); 147 | } 148 | 149 | function spawnDaemon(command: Command, options: Options) { 150 | const args = [process.argv[1], '--daemon']; 151 | 152 | if (options.wait) { 153 | args.push('--wait'); 154 | } 155 | 156 | args.push(command.path, ...command.args); 157 | cp.spawn(process.execPath, args, { detached: true, stdio: "ignore" }); 158 | } 159 | 160 | async function connect(command: Command, handle: string, options: Options): Promise { 161 | try { 162 | return await createConnection(handle); 163 | } catch (err) { 164 | if (options.attach && (err.code === "ENOENT" || err.code === "ECONNREFUSED")) { 165 | console.error("[deemon] No daemon running."); 166 | process.exit(1); 167 | } else if (err.code === "ECONNREFUSED") { 168 | await fs.promises.unlink(handle); 169 | } else if (err.code !== "ENOENT") { 170 | throw err; 171 | } 172 | 173 | spawnDaemon(command, options); 174 | await new Promise((c) => setTimeout(c, 200)); 175 | return await createConnection(handle); 176 | } 177 | } 178 | 179 | interface Options { 180 | readonly daemon: boolean; 181 | readonly kill: boolean; 182 | readonly restart: boolean; 183 | readonly detach: boolean; 184 | readonly wait: boolean; 185 | readonly attach: boolean; 186 | } 187 | 188 | async function main(command: Command, options: Options): Promise { 189 | const handle = getIPCHandle(command); 190 | 191 | if (options.daemon) { 192 | const server = await createServer(handle); 193 | return spawnCommand(server, command, options); 194 | } 195 | 196 | if (options.detach) { 197 | spawnDaemon(command, options); 198 | 199 | console.log("[deemon] Detached from build daemon."); 200 | 201 | if (options.wait) { 202 | console.log("[deemon] Daemon will wait for a client to connect before exiting."); 203 | } 204 | 205 | process.exit(0); 206 | } 207 | 208 | let socket = await connect(command, handle, options); 209 | 210 | if (options.kill) { 211 | socket.write(new Uint8Array([KILL])); 212 | return; 213 | } 214 | 215 | if (options.restart) { 216 | socket.write(new Uint8Array([KILL])); 217 | await new Promise((c) => setTimeout(c, 500)); 218 | socket = await connect(command, handle, options); 219 | } 220 | 221 | socket.write(new Uint8Array([TALK])); 222 | readline.emitKeypressEvents(process.stdin); 223 | 224 | if (process.stdin.isTTY && process.stdin.setRawMode) { 225 | process.stdin.setRawMode(true); 226 | } 227 | 228 | process.stdin.on("keypress", (code) => { 229 | if (code === "\u0003") { 230 | // ctrl c 231 | console.log("[deemon] Detached from build daemon."); 232 | process.exit(0); 233 | } else if (code === "\u0004") { 234 | // ctrl d 235 | console.log("[deemon] Killed build daemon."); 236 | socket.write(new Uint8Array([KILL])); 237 | process.exit(0); 238 | } 239 | }); 240 | 241 | let lastByte: Buffer | null = null; 242 | let lastByteTimer: NodeJS.Timeout | null = null; 243 | 244 | socket.on('data', (data) => { 245 | clearTimeout(lastByteTimer); 246 | 247 | if (data.length === 0) return; 248 | 249 | if (lastByte) { 250 | process.stdout.write(lastByte); 251 | } 252 | 253 | lastByte = data.slice(data.length - 1); 254 | 255 | if (data.length > 1) { 256 | process.stdout.write(data.slice(0, data.length - 1)); 257 | lastByteTimer = setTimeout(() => process.stdout.write(lastByte), 100); 258 | } 259 | }); 260 | 261 | socket.on("close", () => { 262 | const code = lastByte ? lastByte[0] : 0; 263 | console.log("[deemon] Build daemon exited with code", code); 264 | process.exit(code); 265 | }); 266 | } 267 | 268 | if (process.argv.length < 3) { 269 | console.error(`Usage: npx deemon [OPTS] COMMAND [...ARGS] 270 | Options: 271 | --kill Kill the currently running daemon 272 | --detach Detach the daemon 273 | --wait Wait for a client to connect before exiting the daemon (only valid with --detach) 274 | --attach Attach to the currently running daemon, exiting if it doesn't exist 275 | --restart Restart the daemon`); 276 | process.exit(1); 277 | } 278 | 279 | const commandPathIndex = process.argv.findIndex( 280 | (arg, index) => !/^--/.test(arg) && index >= 2 281 | ); 282 | const [commandPath, ...commandArgs] = process.argv.slice(commandPathIndex); 283 | const command: Command = { 284 | path: commandPath, 285 | args: commandArgs, 286 | cwd: process.cwd(), 287 | }; 288 | 289 | const optionsArgv = process.argv.slice(2, commandPathIndex); 290 | const options: Options = { 291 | daemon: optionsArgv.some((arg) => arg === "--daemon"), 292 | kill: optionsArgv.some((arg) => arg === "--kill"), 293 | restart: optionsArgv.some((arg) => arg === "--restart"), 294 | detach: optionsArgv.some((arg) => arg === "--detach"), 295 | wait: optionsArgv.some((arg) => arg === "--wait"), 296 | attach: optionsArgv.some((arg) => arg === "--attach") 297 | }; 298 | 299 | main(command, options).catch((err) => { 300 | console.error(err); 301 | process.exit(1); 302 | }); 303 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "downlevelIteration": true 5 | }, 6 | "include": [ 7 | "src/**/*.ts" 8 | ], 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } --------------------------------------------------------------------------------