#!/bin/sh set -eu source_dir=${1:?usage: install-wechat-health-watch.sh SOURCE_DIR} runtime_source_dir="$source_dir" user_unit_source_dir="$source_dir/deploy/systemd/user" config_source_dir="$source_dir/packages/jwebot-config/src/jwebot_config" sdk_source_dir="$source_dir/packages/jwebot-sdk/src/jwebot_sdk" kernel_source_dir="$source_dir/packages/jwebot-kernel/src/jwebot_kernel" kernel_data_source_dir="$kernel_source_dir/data" official_source_dir="$source_dir/packages/jwebot-official-extensions/src/jwebot_official_extensions" official_adapter_source_dir="$official_source_dir/adapters" official_autochess_data_source_dir="$official_source_dir/data/cs_autochess" target_dir="$HOME/.local/lib/wechat-health-watch" unit_dir="$HOME/.config/systemd/user" config_dir="$HOME/.config/jwebot" environment_file="$config_dir/current/generated/runtime.env" adaptive_state_file="$HOME/.local/state/wechat-adaptive-ocr/recorder-state.json" runtime_python=${JWEBOT_RUNTIME_PYTHON:-/opt/jwebot/venv/bin/python} health_settle_seconds=${HEALTH_SETTLE_SECONDS:-5} edge_health_timeout_seconds=${EDGE_HEALTH_TIMEOUT_SECONDS:-45} adaptive_ready_timeout_seconds=${ADAPTIVE_READY_TIMEOUT_SECONDS:-90} start_timer=${START_TIMER:-1} ocr_script_name=wechat_local_ocr.py script_name=wechat_health_watch.py adaptive_script_name=wechat_adaptive_recorder.py adaptive_bridge_name=wechat_adaptive_gui_bridge.py edge_script_name=jwebot_edge.py edge_delivery_name=jwebot_edge_delivery.py release_update_name=jwebot_release_update.py service_name=wechat-health-watch.service timer_name=wechat-health-watch.timer adaptive_service_name=wechat-ocr-adaptive-recorder.service edge_service_name=jwebot-edge.service edge_memory_service_name=jwebot-edge-memory-sync.service edge_memory_timer_name=jwebot-edge-memory-sync.timer backup_dir="$target_dir/.backup-$(date +%Y%m%d-%H%M%S)-$$" unit_state_file="$backup_dir/systemd-user-state.tsv" adaptive_ready_marker="$backup_dir/adaptive-start.marker" installed=0 managed_units=" $edge_service_name $adaptive_service_name $timer_name $edge_memory_timer_name $service_name $edge_memory_service_name " jwebot_log() { printf '[JWeBot] %s\n' "$*" } unit_enabled_state() { systemctl --user is-enabled "$1" 2>/dev/null || true } unit_active_state() { systemctl --user is-active "$1" 2>/dev/null || true } save_unit_states() { : >"$unit_state_file" for unit in $managed_units; do printf '%s\t%s\t%s\n' \ "$unit" \ "$(unit_enabled_state "$unit")" \ "$(unit_active_state "$unit")" \ >>"$unit_state_file" done } recorded_unit_field() { unit=$1 field=$2 awk -F ' ' -v unit="$unit" -v field="$field" \ '$1 == unit { print $field; exit }' "$unit_state_file" } restore_unit_states() { systemctl --user daemon-reload >/dev/null 2>&1 || true for unit in $managed_units; do systemctl --user stop "$unit" >/dev/null 2>&1 || true done for unit in $managed_units; do enabled=$(recorded_unit_field "$unit" 2>/dev/null || true) case "$enabled" in enabled) systemctl --user unmask "$unit" >/dev/null 2>&1 || true systemctl --user enable "$unit" >/dev/null 2>&1 || true ;; enabled-runtime) systemctl --user unmask "$unit" >/dev/null 2>&1 || true systemctl --user enable --runtime "$unit" >/dev/null 2>&1 || true ;; disabled) systemctl --user unmask "$unit" >/dev/null 2>&1 || true systemctl --user disable "$unit" >/dev/null 2>&1 || true ;; masked) systemctl --user mask "$unit" >/dev/null 2>&1 || true ;; masked-runtime) systemctl --user mask --runtime "$unit" >/dev/null 2>&1 || true ;; esac done for unit in $managed_units; do active=$(recorded_unit_field "$unit" 3 2>/dev/null || true) case "$active" in active|activating|reloading) systemctl --user start "$unit" >/dev/null 2>&1 || true ;; esac done systemctl --user reset-failed >/dev/null 2>&1 || true } restore_file() { name=$1 target=$2 mode=$3 if [ -f "$backup_dir/$name" ]; then install -m "$mode" "$backup_dir/$name" "$target" else rm -f "$target" fi } wait_for_edge_health() { attempt=0 while [ "$attempt" -lt "$edge_health_timeout_seconds" ]; do if systemctl --user is-failed --quiet "$edge_service_name"; then echo "edge service failed before becoming healthy" >&2 return 1 fi if edge_health=$( printf '%s\n' '{"op":"health"}' \ | "$runtime_python" "$target_dir/$edge_script_name" respond \ 2>/dev/null ); then if printf '%s' "$edge_health" \ | "$runtime_python" -c \ 'import json,sys; value=json.load(sys.stdin); raise SystemExit(0 if value.get("status") == "ok" else 1)' then return 0 fi fi attempt=$((attempt + 1)) sleep 1 done echo "edge health did not become ready within ${edge_health_timeout_seconds}s" >&2 return 1 } wait_for_adaptive_ready() { attempt=0 while [ "$attempt" -lt "$adaptive_ready_timeout_seconds" ]; do if systemctl --user is-failed --quiet "$adaptive_service_name"; then echo "[JWeBot] error: adaptive service failed before becoming ready" >&2 return 1 fi if [ -f "$adaptive_state_file" ] \ && [ "$adaptive_state_file" -nt "$adaptive_ready_marker" ] \ && "$runtime_python" - "$adaptive_state_file" "$adaptive_started_epoch" <<'PY' import json import sys from datetime import datetime from pathlib import Path try: value = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) captured_at = datetime.fromisoformat(str(value["last_capture_at"])) ready = ( value.get("initialized") is True and isinstance(value.get("message_signature_scheme"), str) and bool(value["message_signature_scheme"]) and isinstance(value.get("visible_signatures"), list) and captured_at.timestamp() >= int(sys.argv[2]) ) except (KeyError, OSError, TypeError, ValueError, json.JSONDecodeError): ready = False raise SystemExit(0 if ready else 1) PY then return 0 fi attempt=$((attempt + 1)) sleep 1 done echo "[JWeBot] error: adaptive OCR did not become ready within ${adaptive_ready_timeout_seconds}s" >&2 return 1 } rollback() { status=$? trap - EXIT HUP INT TERM if [ "$installed" -eq 1 ]; then jwebot_log "user runtime rollback starting backup=$backup_dir" systemctl --user stop \ "$adaptive_service_name" \ "$edge_service_name" \ "$timer_name" \ "$edge_memory_timer_name" \ >/dev/null 2>&1 || true restore_file "$ocr_script_name" "$target_dir/$ocr_script_name" 0700 restore_file "$script_name" "$target_dir/$script_name" 0700 restore_file "$adaptive_script_name" "$target_dir/$adaptive_script_name" 0700 restore_file "$adaptive_bridge_name" "$target_dir/$adaptive_bridge_name" 0700 restore_file "$edge_script_name" "$target_dir/$edge_script_name" 0700 restore_file "$edge_delivery_name" "$target_dir/$edge_delivery_name" 0700 restore_file "$release_update_name" "$target_dir/$release_update_name" 0700 for package_name in \ jwebot_config \ jwebot_sdk \ jwebot_kernel \ jwebot_official_extensions do rm -rf "$target_dir/$package_name" if [ -d "$backup_dir/$package_name" ]; then cp -a "$backup_dir/$package_name" "$target_dir/$package_name" fi done restore_file "$service_name" "$unit_dir/$service_name" 0644 restore_file "$timer_name" "$unit_dir/$timer_name" 0644 restore_file "$adaptive_service_name" "$unit_dir/$adaptive_service_name" 0644 restore_file "$edge_service_name" "$unit_dir/$edge_service_name" 0644 restore_file "$edge_memory_service_name" "$unit_dir/$edge_memory_service_name" 0644 restore_file "$edge_memory_timer_name" "$unit_dir/$edge_memory_timer_name" 0644 restore_file environment "$environment_file" 0600 restore_unit_states jwebot_log "user runtime rollback ready backup=$backup_dir" fi exit "$status" } trap rollback EXIT HUP INT TERM jwebot_log "user runtime preflight starting" test -x "$runtime_python" for path in \ "$source_dir/VERSION" \ "$source_dir/CHANGELOG.md" \ "$runtime_source_dir/$ocr_script_name" \ "$runtime_source_dir/$script_name" \ "$runtime_source_dir/$adaptive_script_name" \ "$runtime_source_dir/$adaptive_bridge_name" \ "$runtime_source_dir/$edge_script_name" \ "$runtime_source_dir/$edge_delivery_name" \ "$runtime_source_dir/$release_update_name" \ "$user_unit_source_dir/$service_name" \ "$user_unit_source_dir/$timer_name" \ "$user_unit_source_dir/$adaptive_service_name" \ "$user_unit_source_dir/$edge_service_name" \ "$user_unit_source_dir/$edge_memory_service_name" \ "$user_unit_source_dir/$edge_memory_timer_name" do test -f "$path" done for directory in \ "$config_source_dir" \ "$sdk_source_dir" \ "$kernel_source_dir" \ "$official_source_dir" \ "$official_adapter_source_dir" do test -d "$directory" find "$directory" -maxdepth 1 -type f -name '*.py' | grep -q . done test -d "$official_autochess_data_source_dir" find "$official_autochess_data_source_dir" -maxdepth 1 -type f -name '*.json' \ | grep -q . test -f "$environment_file" if systemctl is-active --quiet wechat-local-ocr-recorder.service \ || systemctl is-active --quiet wechat-local-ocr-responder.service then echo "[JWeBot] error: legacy system OCR is active; use root deployer" >&2 exit 1 fi jwebot_log "user runtime preflight ready" jwebot_log "user runtime backup starting path=$backup_dir" install -d -m 0700 "$target_dir" "$unit_dir" "$config_dir" "$backup_dir" save_unit_states for name in \ VERSION \ CHANGELOG.md \ "$ocr_script_name" \ "$script_name" \ "$adaptive_script_name" \ "$adaptive_bridge_name" \ "$edge_script_name" \ "$edge_delivery_name" \ "$release_update_name" do if [ -f "$target_dir/$name" ]; then cp -a "$target_dir/$name" "$backup_dir/$name" fi done for package_name in \ jwebot_config \ jwebot_sdk \ jwebot_kernel \ jwebot_official_extensions do if [ -d "$target_dir/$package_name" ]; then cp -a "$target_dir/$package_name" "$backup_dir/$package_name" fi done for name in \ "$service_name" \ "$timer_name" \ "$adaptive_service_name" \ "$edge_service_name" \ "$edge_memory_service_name" \ "$edge_memory_timer_name" do if [ -f "$unit_dir/$name" ]; then cp -a "$unit_dir/$name" "$backup_dir/$name" fi done if [ -f "$environment_file" ]; then cp -a "$environment_file" "$backup_dir/environment" fi jwebot_log "user runtime backup ready path=$backup_dir" installed=1 jwebot_log "user services stopping" systemctl --user stop \ "$adaptive_service_name" \ "$edge_service_name" \ "$timer_name" \ "$edge_memory_timer_name" \ "$service_name" \ "$edge_memory_service_name" \ >/dev/null 2>&1 || true jwebot_log "user runtime files install starting" install -m 0600 "$source_dir/VERSION" "$target_dir/VERSION" install -m 0600 "$source_dir/CHANGELOG.md" "$target_dir/CHANGELOG.md" install -m 0700 "$runtime_source_dir/$ocr_script_name" "$target_dir/$ocr_script_name" install -m 0700 "$runtime_source_dir/$script_name" "$target_dir/$script_name" install -m 0700 "$runtime_source_dir/$adaptive_script_name" "$target_dir/$adaptive_script_name" install -m 0700 "$runtime_source_dir/$adaptive_bridge_name" "$target_dir/$adaptive_bridge_name" install -m 0700 "$runtime_source_dir/$edge_script_name" "$target_dir/$edge_script_name" install -m 0700 "$runtime_source_dir/$edge_delivery_name" "$target_dir/$edge_delivery_name" install -m 0700 \ "$runtime_source_dir/$release_update_name" \ "$target_dir/$release_update_name" rm -rf \ "$target_dir/jwebot_config" \ "$target_dir/jwebot_sdk" \ "$target_dir/jwebot_kernel" \ "$target_dir/jwebot_official_extensions" install -d -m 0700 \ "$target_dir/jwebot_config" \ "$target_dir/jwebot_sdk" \ "$target_dir/jwebot_kernel" \ "$target_dir/jwebot_kernel/data" \ "$target_dir/jwebot_official_extensions" \ "$target_dir/jwebot_official_extensions/adapters" \ "$target_dir/jwebot_official_extensions/data" \ "$target_dir/jwebot_official_extensions/data/cs_autochess" for path in "$config_source_dir"/*.py; do install -m 0600 "$path" "$target_dir/jwebot_config/$(basename "$path")" done for path in "$sdk_source_dir"/*.py; do install -m 0600 "$path" "$target_dir/jwebot_sdk/$(basename "$path")" done for path in "$kernel_source_dir"/*.py; do install -m 0600 "$path" "$target_dir/jwebot_kernel/$(basename "$path")" done for path in "$kernel_data_source_dir"/*.json; do [ -e "$path" ] || continue install -m 0600 "$path" "$target_dir/jwebot_kernel/data/$(basename "$path")" done for path in "$official_source_dir"/*.py; do install -m 0600 "$path" "$target_dir/jwebot_official_extensions/$(basename "$path")" done for path in "$official_adapter_source_dir"/*.py; do install -m 0600 "$path" \ "$target_dir/jwebot_official_extensions/adapters/$(basename "$path")" done for path in "$official_autochess_data_source_dir"/*.json; do install -m 0600 "$path" \ "$target_dir/jwebot_official_extensions/data/cs_autochess/$(basename "$path")" done for name in \ "$service_name" \ "$timer_name" \ "$adaptive_service_name" \ "$edge_service_name" \ "$edge_memory_service_name" \ "$edge_memory_timer_name" do rendered_unit="$backup_dir/rendered-$name" sed "s#/opt/jwebot/venv/bin/python#$runtime_python#g" \ "$user_unit_source_dir/$name" >"$rendered_unit" install -m 0644 "$rendered_unit" "$unit_dir/$name" done for name in \ "$ocr_script_name" \ "$script_name" \ "$adaptive_script_name" \ "$adaptive_bridge_name" \ "$edge_script_name" \ "$edge_delivery_name" \ "$release_update_name" do "$runtime_python" -m py_compile "$target_dir/$name" done "$runtime_python" -m compileall -q \ "$target_dir/jwebot_config" \ "$target_dir/jwebot_sdk" \ "$target_dir/jwebot_kernel" \ "$target_dir/jwebot_official_extensions" systemd-analyze --user verify \ "$unit_dir/$service_name" \ "$unit_dir/$timer_name" \ "$unit_dir/$adaptive_service_name" \ "$unit_dir/$edge_service_name" \ "$unit_dir/$edge_memory_service_name" \ "$unit_dir/$edge_memory_timer_name" jwebot_log "user runtime files install ready" systemctl --user daemon-reload systemctl --user enable \ "$timer_name" \ "$edge_service_name" \ "$edge_memory_timer_name" \ "$adaptive_service_name" jwebot_log "edge starting" systemctl --user start "$edge_service_name" systemctl --user is-active --quiet "$edge_service_name" wait_for_edge_health jwebot_log "edge active health=ok" : >"$adaptive_ready_marker" adaptive_started_epoch=$(date +%s) jwebot_log "adaptive starting" systemctl --user start "$adaptive_service_name" systemctl --user is-active --quiet "$adaptive_service_name" jwebot_log "adaptive active" restarts_before=$( systemctl --user show "$adaptive_service_name" -p NRestarts --value ) wait_for_adaptive_ready jwebot_log "adaptive ready baseline=captured" sleep "$health_settle_seconds" systemctl --user is-active --quiet "$adaptive_service_name" restarts_after=$( systemctl --user show "$adaptive_service_name" -p NRestarts --value ) test "$restarts_before" = "$restarts_after" jwebot_log "adaptive stability ready restarts=$restarts_after" if [ "$start_timer" -eq 1 ]; then systemctl --user start "$timer_name" "$edge_memory_timer_name" systemctl --user is-active --quiet "$timer_name" systemctl --user is-active --quiet "$edge_memory_timer_name" jwebot_log "timers active" else jwebot_log "timers inactive requested=false" fi installed=0 trap - EXIT HUP INT TERM jwebot_log "user runtime ready" jwebot_log "backup active path=$backup_dir" jwebot_log "edge active health=ok" jwebot_log "adaptive active restarts=$restarts_after"