socialmag-dash crash-looped on a real box. WorkingDirectory was the repo and ExecStart was `python -m http.server`, so socketserver's `import selectors` found the repo's selectors.py and the unit died with "module 'selectors' has no attribute 'SelectSelector'" every 5 seconds. :8321 never bound. This is the SAME root cause already fixed twice this evening — once for `-m socialmag` (console scripts, cli.py) and once for `-m venv` (run python from /). Both fixes were about keeping the repo off sys.path, and then the unit file put it right back on. A `-m` anywhere near this directory is the hazard, not any particular entry point. The dash now serves with --directory and sits in / instead of standing in the tree it serves. The by-hand fallback instructions carried the identical bug and now do the same, with a line saying why. openbridge and refresh were never affected: they exec console scripts, which do not put the working directory on sys.path. That is exactly why cli.py exists. The real fix is renaming selectors.py and queue.py. Until then this trap keeps finding new ways in, and that is not a prediction — it is three for three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
260 lines
11 KiB
Bash
Executable File
260 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# socialmag installer — clone, run this, sign in, done.
|
|
#
|
|
# What it does not do: log you in. socialmag holds no passwords, which is the
|
|
# whole design. The last step is yours and it only happens once.
|
|
set -euo pipefail
|
|
|
|
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
VENV="${SOCIALMAG_VENV:-$REPO/.venv}"
|
|
CDP_ENDPOINT="${SOCIALMAG_CDP_ENDPOINT:-http://127.0.0.1:9336}"
|
|
UNIT_DIR="$HOME/.config/systemd/user"
|
|
HERMES_CONFIG="${HERMES_CONFIG:-$HOME/.hermes/config.yaml}"
|
|
|
|
say() { printf '\n\033[1;36m==>\033[0m %s\n' "$*"; }
|
|
ok() { printf ' \033[32mok\033[0m %s\n' "$*"; }
|
|
warn() { printf ' \033[33m!!\033[0m %s\n' "$*"; }
|
|
die() { printf '\n\033[31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
# ---------------------------------------------------------------- python
|
|
|
|
say "Python"
|
|
command -v python3 >/dev/null 2>&1 || die "python3 not found on PATH"
|
|
# Every python3 call below runs from / rather than from here. The repo root is
|
|
# the package dir, so it ships selectors.py and queue.py — names the stdlib also
|
|
# uses. Python puts the current directory first on sys.path, so `python3 -m venv`
|
|
# run from inside a fresh clone dies at `import subprocess` with
|
|
# "module 'selectors' has no attribute 'SelectSelector'". Running from / is the
|
|
# whole fix. (This is the same trap cli.py exists to keep off users.)
|
|
(cd / && python3 - <<'PY') || die "socialmag needs Python 3.10 or newer"
|
|
import sys
|
|
sys.exit(0 if sys.version_info >= (3, 10) else 1)
|
|
PY
|
|
ok "$(cd / && python3 -V)"
|
|
|
|
# `import venv` succeeding proves nothing: Debian ships the venv module but
|
|
# splits ensurepip into a separate package, and `python3 -m venv` then dies
|
|
# with "ensurepip is not available" only after creating a half-built .venv.
|
|
# Ask for ensurepip by name, before anything is written to disk.
|
|
if ! (cd / && python3 -c "import ensurepip" 2>/dev/null); then
|
|
_pyver="$(cd / && python3 -c 'import sys;print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
|
|
die "python3 has the venv module but not ensurepip, so it cannot create a venv.
|
|
Debian/Ubuntu split this out. Install it and re-run:
|
|
sudo apt install python3.${_pyver#*.}-venv
|
|
(or: sudo apt install python3-venv)"
|
|
fi
|
|
ok "ensurepip present"
|
|
|
|
# ---------------------------------------------------------------- venv
|
|
|
|
say "Virtualenv + package"
|
|
if [[ ! -x "$VENV/bin/python" ]]; then
|
|
# Python's own failure text names the exact missing package. Do not paper over
|
|
# it with a guess — say where to look and get out of the way.
|
|
(cd / && python3 -m venv "$VENV") || die "could not create a venv at $VENV
|
|
Python printed the reason above; it is more specific than anything this
|
|
script could infer. A leftover half-built $VENV may need removing first."
|
|
ok "created $VENV"
|
|
else
|
|
ok "reusing $VENV"
|
|
fi
|
|
|
|
# Editable on purpose. socialmag resolves socialmag.db, state.json and
|
|
# dashboard.json relative to its own __file__; a non-editable install would put
|
|
# all three inside site-packages, where nothing else looks for them.
|
|
(cd / && "$VENV/bin/pip" install --quiet --upgrade pip) >/dev/null
|
|
(cd / && "$VENV/bin/pip" install --quiet -e "$REPO") || die "pip install failed"
|
|
ok "socialmag installed (editable), websockets pulled in"
|
|
|
|
for script in socialmag socialmag-refresh socialmag-openbridge; do
|
|
[[ -x "$VENV/bin/$script" ]] || die "console script $script was not generated — pyproject.toml [project.scripts] did not take"
|
|
done
|
|
ok "console scripts: socialmag, socialmag-refresh, socialmag-openbridge"
|
|
|
|
# ---------------------------------------------------------------- systemd
|
|
|
|
say "systemd user units"
|
|
# systemctl being installed does NOT mean a user bus is reachable. Inside an LXC,
|
|
# over `su`, or on a box with no logind session, `systemctl --user` fails with
|
|
# "Failed to connect to user scope bus". Ask the real question — can we talk to
|
|
# the user manager — instead of asking whether the binary exists.
|
|
USER_BUS=""
|
|
BUS_FIXED_UP=""
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
if systemctl --user show-environment >/dev/null 2>&1; then
|
|
USER_BUS="yes"
|
|
else
|
|
# A shell entered via `su` keeps the ORIGINAL user's XDG_RUNTIME_DIR, so
|
|
# `systemctl --user` looks for the bus in someone else's directory and finds
|
|
# nothing — even when this user's manager is running perfectly well. The
|
|
# runtime dir is /run/user/<uid> by definition, so work it out instead of
|
|
# asking someone to export it by hand and get the digits right.
|
|
_own_runtime="/run/user/$(id -u)"
|
|
if [[ -d "$_own_runtime" && "${XDG_RUNTIME_DIR:-}" != "$_own_runtime" ]]; then
|
|
export XDG_RUNTIME_DIR="$_own_runtime"
|
|
if systemctl --user show-environment >/dev/null 2>&1; then
|
|
USER_BUS="yes"
|
|
BUS_FIXED_UP="yes"
|
|
fi
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$BUS_FIXED_UP" ]]; then
|
|
ok "user bus found at $XDG_RUNTIME_DIR (the inherited XDG_RUNTIME_DIR pointed elsewhere)"
|
|
warn "this shell still has the old value. For later commands in THIS shell:"
|
|
warn " export XDG_RUNTIME_DIR=$XDG_RUNTIME_DIR"
|
|
fi
|
|
|
|
if [[ -z "$USER_BUS" ]]; then
|
|
warn "no reachable systemd user bus — units NOT installed."
|
|
if ! command -v systemctl >/dev/null 2>&1; then
|
|
warn "reason: systemctl is not on PATH."
|
|
else
|
|
warn "reason: systemctl --user cannot reach a user manager (XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-unset})."
|
|
if [[ ! -d "/run/user/$(id -u)" ]]; then
|
|
warn "/run/user/$(id -u) does not exist — $USER has no logind session at all."
|
|
warn " sudo loginctl enable-linger $USER # then re-run; no fresh login needed"
|
|
else
|
|
warn "/run/user/$(id -u) exists but the manager there did not answer."
|
|
warn " systemctl status user@$(id -u).service # start here"
|
|
fi
|
|
fi
|
|
warn "until then, run the three by hand (from anywhere EXCEPT $REPO):"
|
|
warn " $VENV/bin/socialmag-openbridge &"
|
|
warn " $VENV/bin/python -m http.server 8321 --directory $REPO &"
|
|
warn " $VENV/bin/socialmag-refresh # one pass, whenever you want it"
|
|
warn " (-m from inside the repo imports its selectors.py over the stdlib's)"
|
|
else
|
|
mkdir -p "$UNIT_DIR"
|
|
for src in "$REPO"/deploy/user/*.in; do
|
|
dest="$UNIT_DIR/$(basename "${src%.in}")"
|
|
sed -e "s|@REPO@|$REPO|g" \
|
|
-e "s|@VENV@|$VENV|g" \
|
|
-e "s|@CDP_ENDPOINT@|$CDP_ENDPOINT|g" \
|
|
"$src" > "$dest"
|
|
ok "$(basename "$dest")"
|
|
done
|
|
cp "$REPO/deploy/user/socialmag-refresh.timer" "$UNIT_DIR/socialmag-refresh.timer"
|
|
ok "socialmag-refresh.timer"
|
|
|
|
systemctl --user daemon-reload
|
|
systemctl --user enable --now socialmag-dash.service socialmag-openbridge.service >/dev/null
|
|
systemctl --user enable --now socialmag-refresh.timer >/dev/null
|
|
ok "enabled: dash (:8321), openbridge (:8322), refresh timer (30min)"
|
|
|
|
# These die at logout unless lingering is on. Say so rather than let someone
|
|
# discover it when the dashboard is empty tomorrow.
|
|
if ! loginctl show-user "$USER" -p Linger 2>/dev/null | grep -q 'Linger=yes'; then
|
|
warn "user services stop when you log out. To keep them running:"
|
|
warn " sudo loginctl enable-linger $USER"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------- hermes
|
|
|
|
say "Hermes MCP registration"
|
|
|
|
# Hermes installs in more than one shape — /usr/local/bin/hermes wrapping a venv
|
|
# under /usr/local/lib, or ~/.local/bin/hermes from a user install. Read the
|
|
# interpreter out of the wrapper rather than hardcoding one box's layout.
|
|
HERMES_PY=""
|
|
if command -v hermes >/dev/null 2>&1; then
|
|
_hermes_bin="$(command -v hermes)"
|
|
if head -c 2 "$_hermes_bin" 2>/dev/null | grep -q '#!'; then
|
|
# A wrapper script: the python it exec's is the interpreter that loads MCP.
|
|
HERMES_PY="$(grep -oE '"[^"]*/bin/python[0-9.]*"|[^ "]*/bin/python[0-9.]*' "$_hermes_bin" 2>/dev/null | head -1 | tr -d '"')"
|
|
fi
|
|
# A console script generated by pip: its own shebang names the interpreter.
|
|
if [[ -z "$HERMES_PY" ]]; then
|
|
_shebang="$(head -1 "$_hermes_bin" 2>/dev/null)"
|
|
[[ "$_shebang" == '#!'*python* ]] && HERMES_PY="${_shebang#\#!}"
|
|
fi
|
|
fi
|
|
|
|
if ! command -v hermes >/dev/null 2>&1; then
|
|
warn "hermes not on PATH — skipping. Register manually later:"
|
|
printf '\n mcp_servers:\n socialmag:\n command: "%s/bin/socialmag"\n args: []\n env:\n SOCIALMAG_CDP_ENDPOINT: "%s"\n\n' "$VENV" "$CDP_ENDPOINT"
|
|
elif [[ ! -f "$HERMES_CONFIG" ]]; then
|
|
die "hermes is installed but $HERMES_CONFIG does not exist.
|
|
Start hermes once so it writes a config, then re-run this installer."
|
|
else
|
|
# Hermes treats the mcp python package as optional: without it the entire MCP
|
|
# subsystem is a no-op that logs at debug level. A server that never loaded
|
|
# looks exactly like one that loaded and did nothing, so check it here rather
|
|
# than let that be discovered later.
|
|
if [[ -x "$HERMES_PY" ]]; then
|
|
if "$HERMES_PY" -c "import importlib.util,sys; sys.exit(0 if importlib.util.find_spec('mcp') else 1)"; then
|
|
ok "hermes has the mcp package"
|
|
else
|
|
die "hermes cannot load MCP servers: the 'mcp' package is missing from
|
|
$HERMES_PY
|
|
Without it hermes silently ignores every mcp_servers entry — no error, only a
|
|
debug log line. Install it into that interpreter, then re-run:
|
|
$HERMES_PY -m pip install mcp"
|
|
fi
|
|
else
|
|
warn "could not find hermes's interpreter at $HERMES_PY — cannot check for the mcp package"
|
|
warn "if socialmag's tools never appear in hermes, that is the first thing to check"
|
|
fi
|
|
|
|
if grep -qE '^\s*mcp_servers\s*:' "$HERMES_CONFIG"; then
|
|
warn "$HERMES_CONFIG already has an mcp_servers: key — not touching it."
|
|
warn "Merging YAML blind is how someone's other servers disappear. Add this by hand:"
|
|
printf '\n socialmag:\n command: "%s/bin/socialmag"\n args: []\n env:\n SOCIALMAG_CDP_ENDPOINT: "%s"\n\n' "$VENV" "$CDP_ENDPOINT"
|
|
else
|
|
backup="$HERMES_CONFIG.bak-pre-socialmag-$(date +%Y%m%d%H%M%S)"
|
|
cp "$HERMES_CONFIG" "$backup"
|
|
cat >> "$HERMES_CONFIG" <<EOF
|
|
mcp_servers:
|
|
socialmag:
|
|
command: "$VENV/bin/socialmag"
|
|
args: []
|
|
env:
|
|
SOCIALMAG_CDP_ENDPOINT: "$CDP_ENDPOINT"
|
|
EOF
|
|
ok "registered in $HERMES_CONFIG (backup: $(basename "$backup"))"
|
|
fi
|
|
fi
|
|
|
|
# ---------------------------------------------------------------- done
|
|
|
|
cat <<EOF
|
|
|
|
$(printf '\033[1;32mInstalled.\033[0m') Two things left, both yours:
|
|
|
|
1. Start the browser socialmag drives:
|
|
|
|
$REPO/launch-brave.sh
|
|
|
|
2. Sign in to your platforms in that window — Threads, Facebook, Instagram,
|
|
YouTube, TikTok, X. Once. socialmag stores no passwords; it uses the
|
|
session that now belongs to that browser profile.
|
|
|
|
Then:
|
|
|
|
EOF
|
|
|
|
# Only name the dashboard and the journal when units actually got installed.
|
|
# Pointing someone at `journalctl --user -u socialmag-refresh` for a unit that
|
|
# was never created is the same class of lie as a silent fallback: it reads as
|
|
# working and answers with nothing.
|
|
if [[ -n "$USER_BUS" ]]; then
|
|
cat <<EOF
|
|
dashboard http://127.0.0.1:8321/dashboard.html
|
|
check $VENV/bin/socialmag-refresh
|
|
logs journalctl --user -u socialmag-refresh -f
|
|
|
|
EOF
|
|
else
|
|
cat <<EOF
|
|
check $VENV/bin/socialmag-refresh
|
|
dashboard start the file server yourself (see the warnings above), then
|
|
http://127.0.0.1:8321/dashboard.html
|
|
|
|
No units were installed, so nothing refreshes on a timer yet and there is no
|
|
journal to read. Fix the user bus above and re-run to get those.
|
|
|
|
EOF
|
|
fi
|