Verified Commit 95231f34 authored by Aral Balkan's avatar Aral Balkan
Browse files

Implement better error handling; bump version

Differentiates between generic port in use errors and Indie Web Server already being enabled and gives helpful instructions for the latter while succinctly reporting and failing on the former. There should no longer be any silent successes or stack traces for this common error.
parent d09c3805
......@@ -8,11 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- Nothing yet.
## [9.0.1] - 2019-04-30
## [9.1.0] - 2019-04-30
### Fixed
### Added
- Detects already enabled and running server and alerts you instead of silently appearing to have succeeded while continuing to serve the existing site.
- Better error handling.
## [9.0.0] - 2019-04-29
......
......@@ -11,6 +11,8 @@ const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const tcpPortUsed = require('tcp-port-used')
const runtime = require('../lib/runtime')
const ensure = require('../lib/ensure')
const clr = require('../lib/cli').clr
......@@ -23,78 +25,99 @@ function enable (options) {
//
ensure.systemctl()
ensure.serverDaemonNotActive()
ensure.root()
//
// Create the systemd service unit.
//
const pathToServe = options.pathToServe
const binaryExecutable = '/usr/local/bin/web-server'
const sourceDirectory = path.resolve(__dirname, '..', '..')
const nodeExecutable = `node ${path.join(sourceDirectory, 'bin/web-server.js')}`
const executable = runtime.isBinary ? binaryExecutable : nodeExecutable
const absolutePathToServe = path.resolve(pathToServe)
// Expectation: At this point, regardless of whether we are running as a regular
// Node script or as a standalone executable created with Nexe, all paths should
// be set correctly.
// Get the regular account name (i.e, the unprivileged account that is
// running the current process via sudo).
const accountUID = parseInt(process.env.SUDO_UID)
if (!accountUID) {
console.error(`\n 👿 Error: could not get account ID.\n`)
process.exit(1)
}
let accountName
try {
// Courtesy: https://www.unix.com/302402784-post4.html
accountName = childProcess.execSync(`awk -v val=${accountUID} -F ":" '$3==val{print $1}' /etc/passwd`, {env: process.env, stdio: 'pipe'}).toString()
} catch (error) {
console.error(`\n 👿 Error: could not get account name \n${error}.`)
process.exit(1)
}
const unit = `[Unit]
Description=Indie Web Server
Documentation=https://ind.ie/web-server/
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=${accountName}
Environment=PATH=/sbin:/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
RestartSec=1
Restart=always
ExecStart=${executable} global ${absolutePathToServe}
[Install]
WantedBy=multi-user.target
`
// Save the systemd service unit.
fs.writeFileSync('/etc/systemd/system/web-server.service', unit, 'utf-8')
//
// Enable and start systemd service.
//
try {
// Start.
childProcess.execSync('sudo systemctl start web-server', {env: process.env, stdio: 'pipe'})
console.log(`${webServer.version()}\n 😈 Launched as daemon on ${clr(`https://${os.hostname()}`, 'green')} serving ${clr(pathToServe, 'cyan')}\n`)
// Enable.
childProcess.execSync('sudo systemctl enable web-server', {env: process.env, stdio: 'pipe'})
console.log(` 😈 Installed for auto-launch at startup.\n`)
} catch (error) {
console.error(error, `\n 👿 Error: could not enable web server.\n`)
process.exit(1)
}
// While we’ve already checked that the Indie Web Server daemon is not
// active, above, it is still possible that there is another service
// running on port 443. We could ignore this and enable the systemd
// service anyway and this command would succeed and our server would
// start being served when the blocking service is stopped. However, this
// is misleading as the command succeeding makes it appear as if the
// server has started running. So, instead, we detect if the port
// is already in use and, if it is, refuse to install and activate the
// service. This is should provide the least amount of surprise in usage.
tcpPortUsed.check(options.port)
.then(inUse => {
if (inUse) {
console.log(`\n 🤯 Error: Cannot start server. Port ${clr(options.port.toString(), 'cyan')} is already in use.\n`)
process.exit(1)
} else {
// Ensure we are root (we do this here instead of before the asynchronous call to
// avoid any timing-related issues around a restart and a port-in-use error).
ensure.root()
//
// Create the systemd service unit.
//
const pathToServe = options.pathToServe
const binaryExecutable = '/usr/local/bin/web-server'
const sourceDirectory = path.resolve(__dirname, '..', '..')
const nodeExecutable = `node ${path.join(sourceDirectory, 'bin/web-server.js')}`
const executable = runtime.isBinary ? binaryExecutable : nodeExecutable
const absolutePathToServe = path.resolve(pathToServe)
// Expectation: At this point, regardless of whether we are running as a regular
// Node script or as a standalone executable created with Nexe, all paths should
// be set correctly.
// Get the regular account name (i.e, the unprivileged account that is
// running the current process via sudo).
const accountUID = parseInt(process.env.SUDO_UID)
if (!accountUID) {
console.error(`\n 👿 Error: could not get account ID.\n`)
process.exit(1)
}
let accountName
try {
// Courtesy: https://www.unix.com/302402784-post4.html
accountName = childProcess.execSync(`awk -v val=${accountUID} -F ":" '$3==val{print $1}' /etc/passwd`, {env: process.env, stdio: 'pipe'}).toString()
} catch (error) {
console.error(`\n 👿 Error: could not get account name \n${error}.`)
process.exit(1)
}
const unit = `[Unit]
Description=Indie Web Server
Documentation=https://ind.ie/web-server/
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=${accountName}
Environment=PATH=/sbin:/usr/bin:/usr/local/bin
Environment=NODE_ENV=production
RestartSec=1
Restart=always
ExecStart=${executable} global ${absolutePathToServe}
[Install]
WantedBy=multi-user.target
`
// Save the systemd service unit.
fs.writeFileSync('/etc/systemd/system/web-server.service', unit, 'utf-8')
//
// Enable and start systemd service.
//
try {
// Start.
childProcess.execSync('sudo systemctl start web-server', {env: process.env, stdio: 'pipe'})
console.log(`${webServer.version()}\n 😈 Launched as daemon on ${clr(`https://${os.hostname()}`, 'green')} serving ${clr(pathToServe, 'cyan')}\n`)
// Enable.
childProcess.execSync('sudo systemctl enable web-server', {env: process.env, stdio: 'pipe'})
console.log(` 😈 Installed for auto-launch at startup.\n`)
} catch (error) {
console.error(error, `\n 👿 Error: could not enable web server.\n`)
process.exit(1)
}
}
})
}
module.exports = enable
......@@ -8,21 +8,32 @@
//////////////////////////////////////////////////////////////////////
const webServer = require('../../index')
const tcpPortUsed = require('tcp-port-used')
const clr = require('../lib/cli').clr
function serve (options) {
//
// Start a regular server process.
//
const server = webServer.serve({
path: options.pathToServe,
port: options.port,
global: true
})
// Exit on known errors as we have already logged them to console.
// (Otherwise, the stack trace will be output for debugging purposes.)
server.on('indie-web-server-address-already-in-use', () => {
process.exit(1)
tcpPortUsed.check(options.port)
.then(inUse => {
if (inUse) {
console.log(`\n 🤯 Error: Cannot start server. Port ${clr(options.port.toString(), 'cyan')} is already in use.\n`)
process.exit(1)
} else {
//
// Start a regular server process.
//
const server = webServer.serve({
path: options.pathToServe,
port: options.port,
global: true
})
// Exit on known errors as we have already logged them to console.
// (Otherwise, the stack trace will be output for debugging purposes.)
server.on('indie-web-server-address-already-in-use', () => {
process.exit(1)
})
}
})
}
......
......@@ -8,22 +8,27 @@
//////////////////////////////////////////////////////////////////////
const webServer = require('../../index')
const tcpPortUsed = require('tcp-port-used')
const clr = require('../lib/cli').clr
function serve (options) {
//
// Start a regular server process.
//
const server = webServer.serve({
path: options.pathToServe,
port: options.port,
global: false
})
// Exit on known errors as we have already logged them to console.
// (Otherwise, the stack trace will be output for debugging purposes.)
server.on('indie-web-server-address-already-in-use', () => {
process.exit(1)
})
tcpPortUsed.check(options.port)
.then(inUse => {
if (inUse) {
console.log(`\n 🤯 Error: Cannot start server. Port ${clr(options.port.toString(), 'cyan')} is already in use.\n`)
process.exit(1)
} else {
//
// Start a regular server process with locally-trusted security certificates.
//
webServer.serve({
path: options.pathToServe,
port: options.port,
global: false
})
}
})
}
module.exports = serve
{
"name": "@ind.ie/web-server",
"version": "8.0.0",
"version": "9.0.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......@@ -838,6 +838,11 @@
"integrity": "sha1-9dJgKStmDghO/0zbyfCK0yR0SLU=",
"dev": true
},
"deep-is": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
"integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ="
},
"defaults": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz",
......@@ -1931,6 +1936,11 @@
"p-is-promise": "^1.1.0"
}
},
"ip-regex": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz",
"integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk="
},
"ipaddr.js": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.8.0.tgz",
......@@ -2119,11 +2129,26 @@
"has-symbols": "^1.0.0"
}
},
"is-url": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
"integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww=="
},
"is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
"integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA=="
},
"is2": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz",
"integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==",
"requires": {
"deep-is": "^0.1.3",
"ip-regex": "^2.1.0",
"is-url": "^1.2.2"
}
},
"isobject": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz",
......@@ -3459,6 +3484,30 @@
}
}
},
"tcp-port-used": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/tcp-port-used/-/tcp-port-used-1.0.1.tgz",
"integrity": "sha512-rwi5xJeU6utXoEIiMvVBMc9eJ2/ofzB+7nLOdnZuFTmNCLqRiQh2sMG9MqCxHU/69VC/Fwp5dV9306Qd54ll1Q==",
"requires": {
"debug": "4.1.0",
"is2": "2.0.1"
},
"dependencies": {
"debug": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.0.tgz",
"integrity": "sha512-heNPJUJIqC+xB6ayLAMHaIrmN9HKa7aQO8MGqKpvCA+uJYVcvR6l5kgdrhRuwPFHU7P5/A1w0BjByPHwpfTDKg==",
"requires": {
"ms": "^2.1.1"
}
},
"ms": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz",
"integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg=="
}
}
},
"through": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
......
{
"name": "@ind.ie/web-server",
"version": "9.0.1",
"version": "9.1.0",
"description": "A secure and seamless Small Tech personal web server.",
"main": "index.js",
"bin": "bin/web-server.js",
......@@ -31,7 +31,8 @@
"morgan": "^1.9.1",
"recursive-copy": "^2.0.10",
"redirect-https": "^1.3.0",
"shelljs": "^0.8.3"
"shelljs": "^0.8.3",
"tcp-port-used": "^1.0.1"
},
"devDependencies": {
"nexe": "^3.1.0",
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment