├── .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 | [](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 | [](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 | }
--------------------------------------------------------------------------------