--- /dev/null
+/*
+ * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+let assert = require('assert')
+let fs = require('fs')
+let http = require('http')
+let https = require('https')
+let util = require('util')
+
+let fs_readFile = util.promisify(fs.readFile)
+
+let Listener = function(request_func, options, diag) {
+ if (!this instanceof Listener)
+ throw new Error('Listener is a constructor')
+ this.request_func = request_func
+ this.options = options
+ this.diag = diag || false
+
+ this.server = undefined
+}
+
+Listener.prototype.start = async function() {
+ if (this.server === undefined) {
+ if (this.diag)
+ console.log(
+ `start listening on ${this.options.protocol}//${this.options.address}:${this.options.port}`
+ )
+ let server
+ switch (this.options.protocol) {
+ case 'http:':
+ server = http.createServer()
+ break
+ case 'https:':
+ server = https.createServer(
+ {
+ cert: await fs_readFile(this.options.ssl_cert),
+ key: await fs_readFile(this.options.ssl_key)
+ }
+ )
+ break
+ default:
+ assert(false)
+ }
+ server.on(
+ 'request',
+ (request, response) => this.request_func(request, response, this)
+ // ignore returned promise
+ )
+ try {
+ await new Promise(
+ (resolve, reject) => {
+ server.on('listening', () => {resolve()})
+ server.on('error', err => {reject(err)})
+ server.listen(this.options.port)
+ // should remove the listeners afterwards
+ }
+ )
+ }
+ catch (err) {
+ if (!(err instanceof Error) || err.code != 'EADDRINUSE')
+ throw err
+ if (this.diag)
+ console.log(
+ `address ${this.options.protocol}//${this.options.address}:${this.options.port} in use`
+ )
+ return false
+ }
+ this.server = server
+ }
+ return true
+}
+
+Listener.prototype.stop = async function() {
+ if (this.server !== undefined) {
+ if (this.diag)
+ console.log(
+ `stop listening on ${this.options.protocol}//${this.options.address}:${this.options.port}`
+ )
+ await new Promise(
+ (resolve, reject) => {
+ this.server.close(
+ err => {
+ if (err)
+ reject(err)
+ else
+ resolve()
+ }
+ )
+ }
+ )
+ }
+}
+
+module.exports = Listener
* IN THE SOFTWARE.
*/
+let assert = require('assert')
+
let Resources = function(diag) {
if (!this instanceof Resources)
throw new Error('Resources is a constructor')
this.diag = diag || false
}
-Resources.prototype.ref = function(key, factory_func) {
- if (this.diag)
- console.log(`ref ${key}`)
+Resources.prototype.ref = function(key, factory_func, destroy_func) {
result = this.map.get(key)
if (result === undefined) {
- result = {refs: 0, value: factory_func()}
+ result = {refs: 0, value: factory_func(), destroy_func: destroy_func}
this.map.set(key, result)
}
result.refs += 1
+ if (this.diag)
+ console.log(`ref ${key} refs -> ${result.refs}`)
return result.value
}
Resources.prototype.unref = function(key) {
- if (this.diag)
- console.log(`unref ${key}`)
result = this.map.get(key)
assert(result !== undefined && result.refs > 0)
result.refs -= 1
- if (result.refs === 0)
- this.map.del(key)
+ if (this.diag)
+ console.log(`unref ${key} refs -> ${result.refs}`)
+ if (result.refs === 0) {
+ if (result.destroy_func !== undefined)
+ result.destroy_func(result.value)
+ this.map.delete(key)
+ }
}
module.exports = Resources
+/*
+ * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
let JSTCache = require('@ndcode/jst_cache')
+let Listener = require('./Listener')
let Resources = require('./Resources')
let Site = require('./Site')
let assert = require('assert')
if (!this instanceof Server)
throw new Error('Server is a constructor')
this.resources = resources
- this.options = {
- caching: false,
- listen: [
- {
- port: 8080
- },
- {
- port: 8443,
- protocol: 'https:'
- }
- ],
- hosts: {
- 'localhost': {
- type: 'site',
- root: 'site'
- },
- 'localhost.localdomain': {
- type: 'redirect',
- host: 'localhost'
+ this.options = Object.assign(
+ {
+ caching: false,
+ listeners: [
+ {
+ protocol: 'http:'
+ },
+ {
+ protocol: 'https:'
+ }
+ ],
+ hosts: {
+ 'localhost': {
+ type: 'site',
+ root: 'site'
+ },
+ 'localhost.localdomain': {
+ type: 'redirect',
+ host: 'localhost'
+ }
}
- }
- }
- if (options !== undefined)
- Object.assign(this.options, options)
-
- this.jst_cache = this.resources.ref(
- 'jst_cache:.',
- () => new JSTCache('.', {_jst_server: jst_server}, true)
+ },
+ options || {}
)
- this.roots = {}
- this.listen = prev_server === undefined ? [] : prev_server.listen
-}
+ this.roots = prev_server !== undefined ? prev_server.roots : {}
+ this.jst_cache = undefined
-let compare_listen = (a, b) => {
- if (a.port < b.port)
- return -1
- if (a.port > b.port)
- return 1
- if (a.host < b.host)
- return -1
- if (a.host > b.host)
- return 1
- if (a.protocol < b.protocol)
- return -1
- if (a.protocol > b.protocol)
- return 1
- if (a.ssl_cert < b.ssl_cert)
- return -1
- if (a.ssl_cert > b.ssl_cert)
- return 1
- if (a.ssl_key < b.ssl_key)
- return -1
- if (a.ssl_key > b.ssl_key)
- return 1
- return 0
+ this.listeners = undefined
+ this.request_func =
+ (request, response, listener) =>
+ /*await*/ this.respond(request, response, listener.options.protocol)
}
-Server.prototype.attach = function(server, protocol) {
- server.on(
- 'request',
- (request, response) =>
- this.respond(request, response, protocol)
- // ignore returned promise
+Server.prototype.start = function() {
+ assert(this.jst_cache === undefined)
+ this.jst_cache = this.resources.ref(
+ 'jst_cache:.',
+ () => new JSTCache('.', {_jst_server: jst_server}, true)
)
-}
-Server.prototype.refresh_config = async function() {
- let listen = []
- for (let i = 0; i < this.options.listen.length; ++i)
- listen.push(
- Object.assign(
+ assert(this.listeners === undefined)
+ this.listeners = []
+ for (let i = 0; i < this.options.listeners.length; ++i) {
+ let options
+ switch (this.options.listeners[i].protocol) {
+ case 'http:':
+ options = Object.assign(
{
- port: 0,
- host: '0.0.0.0',
- protocol: 'http:',
- ssl_cert: '_ssl/localhost_cert_bundle.pem',
- ssl_key: '_ssl/localhost_key.pem',
+ protocol: 'https:',
+ address: '0.0.0.0',
+ port: 8080
},
- this.options.listen[i]
+ this.options.listeners[i]
)
- )
- listen.sort(compare_listen)
- //console.log('listen', listen)
-
- // stop all servers no longer in configuration file
- let i = 0
- let j = 0
- while (i < this.listen.length)
- switch (
- j < this.listen.length ?
- compare_listen(this.listen[i], listen[j]) :
- -1
- ) {
- case -1:
- if (this.listen[i].server !== undefined) {
- console.log(
- `stop listening on ${this.listen[i].protocol}//${this.listen[i].host}:${this.listen[i].port}`
- )
- await new Promise(
- (resolve, reject) => {
- this.listen[i].server.close(
- err => {
- if (err)
- reject(err)
- resolve()
- }
- )
- }
- )
- }
- ++i
break
- case 0:
- listen[j++].server = this.listen[i++].server
- break
- case 1:
- listen[j++].server = undefined // just to be on the safe side
+ case 'https:':
+ options = Object.assign(
+ {
+ protocol: 'https:',
+ address: '0.0.0.0',
+ port: 8443,
+ ssl_cert: '_ssl/localhost_cert_bundle.pem',
+ ssl_key: '_ssl/localhost_key.pem'
+ },
+ this.options.listeners[i]
+ )
break
+ default:
+ assert(false)
}
+ let listener = this.resources.ref(
+ `listener:${JSON.stringify(options)}`,
+ () => new Listener(/*this.request_func*/undefined, options, true),
+ listener => listener.stop() // ignore returned Promise
+ )
+ listener.request_func = this.request_func
+ this.listeners.push(listener)
+ }
+}
- // then start all newly mentioned servers (or those which need retrying)
- for (i = 0; i < listen.length; ++i)
- if (listen[i].server === undefined) {
- console.log(
- `start listening on ${listen[i].protocol}//${listen[i].host}:${listen[i].port}`
- )
- let server
- switch (listen[i].protocol) {
- case 'http:':
- server = require('http').createServer()
- break
- case 'https:':
- server = require('https').createServer(
- {
- 'cert': await fs_readFile(listen[i].ssl_cert),
- 'key': await fs_readFile(listen[i].ssl_key)
- }
- )
- break
- default:
- assert(false)
- }
- try {
- await new Promise(
- (resolve, reject) => {
- server.on('listening', () => {resolve()})
- server.on('error', err => {reject(err)})
- server.listen(listen[i].port)
- // should remove the listeners afterwards
- }
- )
- }
- catch (err) {
- if (err.code !== 'EADDRINUSE') // err type ??
- throw err
- console.log(
- `address ${listen[i].protocol}//${listen[i].host}:${listen[i].port} in use`
- )
- continue // leaves listen[i].server undefined, will retry
- }
- this.attach(server, listen[i].protocol)
- if (this.socket_io !== undefined)
- this.socket_io.attach(server)
- listen[i].server = server
- }
+Server.prototype.stop = function() {
+ assert(this.jst_cache !== undefined)
+ this.resources.unref('jst_cache:.')
- this.listen = listen
+ assert(this.listeners !== undefined)
+ for (let i = 0; i < this.listeners.length; ++i)
+ this.resources.unref(
+ `listener:${JSON.stringify(this.listeners[i].options)}`
+ )
+}
+
+Server.prototype.kick = async function() {
+ for (let i = 0; i < this.listeners.length; ++i)
+ await this.listeners[i].start()
}
Server.prototype.serve_internal = function(response, status, mime_type, data) {
+/*
+ * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
let JSONCache = require('@ndcode/json_cache')
let JSONCacheRW = require('@ndcode/json_cache_rw')
let JSTCache = require('@ndcode/jst_cache')
throw new Error('Site is a constructor')
this.resources = resources
this.root = root
- this.options = {
- caching: false,
- mime_types: {
- '.css': 'text/css; charset=utf-8',
- '.html': 'text/html; charset=utf-8',
- '.ico': 'image/x-icon',
- '.jpg': 'image/jpeg',
- '.jpeg': 'image/jpeg',
- '.js': 'application/javascript; charset=utf-8',
- '.json': 'application/json; charset=utf-8',
- '.png': 'image/png',
- '.svg': 'image/svg+xml',
- '.xml': 'text/xml; charset=utf-8'
- }
- }
- if (options !== undefined)
- Object.assign(this.options, options)
+ this.options = Object.assign(
+ {
+ caching: false,
+ mime_types: {
+ '.css': 'text/css; charset=utf-8',
+ '.html': 'text/html; charset=utf-8',
+ '.ico': 'image/x-icon',
+ '.jpg': 'image/jpeg',
+ '.jpeg': 'image/jpeg',
+ '.js': 'application/javascript; charset=utf-8',
+ '.json': 'application/json; charset=utf-8',
+ '.png': 'image/png',
+ '.svg': 'image/svg+xml',
+ '.xml': 'text/xml; charset=utf-8'
+ }
+ },
+ options || {}
+ )
this.socket_io_connect_listeners = [] // later will use this for destruction
this.json_cache = resources.ref(
return async (resources, prev_server) => new _jst_server.Server(
resources,
- {},
+ {
+ listeners: [
+ {protocol: 'http:', port: 8080},
+ {protocol: 'https:', port: 8443},
+ ]
+ },
prev_server
)
#!/usr/bin/env node
+/*
+ * Copyright (C) 2018 Nick Downing <nick@ndcode.org>
+ * SPDX-License-Identifier: MIT
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
// must load index first, to avoid circular dependency issue
let jst_server = require('./index')
// use setTimeout() instead of setInterval() to avoid bunches
// of calls after the computer has been suspended for a while
let refresh_config = async () => {
- let prev_listen = server === undefined ? [] : server.listen
let config = await jst_cache.get('_config/server.jst', true)
if (config !== undefined) {
- server = await config(resources, server)
+ let prev_server = server
+ server = await config(resources, prev_server)
+ await server.start()
+ if (prev_server !== undefined)
+ await prev_server.stop()
}
- server.refresh_config(prev_listen)
+ server.kick()
setTimeout(refresh_config, 5000)
// returned Promise will be ignored
}