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.
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
### Added
......
......@@ -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 [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).
- __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:
</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
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.
......
......@@ -232,6 +232,9 @@ const resources = [
// Not sure if this is a different regression in Nexe 4’s resolve dependencies.
// Afaik, it was being included correctly before.
'node_modules/@small-tech/instant/client/bundle.js',
// Bundle the Owncast installation script.
'bin/sh/install-owncast.sh',
]
const input = 'bin/site.js'
......
......@@ -11,18 +11,18 @@
//////////////////////////////////////////////////////////////////////
const os = require('os')
const fs = require('fs')
const fs = require('fs-extra')
const path = require('path')
const childProcess = require('child_process')
const tcpPortUsed = require('tcp-port-used')
const status = require('../lib/status')
const runtime = require('../lib/runtime')
const ensure = require('../lib/ensure')
const clr = require('../../lib/clr')
const Util = require('../../lib/Util')
const Site = require('../../index')
function enable (args) {
......@@ -45,6 +45,12 @@ function enable (args) {
// For details, see: https://source.small-tech.org/site.js/app/-/issues/169
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
// active, above, it is still possible that there is another service
// running on port 443. We could ignore this and enable the systemd
......@@ -73,20 +79,28 @@ function enable (args) {
//
// 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 sourceDirectory = path.resolve(__dirname, '..', '..')
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
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.
absolutePathToServe = args.positional[0]
absolutePathToServe = pathToServe
} 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
absolutePathToServe = paths.absolutePathToServe
}
......@@ -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
// before installing it as a daemon. If there are any issues we want to catch it here
......@@ -230,7 +329,7 @@ function enable (args) {
//
// Enable and start systemd service.
// Enable and start the Site.js systemd service.
//
try {
// Start.
......
......@@ -33,6 +33,13 @@ function status () {
if (daemonDetails !== null) {
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) {
console.log(`\n Stats : ${clr(daemonDetails.statisticsUrl, textColour)}`)
}
......
......@@ -8,6 +8,7 @@
//////////////////////////////////////////////////////////////////////
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const status = require('../lib/status')
const Site = require('../../')
......@@ -28,6 +29,9 @@ function 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 {
// Disable and stop the web server.
childProcess.execSync('sudo systemctl disable site.js', {env: process.env, stdio: 'pipe'})
......@@ -38,6 +42,19 @@ function disable () {
} catch (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) {
throwError(`Could not disable Site.js server (${error}).`)
}
......
......@@ -6,6 +6,8 @@
//
//////////////////////////////////////////////////////////////////////
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const status = require('../lib/status')
const clr = require('../../lib/clr')
......@@ -36,6 +38,19 @@ function start () {
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')
}
......
......@@ -21,7 +21,7 @@ function status () {
if (isWindows) {
// Daemons are not supported on Windows so we know for sure that it is
// 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
......@@ -44,6 +44,27 @@ function status () {
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
if (isEnabled) {
// Parse the systemd unit configuration file to retrieve daemon details.
......@@ -84,7 +105,8 @@ function status () {
siteJSBinary,
statisticsUrl,
pathBeingServed,
optionalOptions
optionalOptions,
owncast
}
}
......
......@@ -6,8 +6,9 @@
//
//////////////////////////////////////////////////////////////////////
const fs = require('fs')
const path = require('path')
const childProcess = require('child_process')
const status = require('../lib/status')
const clr = require('../../lib/clr')
......@@ -32,6 +33,19 @@ function stop () {
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')
}
......
#!/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[@]}"
do
:
cp -r ${FILE_LIST[@]} ${BACKUP_STAGING}/backup
done
pushd ${BACKUP_STAGING} >> /dev/null
tar zcf ${BACKUP_FILE} backup & >> /dev/null
spinner $!
popd >> /dev/null
mv ${BACKUP_STAGING}/${BACKUP_FILE} backup/
rm -rf ${BACKUP_STAGING}
printf "${BLUE}Backed up${NC} your files before upgrading to v${OWNCAST_VERSION} [${GREEN}${NC}]\n"
}
main () {
printf "${PURPLE}${BOLD}Owncast Installer v%s ${NC}\n\n" "$OWNCAST_VERSION"
requireTool "curl"
requireTool "unzip"
requireTool "tar"
requireTool "which"
# Determine operating system & architecture
case $(uname -s) in
"Darwin")
OWNCAST_ARCH="64bit"
PLATFORM="macOS"
FFMPEG_VERSION="4.3.1"
FFMPEG_DOWNLOAD_URL="https://evermeet.cx/ffmpeg/ffmpeg-${FFMPEG_VERSION}.zip"
FFMPEG_TARGET_FILE="${INSTALL_TEMP_DIRECTORY}/ffmpeg.zip"
;;
"Linux")
case "$(uname -m)" in
"x86_64")
FFMPEG_ARCH="linux-x64"
OWNCAST_ARCH="64bit"
;;
i?86)
FFMPEG_ARCH="linux-ia32"
OWNCAST_ARCH="32bit"
;;
armv7?)
FFMPEG_ARCH="linux-arm"
OWNCAST_ARCH="arm7"
;;
*)
errorAndExit "Unsupported CPU architecture $(uname -m)"
;;
esac
PLATFORM="linux"
FFMPEG_VERSION="b4.3.1"
FFMPEG_DOWNLOAD_URL="https://github.com/eugeneware/ffmpeg-static/releases/download/${FFMPEG_VERSION}/${FFMPEG_ARCH}"
FFMPEG_TARGET_FILE="${OWNCAST_INSTALL_DIRECTORY}/ffmpeg"
;;
*)
errorAndExit "Unsupported operating system $(uname -s)"
;;
esac
# Build release download URL
OWNCAST_URL="https://github.com/owncast/owncast/releases/download/v${OWNCAST_VERSION}/owncast-${OWNCAST_VERSION}-${PLATFORM}-${OWNCAST_ARCH}.zip"
OWNCAST_TARGET_FILE="${INSTALL_TEMP_DIRECTORY}/owncast-${OWNCAST_VERSION}-${PLATFORM}-${OWNCAST_ARCH}.zip"
# If the install directory exists already then cd into it and upgrade
if [[ -d "$OWNCAST_INSTALL_DIRECTORY" && -x "$OWNCAST_INSTALL_DIRECTORY/owncast" ]]; then
printf "${BLUE}Existing install found${NC} in ${OWNCAST_INSTALL_DIRECTORY}. Will update it to v${OWNCAST_VERSION}. If this is incorrect remove the directory and rerun the installer.\n"
cd $OWNCAST_INSTALL_DIRECTORY
OWNCAST_INSTALL_DIRECTORY="./"
backupInstall
# If the owncast binary exists then upgrade
elif [ -x ./owncast ]; then
printf "${BLUE}Existing install found${NC} in this directory. Will update it to v${OWNCAST_VERSION}. If this is incorrect remove the directory and rerun the installer.\n"
backupInstall
OWNCAST_INSTALL_DIRECTORY="./"
else
# Create target directory
mkdir -p "$OWNCAST_INSTALL_DIRECTORY"
printf "${GREEN}Created${NC} directory [${GREEN}${NC}]\n"
fi
# Download release
printf "${BLUE}Downloading${NC} Owncast v${OWNCAST_VERSION} for ${PLATFORM}"
curl -s -L ${OWNCAST_URL} --output "${OWNCAST_TARGET_FILE}" &
spinner $!
printf "${GREEN}Downloaded${NC} Owncast v${OWNCAST_VERSION} for ${PLATFORM} [${GREEN}${NC}]\n"
# Unzip release
unzip -oq "$OWNCAST_TARGET_FILE" -d "$OWNCAST_INSTALL_DIRECTORY"
# Delete release zip file
rm "$OWNCAST_TARGET_FILE"
# Check for ffmpeg
which ffmpeg >> /dev/null && EC=$? || EC=$?
if [ $EC -ne 0 ]; then
# Download ffmpeg
printf "${BLUE}Downloading${NC} ffmpeg v${FFMPEG_VERSION} "
curl -s -L ${FFMPEG_DOWNLOAD_URL} --output "${FFMPEG_TARGET_FILE}" &
spinner $!
printf "${GREEN}Downloaded${NC} ffmpeg because it was not found on your system [${GREEN}${NC}]\n"
if [[ "$FFMPEG_TARGET_FILE" == *.zip ]]; then
unzip -oq "$FFMPEG_TARGET_FILE" -d "$OWNCAST_INSTALL_DIRECTORY"
rm "$FFMPEG_TARGET_FILE"
fi
chmod u+x "${OWNCAST_INSTALL_DIRECTORY}/ffmpeg"
fi
_success=true
printf "\n"
printf "${GREEN}Success!${NC} Run owncast by changing to the ${BOLD}owncast${NC} directory and run ${BOLD}./owncast${NC}.\n"
printf "The default port is ${BOLD}8080${NC} and the default streaming key is ${BOLD}abc123${NC}.\n"
printf "Visit ${UNDERLINE}https://owncast.online/docs/configuration/${NC} to learn how to configure your new Owncast server."
printf "\n\n"
}
main
{
"name": "@small-tech/site.js",
"version"