#!/bin/sh # Installs or upgrades the Zoxobit Helper on Linux with systemd, or on a # Mac without the Zoxobit app: # # curl -fsSL https://zoxohome.com/download/install.sh | sh # # It downloads the version zoxohome.com offers and checks it against # SHA256SUMS.txt. On Linux it puts zoxobit-server in /usr/local/bin and # its unit in /etc/systemd/system, and starts it (or restarts it on an # upgrade); its state in /var/lib/zoxobit-server is left alone. On a Mac # it lays out what the .pkg installer does, without the package: the # binary in /usr/local/bin, its launch agent in /Library/LaunchAgents, # started for whoever is at the screen, with the state in # ~/.zoxobit-server. curl sets no quarantine flag, so Gatekeeper doesn't # ask for a notarized download. Either way an upgrade keeps the homes and # the setup code. It ends with the web page's address and one-time code; # it never prints the Apple Home setup code. # # All the work is in main, called on the last line, so a download cut # short runs nothing. ZOXOBIT_ROOT puts the files under another root # (for tests); ZOXOBIT_BASE downloads from somewhere else. set -eu base=${ZOXOBIT_BASE:-https://zoxohome.com/download} # zoxobit.com has the same files; it's tried when zoxohome.com can't be read. old_base=https://zoxobit.com/download root=${ZOXOBIT_ROOT:-} bin=$root/usr/local/bin/zoxobit-server unit=$root/etc/systemd/system/zoxobit-server.service dir=/var/lib/zoxobit-server agent=me.zoxobit.server plist=$root/Library/LaunchAgents/$agent.plist # Every zoxobit-server.service the helper has shipped (sha256): one of # these is replaced on an upgrade; any other is someone's edit and stays. # Add the new one here whenever deploy/zoxobit-server.service changes # (TestInstallScriptKnowsUnit checks). shipped_units=' 2aca644c36ed7048a5b126b3e9a0134d9a159880c3bfca108aea3a6f04c855a9 ce7977c5aa89c3c6cf307027e0e89430fb436555565f80d87b8de260ae9161b0 b3b1e12526a46680023fe20b7e6cbf424024ec88aa22ddbb8a8f9d379f898cae 75130d26463607d261fb968542ef98973b148892f4955bff048bb9e06ce1b488 fdf15a0478f971c3d76a234764401e8ac70684d4db9bf25b63ec473cb9d7b230 651cd3f11de034cf5a366aa7f8944052fc6c8427b2f1941e97117681f1578896 a767a372b8f27bef824075dfe5cc41ea39f85bc977ba88fea0162f136675c50e ' say() { printf '%s\n' "$*"; } die() { printf 'ZoxoHome Helper install: %s\n' "$*" >&2 exit 1 } main() { case $(uname -s) in Linux) linux ;; Darwin) mac ;; *) die "this is for Linux and macOS; for Windows see https://zoxohome.com/helper/" ;; esac } # prepare checks for curl and a checksum tool, gets sudo ready, makes # $tmp and reads the version on offer into $version. prepare() { command -v curl >/dev/null 2>&1 || die "it needs curl" if command -v sha256sum >/dev/null 2>&1; then sum="sha256sum" elif command -v shasum >/dev/null 2>&1; then sum="shasum -a 256" else die "it needs sha256sum to check the download" fi sudo= if [ "$(id -u)" -ne 0 ]; then command -v sudo >/dev/null 2>&1 || die "run it as root, or install sudo" sudo=sudo say "Installing needs root, so sudo may ask for your password." fi tmp=$(mktemp -d) trap 'rm -rf "$tmp"' EXIT trap 'exit 130' INT TERM version=$(curl -fsSL "$base/VERSION.txt" | cut -d' ' -f1) || true if [ -z "${ZOXOBIT_BASE:-}" ] && ! expr "$version" : '[0-9]*\.[0-9]*\.[0-9]*' >/dev/null; then base=$old_base version=$(curl -fsSL "$base/VERSION.txt" | cut -d' ' -f1) || true fi case $version in [0-9]*.[0-9]*.[0-9]*) ;; *) die "couldn't read the current version from $base/VERSION.txt" ;; esac } # fetch downloads $1 into $tmp and checks it against SHA256SUMS.txt. fetch() { curl -fsSL -o "$tmp/$1" "$base/$1" || die "couldn't download $base/$1" curl -fsSL -o "$tmp/SHA256SUMS.txt" "$base/SHA256SUMS.txt" || die "couldn't download $base/SHA256SUMS.txt" want=$(grep " $1\$" "$tmp/SHA256SUMS.txt" | cut -d' ' -f1) || true [ -n "$want" ] || die "$1 isn't in SHA256SUMS.txt" got=$(cd "$tmp" && $sum "$1" | cut -d' ' -f1) [ "$got" = "$want" ] || die "$1 doesn't match its checksum, so nothing was installed" } # installed is the version in $bin before this install, if any. installed() { if [ -x "$bin" ]; then "$bin" -version 2>/dev/null || true fi } # done_message says what this install did, given the version before it. done_message() { if [ -n "$1" ] && [ "$1" != "$version" ]; then say "Upgraded the ZoxoHome Helper from $1 to $version." else say "$2" fi } # wait_code prints the web page's one-time code for state directory $1, # run as $2 ("" or sudo); the helper writes it as it starts. wait_code() { i=0 while [ $i -lt 20 ]; do if c=$($2 "$bin" ui code -dir "$1" 2>/dev/null); then printf '%s\n' "$c" | sed -n 1p return 0 fi i=$((i + 1)) sleep 1 done return 1 } linux() { case $(uname -m) in x86_64 | amd64) arch=amd64 ;; aarch64 | arm64) arch=arm64 ;; armv7* | armv8l) arch=arm ;; *) die "there's no download for a $(uname -m) CPU, only x86-64, 64-bit Arm and ARMv7" ;; esac command -v systemctl >/dev/null 2>&1 || die "it needs systemd, and there's no systemctl here" case $(systemctl is-system-running 2>/dev/null || true) in "" | offline | unknown) die "systemd isn't running this computer (a container?), so nothing would keep the helper running" ;; esac prepare file=zoxobit-server-$version-linux-$arch.tar.gz say "Downloading the ZoxoHome Helper $version for $arch..." fetch "$file" tar -xzf "$tmp/$file" -C "$tmp" zoxobit-server zoxobit-server.service || die "couldn't unpack $file" # The web page's Update button (1.0.167 on); older downloads lack it. tar -xzf "$tmp/$file" -C "$tmp" zoxobit-server-update.service zoxobit-server-update.path 2>/dev/null || true old=$(installed) keep= if [ -f "$unit" ] && ! cmp -s "$unit" "$tmp/zoxobit-server.service"; then have=$($sum "$unit" | cut -d' ' -f1) case $shipped_units in *"$have"*) ;; *) keep=1 ;; esac fi # The unit asks for these groups; one missing stops it starting. for g in video render; do getent group "$g" >/dev/null || $sudo groupadd --system "$g" || die "couldn't add the $g group the unit needs" done $sudo mkdir -p "${bin%/*}" "${unit%/*}" $sudo install -m 0755 "$tmp/zoxobit-server" "$bin" if [ -n "$keep" ]; then $sudo install -m 0644 "$tmp/zoxobit-server.service" "$unit.new" say "You've edited $unit, so it stays as it is; this version's unit is beside it as $unit.new. Flags are best kept in a drop-in (sudo systemctl edit zoxobit-server)." else $sudo install -m 0644 "$tmp/zoxobit-server.service" "$unit" fi # These two only run zoxobit-server update as root when the page's # Update button asks; nobody edits them, so they're always replaced. updater= if [ -f "$tmp/zoxobit-server-update.service" ] && [ -f "$tmp/zoxobit-server-update.path" ]; then $sudo install -m 0644 "$tmp/zoxobit-server-update.service" "${unit%/*}/zoxobit-server-update.service" $sudo install -m 0644 "$tmp/zoxobit-server-update.path" "${unit%/*}/zoxobit-server-update.path" updater=1 fi $sudo systemctl daemon-reload $sudo systemctl enable zoxobit-server >/dev/null 2>&1 $sudo systemctl restart zoxobit-server if [ -n "$updater" ]; then $sudo systemctl enable --now zoxobit-server-update.path >/dev/null 2>&1 || say "systemd didn't take zoxobit-server-update.path, so the web page's Update button won't work; running this again updates it." fi done_message "$old" "The ZoxoHome Helper $version is running. It starts at boot and restarts if it stops." addr=$(hostname -I 2>/dev/null | cut -d' ' -f1) || addr= [ -n "$addr" ] || addr=$(hostname) say "" say "Next, open http://$addr:8750 on a phone or computer at home." wait_code "$dir" "$sudo" || say "It asks for a one-time code: sudo zoxobit-server ui code -dir $dir prints it." say "It takes you through the settings and shows the QR code for the Home app." say "Or draw the QR code here: sudo zoxobit-server -dir $dir -pin" } # The launch agent Zoxobit for Mac registers for its own copy of the # helper (Settings > Zoxobit Helper > Run on This Mac). app_agent=me.pkar.zoxobit.helper mac() { # Started for whoever is at the screen; with nobody there, it starts # at the next login. user=$(stat -f %Su /dev/console 2>/dev/null) || user= case $user in root | loginwindow) user= ;; esac prepare # Someone else at the screen (or this is root): their launch agents # and files are reached through sudo, or directly as root. uid= other= if [ -n "$user" ]; then uid=$(id -u "$user" 2>/dev/null) || die "couldn't find the user at the screen, $user" [ "$user" = "$(id -un)" ] || other=1 fi as= [ -z "$other" ] || as=$sudo if [ -n "$user" ] && $as launchctl print "gui/$uid/$app_agent" >/dev/null 2>&1; then die "ZoxoHome for Mac already runs the helper on this Mac (Settings > ZoxoHome Helper > Run on This Mac). Keep that one, or turn it off there first: two helpers would clash over their ports." fi file=zoxobit-server-$version-macos.tar.gz say "Downloading the ZoxoHome Helper $version for macOS..." fetch "$file" tar -xzf "$tmp/$file" -C "$tmp" zoxobit-server "$agent.plist" || die "couldn't unpack $file" old=$(installed) $sudo mkdir -p "${bin%/*}" "${plist%/*}" $sudo install -m 0755 "$tmp/zoxobit-server" "$bin" $sudo install -m 0644 "$tmp/$agent.plist" "$plist" if [ -n "$user" ]; then # Its state is in the home of the user at the screen. if [ -z "$other" ]; then home=$HOME else home=$(dscl . -read "/Users/$user" NFSHomeDirectory 2>/dev/null | sed 's/^NFSHomeDirectory: //') fi state=$home/.zoxobit-server # As the .pkg's postinstall: -pin first, so the setup is made # before the helper starts; -open shows the page only if no home # has the helper yet. Its output has the setup code: dropped. if [ -z "$other" ]; then "$bin" -dir "$state" -pin -open >/dev/null 2>&1 || true launchctl bootout "gui/$uid/$agent" 2>/dev/null || true launchctl bootstrap "gui/$uid" "$plist" || die "launchctl couldn't start $plist" else $sudo launchctl asuser "$uid" sudo -u "$user" "$bin" -dir "$state" -pin -open >/dev/null 2>&1 || true $sudo launchctl bootout "gui/$uid/$agent" 2>/dev/null || true $sudo launchctl bootstrap "gui/$uid" "$plist" || die "launchctl couldn't start $plist" fi fi if [ -z "$user" ]; then done_message "$old" "The ZoxoHome Helper $version is installed." say "Nobody is logged in at the screen, so it starts at the next login, for whoever logs in." say "Then: zoxobit-server ui code prints the web page's one-time code, and zoxobit-server -pin draws the QR code for the Home app." return 0 fi done_message "$old" "The ZoxoHome Helper $version is running. It starts when $user logs in and restarts if it stops." addr=$(ipconfig getifaddr en0 2>/dev/null) || addr=$(ipconfig getifaddr en1 2>/dev/null) || addr= [ -n "$addr" ] || addr=$(hostname) say "" say "Next, open http://localhost:8750 on this Mac (it opens by itself if no home has the helper yet), or http://$addr:8750 on a phone or computer at home." say "macOS asks once whether it may accept incoming connections and find devices on your network. Allow both." wait_code "$state" "$as" || say "It asks for a one-time code: zoxobit-server ui code prints it." say "It takes you through the settings and shows the QR code for the Home app." say "Or draw the QR code here: zoxobit-server -pin" } main "$@"