Separate out the listen (and other) config into _config/server.json which is read...
authorNick Downing <nick@ndcode.org>
Tue, 30 Oct 2018 04:58:06 +0000 (15:58 +1100)
committerNick Downing <nick@ndcode.org>
Tue, 30 Oct 2018 04:58:20 +0000 (15:58 +1100)
Server.js
_config/mime_types.json [deleted file]
_config/server.json [new file with mode: 0644]
_config/server.jst [deleted file]
_config/sites.jst [new file with mode: 0644]
jst_server.js

index 7dfe9b8..7e4ad5b 100644 (file)
--- a/Server.js
+++ b/Server.js
@@ -16,16 +16,13 @@ let fs_mkdir = util.promisify(fs.mkdir)
 let fs_readFile = util.promisify(fs.readFile)
 let yauzl_open = util.promisify(yauzl.open)
 
-let Server = function(sites, enable_socket_io, enable_caching) {
+let Server = function() {
   if (!this instanceof Server)
     throw Error('Server is a constructor')
 
-  this.sites = sites
-  this.socket_io = enable_socket_io ? new (require('socket.io'))() : undefined
-  this.enable_caching = enable_caching || false
-
-  this.http_servers = []
-  this.https_servers = []
+  this.mime_types = {}
+  this.listen = []
+  this.enable_caching = false
 
   this.build_cache_email = new BuildCache()
   this.build_cache_json = new BuildCache()
@@ -40,40 +37,6 @@ let Server = function(sites, enable_socket_io, enable_caching) {
   this.mime_type_default = 'application/octet-stream'
 }
 
-Server.prototype.listen = async function(port, protocol, ssl_cert, ssl_key) {
-  console.log(`listen on port ${port} protocol ${protocol}`)
-  let server
-  switch (protocol) {
-  case 'http:':
-    server = require('http').createServer()
-    this.http_servers.push(server) // in future use this for shutdown/reload
-    break;
-  case 'https:':
-    server = require('https').createServer(
-      {
-        'cert': await fs_readFile(
-          ssl_cert || '_ssl/localhost_cert_bundle.pem'
-        ),
-        'key': await fs_readFile(
-          ssl_key || '_ssl/localhost_key.pem'
-        )
-      }
-    )
-    this.https_servers.push(server) // in future use this for shutdown/reload
-    break;
-  default:
-    assert(false);
-  }
-  server.listen(port)
-  server.on(
-    'request',
-    (request, response) =>
-      this.respond(request, response, protocol) // ignore returned promise
-  )
-  if (this.socket_io !== undefined)
-    this.socket_io.attach(server)
-}
-
 Server.prototype.get_email = function(pathname) {
   return this.build_cache_email.get(
     pathname,
@@ -201,7 +164,7 @@ Server.prototype.get_zip = function(pathname) {
           (resolve, reject) => {
             write_stream.
             on('finish', () => {resolve(write_stream.getContents())}).
-            on('error', () => {reject()})
+            on('error', err => {reject(err)})
           }
         )
         read_stream.pipe(write_stream)
@@ -252,12 +215,150 @@ Server.prototype.modify_json =
     )
   }
 
+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
+}
+
+Server.prototype.attach = function(server, protocol) {
+  server.on(
+    'request',
+    (request, response) =>
+      this.respond(request, response, protocol)
+      // ignore returned promise
+  )
+}
+
 Server.prototype.refresh_config = async function() {
-  this.mime_types = await this.get_json('_config/mime_types.json')
+  let config = await this.get_json('_config/server.json')
+
+  this.enable_caching = config.enable_caching
+
+  this.mime_types = config.mime_types
   this.mime_type_html =
     Object.prototype.hasOwnProperty.call(this.mime_types, '.html') ?
     this.mime_types['.html'] :
     this.mime_type_default
+
+  let listen = []
+  for (let i = 0; i < config.listen.length; ++i)
+    listen.push(
+      Object.assign(
+        {
+          port: 0,
+          host: '0.0.0.0',
+          protocol: 'http:',
+          ssl_cert: '_ssl/localhost_cert_bundle.pem',
+          ssl_key: '_ssl/localhost_key.pem',
+        },
+        config.listen[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
+      break
+    }
+
+  // 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
+    }
+
+  this.listen = listen
 }
 
 Server.prototype.serve = function(response, status, mime_type, data, message) {
@@ -322,8 +423,8 @@ Server.prototype.redirect = function(response, location, message) {
 let site_factory_default = async (server, root) => new Site(server, root)
 
 Server.prototype.respond = async function(request, response, protocol) {
+  let sites = await this.get_jst('.', '/_config/sites.jst')
   try {
-    await this.refresh_config()
     let parsed_url = url.parse(
       protocol + '//' + (request.headers.host || 'localhost') + request.url,
       true
@@ -331,7 +432,7 @@ Server.prototype.respond = async function(request, response, protocol) {
     //console.log('parsed_url', parsed_url)
 
     if (
-      !Object.prototype.hasOwnProperty.call(this.sites, parsed_url.hostname)
+      !Object.prototype.hasOwnProperty.call(sites, parsed_url.hostname)
     ) {
       this.die(
         response,
@@ -341,7 +442,7 @@ Server.prototype.respond = async function(request, response, protocol) {
       return
     }
 
-    let site = this.sites[parsed_url.hostname]
+    let site = sites[parsed_url.hostname]
     site.respond(
       {
         mime_type: this.mime_type_default,
diff --git a/_config/mime_types.json b/_config/mime_types.json
deleted file mode 100644 (file)
index 6746747..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-{
-  ".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"
-}
diff --git a/_config/server.json b/_config/server.json
new file mode 100644 (file)
index 0000000..c0fffbb
--- /dev/null
@@ -0,0 +1,19 @@
+{
+  "enable_caching": false,
+  "listen": [
+    {"port": 8080},
+    {"port": 8443, "protocol": "https:"}
+  ],
+  "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"
+  }
+}
diff --git a/_config/server.jst b/_config/server.jst
deleted file mode 100644 (file)
index 34202a5..0000000
+++ /dev/null
@@ -1,15 +0,0 @@
-let Server = require('../Server')
-let SiteRedirect = require('../SiteRedirect')
-let SiteRootConfig = require('../SiteRootConfig')
-
-let server = new Server(
-  true, // enable_socket_io
-  false // enable_caching
-)
-server.sites = {
-  'localhost': new SiteRootConfig(server, 'site', '/_config/site.jst'),
-  'localhost.localdomain': new SiteRedirect(server, 'localhost')
-}
-await server.listen(8080, 'http:')
-await server.listen(8443, 'https:')
-return server // later will use to destroy when _config/server.jst changes
diff --git a/_config/sites.jst b/_config/sites.jst
new file mode 100644 (file)
index 0000000..9b2fc64
--- /dev/null
@@ -0,0 +1,7 @@
+let SiteRedirect = require('../SiteRedirect')
+let SiteRootConfig = require('../SiteRootConfig')
+
+return {
+  'localhost': new SiteRootConfig(_server, 'site', '/_config/site.jst'),
+  'localhost.localdomain': new SiteRedirect(_server, 'localhost')
+}
index d5164df..de71534 100755 (executable)
@@ -1,14 +1,14 @@
 #!/usr/bin/env node
 
-let js_template = require('js_template')
+let Server = require('./Server')
 
-;(
-  async () => {
-    try {
-      await js_template('.', '.', '/_config/server.jst')
-    }
-    catch (err) {
-      console.error(err.stack || err.message)
-    }
-  }
-)() // ignore returned promise
+let server = new Server()
+
+// refresh the config immediately, then every 5 seconds,
+// use setTimeout() instead of setInterval() to avoid bunches
+// of calls after the computer has been suspended for a while
+let refresh_config = () => {
+  server.refresh_config()
+  setTimeout(refresh_config, 5000)
+}
+refresh_config()