From eb8603f7765aef12d35b53b49f1a8ff75367b528 Mon Sep 17 00:00:00 2001 From: Alexis Tyler Date: Fri, 25 Oct 2019 16:56:27 +1030 Subject: [PATCH] fix(module:app/server): fix EADDRINUSE --- app/server.js | 222 +++++++++++++++++++++++++++----------------------- 1 file changed, 121 insertions(+), 101 deletions(-) diff --git a/app/server.js b/app/server.js index 32250fc86..9e6aaee1a 100644 --- a/app/server.js +++ b/app/server.js @@ -9,8 +9,6 @@ module.exports = function ($injector, path, fs, net, express, config, log, getEndpoints, stoppable, http) { const app = express(); const port = config.get('graphql-api-port'); - let server; - const {ApolloServer} = $injector.resolve('apollo-server-express'); const graphql = $injector.resolvePath(path.join(__dirname, '/graphql')); @@ -34,69 +32,126 @@ module.exports = function ($injector, path, fs, net, express, config, log, getEn res.status(error.status || 500).send(error); }); - // Return an object with start and stop methods. + // Generate types and schema for core modules + // { + // const jsdocx = $injector.resolve('jsdoc-x'); + // const path = $injector.resolve('path'); + // const paths = $injector.resolve('paths'); + // const moduleDir = path.join(paths.get('core'), '/modules/'); + + // console.info('----------------------------') + // console.info('Parsing core modules') + // const docs = jsdocx.parse(`${moduleDir}/**/*.js`) + // .then(docs => { + // console.log('%s', JSON.stringify(docs, null, 0)) + // // const x = gql` + // // type Disk { + // // id: String! + // // } + // // `; + // }) + // .catch(error => console.error(error.stack)); + // console.info('----------------------------') + // } + // (() => { + // const documentedTypeDefs = docs + // .filter(doc => !doc.undocumented) + // .filter(doc => doc.kind === 'typedef') + // .filter(doc => !doc.type.names.find(name => name.startsWith('Array'))); + + // documentedTypeDefs.map(doc => { + // const props = doc.properties ? Object.values(doc.properties).map(prop => { + // const desc = prop.description ? ('"""' + prop.description + '"""') : ''; + // const reservedWords = { + // boolean: 'Boolean', + // number: 'Number', + // string: 'String' + // }; + // const propType = prop.type.names[0]; + // const type = Object.keys(reservedWords).includes(propType) ? reservedWords[propType] : propType; + + // if (doc.name === 'DeviceInfo') { + // console.log({ doc }); + // } + + // return `${desc}\n${prop.name}: ${prop.optional ? '[' : ''}${type || 'JSON'}${!prop.optional ? '!' : ']'}`; + // }) : []; + // const template = ` + // type ${doc.name} { + // ${props.join('\n')} + // } + // `; + + // return template; + // }) + // .forEach(doc => console.info('%s', doc)); + // })() + + const httpServer = http.createServer(app); + const server = stoppable(httpServer); + + const handleError = error => { + if (error.code !== 'EADDRINUSE') { + throw error; + } + + if (!isNaN(parseInt(port, 10))) { + throw error; + } + + server.close(); + + net.connect({ + path: port + }, () => { + // Really in use: re-throw + throw error; + }).on('error', error => { + if (error.code !== 'ECONNREFUSED') { + log.error(error); + + process.exitCode = 1; + } + + // Not in use: delete it and re-listen + fs.unlinkSync(port); + + setTimeout(() => { + server.listen(port); + }, 1000); + }); + }; + + // Port is a UNIX socket file + if (isNaN(parseInt(port, 10))) { + server.on('listening', () => { + // In production this will let pm2 know we're ready + if (process.send) { + process.send('ready'); + } + + // Set permissions + return fs.chmodSync(port, 660); + }); + + server.on('error', handleError); + + process.on('uncaughtException', error => { + // Skip EADDRINUSE as it's already handled above + if (error.code !== 'EADDRINUSE') { + throw error; + } + }); + } + + // Add graphql subscription handlers + graphApp.installSubscriptionHandlers(server); + + // Return an object with a server and start/stop async methods. return { + server, async start() { - // Generate types and schema for core modules - // { - // const jsdocx = $injector.resolve('jsdoc-x'); - // const path = $injector.resolve('path'); - // const paths = $injector.resolve('paths'); - // const moduleDir = path.join(paths.get('core'), '/modules/'); - - // console.info('----------------------------') - // console.info('Parsing core modules') - // const docs = jsdocx.parse(`${moduleDir}/**/*.js`) - // .then(docs => { - // console.log('%s', JSON.stringify(docs, null, 0)) - // // const x = gql` - // // type Disk { - // // id: String! - // // } - // // `; - // }) - // .catch(error => console.error(error.stack)); - // console.info('----------------------------') - // } - // (() => { - // const documentedTypeDefs = docs - // .filter(doc => !doc.undocumented) - // .filter(doc => doc.kind === 'typedef') - // .filter(doc => !doc.type.names.find(name => name.startsWith('Array'))); - - // documentedTypeDefs.map(doc => { - // const props = doc.properties ? Object.values(doc.properties).map(prop => { - // const desc = prop.description ? ('"""' + prop.description + '"""') : ''; - // const reservedWords = { - // boolean: 'Boolean', - // number: 'Number', - // string: 'String' - // }; - // const propType = prop.type.names[0]; - // const type = Object.keys(reservedWords).includes(propType) ? reservedWords[propType] : propType; - - // if (doc.name === 'DeviceInfo') { - // console.log({ doc }); - // } - - // return `${desc}\n${prop.name}: ${prop.optional ? '[' : ''}${type || 'JSON'}${!prop.optional ? '!' : ']'}`; - // }) : []; - // const template = ` - // type ${doc.name} { - // ${props.join('\n')} - // } - // `; - - // return template; - // }) - // .forEach(doc => console.info('%s', doc)); - // })() - - const httpServer = http.createServer(app); - - graphApp.installSubscriptionHandlers(httpServer); - - server = stoppable(httpServer).listen(port, () => { + return server.listen(port, () => { // Downgrade process user to owner of this file return fs.stat(__filename, (error, stats) => { if (error) { @@ -106,43 +161,6 @@ module.exports = function ($injector, path, fs, net, express, config, log, getEn return process.setuid(stats.uid); }); }); - - // Port is a UNIX socket file - if (isNaN(parseInt(port, 10))) { - server.on('listening', () => { - // In production this will let pm2 know we're ready - if (process.send) { - process.send('ready'); - } - - // Set permissions - return fs.chmodSync(port, 660); - }); - - // Double-check EADDRINUSE - server.on('error', error => { - if (error.code !== 'EADDRINUSE') { - throw error; - } - - net.connect({ - path: port - }, () => { - // Really in use: re-throw - throw error; - }).on('error', error => { - if (error.code !== 'ECONNREFUSED') { - throw error; - } - - // Not in use: delete it and re-listen - fs.unlinkSync(port); - server.listen(port); - }); - }); - } - - return server; }, stop() { // Stop the server from accepting new connections and close existing connections @@ -154,7 +172,9 @@ module.exports = function ($injector, path, fs, net, express, config, log, getEn process.exit(1); } - log.info('Server shutting down..'); + const name = process.title; + const serverName = `@unraid/${name}`; + log.info(`Successfully stopped ${serverName}`); // Gracefully exit process.exitCode = 0;