import { writeFileSync, unlinkSync, statSync, readFileSync } from 'fs'; import { createServer } from 'net'; import { createServer as createServerHttp } from 'http'; import { removeExistingSocketFile, registerSocketFileCleanup, } from './socket_file.js'; import { EventEmitter } from 'events'; import { tmpdir, userInfo, platform } from 'os'; import { main, getGroupInfo } from './webapp_server'; import express from 'express'; const testSocketFile = `${tmpdir()}/socket_file_tests`; const getChownInfo = async (filePath) => { try { const stats = await statSync(filePath); return { uid: stats.uid, gid: stats.gid }; } catch (error) { console.error(`Error fetching ownership info for ${filePath}:`, error.message); return null; } }; const isMacOS = () => { return platform() === 'darwin'; }; const removeTestSocketFile = () => { try { unlinkSync(testSocketFile); } catch (error) { // Do nothing } } Tinytest.add("socket file - don't remove a non-socket file", test => { writeFileSync(testSocketFile, ""); test.throws( () => { removeExistingSocketFile(testSocketFile); }, /An existing file was found/ ); removeTestSocketFile() }); Tinytest.addAsync( 'socket file - remove a previously existing socket file', (test, done) => { removeTestSocketFile(); const server = createServer(); server.listen(testSocketFile); server.on('listening', Meteor.bindEnvironment(() => { test.isNotUndefined(statSync(testSocketFile)); removeExistingSocketFile(testSocketFile); test.throws( () => { statSync(testSocketFile); }, /ENOENT/ ); server.close(); done(); })); } ); Tinytest.add( 'socket file - no existing socket file, nothing to remove', test => { removeTestSocketFile(); removeExistingSocketFile(testSocketFile); } ); Tinytest.add('socket file - remove socket file on exit', test => { const testEventEmitter = new EventEmitter(); registerSocketFileCleanup(testSocketFile, testEventEmitter); ['exit', 'SIGINT', 'SIGHUP', 'SIGTERM'].forEach(signal => { writeFileSync(testSocketFile, ""); test.isNotUndefined(statSync(testSocketFile)); testEventEmitter.emit(signal); test.throws( () => { statSync(testSocketFile); }, /ENOENT/ ); }); }); function prepareServer() { removeTestSocketFile(); removeExistingSocketFile(testSocketFile); const testEventEmitter = new EventEmitter(); registerSocketFileCleanup(testSocketFile, testEventEmitter); const server = createServer(); server.listen(testSocketFile); const app = express(); const httpServer = createServerHttp(app); return { httpServer, server }; } function closeServer({ httpServer, server }) { return new Promise((resolve) => { httpServer.on( "listening", Meteor.bindEnvironment(() => { process.env.UNIX_SOCKET_PATH = ""; process.env.UNIX_SOCKET_GROUP = ""; removeExistingSocketFile(testSocketFile); server.close(); httpServer.close(); resolve(); }) ); }); } testAsyncMulti( "socket usage - use socket file for inter-process communication", [ async (test) => { // use UNIX_SOCKET_PATH const { httpServer, server } = prepareServer(); process.env.UNIX_SOCKET_PATH = testSocketFile; const result = await main({ httpServer }); test.equal(result, "DAEMON"); const currentGid = userInfo({ encoding: "utf8" })?.gid; test.equal((await getChownInfo(testSocketFile))?.gid, currentGid); return closeServer({ httpServer, server }); }, async (test) => { // use UNIX_SOCKET_PATH and UNIX_SOCKET_GROUP const { httpServer, server } = prepareServer(); const groupToUse = Boolean(process.env.TRAVIS) && 'travis' || (isMacOS() ? 'staff' : 'root'); process.env.UNIX_SOCKET_PATH = testSocketFile; process.env.UNIX_SOCKET_GROUP = groupToUse; process.env.UNIX_SOCKET_PERMISSIONS = '777'; const result = await main({ httpServer }); test.equal(result, "DAEMON"); test.equal((await getChownInfo(testSocketFile))?.gid, getGroupInfo(process.env.UNIX_SOCKET_GROUP)?.gid); return closeServer({ httpServer, server }); }, ] );