Repository: mantoni/eslint_d.js
Pull request #317 (2024-09-26 17:28:13)

feat: implement is alive check
Based on #316

In this PR:
- Implemented isAlive check for start and status commands
- Added separated option for commands in forwarder and service
- Created helper function to obtain socket in forwarder


Patch:
diff --git a/bin/eslint_d.js b/bin/eslint_d.js
index 7355fd9..a000910 100755
--- a/bin/eslint_d.js
+++ b/bin/eslint_d.js
@@ -1,8 +1,8 @@
 #!/usr/bin/env node
 
-import { loadConfig } from '../lib/config.js';
+import { loadConfig, removeConfig } from '../lib/config.js';
 import { createResolver } from '../lib/resolver.js';
-import { forwardToDaemon } from '../lib/forwarder.js';
+import { forwardToDaemon, isAlive } from '../lib/forwarder.js';
 import { launchDaemon, stopDaemon } from '../lib/launcher.js';
 import { filesHash } from '../lib/hash.js';
 
@@ -31,11 +31,17 @@ const command = process.argv[2];
   ]);
   switch (command) {
     case 'start':
-      if (config) {
+      if (await isAlive(config)) {
         console.log('eslint_d: Already running');
-      } else {
-        await launchDaemon(resolver, hash);
+        return;
+      }
+
+      if (config) {
+        await removeConfig(resolver);
       }
+
+      await launchDaemon(resolver, hash);
+
       return;
     case 'stop':
       if (config) {
diff --git a/lib/commands.js b/lib/commands.js
new file mode 100644
index 0000000..da2b6b3
--- /dev/null
+++ b/lib/commands.js
@@ -0,0 +1,3 @@
+export const PING_COMMAND = 'ESLINT_D_PING';
+export const SHUTDOWN_COMMAND = 'ESLINT_D_SHUTDOWN';
+export const LINT_COMMAND = 'ESLINT_D_LINT';
diff --git a/lib/forwarder.js b/lib/forwarder.js
index 9a0b60d..8ec395e 100644
--- a/lib/forwarder.js
+++ b/lib/forwarder.js
@@ -1,15 +1,45 @@
 import net from 'node:net';
 import supportsColor from 'supports-color';
 import { removeConfig } from '../lib/config.js';
+import { LINT_COMMAND, PING_COMMAND } from './commands.js';
 
 /**
- * @import { Config} from '../lib/config.js'
- * @import { Resolver} from '../lib/resolver.js'
+ * @import { Config } from './config.js'
+ * @import { Resolver } from './resolver.js'
  */
 
 const EXIT_TOKEN_REGEXP = new RegExp(/EXIT([0-9]{3})/);
 const EXIT_TOKEN_LENGTH = 7;
 
+/**
+ * @param {Config | null} config
+ * @returns {Promise<boolean>}
+ */
+export function isAlive(config) {
+  return forwardCommandToDaemon(config, PING_COMMAND)
+    .then(() => true)
+    .catch(() => false);
+}
+
+/**
+ * @param {Config | null} config
+ * @param {string} command
+ * @returns {Promise<void>}
+ */
+export async function forwardCommandToDaemon(config, command) {
+  if (!config) {
+    return Promise.reject(new Error('config not found'));
+  }
+
+  const socket = await connectToDaemon(config);
+  socket.write(JSON.stringify([config.token, command]));
+  socket.end();
+
+  return new Promise((resolve, reject) => {
+    socket.on('end', () => resolve()).on('error', (err) => reject(err));
+  });
+}
+
 /**
  * @param {Resolver} resolver
  * @param {Config} config
@@ -38,9 +68,24 @@ export async function forwardToDaemon(resolver, config) {
     );
   }
 
-  const socket = net.connect(config.port, '127.0.0.1');
+  let socket;
+  try {
+    socket = await connectToDaemon(config);
+  } catch (/** @type {any} */ err) {
+    if (err['code'] === 'ECONNREFUSED') {
+      console.error(`eslint_d: ${err} - removing config`);
+      await removeConfig(resolver);
+    } else {
+      console.error(`eslint_d: ${err}`);
+    }
+    // eslint-disable-next-line require-atomic-updates
+    process.exitCode = 1;
+    return;
+  }
+
   const args = [
     config.token,
+    LINT_COMMAND,
     stdout ? stdout.level : 0,
     process.cwd(),
     eslint_args
@@ -90,13 +135,8 @@ export async function forwardToDaemon(resolver, config) {
         process.exitCode = 1;
       }
     })
-    .on('error', async (err) => {
-      if (err['code'] === 'ECONNREFUSED') {
-        console.error(`eslint_d: ${err} - removing config`);
-        await removeConfig(resolver);
-      } else {
-        console.error(`eslint_d: ${err}`);
-      }
+    .on('error', (err) => {
+      console.error(`eslint_d: ${err}`);
       process.exitCode = 1;
     });
 
@@ -113,6 +153,22 @@ export async function forwardToDaemon(resolver, config) {
   }
 }
 
+/**
+ * @param {Config} config
+ * @returns {Promise<net.Socket>}
+ */
+function connectToDaemon(config) {
+  return new Promise((resolve, reject) => {
+    const socket = net.connect(config.port, '127.0.0.1');
+    socket
+      .on('connect', () => resolve(socket))
+      .on('connectionAttemptFailed', (err) => {
+        reject(err);
+        socket.end();
+      });
+  });
+}
+
 function readStdin() {
   return new Promise((resolve, reject) => {
     let content = '';
diff --git a/lib/launcher.js b/lib/launcher.js
index dfe62fd..c7db4a2 100644
--- a/lib/launcher.js
+++ b/lib/launcher.js
@@ -1,8 +1,9 @@
 import fs from 'node:fs';
-import net from 'node:net';
 import os from 'node:os';
 import child_process from 'node:child_process';
 import { loadConfig, removeConfig } from './config.js';
+import { forwardCommandToDaemon } from './forwarder.js';
+import { SHUTDOWN_COMMAND } from './commands.js';
 
 /**
  * @import { Config } from './config.js'
@@ -64,12 +65,7 @@ export async function stopDaemon(resolver, config) {
  */
 function platformAwareStopDaemon(config) {
   if (os.platform() === 'win32') {
-    return new Promise((resolve, reject) => {
-      const socket = net.connect(config.port, '127.0.0.1');
-      socket.once('error', reject);
-      socket.write(JSON.stringify([config.token, 'ESLINT_D_STOP']));
-      socket.end(() => resolve(undefined));
-    });
+    return forwardCommandToDaemon(config, SHUTDOWN_COMMAND);
   }
 
   process.kill(config.pid, 'SIGTERM');
diff --git a/lib/service.js b/lib/service.js
index 622c674..150aa24 100644
--- a/lib/service.js
+++ b/lib/service.js
@@ -1,4 +1,5 @@
 import { createRequire } from 'node:module';
+import { LINT_COMMAND, PING_COMMAND, SHUTDOWN_COMMAND } from './commands.js';
 
 /**
  * @import { Socket } from 'node:net'
@@ -40,14 +41,14 @@ export function createService(resolver, token, shutdown) {
           text = content.slice(newline + 1);
           content = content.slice(0, newline);
         }
-        const [request_token, color_level, cwd, argv] = JSON.parse(content);
+        const [request_token, command, color_level, cwd, argv] =
+          JSON.parse(content);
         if (request_token !== token) {
           con.end();
           return;
         }
-        if (color_level === 'ESLINT_D_STOP') {
-          shutdown();
-          con.end();
+        if (command !== LINT_COMMAND) {
+          onCommand(con, command);
           return;
         }
 
@@ -74,4 +75,23 @@ export function createService(resolver, token, shutdown) {
         process.stderr.write = stderr_write;
       });
   };
+
+  /**
+   * @param {Socket} con
+   * @param {string} command
+   */
+  function onCommand(con, command) {
+    switch (command) {
+      case SHUTDOWN_COMMAND:
+        shutdown();
+        break;
+      case PING_COMMAND:
+        break;
+      default:
+        con.write(`Unknown command: ${command}`);
+    }
+
+    con.end();
+    con.destroySoon();
+  }
 }
diff --git a/lib/status.js b/lib/status.js
index 7908d77..255da71 100644
--- a/lib/status.js
+++ b/lib/status.js
@@ -1,3 +1,5 @@
+import { isAlive } from './forwarder.js';
+
 /**
  * @import { Resolver } from './resolver.js'
  * @import { Config } from './config.js'
@@ -7,8 +9,10 @@
  * @param {Resolver} resolver
  * @param {Config | null} config
  */
-export function status(resolver, config) {
-  const running = config ? `Running (${config.pid})` : 'Not running';
+export async function status(resolver, config) {
+  const isRunning = config ? await isAlive(config) : false;
+  const running =
+    config && isRunning ? `Running (${config.pid})` : 'Not running';
   const eslint_origin = resolver.bundled ? 'bundled' : 'local';
   const eslint_version = resolver.require(
     `${resolver.base}/package.json`


Test patch:
diff --git a/lib/forwarder.test.js b/lib/forwarder.test.js
index ea4ec28..0819cb3 100644
--- a/lib/forwarder.test.js
+++ b/lib/forwarder.test.js
@@ -3,8 +3,9 @@ import { PassThrough } from 'node:stream';
 import fs from 'node:fs/promises';
 import supportsColor from 'supports-color';
 import { assert, refute, match, sinon } from '@sinonjs/referee-sinon';
-import { forwardToDaemon } from './forwarder.js';
+import { forwardToDaemon, isAlive } from './forwarder.js';
 import { createResolver } from './resolver.js';
+import { LINT_COMMAND } from './commands.js';
 
 describe('lib/forwarder', () => {
   const resolver = createResolver();
@@ -38,6 +39,39 @@ describe('lib/forwarder', () => {
     sinon.replace(process, 'argv', argv);
   });
 
+  context('isAlive', () => {
+    it('connects to 127.0.0.1 on port from config', () => {
+      isAlive(config);
+
+      assert.calledOnceWith(net.connect, config.port, '127.0.0.1');
+    });
+
+    it('handles response without errors', async () => {
+      const chunks = [];
+      sinon.replace(
+        socket,
+        'read',
+        sinon.fake(() => (chunks.length ? chunks.shift() : null))
+      );
+
+      const result = isAlive(config);
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(2).callback(); // readable
+
+      assert.equals(await result, true);
+    });
+
+    it('handles ECONNREFUSED', async () => {
+      const result = isAlive(config);
+      const err = new Error('Connection refused');
+      err['code'] = 'ECONNREFUSED';
+      socket.on.getCall(1).callback(err); // error
+
+      assert.equals(await result, false);
+    });
+  });
+
   context('forwardToDaemon', () => {
     it('connects to 127.0.0.1 on port from config', () => {
       forwardToDaemon(resolver, config);
@@ -45,14 +79,16 @@ describe('lib/forwarder', () => {
       assert.calledOnceWith(net.connect, config.port, '127.0.0.1');
     });
 
-    it('writes token, color level, cwd and argv to socket', () => {
+    it('writes token, color level, cwd and argv to socket', async () => {
       sinon.replace(process, 'cwd', sinon.fake.returns('the/cwd'));
 
       forwardToDaemon(resolver, config);
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
 
       assert.calledOnceWith(
         socket.write,
-        `["token",${color_level},"the/cwd",[${JSON.stringify(process.argv0)},"eslint_d"]]`
+        `["token","${LINT_COMMAND}",${color_level},"the/cwd",[${JSON.stringify(process.argv0)},"eslint_d"]]`
       );
       assert.calledOnce(socket.end);
     });
@@ -83,13 +119,15 @@ describe('lib/forwarder', () => {
 
       forwardToDaemon(resolver, config);
       await new Promise(setImmediate);
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
 
       assert.calledThrice(socket.write);
       assert.calledWith(socket.write, '\n');
       assert.calledWith(socket.write, 'text from stdin');
     });
 
-    it('forwards socket response to stdout', () => {
+    it('forwards socket response to stdout', async () => {
       const chunks = ['response ', 'from daemon'];
       sinon.replace(
         socket,
@@ -99,8 +137,10 @@ describe('lib/forwarder', () => {
       sinon.replace(process.stdout, 'write', sinon.fake());
 
       forwardToDaemon(resolver, config);
-      socket.on.firstCall.callback(); // readable
-      socket.on.secondCall.callback(); // end
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(2).callback(); // readable
+      socket.on.getCall(3).callback(); // end
 
       assert.calledThrice(process.stdout.write);
       assert.calledWith(process.stdout.write, 're');
@@ -108,7 +148,7 @@ describe('lib/forwarder', () => {
       assert.calledWith(process.stdout.write, ' daemon');
     });
 
-    it('handles "EXIT000" from response', () => {
+    it('handles "EXIT000" from response', async () => {
       const chunks = ['response from daemonEXIT000'];
       sinon.replace(
         socket,
@@ -118,15 +158,17 @@ describe('lib/forwarder', () => {
       sinon.replace(process.stdout, 'write', sinon.fake());
 
       forwardToDaemon(resolver, config);
-      socket.on.firstCall.callback(); // readable
-      socket.on.secondCall.callback(); // end
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(2).callback(); // readable
+      socket.on.getCall(3).callback(); // end
 
       assert.calledWith(process.stdout.write, 'response from daemon');
       assert.equals(process.exitCode, 0);
       refute.called(console.error);
     });
 
-    it('handles "EXIT001" from response', () => {
+    it('handles "EXIT001" from response', async () => {
       const chunks = ['response from daemonEXIT001'];
       sinon.replace(
         socket,
@@ -136,15 +178,17 @@ describe('lib/forwarder', () => {
       sinon.replace(process.stdout, 'write', sinon.fake());
 
       forwardToDaemon(resolver, config);
-      socket.on.firstCall.callback(); // readable
-      socket.on.secondCall.callback(); // end
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(2).callback(); // readable
+      socket.on.getCall(3).callback(); // end
 
       assert.calledWith(process.stdout.write, 'response from daemon');
       assert.equals(process.exitCode, 1);
       refute.called(console.error);
     });
 
-    it('handles "EXIT123" from response', () => {
+    it('handles "EXIT123" from response', async () => {
       const chunks = ['response from daemonEXIT123'];
       sinon.replace(
         socket,
@@ -154,15 +198,17 @@ describe('lib/forwarder', () => {
       sinon.replace(process.stdout, 'write', sinon.fake());
 
       forwardToDaemon(resolver, config);
-      socket.on.firstCall.callback(); // readable
-      socket.on.secondCall.callback(); // end
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(2).callback(); // readable
+      socket.on.getCall(3).callback(); // end
 
       assert.calledWith(process.stdout.write, 'response from daemon');
       assert.equals(process.exitCode, 123);
       refute.called(console.error);
     });
 
-    it('handles "EXIT001" inside response', () => {
+    it('handles "EXIT001" inside response', async () => {
       const chunks = ['response EXIT001', ' from daemonEXIT002'];
       sinon.replace(
         socket,
@@ -172,8 +218,10 @@ describe('lib/forwarder', () => {
       sinon.replace(process.stdout, 'write', sinon.fake());
 
       forwardToDaemon(resolver, config);
-      socket.on.firstCall.callback(); // readable
-      socket.on.secondCall.callback(); // end
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(2).callback(); // readable
+      socket.on.getCall(3).callback(); // end
 
       assert.calledWith(process.stdout.write, 'response ');
       assert.calledWith(process.stdout.write, 'EXIT001 from daemon');
@@ -181,7 +229,7 @@ describe('lib/forwarder', () => {
       refute.called(console.error);
     });
 
-    it('logs error and sets exitCode to 1 if response does not end with EXIT marker', () => {
+    it('logs error and sets exitCode to 1 if response does not end with EXIT marker', async () => {
       const chunks = ['response from daemon'];
       sinon.replace(
         socket,
@@ -191,8 +239,10 @@ describe('lib/forwarder', () => {
       sinon.replace(process.stdout, 'write', sinon.fake());
 
       forwardToDaemon(resolver, config);
-      socket.on.firstCall.callback(); // readable
-      socket.on.secondCall.callback(); // end
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(2).callback(); // readable
+      socket.on.getCall(3).callback(); // end
 
       assert.calledWith(process.stdout.write, 'response from');
       assert.calledWith(process.stdout.write, ' daemon');
@@ -200,24 +250,27 @@ describe('lib/forwarder', () => {
       assert.calledOnceWith(console.error, 'eslint_d: unexpected response');
     });
 
-    it('logs error from stream', () => {
+    it('logs error from stream', async () => {
       sinon.replace(fs, 'unlink', sinon.fake.resolves());
 
       forwardToDaemon(resolver, config);
-      socket.on.thirdCall.callback(new Error('Ouch!')); // error
+      socket.on.getCall(0).callback(); // connect
+      await new Promise(setImmediate);
+      socket.on.getCall(4).callback(new Error('Ouch!')); // error
 
       assert.calledOnceWith(console.error, 'eslint_d: Error: Ouch!');
       assert.equals(process.exitCode, 1);
       refute.called(fs.unlink);
     });
 
-    it('logs ECONNREFUSED error from stream and removes config', () => {
+    it('logs ECONNREFUSED error from stream and removes config', async () => {
       sinon.replace(fs, 'unlink', sinon.fake.resolves());
 
       forwardToDaemon(resolver, config);
       const err = new Error('Connection refused');
       err['code'] = 'ECONNREFUSED';
-      socket.on.thirdCall.callback(err); // error
+      socket.on.getCall(1).callback(err); // connectionAttemptFailed
+      await new Promise(setImmediate);
 
       assert.calledOnceWith(
         console.error,
@@ -250,11 +303,13 @@ describe('lib/forwarder', () => {
 
         forwardToDaemon(resolver, config);
         await new Promise(setImmediate);
+        socket.on.getCall(0).callback(); // connect
+        await new Promise(setImmediate);
 
         assert.calledThrice(socket.write);
         assert.calledWith(
           socket.write,
-          `["token",${color_level},"cwd",[${JSON.stringify(process.argv0)},"eslint_d","--stdin","--fix-dry-run","--format","json","--other","--options"]]`
+          `["token","${LINT_COMMAND}",${color_level},"cwd",[${JSON.stringify(process.argv0)},"eslint_d","--stdin","--fix-dry-run","--format","json","--other","--options"]]`
         );
         assert.calledWith(socket.write, '\n');
         assert.calledWith(socket.write, 'text from stdin');
@@ -273,8 +328,10 @@ describe('lib/forwarder', () => {
 
         forwardToDaemon(resolver, config);
         await new Promise(setImmediate);
-        socket.on.firstCall.callback(); // readable
-        socket.on.secondCall.callback(); // end
+        socket.on.getCall(0).callback(); // connect
+        await new Promise(setImmediate);
+        socket.on.getCall(2).callback(); // readable
+        socket.on.getCall(3).callback(); // end
 
         assert.calledWith(process.stdout.write, 'response from daemon');
         assert.equals(process.exitCode, 0);
@@ -293,8 +350,10 @@ describe('lib/forwarder', () => {
 
         forwardToDaemon(resolver, config);
         await new Promise(setImmediate);
-        socket.on.firstCall.callback(); // readable
-        socket.on.secondCall.callback(); // end
+        socket.on.getCall(0).callback(); // connect
+        await new Promise(setImmediate);
+        socket.on.getCall(2).callback(); // readable
+        socket.on.getCall(3).callback(); // end
 
         assert.calledWith(process.stdout.write, 'text from stdin');
         assert.equals(process.exitCode, 0);
@@ -313,8 +372,10 @@ describe('lib/forwarder', () => {
 
         forwardToDaemon(resolver, config);
         await new Promise(setImmediate);
-        socket.on.firstCall.callback(); // readable
-        socket.on.secondCall.callback(); // end
+        socket.on.getCall(0).callback(); // connect
+        await new Promise(setImmediate);
+        socket.on.getCall(2).callback(); // readable
+        socket.on.getCall(3).callback(); // end
 
         assert.calledWith(process.stdout.write, 'text from stdin');
         assert.equals(process.exitCode, 1);
@@ -339,8 +400,10 @@ describe('lib/forwarder', () => {
 
         forwardToDaemon(resolver, config);
         await new Promise(setImmediate);
-        socket.on.firstCall.callback(); // readable
-        socket.on.secondCall.callback(); // end
+        socket.on.getCall(0).callback(); // connect
+        await new Promise(setImmediate);
+        socket.on.getCall(2).callback(); // readable
+        socket.on.getCall(3).callback(); // end
 
         assert.calledWith(process.stdout.write, 'response from daemon');
         assert.equals(process.exitCode, 1);
diff --git a/lib/launcher.test.js b/lib/launcher.test.js
index d28229a..129b810 100644
--- a/lib/launcher.test.js
+++ b/lib/launcher.test.js
@@ -8,6 +8,7 @@ import { assert, refute, sinon } from '@sinonjs/referee-sinon';
 import { createResolver } from './resolver.js';
 import { launchDaemon, stopDaemon } from './launcher.js';
 import { strictEqual } from 'node:assert';
+import { SHUTDOWN_COMMAND } from './commands.js';
 
 describe('lib/launcher', () => {
   const resolver = createResolver();
@@ -287,7 +288,7 @@ describe('lib/launcher', () => {
               data += buf.toString();
             })
             .on('end', () => {
-              strictEqual(data, '["token","ESLINT_D_STOP"]');
+              strictEqual(data, `["token","${SHUTDOWN_COMMAND}"]`);
               done();
             });
         });
diff --git a/lib/service.test.js b/lib/service.test.js
index 2e31bf5..381f35b 100644
--- a/lib/service.test.js
+++ b/lib/service.test.js
@@ -2,6 +2,7 @@ import { Socket } from 'node:net';
 import { assert, refute, sinon } from '@sinonjs/referee-sinon';
 import { createResolver } from './resolver.js';
 import { createService } from './service.js';
+import { LINT_COMMAND, SHUTDOWN_COMMAND } from './commands.js';
 
 describe('lib/service', () => {
   context('createService', () => {
@@ -41,14 +42,15 @@ describe('lib/service', () => {
 
     /**
      * @param {string} request_token
+     * @param {string} command
      * @param {string} color_level
      * @param {string} cwd
      * @param {string[]} argv
      * @param {string} [text]
      */
-    function send(request_token, color_level, cwd, argv, text) {
+    function send(request_token, command, color_level, cwd, argv, text) {
       const chunks = [
-        `["${request_token}",${color_level},`,
+        `["${request_token}","${command}",${color_level},`,
         `${JSON.stringify(cwd)},${JSON.stringify(argv)}]`
       ];
       if (text !== undefined) {
@@ -69,7 +71,7 @@ describe('lib/service', () => {
     });
 
     it('immediately ends connection if token does not match', () => {
-      send('invalid', '0', '/', []);
+      send('invalid', LINT_COMMAND, '0', '/', []);
 
       assert.calledOnceWithExactly(con.end);
       refute.called(con.write);
@@ -78,7 +80,7 @@ describe('lib/service', () => {
     });
 
     it('sets chalk.level to given color and changes directory', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
 
       await eslint_promise.resolve(0);
       assert.equals(chalk.level, 3);
@@ -86,7 +88,7 @@ describe('lib/service', () => {
     });
 
     it('replaces stdout with a function that write to the socket', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
       process.stdout.write('test');
 
       await eslint_promise.resolve(0);
@@ -94,7 +96,7 @@ describe('lib/service', () => {
     });
 
     it('replaces stderr with a function that write to the socket', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
       process.stderr.write('test');
 
       await eslint_promise.resolve(0);
@@ -102,21 +104,21 @@ describe('lib/service', () => {
     });
 
     it('invokes eslint.execute with given args and no text', async () => {
-      send(token, '3', '/', ['--fix']);
+      send(token, LINT_COMMAND, '3', '/', ['--fix']);
 
       await eslint_promise.resolve(0);
       assert.calledOnceWith(eslint.execute, ['--fix'], null, true);
     });
 
     it('invokes eslint.execute with given text', async () => {
-      send(token, '3', '/', [], 'some text');
+      send(token, LINT_COMMAND, '3', '/', [], 'some text');
 
       await eslint_promise.resolve(0);
       assert.calledOnceWith(eslint.execute, [], 'some text', true);
     });
 
     it('ends connection with "EXIT000" if eslint returns 0', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
 
       await eslint_promise.resolve(0);
       refute.called(con.write);
@@ -124,7 +126,7 @@ describe('lib/service', () => {
     });
 
     it('ends connection with "EXIT001" if eslint returns 1', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
 
       await eslint_promise.resolve(1);
       refute.called(con.write);
@@ -132,7 +134,7 @@ describe('lib/service', () => {
     });
 
     it('ends connection with "EXIT002" if eslint returns 2', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
 
       await eslint_promise.resolve(2);
       refute.called(con.write);
@@ -140,7 +142,7 @@ describe('lib/service', () => {
     });
 
     it('ends connection with "EXIT123" if eslint returns 123', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
 
       await eslint_promise.resolve(123);
       refute.called(con.write);
@@ -148,15 +150,15 @@ describe('lib/service', () => {
     });
 
     it('ends connection with "EXIT001" if eslint throws', async () => {
-      send(token, '3', '/', []);
+      send(token, LINT_COMMAND, '3', '/', []);
 
       await eslint_promise.reject(new Error('Ouch!'));
       assert.calledOnceWith(con.write, 'Error: Ouch!');
       assert.calledOnceWith(con.end, 'EXIT001');
     });
 
-    it('shutdown daemon if ESLINT_D_STOP received', async () => {
-      send(token, '"ESLINT_D_STOP"', '/', []);
+    it('shutdown daemon if shutdown command received', async () => {
+      send(token, SHUTDOWN_COMMAND, '3', '/', []);
 
       await shutdown_promise;
     });
diff --git a/lib/status.test.js b/lib/status.test.js
index 2ad7e09..142eb85 100644
--- a/lib/status.test.js
+++ b/lib/status.test.js
@@ -1,21 +1,48 @@
+import net from 'node:net';
+import { PassThrough } from 'node:stream';
 import { assert, sinon } from '@sinonjs/referee-sinon';
 import { status } from './status.js';
 
 describe('lib/status', () => {
+  let socket;
   const config = { token: 'token', port: 123, pid: 456, hash: 'hash' };
 
   beforeEach(() => {
+    socket = new PassThrough();
+    sinon.replace(socket, 'write', sinon.fake());
+    sinon.replace(socket, 'end', sinon.fake());
+    sinon.replace(net, 'connect', sinon.fake.returns(socket));
     sinon.replace(console, 'log', sinon.fake());
   });
 
-  it('prints status when running and bundled', () => {
+  /**
+   * @param {boolean} isAlive
+   */
+  function setDaemonState(isAlive) {
+    sinon.replace(
+      socket,
+      'on',
+      sinon.fake((event, cb) => {
+        if (isAlive && event !== 'error') {
+          cb();
+        }
+        if (!isAlive && event === 'error') {
+          cb();
+        }
+        return socket;
+      })
+    );
+  }
+
+  it('prints status when running and bundled', async () => {
+    setDaemonState(true);
     const resolver = {
       base: '/path/to/eslint',
       bundled: true,
       require: sinon.fake.returns({ version: '1.2.3' })
     };
 
-    status(resolver, config);
+    await status(resolver, config);
 
     assert.calledOnceWith(
       console.log,
@@ -23,14 +50,15 @@ describe('lib/status', () => {
     );
   });
 
-  it('prints status when running and local', () => {
+  it('prints status when running and local', async () => {
+    setDaemonState(true);
     const resolver = {
       base: '/path/to/eslint',
       bundled: false,
       require: sinon.fake.returns({ version: '1.2.3' })
     };
 
-    status(resolver, config);
+    await status(resolver, config);
 
     assert.calledOnceWith(
       console.log,
@@ -38,14 +66,15 @@ describe('lib/status', () => {
     );
   });
 
-  it('prints status when not running and bundled', () => {
+  it('prints status when not running and bundled', async () => {
+    setDaemonState(false);
     const resolver = {
       base: '/path/to/eslint',
       bundled: true,
       require: sinon.fake.returns({ version: '1.2.3' })
     };
 
-    status(resolver, null);
+    await status(resolver, null);
 
     assert.calledOnceWith(
       console.log,
@@ -53,14 +82,15 @@ describe('lib/status', () => {
     );
   });
 
-  it('prints status when not running and local', () => {
+  it('prints status when not running and local', async () => {
+    setDaemonState(false);
     const resolver = {
       base: '/path/to/eslint',
       bundled: false,
       require: sinon.fake.returns({ version: '1.2.3' })
     };
 
-    status(resolver, null);
+    await status(resolver, null);
 
     assert.calledOnceWith(
       console.log,