start

tcmts.deploy.server.start · #iax732z7 · sh file

#!/bin/sh
# tcm.codes: serve the codebase on the volume, making it the first time, and the site to anyone: the home page at /,
# the docs under /docs (their old addresses, at the root, redirected there: --moved), the web UI at /forge, and tcm's
# registry at /registry. Anyone can pull it (--public); TCM_TOKEN is the maintainer's, and contributors have tokens of their own
# (`tcm user add`). Railway terminates TLS in front of it, so it serves plain HTTP inside (--insecure); clients use the
# https:// domain. It never runs code for them. The examples that are apps run beside it, under the docs'
# /docs/examples/<name>/; the registry is a service of its own (tcm-hub), reached over Railway's private network.
set -eu
: "${TCM_TOKEN:?set TCM_TOKEN: the maintainer's token}"
TCM=/app/node_modules/tcm-ts/bin/tcm.js
DIR="${TCM_DIR:-/data/tcm-ts}"
EXAMPLES="${TCM_EXAMPLES_DIR:-/data/examples}"
# the docs site, and where it is
DOCS=/app/site/docs
AT=/docs
# the registry's server, at /registry (empty: none): its service is still named tcm-hub, from when the registry was
# called the hub (renaming it changes this private address)
REGISTRY="${TCM_REGISTRY_PROXY-http://tcm-hub.railway.internal:8080}"
# how often notes and polls start over, in seconds
RESET="${TCM_EXAMPLES_RESET:-21600}"
mkdir -p "$DIR" "$EXAMPLES"
cd "$DIR"
if [ ! -f .tcm/codebase.sqlite ]; then
  node "$TCM" init > /dev/null
  rm -f scratch.ts
fi
# a tcm with a new hash format moves the codebase to it first (history and all; the same hashes a clone migrating
# gets, so they go on syncing), keeping it as it was beside it
node "$TCM" migrate --if-needed

# Whether an example answers on a port (with node: the image has no curl).
answers() {
  node -e 'fetch(process.argv[1]).then((r) => process.exit(r.status < 500 ? 0 : 1), () => process.exit(1))' \
    "http://127.0.0.1:$1$AT/examples/$2/"
}

# An example, running: built from the site's download of it the first time (and again when the download or the tcm
# changes), then served by `tcm start` under /docs/examples/<name>/, on one of two ports: a new instance (a new version, a
# fresh start) starts on the other, and the one serving stops only once it answers, `tcm serve --proxy` asking
# whichever answers. So while a new version is built, the one before goes on serving, and the switch drops nothing.
# With none before (the first deploy), the path's page says it's starting. With reset=yes, a fresh instance with no
# data (tables, sessions) replaces it every six hours, so what visitors write doesn't stay.
run_example() {
  name=$1 app=$2 port=$3 reset=$4
  shift 4
  flags="$*"
  tgz=$DOCS/examples/$name.tar.gz
  key=$(cat "$tgz" /app/tcm-ts.tgz | sha256sum | cut -c1-16)
  dir=$EXAMPLES/$name-$key
  serving="" at=""
  # an instance from a directory on the port the one serving isn't on, replacing it once it answers; fails if it ends
  # first (the one serving goes on)
  replace() {
    next=$port
    if [ "$at" = "$port" ]; then next=$((port + 100)); fi
    # (flags split into words on purpose)
    # shellcheck disable=SC2086
    (cd "$1" && exec node "$TCM" start "$app" --port "$next" --base "$AT/examples/$name" $flags) &
    started=$!
    while kill -0 "$started" 2>/dev/null && ! answers "$next" "$name"; do sleep 1; done
    if ! kill -0 "$started" 2>/dev/null; then return 1; fi
    if [ -n "$serving" ]; then
      kill "$serving" 2>/dev/null || true
      wait "$serving" 2>/dev/null || true
    fi
    serving=$started at=$next
  }
  before=""
  # (a build is done when it says so: one stopped halfway, by a deploy, is built again)
  if [ ! -f "$dir/.built" ]; then
    for old in "$EXAMPLES/$name"-*; do
      if [ "$old" != "$dir" ] && [ -f "$old/.built" ]; then before=$old; fi
    done
    if [ -n "$before" ]; then replace "$before" || true; fi
    rm -rf "$dir"
    work=$(mktemp -d)
    tar -xzf "$tgz" -C "$work"
    if TCM_BIN=$TCM TCM_CODEBASE= NO_TESTS=1 "$work/$name/setup.sh" "$dir" > "$EXAMPLES/$name.log" 2>&1; then
      # its first build of the app, made before it's served
      (cd "$dir" && node "$TCM" build "$app" > /dev/null 2>&1) || true
      touch "$dir/.built"
    else
      echo "example $name didn't build: $EXAMPLES/$name.log"
      rm -rf "$dir"
      # (the one before goes on)
      if [ -z "$before" ]; then return; fi
      dir=$before before=""
    fi
    rm -rf "$work"
  fi
  while true; do
    if ! replace "$dir"; then
      sleep 5
      continue
    fi
    rm -rf "$dir/.tcm/data.old"
    for old in "$EXAMPLES/$name"-*; do
      if [ "$old" != "$dir" ]; then rm -rf "$old"; fi
    done
    if [ "$reset" = yes ]; then
      waited=0
      while kill -0 "$serving" 2>/dev/null && [ "$waited" -lt "$RESET" ]; do
        sleep 5
        waited=$((waited + 5))
      done
      # the one serving keeps its data (open) while the fresh one starts without any
      if kill -0 "$serving" 2>/dev/null; then
        rm -rf "$dir/.tcm/data.old"
        mv "$dir/.tcm/data" "$dir/.tcm/data.old" 2>/dev/null || true
      fi
    else
      wait "$serving" || true
    fi
  done
}
# the apps among the examples, as the site lists them (`<name> <app> <reset> <flags>`), each on two ports of its own
proxies=""
port=7801
while read -r name app reset flags; do
  [ -n "$name" ] || continue
  # (flags split into words on purpose)
  # shellcheck disable=SC2086
  run_example "$name" "$app" "$port" "$reset" $flags &
  proxies="$proxies${proxies:+,}$AT/examples/$name=http://127.0.0.1:$port|http://127.0.0.1:$((port + 100))"
  port=$((port + 1))
done < "$DOCS/examples/apps.txt"
if [ -n "$REGISTRY" ]; then proxies="$proxies${proxies:+,}/registry=$REGISTRY"; fi

cd "$DIR"
exec node "$TCM" serve --host 0.0.0.0 --port "${PORT:-7737}" --insecure --no-run --public --site /app/site --moved "$AT" ${proxies:+--proxy "$proxies"}