openvibers/openvibe.link Commits
Disable the Adeept stock server from crontab and rc.local autostart
Changes
@@@ -79,4 +79,5 @@ jobs:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4-- - run: shellcheck -s sh install/install.sh scripts/dist.sh++ - run: shellcheck -s sh install/install.sh scripts/dist.sh scripts/test-install-stock-server.sh++ - run: sh scripts/test-install-stock-server.sh@@@ -16,6 +16,12 @@# Environment:# OPENVIBE_NODE_BASE download base URL (default: GitHub releases of OpenVibers/OpenVibe.Node).# OPENVIBE_SERVER OpenVibe.Bot origin (default https://openvibe.bot).++#++# The Adeept stock-server check can be pointed at plain files for testing (no root, no crontab command):++# OPENVIBE_INSTALL_STOCK_ONLY=1 run only that check and exit.++# OPENVIBE_CRONTAB_ROOT=FILE edit FILE as root's crontab instead of the real one.++# OPENVIBE_CRONTAB_USER=FILE edit FILE as the invoking user's crontab instead of the real one.++# OPENVIBE_RC_LOCAL=FILE edit FILE as rc.local instead of /etc/rc.local.set -euREPO_URL="https://github.com/OpenVibers/OpenVibe.Node"@@@ -49,6 +55,102 @@ case "$ROBOT" in adeept|adeept-mecanum|cozmo|none) ;; *) die "--robot must be ad# Root for the service, /usr/local/bin and /etc.SUDO=""++++# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----++# Its installer autostarts it from the user's crontab (or root's, with an @reboot line) or from /etc/rc.local,++# depending on kit version. Besides the systemd unit and any running process, comment out those autostart lines++# (marked, never deleted) so they cannot start it again.++STOCK_SERVER_RE='Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py'++STOCK_SERVER_MARKER='#openvibe-node-disabled:'++CRON_USER="${SUDO_USER:-$(id -un 2>/dev/null || printf '%s' root)}"++++# Comment out the stock-server lines in one crontab-like file: read $1, write $2, print one line per disabled++# entry. A line whose first non-blank character is "#" is left alone, so our marker makes a second run a no-op.++disable_stock_lines() {++ _src="$1" _dst="$2" _where="$3"++ : > "$_dst"++ while IFS= read -r _line || [ -n "$_line" ]; do++ _trim="${_line#"${_line%%[![:space:]]*}"}"++ if [ "${_trim#\#}" = "$_trim" ] && printf '%s\n' "$_line" | grep -Eq "$STOCK_SERVER_RE"; then++ printf '%s %s\n' "$STOCK_SERVER_MARKER" "$_line" >> "$_dst"++ say "disabled the Adeept kit's stock server autostart in $_where"++ else++ printf '%s\n' "$_line" >> "$_dst"++ fi++ done < "$_src"++}++++# Edit a plain crontab-like file in place (the test-override paths). No crontab command and no sudo.++disable_stock_file() {++ _file="$1" _where="$2"++ [ -f "$_file" ] || return 0++ _before="$(mktemp)" _after="$(mktemp)"++ cp "$_file" "$_before"++ disable_stock_lines "$_before" "$_after" "$_where"++ if ! cmp -s "$_before" "$_after"; then cp "$_after" "$_file"; fi++ rm -f "$_before" "$_after"++}++++# Comment out stock-server lines in one user's crontab and reinstall it only if it changed.++disable_stock_crontab() {++ _user="$1"++ command -v crontab >/dev/null 2>&1 || return 0++ _before="$(mktemp)" _after="$(mktemp)"++ $SUDO crontab -u "$_user" -l > "$_before" 2>/dev/null || true++ if [ ! -s "$_before" ]; then rm -f "$_before" "$_after"; return 0; fi++ disable_stock_lines "$_before" "$_after" "the crontab of $_user"++ if ! cmp -s "$_before" "$_after"; then $SUDO crontab -u "$_user" "$_after" || true; fi++ rm -f "$_before" "$_after"++}++++# Comment out stock-server lines in rc.local (the real path; needs root to write it back).++disable_stock_rc_local() {++ _rc="$1"++ [ -f "$_rc" ] || return 0++ _before="$(mktemp)" _after="$(mktemp)"++ $SUDO cp "$_rc" "$_before" 2>/dev/null || { rm -f "$_before" "$_after"; return 0; }++ disable_stock_lines "$_before" "$_after" "$_rc"++ if ! cmp -s "$_before" "$_after"; then $SUDO cp "$_after" "$_rc" || true; fi++ rm -f "$_before" "$_after"++}++++disable_stock_server() {++ if command -v systemctl >/dev/null 2>&1; then++ unit=Adeept_Robot.service++ if systemctl is-enabled "$unit" >/dev/null 2>&1 || systemctl is-active "$unit" >/dev/null 2>&1; then++ say "disabling the Adeept kit's stock server ($unit): it listens on 0.0.0.0:8888 with a fixed password"++ $SUDO systemctl disable --now "$unit" || true++ fi++ fi++ if command -v pgrep >/dev/null 2>&1 && pgrep -f "$STOCK_SERVER_RE" >/dev/null 2>&1; then++ say "stopping a running Adeept stock server process"++ $SUDO pkill -f "$STOCK_SERVER_RE" || true++ fi++ # Autostart: root's crontab and the invoking user's, then rc.local. OPENVIBE_CRONTAB_* / OPENVIBE_RC_LOCAL++ # are test-only file overrides so the checks can run without root and without touching the real system.++ if [ -n "${OPENVIBE_CRONTAB_ROOT:-}" ]; then++ disable_stock_file "$OPENVIBE_CRONTAB_ROOT" "the root crontab"++ else++ disable_stock_crontab root++ fi++ if [ -n "${OPENVIBE_CRONTAB_USER:-}" ]; then++ disable_stock_file "$OPENVIBE_CRONTAB_USER" "the crontab of $CRON_USER"++ elif [ "$CRON_USER" != root ]; then++ disable_stock_crontab "$CRON_USER"++ fi++ if [ -n "${OPENVIBE_RC_LOCAL:-}" ]; then++ disable_stock_file "$OPENVIBE_RC_LOCAL" "rc.local ($OPENVIBE_RC_LOCAL)"++ else++ disable_stock_rc_local /etc/rc.local++ fi++}++++# Test hook: run only the stock-server check against the OPENVIBE_* file overrides, then stop.++if [ "${OPENVIBE_INSTALL_STOCK_ONLY:-0}" = 1 ]; then++ disable_stock_server++ exit 0++fi++if [ "$(id -u)" -ne 0 ]; thencommand -v sudo >/dev/null 2>&1 || die "run as root (sudo is not installed)"SUDO="sudo"@@@ -166,17 +268,7 @@ elsefi# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ------if command -v systemctl >/dev/null 2>&1; then-- unit=Adeept_Robot.service-- if systemctl is-enabled "$unit" >/dev/null 2>&1 || systemctl is-active "$unit" >/dev/null 2>&1; then-- say "disabling the Adeept kit's stock server ($unit): it listens on 0.0.0.0:8888 with a fixed password"-- $SUDO systemctl disable --now "$unit" || true-- fi--fi--if command -v pgrep >/dev/null 2>&1 && pgrep -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" >/dev/null 2>&1; then-- say "stopping a running Adeept stock server process"-- $SUDO pkill -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" || true--fi++disable_stock_server# ---- service ----if [ "$SERVICE" = 1 ]; thennnew file mode 100644@@@ -0,0 +1,99 @@++#!/bin/sh++# Tests install.sh's handling of the Adeept kit's stock-server autostart: crontab (root's and the invoking++# user's, including @reboot lines) and /etc/rc.local. The OPENVIBE_* test overrides point the checks at files in++# a temp dir, so this needs no root and touches no real crontab or /etc/rc.local.++set -eu++++here="$(cd "$(dirname "$0")" && pwd)"++installer="$here/../install/install.sh"++++fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }++++tmp="$(mktemp -d)"++trap 'rm -rf "$tmp"' EXIT++root_cron="$tmp/root-crontab"++user_cron="$tmp/user-crontab"++rc_local="$tmp/rc.local"++++cat > "$root_cron" <<'EOF'++# root's crontab++PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin++@reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/WebServer.py++0 4 * * * /usr/bin/backup.sh++EOF++++cat > "$user_cron" <<'EOF'++@reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/app.py++*/5 * * * * /home/pi/adeept_awr/Server/Server_OrdinaryWheels/helper.py++EOF++++cat > "$rc_local" <<'EOF'++#!/bin/sh -e++python3 /home/pi/adeept_awr/Server/Server_MecanumWheels/WebServer.py &++/usr/local/bin/other-thing &++exit 0++EOF++++run() {++ OPENVIBE_INSTALL_STOCK_ONLY=1 \++ OPENVIBE_CRONTAB_ROOT="$root_cron" \++ OPENVIBE_CRONTAB_USER="$user_cron" \++ OPENVIBE_RC_LOCAL="$rc_local" \++ sh "$installer"++}++++out="$(run)"++printf '%s\n' "$out"++++# One line per disabled thing.++disabled="$(printf '%s\n' "$out" | grep -c "stock server autostart" || true)"++[ "$disabled" = 3 ] || fail "expected 3 disabled lines, got $disabled"++++# The stock lines are commented out with the marker, not deleted.++grep -q '^#openvibe-node-disabled: @reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/WebServer\.py$' "$root_cron" ||++ fail "root crontab stock line not commented out"++grep -q '^#openvibe-node-disabled: @reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/app\.py$' "$user_cron" ||++ fail "user crontab @reboot stock line not commented out"++grep -q '^#openvibe-node-disabled: python3 /home/pi/adeept_awr/Server/Server_MecanumWheels/WebServer\.py &$' "$rc_local" ||++ fail "rc.local stock line not commented out"++++# Unrelated lines are untouched.++grep -qx '0 4 \* \* \* /usr/bin/backup.sh' "$root_cron" || fail "unrelated root crontab line changed"++grep -qx 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin' "$root_cron" || fail "root crontab PATH line changed"++grep -qxF "# root's crontab" "$root_cron" || fail "root crontab comment changed"++grep -qx '\*/5 \* \* \* \* /home/pi/adeept_awr/Server/Server_OrdinaryWheels/helper.py' "$user_cron" ||++ fail "unrelated user crontab line changed"++grep -qx '/usr/local/bin/other-thing &' "$rc_local" || fail "unrelated rc.local line changed"++grep -qx 'exit 0' "$rc_local" || fail "rc.local exit line changed"++++# No stock-server line is left active.++if grep -Eq '^[[:space:]]*[^#[:space:]].*Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py' \++ "$root_cron" "$user_cron" "$rc_local"; then++ fail "a stock-server line is still active"++fi++++# A second run is a no-op: nothing reported and nothing changed.++cp "$root_cron" "$tmp/root.before"++cp "$user_cron" "$tmp/user.before"++cp "$rc_local" "$tmp/rc.before"++out2="$(run)"++printf '%s\n' "$out2"++if printf '%s\n' "$out2" | grep -q "stock server autostart"; then fail "second run reported more changes"; fi++cmp -s "$root_cron" "$tmp/root.before" || fail "second run changed the root crontab"++cmp -s "$user_cron" "$tmp/user.before" || fail "second run changed the user crontab"++cmp -s "$rc_local" "$tmp/rc.before" || fail "second run changed rc.local"++++# A host without the kit: unrelated crontab/rc.local content is left alone and nothing is reported.++none="$tmp/none"++mkdir -p "$none"++printf '0 5 * * * /usr/bin/echo hello\n' > "$none/cron"++printf 'PATH=/bin\n' > "$none/cron2"++printf '#!/bin/sh\nexit 0\n' > "$none/rc"++out3="$(OPENVIBE_INSTALL_STOCK_ONLY=1 OPENVIBE_CRONTAB_ROOT="$none/cron" OPENVIBE_CRONTAB_USER="$none/cron2" \++ OPENVIBE_RC_LOCAL="$none/rc" sh "$installer")"++printf '%s\n' "$out3"++if printf '%s\n' "$out3" | grep -q "stock server"; then fail "reported changes on a host without the kit"; fi++grep -qx '0 5 \* \* \* /usr/bin/echo hello' "$none/cron" || fail "unrelated cron line changed without the kit"++grep -qx '#!/bin/sh' "$none/rc" || fail "unrelated rc.local changed without the kit"++++printf 'ok: install.sh stock-server crontab/rc.local handling\n'
@@ -79,4 +79,5 @@ jobs:
@@ -79,4 +79,5 @@ jobs:
runs-on: ubuntu-latest
runs-on: ubuntu-latest
steps:
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- - run: shellcheck -s sh install/install.sh scripts/dist.sh
+ - run: shellcheck -s sh install/install.sh scripts/dist.sh scripts/test-install-stock-server.sh
+ - run: sh scripts/test-install-stock-server.sh
@@ -16,6 +16,12 @@
@@ -16,6 +16,12 @@
# Environment:
# Environment:
# OPENVIBE_NODE_BASE download base URL (default: GitHub releases of OpenVibers/OpenVibe.Node).
# OPENVIBE_NODE_BASE download base URL (default: GitHub releases of OpenVibers/OpenVibe.Node).
# OPENVIBE_SERVER OpenVibe.Bot origin (default https://openvibe.bot).
# OPENVIBE_SERVER OpenVibe.Bot origin (default https://openvibe.bot).
+#
+# The Adeept stock-server check can be pointed at plain files for testing (no root, no crontab command):
+# OPENVIBE_INSTALL_STOCK_ONLY=1 run only that check and exit.
+# OPENVIBE_CRONTAB_ROOT=FILE edit FILE as root's crontab instead of the real one.
+# OPENVIBE_CRONTAB_USER=FILE edit FILE as the invoking user's crontab instead of the real one.
+# OPENVIBE_RC_LOCAL=FILE edit FILE as rc.local instead of /etc/rc.local.
set -eu
set -eu
REPO_URL="https://github.com/OpenVibers/OpenVibe.Node"
REPO_URL="https://github.com/OpenVibers/OpenVibe.Node"
@@ -49,6 +55,102 @@ case "$ROBOT" in adeept|adeept-mecanum|cozmo|none) ;; *) die "--robot must be ad
@@ -49,6 +55,102 @@ case "$ROBOT" in adeept|adeept-mecanum|cozmo|none) ;; *) die "--robot must be ad
# Root for the service, /usr/local/bin and /etc.
# Root for the service, /usr/local/bin and /etc.
SUDO=""
SUDO=""
+
+# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----
+# Its installer autostarts it from the user's crontab (or root's, with an @reboot line) or from /etc/rc.local,
+# depending on kit version. Besides the systemd unit and any running process, comment out those autostart lines
+# (marked, never deleted) so they cannot start it again.
+STOCK_SERVER_RE='Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py'
+STOCK_SERVER_MARKER='#openvibe-node-disabled:'
+CRON_USER="${SUDO_USER:-$(id -un 2>/dev/null || printf '%s' root)}"
+
+# Comment out the stock-server lines in one crontab-like file: read $1, write $2, print one line per disabled
+# entry. A line whose first non-blank character is "#" is left alone, so our marker makes a second run a no-op.
+disable_stock_lines() {
+ _src="$1" _dst="$2" _where="$3"
+ : > "$_dst"
+ while IFS= read -r _line || [ -n "$_line" ]; do
+ _trim="${_line#"${_line%%[![:space:]]*}"}"
+ if [ "${_trim#\#}" = "$_trim" ] && printf '%s\n' "$_line" | grep -Eq "$STOCK_SERVER_RE"; then
+ printf '%s %s\n' "$STOCK_SERVER_MARKER" "$_line" >> "$_dst"
+ say "disabled the Adeept kit's stock server autostart in $_where"
+ else
+ printf '%s\n' "$_line" >> "$_dst"
+ fi
+ done < "$_src"
+}
+
+# Edit a plain crontab-like file in place (the test-override paths). No crontab command and no sudo.
+disable_stock_file() {
+ _file="$1" _where="$2"
+ [ -f "$_file" ] || return 0
+ _before="$(mktemp)" _after="$(mktemp)"
+ cp "$_file" "$_before"
+ disable_stock_lines "$_before" "$_after" "$_where"
+ if ! cmp -s "$_before" "$_after"; then cp "$_after" "$_file"; fi
+ rm -f "$_before" "$_after"
+}
+
+# Comment out stock-server lines in one user's crontab and reinstall it only if it changed.
+disable_stock_crontab() {
+ _user="$1"
+ command -v crontab >/dev/null 2>&1 || return 0
+ _before="$(mktemp)" _after="$(mktemp)"
+ $SUDO crontab -u "$_user" -l > "$_before" 2>/dev/null || true
+ if [ ! -s "$_before" ]; then rm -f "$_before" "$_after"; return 0; fi
+ disable_stock_lines "$_before" "$_after" "the crontab of $_user"
+ if ! cmp -s "$_before" "$_after"; then $SUDO crontab -u "$_user" "$_after" || true; fi
+ rm -f "$_before" "$_after"
+}
+
+# Comment out stock-server lines in rc.local (the real path; needs root to write it back).
+disable_stock_rc_local() {
+ _rc="$1"
+ [ -f "$_rc" ] || return 0
+ _before="$(mktemp)" _after="$(mktemp)"
+ $SUDO cp "$_rc" "$_before" 2>/dev/null || { rm -f "$_before" "$_after"; return 0; }
+ disable_stock_lines "$_before" "$_after" "$_rc"
+ if ! cmp -s "$_before" "$_after"; then $SUDO cp "$_after" "$_rc" || true; fi
+ rm -f "$_before" "$_after"
+}
+
+disable_stock_server() {
+ if command -v systemctl >/dev/null 2>&1; then
+ unit=Adeept_Robot.service
+ if systemctl is-enabled "$unit" >/dev/null 2>&1 || systemctl is-active "$unit" >/dev/null 2>&1; then
+ say "disabling the Adeept kit's stock server ($unit): it listens on 0.0.0.0:8888 with a fixed password"
+ $SUDO systemctl disable --now "$unit" || true
+ fi
+ fi
+ if command -v pgrep >/dev/null 2>&1 && pgrep -f "$STOCK_SERVER_RE" >/dev/null 2>&1; then
+ say "stopping a running Adeept stock server process"
+ $SUDO pkill -f "$STOCK_SERVER_RE" || true
+ fi
+ # Autostart: root's crontab and the invoking user's, then rc.local. OPENVIBE_CRONTAB_* / OPENVIBE_RC_LOCAL
+ # are test-only file overrides so the checks can run without root and without touching the real system.
+ if [ -n "${OPENVIBE_CRONTAB_ROOT:-}" ]; then
+ disable_stock_file "$OPENVIBE_CRONTAB_ROOT" "the root crontab"
+ else
+ disable_stock_crontab root
+ fi
+ if [ -n "${OPENVIBE_CRONTAB_USER:-}" ]; then
+ disable_stock_file "$OPENVIBE_CRONTAB_USER" "the crontab of $CRON_USER"
+ elif [ "$CRON_USER" != root ]; then
+ disable_stock_crontab "$CRON_USER"
+ fi
+ if [ -n "${OPENVIBE_RC_LOCAL:-}" ]; then
+ disable_stock_file "$OPENVIBE_RC_LOCAL" "rc.local ($OPENVIBE_RC_LOCAL)"
+ else
+ disable_stock_rc_local /etc/rc.local
+ fi
+}
+
+# Test hook: run only the stock-server check against the OPENVIBE_* file overrides, then stop.
+if [ "${OPENVIBE_INSTALL_STOCK_ONLY:-0}" = 1 ]; then
+ disable_stock_server
+ exit 0
+fi
+
if [ "$(id -u)" -ne 0 ]; then
if [ "$(id -u)" -ne 0 ]; then
command -v sudo >/dev/null 2>&1 || die "run as root (sudo is not installed)"
command -v sudo >/dev/null 2>&1 || die "run as root (sudo is not installed)"
SUDO="sudo"
SUDO="sudo"
@@ -166,17 +268,7 @@ else
@@ -166,17 +268,7 @@ else
fi
fi
# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----
# ---- the Adeept kit's stock server: never run it (fixed admin:123456 login on 0.0.0.0:8888, MJPEG on :5000) ----
-if command -v systemctl >/dev/null 2>&1; then
- unit=Adeept_Robot.service
- if systemctl is-enabled "$unit" >/dev/null 2>&1 || systemctl is-active "$unit" >/dev/null 2>&1; then
- say "disabling the Adeept kit's stock server ($unit): it listens on 0.0.0.0:8888 with a fixed password"
- $SUDO systemctl disable --now "$unit" || true
- fi
-fi
-if command -v pgrep >/dev/null 2>&1 && pgrep -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" >/dev/null 2>&1; then
- say "stopping a running Adeept stock server process"
- $SUDO pkill -f "Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py" || true
-fi
+disable_stock_server
# ---- service ----
# ---- service ----
if [ "$SERVICE" = 1 ]; then
if [ "$SERVICE" = 1 ]; then
new file mode 100644
new file mode 100644
@@ -0,0 +1,99 @@
@@ -0,0 +1,99 @@
+#!/bin/sh
+# Tests install.sh's handling of the Adeept kit's stock-server autostart: crontab (root's and the invoking
+# user's, including @reboot lines) and /etc/rc.local. The OPENVIBE_* test overrides point the checks at files in
+# a temp dir, so this needs no root and touches no real crontab or /etc/rc.local.
+set -eu
+
+here="$(cd "$(dirname "$0")" && pwd)"
+installer="$here/../install/install.sh"
+
+fail() { printf 'FAIL: %s\n' "$*" >&2; exit 1; }
+
+tmp="$(mktemp -d)"
+trap 'rm -rf "$tmp"' EXIT
+root_cron="$tmp/root-crontab"
+user_cron="$tmp/user-crontab"
+rc_local="$tmp/rc.local"
+
+cat > "$root_cron" <<'EOF'
+# root's crontab
+PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
+@reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/WebServer.py
+0 4 * * * /usr/bin/backup.sh
+EOF
+
+cat > "$user_cron" <<'EOF'
+@reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/app.py
+*/5 * * * * /home/pi/adeept_awr/Server/Server_OrdinaryWheels/helper.py
+EOF
+
+cat > "$rc_local" <<'EOF'
+#!/bin/sh -e
+python3 /home/pi/adeept_awr/Server/Server_MecanumWheels/WebServer.py &
+/usr/local/bin/other-thing &
+exit 0
+EOF
+
+run() {
+ OPENVIBE_INSTALL_STOCK_ONLY=1 \
+ OPENVIBE_CRONTAB_ROOT="$root_cron" \
+ OPENVIBE_CRONTAB_USER="$user_cron" \
+ OPENVIBE_RC_LOCAL="$rc_local" \
+ sh "$installer"
+}
+
+out="$(run)"
+printf '%s\n' "$out"
+
+# One line per disabled thing.
+disabled="$(printf '%s\n' "$out" | grep -c "stock server autostart" || true)"
+[ "$disabled" = 3 ] || fail "expected 3 disabled lines, got $disabled"
+
+# The stock lines are commented out with the marker, not deleted.
+grep -q '^#openvibe-node-disabled: @reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/WebServer\.py$' "$root_cron" ||
+ fail "root crontab stock line not commented out"
+grep -q '^#openvibe-node-disabled: @reboot python3 /home/pi/adeept_awr/Server/Server_OrdinaryWheels/app\.py$' "$user_cron" ||
+ fail "user crontab @reboot stock line not commented out"
+grep -q '^#openvibe-node-disabled: python3 /home/pi/adeept_awr/Server/Server_MecanumWheels/WebServer\.py &$' "$rc_local" ||
+ fail "rc.local stock line not commented out"
+
+# Unrelated lines are untouched.
+grep -qx '0 4 \* \* \* /usr/bin/backup.sh' "$root_cron" || fail "unrelated root crontab line changed"
+grep -qx 'PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin' "$root_cron" || fail "root crontab PATH line changed"
+grep -qxF "# root's crontab" "$root_cron" || fail "root crontab comment changed"
+grep -qx '\*/5 \* \* \* \* /home/pi/adeept_awr/Server/Server_OrdinaryWheels/helper.py' "$user_cron" ||
+ fail "unrelated user crontab line changed"
+grep -qx '/usr/local/bin/other-thing &' "$rc_local" || fail "unrelated rc.local line changed"
+grep -qx 'exit 0' "$rc_local" || fail "rc.local exit line changed"
+
+# No stock-server line is left active.
+if grep -Eq '^[[:space:]]*[^#[:space:]].*Server_(Ordinary|Mecanum)Wheels/(WebServer|APPServer|GUIServer|app)\.py' \
+ "$root_cron" "$user_cron" "$rc_local"; then
+ fail "a stock-server line is still active"
+fi
+
+# A second run is a no-op: nothing reported and nothing changed.
+cp "$root_cron" "$tmp/root.before"
+cp "$user_cron" "$tmp/user.before"
+cp "$rc_local" "$tmp/rc.before"
+out2="$(run)"
+printf '%s\n' "$out2"
+if printf '%s\n' "$out2" | grep -q "stock server autostart"; then fail "second run reported more changes"; fi
+cmp -s "$root_cron" "$tmp/root.before" || fail "second run changed the root crontab"
+cmp -s "$user_cron" "$tmp/user.before" || fail "second run changed the user crontab"
+cmp -s "$rc_local" "$tmp/rc.before" || fail "second run changed rc.local"
+
+# A host without the kit: unrelated crontab/rc.local content is left alone and nothing is reported.
+none="$tmp/none"
+mkdir -p "$none"
+printf '0 5 * * * /usr/bin/echo hello\n' > "$none/cron"
+printf 'PATH=/bin\n' > "$none/cron2"
+printf '#!/bin/sh\nexit 0\n' > "$none/rc"
+out3="$(OPENVIBE_INSTALL_STOCK_ONLY=1 OPENVIBE_CRONTAB_ROOT="$none/cron" OPENVIBE_CRONTAB_USER="$none/cron2" \
+ OPENVIBE_RC_LOCAL="$none/rc" sh "$installer")"
+printf '%s\n' "$out3"
+if printf '%s\n' "$out3" | grep -q "stock server"; then fail "reported changes on a host without the kit"; fi
+grep -qx '0 5 \* \* \* /usr/bin/echo hello' "$none/cron" || fail "unrelated cron line changed without the kit"
+grep -qx '#!/bin/sh' "$none/rc" || fail "unrelated rc.local changed without the kit"
+
+printf 'ok: install.sh stock-server crontab/rc.local handling\n'