diff --git a/lib/config.js b/lib/config.js index c0b8328..5629196 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,5 +1,8 @@ import debug from 'debug'; +import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; const log = debug('eslint_d:config'); @@ -67,5 +70,9 @@ export async function removeConfig(resolver) { * @returns {string} */ export function configFile(resolver) { - return `${resolver.base}/.eslint_d`; + const hash = createHash('sha256'); + + hash.update(path.resolve(resolver.base).replace('\\', '/')); + + return path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); } diff --git a/lib/config.test.js b/lib/config.test.js index 0854014..554f8f8 100644 --- a/lib/config.test.js +++ b/lib/config.test.js @@ -1,7 +1,10 @@ +import { createHash } from 'node:crypto'; import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; import { assert, sinon } from '@sinonjs/referee-sinon'; -import { loadConfig, writeConfig, removeConfig } from './config.js'; +import { configFile, loadConfig, writeConfig, removeConfig } from './config.js'; describe('lib/config', () => { const resolver = { @@ -16,7 +19,7 @@ describe('lib/config', () => { loadConfig(resolver); - assert.calledOnceWith(fs.readFile, 'some/base/.eslint_d', 'utf8'); + assert.calledOnceWith(fs.readFile, configFile(resolver), 'utf8'); }); it('returns parsed config from file content', async () => { @@ -75,7 +78,7 @@ describe('lib/config', () => { await assert.resolves(promise); assert.calledOnceWith( fs.writeFile, - 'some/base/.eslint_d', + configFile(resolver), 'token 123 456 hash' ); }); @@ -88,7 +91,20 @@ describe('lib/config', () => { const promise = removeConfig(resolver); await assert.resolves(promise); - assert.calledOnceWith(fs.unlink, 'some/base/.eslint_d'); + assert.calledOnceWith(fs.unlink, configFile(resolver)); + }); + }); + + context('configFile', () => { + it('returns unique config file path inside temp dir', () => { + const hash = createHash('sha256'); + + hash.update(path.resolve(resolver.base).replace('\\', '/')); + + assert.equals( + configFile(resolver), + path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`) + ); }); }); }); diff --git a/lib/daemon.test.js b/lib/daemon.test.js index c8c4754..b20138d 100644 --- a/lib/daemon.test.js +++ b/lib/daemon.test.js @@ -4,6 +4,11 @@ import { normalize, resolve } from 'node:path'; import crypto from 'node:crypto'; import net from 'node:net'; import { assert, refute, match, sinon } from '@sinonjs/referee-sinon'; +import { configFile } from './config.js'; + +/** + * @import {Resolver} from './resolver.js' + */ describe('lib/daemon', () => { const randome_bytes = Buffer.from('token'); @@ -15,6 +20,15 @@ describe('lib/daemon', () => { let watcher; let argv; + /** + * @type {Resolver} + */ + const resolverMock = { + base, + bundled: false, + require: sinon.stub() + }; + function returnThis() { // @ts-ignore return this; @@ -59,7 +73,7 @@ describe('lib/daemon', () => { assert.calledOnceWith( fs_promises.writeFile, - `${base}/.eslint_d`, + configFile(resolverMock), `${randome_bytes.toString('hex')} 1234 ${process.pid} hash` ); refute.called(fs.watch); @@ -72,7 +86,9 @@ describe('lib/daemon', () => { fs_write_promise.resolve(); await new Promise(setImmediate); - assert.calledOnceWith(fs.watch, `${base}/.eslint_d`, { persistent: false }); + assert.calledOnceWith(fs.watch, configFile(resolverMock), { + persistent: false + }); }); it('logs error runs shutdown if config file write fails', async () => { @@ -85,7 +101,7 @@ describe('lib/daemon', () => { refute.called(fs.watch); assert.calledOnceWith(console.error, 'eslint_d: Error: Oh noes!'); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); @@ -102,7 +118,7 @@ describe('lib/daemon', () => { assert.calledOnce(watcher.close); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); @@ -147,7 +163,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); server.close.resetHistory(); fs_promises.unlink['resetHistory'](); @@ -188,7 +204,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); @@ -214,7 +230,7 @@ describe('lib/daemon', () => { await new Promise(setImmediate); assert.calledOnceWith(server.close, match.func); - assert.calledOnceWith(fs_promises.unlink, `${base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolverMock)); assert.calledOnceWith(process.exit, 0); }); }); diff --git a/lib/forwarder.test.js b/lib/forwarder.test.js index f957329..1bee356 100644 --- a/lib/forwarder.test.js +++ b/lib/forwarder.test.js @@ -2,10 +2,12 @@ import net from 'node:net'; import child_process from 'node:child_process'; import EventEmitter from 'node:events'; import { PassThrough } from 'node:stream'; +import path from 'node:path'; import fs from 'node:fs'; import fs_promises from 'node:fs/promises'; import supportsColor from 'supports-color'; import { assert, refute, match, sinon } from '@sinonjs/referee-sinon'; +import { configFile } from './config.js'; import { forwardToDaemon, isAlive } from './forwarder.js'; import { createResolver } from './resolver.js'; import { LINT_COMMAND } from './commands.js'; @@ -191,6 +193,7 @@ describe('lib/forwarder', () => { }); it('launch daemon on first connection refused', async () => { + const resolverConfigFile = configFile(resolver); const chunks = ['response ', 'from daemon']; sinon.replace( socket, @@ -207,12 +210,12 @@ describe('lib/forwarder', () => { await new Promise(setImmediate); // removeConfig - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, resolverConfigFile); await new Promise(setImmediate); // spawn assert.calledOnce(child_process.spawn); // watch config - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); await new Promise(setImmediate); // read config read_file_promise.resolve(JSON.stringify(config)); @@ -346,6 +349,8 @@ describe('lib/forwarder', () => { }); it('logs error on second ECONNREFUSED', async () => { + const resolverConfigFile = configFile(resolver); + sinon.replace(fs_promises, 'unlink', sinon.fake.resolves()); forwardToDaemon(resolver, config); @@ -355,12 +360,12 @@ describe('lib/forwarder', () => { await new Promise(setImmediate); // removeConfig - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, resolverConfigFile); await new Promise(setImmediate); // spawn assert.calledOnce(child_process.spawn); // watch config - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); await new Promise(setImmediate); // read config read_file_promise.resolve(JSON.stringify(config)); diff --git a/lib/launcher.js b/lib/launcher.js index d53afee..45bfe04 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -1,8 +1,9 @@ +import path from 'node:path'; import fs from 'node:fs'; import os from 'node:os'; import child_process from 'node:child_process'; import debug from 'debug'; -import { loadConfig, removeConfig } from './config.js'; +import { configFile, loadConfig, removeConfig } from './config.js'; import { forwardCommandToDaemon } from './forwarder.js'; import { SHUTDOWN_COMMAND } from './commands.js'; @@ -51,7 +52,7 @@ export async function launchDaemon(resolver, hash, is_debug_mode) { daemon_process.unref(); } - await waitForConfig(resolver.base); + await waitForConfig(configFile(resolver)); config = await loadConfig(resolver); } catch (err) { error = err; @@ -72,7 +73,7 @@ export async function stopDaemon(resolver, config) { log('Stopping daemon %o', config); try { await Promise.all([ - waitForConfig(resolver.base), + waitForConfig(configFile(resolver)), platformAwareStopDaemon(config) ]); } catch (err) { @@ -127,16 +128,17 @@ function getIDLE(ppid) { } /** - * @param {string} base + * @param {string} resolverConfigFile * @returns {Promise} */ -function waitForConfig(base) { +function waitForConfig(resolverConfigFile) { return new Promise((resolve, reject) => { + const configBasename = path.basename(resolverConfigFile); let timeout = null; const watcher = fs - .watch(base) + .watch(path.dirname(resolverConfigFile)) .on('change', (type, filename) => { - if (type === 'rename' && filename === '.eslint_d') { + if (type === 'rename' && filename === configBasename) { clearTimeout(timeout); watcher.close(); resolve(); diff --git a/lib/launcher.test.js b/lib/launcher.test.js index 933dab2..aaff5fb 100644 --- a/lib/launcher.test.js +++ b/lib/launcher.test.js @@ -1,3 +1,4 @@ +import path from 'node:path'; import fs from 'node:fs'; import fs_promises from 'node:fs/promises'; import os from 'node:os'; @@ -5,6 +6,7 @@ import net from 'node:net'; import child_process from 'node:child_process'; import EventEmitter from 'node:events'; import { assert, refute, match, sinon } from '@sinonjs/referee-sinon'; +import { configFile } from './config.js'; import { createResolver } from './resolver.js'; import { launchDaemon, stopDaemon } from './launcher.js'; import { strictEqual } from 'node:assert'; @@ -169,7 +171,7 @@ describe('lib/launcher', () => { assert.calledWith(process.on, 'SIGINT', match.func); process.on['callback'](); - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolver)); }); it('fails if idle is not a number', () => { @@ -186,14 +188,15 @@ describe('lib/launcher', () => { it('waits for config file to appear and resolves', async () => { const promise = launchDaemon(resolver, 'hash', false); + const resolverConfigFile = configFile(resolver); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(resolverConfigFile)); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); assert.calledOnceWith(watcher.close); await Promise.resolve(); - assert.calledOnceWith(fs_promises.readFile, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.readFile, configFile(resolver)); read_file_promise.resolve('token 123 456 hash'); @@ -223,7 +226,7 @@ describe('lib/launcher', () => { const promise = launchDaemon(resolver, 'hash', false); const error = new Error('read error'); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(configFile(resolver))); read_file_promise.reject(error); await assert.resolves(promise, null); @@ -275,12 +278,13 @@ describe('lib/launcher', () => { it('waits for the config to be removed and resolves', async () => { const promise = stopDaemon(resolver, config); + const resolverConfigFile = configFile(resolver); await new Promise(setImmediate); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(resolverConfigFile)); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); assert.calledOnceWith(watcher.close); await assert.resolves(promise, undefined); @@ -301,13 +305,13 @@ describe('lib/launcher', () => { console.error, 'eslint_d: Error: kill error - removing config' ); - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolver)); }); it('does not wait for the config file removal', async () => { const promise = stopDaemon(resolver, config); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(configFile(resolver))); await assert.resolves(promise, undefined); }); }); @@ -366,6 +370,7 @@ describe('lib/launcher', () => { it('waits for the config to be removed and resolves', async () => { const promise = stopDaemon(resolver, config); + const resolverConfigFile = configFile(resolver); await new Promise((resolve) => { server.on('connection', (socket) => { @@ -373,9 +378,9 @@ describe('lib/launcher', () => { }); }); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(resolverConfigFile)); - watcher.emit('change', 'rename', '.eslint_d'); + watcher.emit('change', 'rename', path.basename(resolverConfigFile)); assert.calledOnceWith(watcher.close); await assert.resolves(promise, undefined); @@ -396,13 +401,13 @@ describe('lib/launcher', () => { console.error, 'eslint_d: Error: kill error - removing config' ); - assert.calledOnceWith(fs_promises.unlink, `${resolver.base}/.eslint_d`); + assert.calledOnceWith(fs_promises.unlink, configFile(resolver)); }); it('does not wait for the config file removal', async () => { const promise = stopDaemon(resolver, config); - assert.calledOnceWith(fs.watch, resolver.base); + assert.calledOnceWith(fs.watch, path.dirname(configFile(resolver))); await assert.resolves(promise, undefined); }); }); diff --git a/test/test.integration.js b/test/test.integration.js index cfe858f..9adfcbb 100644 --- a/test/test.integration.js +++ b/test/test.integration.js @@ -1,6 +1,8 @@ import child_process from 'node:child_process'; +import { createHash } from 'node:crypto'; import path from 'node:path'; import fs from 'node:fs/promises'; +import os from 'node:os'; import { createRequire } from 'node:module'; import { assert, refute } from '@sinonjs/referee-sinon'; @@ -87,7 +89,12 @@ describe('integration tests', () => { const { version: eslint_version } = require( require.resolve('eslint/package.json', { paths: [cwd] }) ); - const config = `${cwd}/node_modules/eslint/.eslint_d`; + const eslintPath = `${cwd}/node_modules/eslint`; + const hash = createHash('sha256'); + + hash.update(path.resolve(eslintPath).replace('\\', '/')); + + const config = path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); let pid; after(unlinkHook(config)); @@ -191,7 +198,12 @@ describe('integration tests', () => { ).forEach((fixture) => { context(fixture, () => { const cwd = path.resolve(`test/fixture/${fixture}`); - const config = `${cwd}/node_modules/eslint/.eslint_d`; + const eslintPath = `${cwd}/node_modules/eslint`; + const hash = createHash('sha256'); + + hash.update(path.resolve(eslintPath).replace('\\', '/')); + + const config = path.join(os.tmpdir(), `${hash.digest('hex')}.eslint_d`); after(unlinkHook(config));