New scheme to deal with listeners, there is a Listener object for each listener encap...
authorNick Downing <nick@ndcode.org>
Tue, 4 Dec 2018 06:46:40 +0000 (17:46 +1100)
committerNick Downing <nick@ndcode.org>
Tue, 4 Dec 2018 06:46:40 +0000 (17:46 +1100)
Listener.js [new file with mode: 0644]
Resources.js
Server.js
Site.js
_config/server.jst
cli.js

diff --git a/Listener.js b/Listener.js
new file mode 100644 (file)
index 0000000..8dd0968
--- /dev/null
@@ -0,0 +1,114 @@
+/*
+ * 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
index 9041d73..2752035 100644 (file)
@@ -21,6 +21,8 @@
  * IN THE SOFTWARE.
  */
 
+let assert = require('assert')
+
 let Resources = function(diag) {
   if (!this instanceof Resources)
     throw new Error('Resources is a constructor')
@@ -28,26 +30,29 @@ let Resources = function(diag) {
   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
index 6cd296f..1e09429 100644 (file)
--- a/Server.js
+++ b/Server.js
@@ -1,4 +1,28 @@
+/*
+ * 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')
@@ -13,174 +37,101 @@ let Server = function(resources, options, prev_server) {
   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) {
diff --git a/Site.js b/Site.js
index b9901ec..36fb5d9 100644 (file)
--- a/Site.js
+++ b/Site.js
@@ -1,3 +1,26 @@
+/*
+ * 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')
@@ -23,23 +46,24 @@ let Site = function(resources, root, options) {
     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(
index cadf50d..f8d6b82 100644 (file)
@@ -1,5 +1,10 @@
 return async (resources, prev_server) => new _jst_server.Server(
   resources,
-  {},
+  {
+    listeners: [
+      {protocol: 'http:', port: 8080},
+      {protocol: 'https:', port: 8443},
+    ]
+  },
   prev_server
 )
diff --git a/cli.js b/cli.js
index 52d3892..f8979da 100755 (executable)
--- a/cli.js
+++ b/cli.js
@@ -1,5 +1,28 @@
 #!/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')
 
@@ -18,12 +41,15 @@ let server = undefined
 // 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
 }