Commit ff99f4f2 authored by Aral Balkan's avatar Aral Balkan
Browse files

Merge branch 'owncast'

parents 09901417 85ba7f7e
...@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file. ...@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [16.3.0] - 2021-04-14
### Added
- Owncast support.
You can now install and run a production-ready Owncast server with the following command: `site enable --owncast`.
## [16.2.0] - 2021-04-13 ## [16.2.0] - 2021-04-13
### Added ### Added
......
...@@ -41,6 +41,8 @@ We exist in part thanks to patronage by people like you. If you share [our visio ...@@ -41,6 +41,8 @@ We exist in part thanks to patronage by people like you. If you share [our visio
- __Includes [Hugo static site generator](#static-site-generation).__ - __Includes [Hugo static site generator](#static-site-generation).__
- __Includes [one command installation of production-ready Owncast server](#owncast-integration).__
- __[Sync](#sync) to deploy__ (uses rsync for quick deployments). Can also [Live Sync](#live-sync) for live blogging, etc. For sites that implement the [Small Web](https://ar.al/2020/08/07/what-is-the-small-web/) conventions, you can also use the simplified [pull and push commands](#pull-and-push). - __[Sync](#sync) to deploy__ (uses rsync for quick deployments). Can also [Live Sync](#live-sync) for live blogging, etc. For sites that implement the [Small Web](https://ar.al/2020/08/07/what-is-the-small-web/) conventions, you can also use the simplified [pull and push commands](#pull-and-push).
- __Has privacy-respecting [ephemeral statics](#ephemeral-statistics)__. Gives you insight into how your site is being used, not into the people using it. - __Has privacy-respecting [ephemeral statics](#ephemeral-statistics)__. Gives you insight into how your site is being used, not into the people using it.
...@@ -1504,6 +1506,30 @@ This is the JavaScript that’s injected into your page: ...@@ -1504,6 +1506,30 @@ This is the JavaScript that’s injected into your page:
</script> </script>
``` ```
## Owncast integration
[Owncast](https://owncast.online/) is a self-hosted live video and web chat server. Site.js is the easiest way to set up and use Owncast on your production server.
Run:
```shell
site enable --owncast
```
__That’s it!__
### What it does:
- Install Owncast if it isn’t already installed (the Owncast installer, in turn, will install ffmpeg if it isn’t already installed).
- Set up Owncast as a systemd service.
- Set up Site.js as a systemd service.
- Start serving Owncast at http://localhost:8080 and the chat at ws://localhost:8080
- Start Site.js as a TLS proxy at https://your.hostname to serve your Owncast instance over HTTPS and WSS.
As usual, your Let’s Encrypt certificates will be automatically provisioned when you first hit your Owncast instance and renewed automatically for you from there on in.
Note that currently, while Site.js will get automatic updates, Owncast will not. However, newer versions of Site.js will always install the latest release version of Owncast. So, to update Owncast, simply disable and re-enable your server using the command above.
## API ## API
You can also include Site.js as a Node module into your Node project. This section details the API you can use if you do that. You can also include Site.js as a Node module into your Node project. This section details the API you can use if you do that.
......
...@@ -220,8 +220,8 @@ if (platform === 'linux' && cpuArchitecture === 'arm64') { currentPlatformBinary ...@@ -220,8 +220,8 @@ if (platform === 'linux' && cpuArchitecture === 'arm64') { currentPlatformBinary
// Common resources. // Common resources.
const resources = [ const resources = [
'manifest.json', // App-specific metadata generated by this build script (version, etc.) 'manifest.json', // App-specific metadata generated by this build script (version, etc.)
'bin/commands/*', // Conditionally required based on command-line argument. 'bin/commands/*', // Conditionally required based on command-line argument.
// nexe@next does not appear to be making use of the pkg→assets setting in // nexe@next does not appear to be making use of the pkg→assets setting in
// the package.json files of modules. Instead, we specify the files here. // the package.json files of modules. Instead, we specify the files here.
...@@ -232,6 +232,9 @@ const resources = [ ...@@ -232,6 +232,9 @@ const resources = [
// Not sure if this is a different regression in Nexe 4’s resolve dependencies. // Not sure if this is a different regression in Nexe 4’s resolve dependencies.
// Afaik, it was being included correctly before. // Afaik, it was being included correctly before.
'node_modules/@small-tech/instant/client/bundle.js', 'node_modules/@small-tech/instant/client/bundle.js',
// Bundle the Owncast installation script.
'bin/sh/install-owncast.sh',
] ]
const input = 'bin/site.js' const input = 'bin/site.js'
......
...@@ -11,18 +11,18 @@ ...@@ -11,18 +11,18 @@
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
const os = require('os') const os = require('os')
const fs = require('fs') const fs = require('fs-extra')
const path = require('path') const path = require('path')
const childProcess = require('child_process') const childProcess = require('child_process')
const tcpPortUsed = require('tcp-port-used') const tcpPortUsed = require('tcp-port-used')
const status = require('../lib/status')
const runtime = require('../lib/runtime') const runtime = require('../lib/runtime')
const ensure = require('../lib/ensure') const ensure = require('../lib/ensure')
const clr = require('../../lib/clr') const clr = require('../../lib/clr')
const Util = require('../../lib/Util') const Util = require('../../lib/Util')
const Site = require('../../index') const Site = require('../../index')
function enable (args) { function enable (args) {
...@@ -45,6 +45,12 @@ function enable (args) { ...@@ -45,6 +45,12 @@ function enable (args) {
// For details, see: https://source.small-tech.org/site.js/app/-/issues/169 // For details, see: https://source.small-tech.org/site.js/app/-/issues/169
ensure.privilegedPortsAreDisabled() ensure.privilegedPortsAreDisabled()
// Check that a service is not already enabled.
if (status().isEnabled) {
console.log(`\n ❌ ${clr('❨site.js❩ Error:', 'red')} A Site.js service is already enabled.\n\n ${clr('Please disable it before retrying using:', 'yellow')} site ${clr('disable', 'green')}\n`)
process.exit(1)
}
// While we’ve already checked that the Site.js daemon is not // While we’ve already checked that the Site.js daemon is not
// active, above, it is still possible that there is another service // active, above, it is still possible that there is another service
// running on port 443. We could ignore this and enable the systemd // running on port 443. We could ignore this and enable the systemd
...@@ -73,20 +79,28 @@ function enable (args) { ...@@ -73,20 +79,28 @@ function enable (args) {
// //
// Create the systemd service unit. // Create the systemd service unit.
// //
const _pathToServe = args.positional.length === 1 ? args.positional[0] : '.' let pathToServe = args.positional.length === 1 ? args.positional[0] : '.'
const binaryExecutable = '/usr/local/bin/site' const binaryExecutable = '/usr/local/bin/site'
const sourceDirectory = path.resolve(__dirname, '..', '..') const sourceDirectory = path.resolve(__dirname, '..', '..')
const executable = runtime.isBinary ? binaryExecutable : `${childProcess.execSync('which node').toString().trim()} ${path.join(sourceDirectory, 'bin/site.js')}` const executable = runtime.isBinary ? binaryExecutable : `${childProcess.execSync('which node').toString().trim()} ${path.join(sourceDirectory, 'bin/site.js')}`
// It is a common mistake to start the server in a .dynamic folder (or subfolder)
// or a .hugo folder or subfolder. In these cases, try to recover and do the right thing.
let pathToServe
let absolutePathToServe let absolutePathToServe
if (args.positional[0].startsWith(':')) {
if (args.named['owncast']) {
console.log(' 💮️ ❨site.js❩ Owncast setup requested.')
// This is going to be a proxy server for Owncast (at its default port).
// Override any setting that might have been passed (it should not have been).
pathToServe = ':8080'
}
if (pathToServe.startsWith(':')) {
// This is a proxy server, leave as is. // This is a proxy server, leave as is.
absolutePathToServe = args.positional[0] absolutePathToServe = pathToServe
} else { } else {
const paths = Util.magicallyRewritePathToServeIfNecessary(args.positional[0], _pathToServe) // It is a common mistake to start the server in a .dynamic folder (or subfolder)
// or a .hugo folder or subfolder. In these cases, try to recover and do the right thing.
const paths = Util.magicallyRewritePathToServeIfNecessary(args.positional[0], pathToServe)
pathToServe = paths.pathToServe pathToServe = paths.pathToServe
absolutePathToServe = paths.absolutePathToServe absolutePathToServe = paths.absolutePathToServe
} }
...@@ -205,9 +219,94 @@ function enable (args) { ...@@ -205,9 +219,94 @@ function enable (args) {
} }
// //
// Save the systemd service unit. // Save the Site.js systemd service unit.
//
const systemdServicesDirectory = path.join('/', 'etc', 'systemd', 'system')
const siteJsServiceFilePath = path.join(systemdServicesDirectory, 'site.js.service')
fs.writeFileSync(siteJsServiceFilePath, unit, 'utf-8')
//
// Owncast integration. If the --owncast flag is supplied, also:
// - (a) install owncast if it doesn’t already exist.
// - (b) create and install the systemd unit for owncast if it doesn’t already exist.
// //
fs.writeFileSync('/etc/systemd/system/site.js.service', unit, 'utf-8') if (args.named['owncast']) {
console.log(' 💮️ ❨site.js❩ Setting up to serve your Owncast instance.')
// Is Owncast installed? If so, just use it.
// Otherwise, install it.
// Note: we expect Owncast to be installed in ~/owncast.
const owncastDirectory = path.join(Util.unprivilegedHomeDirectory(), 'owncast')
const owncastBinaryPath = path.join(owncastDirectory, 'owncast')
try {
fs.accessSync(owncastBinaryPath, fs.constants.X_OK)
} catch (error) {
// The Owncast binary is not where we expect it to be.
// Install Owncast there.
console.log(` 💮️ ❨site.js❩ Owncast installation not found at ${owncastDirectory}, running installation script…`)
// Ensure that the directory is empty and exists.
if (fs.existsSync(owncastDirectory)) {
console.log(` 💮️ ❨site.js❩ Owncast directory exists at ${owncastDirectory}, removing it before installation.`)
fs.removeSync(owncastDirectory)
}
try {
// Copy the installation script to our settings directory
// and run it from there (for when we’re running from within a Nexe bundle).
const internalOwncastInstallationScriptPath = path.resolve(path.join(__dirname, '..', 'sh', 'install-owncast.sh'))
const installationScript = fs.readFileSync(internalOwncastInstallationScriptPath, 'utf-8')
const externalOwncastInstallationScriptPath = path.join(Site.settingsDirectory, 'install-owncast.sh')
fs.writeFileSync(externalOwncastInstallationScriptPath, installationScript, {encoding: 'utf-8', mode: 0o755})
childProcess.execSync(`OWNCAST_INSTALL_DIRECTORY=${owncastDirectory} ${externalOwncastInstallationScriptPath}`, {env: process.env, stdio: 'pipe'})
console.log(` 💮️ ❨site.js❩ Owncast installed at ${owncastDirectory}.`)
} catch (error) {
console.log(error, `\n ❌ ${clr('❨site.js❩ Error:', 'red')} Could not install Owncast.\n`)
process.exit(1)
}
}
console.log(' 💮️ ❨site.js❩ Owncast installation is OK.')
// Is the Owncast service installed? If so, leave it be.
// Otherwise, install it.
const owncastServiceFilePath = path.join(systemdServicesDirectory, 'owncast.service')
if (!fs.existsSync(owncastServiceFilePath)) {
// Create Owncast service unit based on template at
// https://github.com/owncast/owncast/blob/develop/examples/owncast-sample.service
const owncastUnit = `
[Unit]
Description=Owncast
[Service]
Type=simple
WorkingDirectory=${owncastDirectory}
ExecStart=${owncastBinaryPath}
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
`
fs.writeFileSync(owncastServiceFilePath, owncastUnit, 'utf-8')
}
console.log(' 💮️ ❨site.js❩ Owncast service unit is installed.')
// Also start the Owncast service.
try {
// Start.
childProcess.execSync('sudo systemctl start owncast', {env: process.env, stdio: 'pipe'})
console.log(` 💮️ ❨site.js❩ Owncast launched as daemon.`)
// Enable.
childProcess.execSync('sudo systemctl enable owncast', {env: process.env, stdio: 'pipe'})
console.log(` 💮️ ❨site.js❩ Owncast daemon installed for auto-launch at startup.`)
} catch (error) {
console.log(error, `\n ❌ ${clr('❨site.js❩ Error:', 'red')} Could not enable Owncast server.\n`)
process.exit(1)
}
}
// Pre-flight check: run the server normally and ensure that it starts up properly // Pre-flight check: run the server normally and ensure that it starts up properly
// before installing it as a daemon. If there are any issues we want to catch it here // before installing it as a daemon. If there are any issues we want to catch it here
...@@ -230,7 +329,7 @@ function enable (args) { ...@@ -230,7 +329,7 @@ function enable (args) {
// //
// Enable and start systemd service. // Enable and start the Site.js systemd service.
// //
try { try {
// Start. // Start.
......
...@@ -33,6 +33,13 @@ function status () { ...@@ -33,6 +33,13 @@ function status () {
if (daemonDetails !== null) { if (daemonDetails !== null) {
const textColour = isActive ? 'green' : 'red' const textColour = isActive ? 'green' : 'red'
if (daemonDetails.owncast.isEnabled) {
const owncastActiveState = daemonDetails.owncast.isActive ? clr('active', 'green') : clr('inactive', 'red')
const owncastEnabledState = clr('enabled', 'green')
console.log(`\n Owncast: ${owncastActiveState} and ${owncastEnabledState}.`)
}
if (isActive) { if (isActive) {
console.log(`\n Stats : ${clr(daemonDetails.statisticsUrl, textColour)}`) console.log(`\n Stats : ${clr(daemonDetails.statisticsUrl, textColour)}`)
} }
......
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
const fs = require('fs') const fs = require('fs')
const path = require('path')
const childProcess = require('child_process') const childProcess = require('child_process')
const status = require('../lib/status') const status = require('../lib/status')
const Site = require('../../') const Site = require('../../')
...@@ -28,6 +29,9 @@ function disable () { ...@@ -28,6 +29,9 @@ function disable () {
throwError('Site.js server is not enabled. Nothing to disable.') throwError('Site.js server is not enabled. Nothing to disable.')
} }
const systemdServicesDirectory = path.join('/', 'etc', 'systemd', 'system')
const owncastServiceFilePath = path.join(systemdServicesDirectory, 'owncast.service')
try { try {
// Disable and stop the web server. // Disable and stop the web server.
childProcess.execSync('sudo systemctl disable site.js', {env: process.env, stdio: 'pipe'}) childProcess.execSync('sudo systemctl disable site.js', {env: process.env, stdio: 'pipe'})
...@@ -38,6 +42,19 @@ function disable () { ...@@ -38,6 +42,19 @@ function disable () {
} catch (error) { } catch (error) {
throwError(`Site.js server disabled but could not delete the systemd service file (${error}).`) throwError(`Site.js server disabled but could not delete the systemd service file (${error}).`)
} }
// If we’re managing Owncast, disable that also.
if (fs.existsSync(owncastServiceFilePath)) {
console.log(' 💮️ ❨site.js❩ Also disabling Owncast service.')
childProcess.execSync('sudo systemctl disable owncast', {env: process.env, stdio: 'pipe'})
childProcess.execSync('sudo systemctl stop owncast', {env: process.env, stdio: 'pipe'})
try {
// And remove the systemd service file we created.
fs.unlinkSync('/etc/systemd/system/owncast.service')
} catch (error) {
throwError(`Owncast server disabled but could not delete the systemd service file (${error}).`)
}
}
} catch (error) { } catch (error) {
throwError(`Could not disable Site.js server (${error}).`) throwError(`Could not disable Site.js server (${error}).`)
} }
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
// //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process') const childProcess = require('child_process')
const status = require('../lib/status') const status = require('../lib/status')
const clr = require('../../lib/clr') const clr = require('../../lib/clr')
...@@ -36,6 +38,19 @@ function start () { ...@@ -36,6 +38,19 @@ function start () {
throwError(`Could not start Site.js server (${error}).`) throwError(`Could not start Site.js server (${error}).`)
} }
// Also see if we should start the Owncast service.
const systemdServicesDirectory = path.join('/', 'etc', 'systemd', 'system')
const owncastServiceFilePath = path.join(systemdServicesDirectory, 'owncast.service')
if (fs.existsSync(owncastServiceFilePath)) {
console.log(' 💮️ ❨site.js❩ Also starting Owncast service.')
try {
// Start the Owncast service.
childProcess.execSync('sudo systemctl start owncast', {env: process.env, stdio: 'pipe'})
} catch (error) {
throwError(`Could not start Owncast service (${error}).`)
}
}
console.log('\n 🎈 ❨site.js❩ Server started.\n') console.log('\n 🎈 ❨site.js❩ Server started.\n')
} }
......
...@@ -21,7 +21,7 @@ function status () { ...@@ -21,7 +21,7 @@ function status () {
if (isWindows) { if (isWindows) {
// Daemons are not supported on Windows so we know for sure that it is // Daemons are not supported on Windows so we know for sure that it is
// neither active nor enabled :) // neither active nor enabled :)
return { isActive: false, isEnabled: false } return { isActive: false, isEnabled: false, daemonDetails: {} }
} }
// Note: do not call ensure.systemctl() here as it will // Note: do not call ensure.systemctl() here as it will
...@@ -44,6 +44,27 @@ function status () { ...@@ -44,6 +44,27 @@ function status () {
isEnabled = false isEnabled = false
} }
let owncastIsActive
try {
childProcess.execSync('systemctl is-active owncast', {env: process.env, stdio: 'pipe'})
owncastIsActive = true
} catch (error) {
owncastIsActive = false
}
let owncastIsEnabled
try {
childProcess.execSync('systemctl is-enabled owncast', {env: process.env, stdio: 'pipe'})
owncastIsEnabled = true
} catch (error) {
owncastIsEnabled = false
}
const owncast = {
isActive: owncastIsActive,
isEnabled: owncastIsEnabled
}
let daemonDetails = null let daemonDetails = null
if (isEnabled) { if (isEnabled) {
// Parse the systemd unit configuration file to retrieve daemon details. // Parse the systemd unit configuration file to retrieve daemon details.
...@@ -84,7 +105,8 @@ function status () { ...@@ -84,7 +105,8 @@ function status () {
siteJSBinary, siteJSBinary,
statisticsUrl, statisticsUrl,
pathBeingServed, pathBeingServed,
optionalOptions optionalOptions,
owncast
} }
} }
......
...@@ -6,8 +6,9 @@ ...@@ -6,8 +6,9 @@
// //
////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process') const childProcess = require('child_process')
const status = require('../lib/status') const status = require('../lib/status')
const clr = require('../../lib/clr') const clr = require('../../lib/clr')
...@@ -32,6 +33,19 @@ function stop () { ...@@ -32,6 +33,19 @@ function stop () {
throwError(`Could not stop Site.js server (${error}).`) throwError(`Could not stop Site.js server (${error}).`)
} }
// Also see if we should stop the Owncast service.
const systemdServicesDirectory = path.join('/', 'etc', 'systemd', 'system')
const owncastServiceFilePath = path.join(systemdServicesDirectory, 'owncast.service')
if (fs.existsSync(owncastServiceFilePath)) {
console.log(' 💮️ ❨site.js❩ Also stopping Owncast service.')
try {
// Start the Owncast service.
childProcess.execSync('sudo systemctl stop owncast', {env: process.env, stdio: 'pipe'})
} catch (error) {
throwError(`Could not stop Owncast service (${error}).`)
}
}
console.log('\n 🎈 ❨site.js❩ Server stopped.\n') console.log('\n 🎈 ❨site.js❩ Server stopped.\n')
} }
......
#!/usr/bin/env bash
# shellcheck disable=SC2059
set -o errexit
set -o nounset
set -o pipefail
# Install configuration
if ! [ "${OWNCAST_VERSION:-}" ]; then
OWNCAST_VERSION="0.0.6"
fi
if ! [ "${OWNCAST_INSTALL_DIRECTORY:-}" ]; then
OWNCAST_INSTALL_DIRECTORY="$(pwd)/owncast"
fi
INSTALL_TEMP_DIRECTORY="$(mktemp -d)"
# Set up an exit handler so we can print a help message on failures.
_success=false
shutdown () {
if [ $_success = false ]; then
printf "Your Owncast installation did not complete successfully.\n"
printf "Please report your issue at https://github.com/owncast/owncast/issues\n"
fi
rm -rf "$INSTALL_TEMP_DIRECTORY"
}
trap shutdown INT TERM ABRT EXIT
# Formatting escape codes.
RED='\033[0;31m'
PURPLE='\033[0;35m'
BLUE='\033[1;34m'
GREEN='\033[1;32m'
BOLD='\033[1m'
UNDERLINE='\033[4m'
NC='\033[0m' # No Color
# Activity spinner for background processes.
spinner() {
local -r delay='0.3'
local spinstr='\|/-'
local temp
while ps -p "$1" >> /dev/null; do
temp="${spinstr#?}"
printf " [${BLUE}%c${NC}] " "${spinstr}"
spinstr=${temp}${spinstr%"${temp}"}
sleep "${delay}"
printf "\b\b\b\b\b\b"
done
printf "\r"
}
# Print an error message and exit the program.
errorAndExit() {
printf "${RED}ERROR:${NC} %s" "$1"
exit 1;
}
# Check for a required tool, or exit
requireTool() {
which "$1" >> /dev/null && EC=$? || EC=$?
if [ $EC != 0 ]; then
errorAndExit "Could not locate \"$1\", which is required for installation. Please it install it on your system."
fi
}
# Backup the existing install
backupInstall() {
BACKUP_STAGING="$(mktemp -d)"
mkdir ${BACKUP_STAGING}/backup
BACKUP_DIR="backup"
TIMESTAMP=$(date +%s)
BACKUP_FILE="${TIMESTAMP}-v${OWNCAST_VERSION}".tar.gz
printf "${BLUE}Backing up${NC} your files before upgrading to v${OWNCAST_VERSION}"
FILE_LIST=(
"webroot/*.html"
"webroot/styles/"
"webroot/js"
"webroot/img"
"data/"
"*.yaml"
)
# Make backup directory if it doesn't exist
[[ -d $BACKUP_DIR ]] || mkdir $BACKUP_DIR
for i in "${FILE_LIST[@]}"