mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-10 20:07:51 +08:00
420 lines
11 KiB
Bash
Executable File
420 lines
11 KiB
Bash
Executable File
#!/bin/sh /etc/rc.common
|
|
# luci-app-hermes — procd init script
|
|
|
|
USE_PROCD=1
|
|
START=99
|
|
STOP=10
|
|
|
|
EXTRA_COMMANDS="setup status_service restart_gateway"
|
|
EXTRA_HELP=" setup Install Python and Hermes Agent
|
|
status_service Show service status
|
|
restart_gateway Restart only Gateway instance (does not affect Web PTY)"
|
|
|
|
HERMES_BASE="/opt/hermes"
|
|
HERMES_VENV="${HERMES_BASE}/venv"
|
|
HERMES_DATA="${HERMES_BASE}/data"
|
|
HERMES_CONFIG="${HERMES_DATA}/config.yaml"
|
|
HERMES_BIN="${HERMES_VENV}/bin/hermes"
|
|
PYTHON_BIN="${HERMES_VENV}/bin/python3"
|
|
|
|
# ── OverlayFS compatibility fix ──
|
|
# Docker bind mount (/overlay/upper/opt/docker) makes /opt unwritable
|
|
# Solution: bind mount upper layer /opt to merged view /opt
|
|
_hermes_fix_opt() {
|
|
mkdir -p /opt/hermes/.probe 2>/dev/null && { rmdir /opt/hermes/.probe 2>/dev/null; return 0; }
|
|
if [ -d /overlay/upper/opt ]; then
|
|
mkdir -p /overlay/upper/opt/hermes 2>/dev/null
|
|
mount --bind /overlay/upper/opt /opt 2>/dev/null && return 0
|
|
fi
|
|
return 1
|
|
}
|
|
_hermes_fix_opt
|
|
|
|
# ── Port listening check ──
|
|
_ensure_port_free() {
|
|
local p="$1"
|
|
local i=0 max_wait=3
|
|
|
|
# Check if port is already in use
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
fi
|
|
|
|
# Port is occupied, try to clean up
|
|
for hermes_pid in $(pgrep -f "hermes.*gateway" 2>/dev/null); do
|
|
kill "$hermes_pid" 2>/dev/null
|
|
done
|
|
|
|
while [ $i -lt $max_wait ]; do
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
fi
|
|
sleep 1
|
|
i=$((i + 1))
|
|
done
|
|
|
|
# Last resort: SIGKILL
|
|
local port_pid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
port_pid=$(ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
|
|
fi
|
|
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1
|
|
return 0
|
|
}
|
|
|
|
port_listening() {
|
|
local p="$1"
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} "
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} "
|
|
fi
|
|
}
|
|
|
|
sync_uci_to_yaml() {
|
|
# Sync UCI config to /root/.hermes/config.yaml
|
|
local port bind api_endpoint api_key model
|
|
port=$(uci -q get hermes.main.port || echo "3000")
|
|
bind=$(uci -q get hermes.main.bind || echo "lan")
|
|
api_endpoint=$(uci -q get hermes.main.api_endpoint || echo "https://api.boos.lat/v1")
|
|
api_key=$(uci -q get hermes.main.api_key || echo "")
|
|
model=$(uci -q get hermes.main.model || echo "opus4.6")
|
|
|
|
# Map UCI bind to listen address
|
|
local listen_host="127.0.0.1"
|
|
case "$bind" in
|
|
lan) listen_host="0.0.0.0" ;;
|
|
loopback) listen_host="127.0.0.1" ;;
|
|
all) listen_host="0.0.0.0" ;;
|
|
*) listen_host="0.0.0.0" ;;
|
|
esac
|
|
|
|
# Ensure config directory exists
|
|
mkdir -p /root/.hermes 2>/dev/null
|
|
mkdir -p "$(dirname "$HERMES_CONFIG")" 2>/dev/null
|
|
|
|
# Generate token if not set
|
|
local token
|
|
token=$(uci -q get hermes.main.token || echo "")
|
|
if [ -z "$token" ]; then
|
|
token=$(head -c 24 /dev/urandom | hexdump -e '24/1 "%02x"' 2>/dev/null || openssl rand -hex 24 2>/dev/null || echo "hermes_$(date +%s)")
|
|
uci set hermes.main.token="$token"
|
|
uci commit hermes 2>/dev/null
|
|
fi
|
|
|
|
# Write config.yaml using shell (no Python dependency at this point)
|
|
cat > /root/.hermes/config.yaml << YAMLEOF
|
|
# Hermes Agent configuration — auto-generated by luci-app-hermes
|
|
# UCI sync: $(date '+%Y-%m-%d %H:%M:%S')
|
|
|
|
gateway:
|
|
port: ${port}
|
|
bind: ${listen_host}
|
|
auth:
|
|
mode: token
|
|
token: ${token}
|
|
|
|
api:
|
|
endpoint: "${api_endpoint}"
|
|
key: "${api_key}"
|
|
|
|
model:
|
|
primary: "${model}"
|
|
|
|
tools:
|
|
profile: coding
|
|
|
|
update:
|
|
checkOnStart: false
|
|
YAMLEOF
|
|
|
|
# Also sync to HERMES_DATA if different
|
|
if [ -n "$HERMES_CONFIG" ] && [ "$HERMES_CONFIG" != "/root/.hermes/config.yaml" ]; then
|
|
mkdir -p "$(dirname "$HERMES_CONFIG")" 2>/dev/null
|
|
cp /root/.hermes/config.yaml "$HERMES_CONFIG" 2>/dev/null || true
|
|
fi
|
|
|
|
chmod 600 /root/.hermes/config.yaml 2>/dev/null || true
|
|
}
|
|
|
|
start_service() {
|
|
local enabled port bind pty_port api_endpoint api_key model
|
|
config_load hermes
|
|
config_get enabled main enabled "0"
|
|
config_get port main port "3000"
|
|
config_get bind main bind "lan"
|
|
config_get pty_port main pty_port "3001"
|
|
config_get api_endpoint main api_endpoint "https://api.boos.lat/v1"
|
|
config_get api_key main api_key ""
|
|
config_get model main model "opus4.6"
|
|
|
|
[ "$enabled" = "1" ] || {
|
|
echo "hermes is disabled. Set enabled to 1 in /etc/config/hermes"
|
|
return 0
|
|
}
|
|
|
|
# Check Python
|
|
if [ ! -x "$PYTHON_BIN" ]; then
|
|
echo "Python not found: $PYTHON_BIN"
|
|
echo "Please run: hermes-env setup"
|
|
return 1
|
|
fi
|
|
|
|
# Check hermes binary
|
|
if [ ! -x "$HERMES_BIN" ]; then
|
|
echo "Hermes not installed. Please run: hermes-env setup"
|
|
return 1
|
|
fi
|
|
|
|
# Sync UCI to config.yaml
|
|
sync_uci_to_yaml
|
|
|
|
# Ensure gateway port is not occupied by stale processes
|
|
_ensure_port_free "$port"
|
|
|
|
# Calculate memory limit based on system RAM
|
|
local mem_limit=""
|
|
local total_kb
|
|
total_kb=$(awk '/MemTotal/{print $2}' /proc/meminfo 2>/dev/null || echo "0")
|
|
if [ "$total_kb" -ge 3000000 ] 2>/dev/null; then
|
|
mem_limit="512m"
|
|
elif [ "$total_kb" -ge 1500000 ] 2>/dev/null; then
|
|
mem_limit="256m"
|
|
elif [ "$total_kb" -ge 700000 ] 2>/dev/null; then
|
|
mem_limit="128m"
|
|
else
|
|
mem_limit="64m"
|
|
fi
|
|
|
|
# Start Hermes Gateway (main service, foreground)
|
|
procd_open_instance "gateway"
|
|
procd_set_param command "$HERMES_BIN" gateway run \
|
|
--port "$port" --bind "$bind"
|
|
procd_set_param env \
|
|
HOME="/root" \
|
|
HERMES_HOME="/root/.hermes" \
|
|
HERMES_CONFIG="/root/.hermes/config.yaml" \
|
|
PATH="${HERMES_VENV}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" \
|
|
PYTHONPATH="${HERMES_VENV}/lib/python3*/site-packages" \
|
|
HERMES_API_ENDPOINT="$api_endpoint" \
|
|
HERMES_API_KEY="$api_key" \
|
|
HERMES_MODEL="$model"
|
|
procd_set_param respawn 3600 10 5
|
|
procd_set_param stdout 1
|
|
procd_set_param stderr 1
|
|
procd_set_param pidfile /var/run/hermes.pid
|
|
procd_close_instance
|
|
|
|
# Start Web PTY config terminal (auxiliary service)
|
|
local pty_token
|
|
pty_token=$(uci -q get hermes.main.pty_token || echo "")
|
|
if [ -z "$pty_token" ]; then
|
|
pty_token=$(head -c 16 /dev/urandom | hexdump -e '16/1 "%02x"' 2>/dev/null || openssl rand -hex 16 2>/dev/null || echo "pty_$(date +%s)")
|
|
uci set hermes.main.pty_token="$pty_token"
|
|
uci commit hermes 2>/dev/null
|
|
fi
|
|
|
|
procd_open_instance "pty"
|
|
procd_set_param command "$PYTHON_BIN" /usr/share/hermes/web-pty.py
|
|
procd_set_param env \
|
|
HERMES_PTY_PORT="$pty_port" \
|
|
HERMES_PTY_TOKEN="$pty_token" \
|
|
HERMES_VENV="$HERMES_VENV" \
|
|
HERMES_DATA="$HERMES_DATA" \
|
|
HOME="/root" \
|
|
PATH="${HERMES_VENV}/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
|
procd_set_param respawn 3600 5 5
|
|
procd_set_param stdout 1
|
|
procd_set_param stderr 1
|
|
procd_set_param pidfile /var/run/hermes-pty.pid
|
|
procd_close_instance
|
|
}
|
|
|
|
_ensure_port_free() {
|
|
local p="$1"
|
|
local i=0 max_wait=3
|
|
|
|
# Check if port is already in use
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
fi
|
|
|
|
# Port is occupied, try to clean up
|
|
for hermes_pid in $(pgrep -f "hermes.*gateway" 2>/dev/null); do
|
|
kill "$hermes_pid" 2>/dev/null
|
|
done
|
|
|
|
while [ $i -lt $max_wait ]; do
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${p} " || return 0
|
|
fi
|
|
sleep 1
|
|
i=$((i + 1))
|
|
done
|
|
|
|
# Last resort: SIGKILL
|
|
local port_pid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
port_pid=$(ss -tulnp 2>/dev/null | grep ":${p} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${p} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
|
|
fi
|
|
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null && sleep 1
|
|
return 0
|
|
}
|
|
|
|
stop_service() {
|
|
local port
|
|
port=$(uci -q get hermes.main.port || echo "3000")
|
|
|
|
# Get all related PIDs
|
|
local gw_pids=""
|
|
gw_pids=$(pgrep -f "hermes.*gateway" 2>/dev/null)
|
|
gw_pids="$gw_pids $(pgrep -f "hermes.*gateway.*run" 2>/dev/null)"
|
|
|
|
# Send SIGTERM to all processes
|
|
for pid in $gw_pids; do
|
|
[ -n "$pid" ] && kill "$pid" 2>/dev/null
|
|
done
|
|
|
|
# Wait for port to be released
|
|
local wait_count=0
|
|
local max_wait=4
|
|
while [ $wait_count -lt $max_wait ]; do
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
fi
|
|
sleep 1
|
|
wait_count=$((wait_count + 1))
|
|
done
|
|
|
|
# Force kill if still running
|
|
if [ $wait_count -ge $max_wait ]; then
|
|
local port_pid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
|
|
fi
|
|
if [ -n "$port_pid" ]; then
|
|
kill -9 "$port_pid" 2>/dev/null
|
|
sleep 1
|
|
fi
|
|
fi
|
|
}
|
|
|
|
service_triggers() {
|
|
procd_add_reload_trigger "hermes"
|
|
}
|
|
|
|
reload_service() {
|
|
stop
|
|
sleep 1
|
|
start
|
|
}
|
|
|
|
setup() {
|
|
echo "Calling hermes-env setup..."
|
|
/usr/bin/hermes-env setup
|
|
}
|
|
|
|
restart_gateway() {
|
|
# Restart only the Gateway process, does not affect PTY
|
|
local port
|
|
port=$(uci -q get hermes.main.port || echo "3000")
|
|
|
|
# Get gateway PID
|
|
local port_pid="" gw_pid=""
|
|
if command -v ss >/dev/null 2>&1; then
|
|
port_pid=$(ss -tulnp 2>/dev/null | grep ":${port} " | sed -n 's/.*pid=\([0-9]*\).*/\1/p' | head -1)
|
|
else
|
|
port_pid=$(netstat -tulnp 2>/dev/null | grep ":${port} " | sed -n 's|.* \([0-9]*\)/.*|\1|p' | head -1)
|
|
fi
|
|
gw_pid=$(ubus call service list '{"name":"hermes"}' 2>/dev/null | \
|
|
jsonfilter -e '$.hermes.instances.gateway.pid' 2>/dev/null) || true
|
|
|
|
# Send SIGTERM
|
|
[ -n "$port_pid" ] && kill "$port_pid" 2>/dev/null
|
|
[ -n "$gw_pid" ] && kill -0 "$gw_pid" 2>/dev/null && kill "$gw_pid" 2>/dev/null
|
|
|
|
# Kill any remaining hermes gateway processes
|
|
for pid in $(pgrep -f "hermes.*gateway" 2>/dev/null) $(pgrep -f "hermes.*gateway.*run" 2>/dev/null); do
|
|
kill "$pid" 2>/dev/null
|
|
done
|
|
|
|
# Wait for port to be released
|
|
local wait_count=0
|
|
while [ $wait_count -lt 3 ]; do
|
|
if command -v ss >/dev/null 2>&1; then
|
|
ss -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
else
|
|
netstat -tulnp 2>/dev/null | grep -q ":${port} " || break
|
|
fi
|
|
sleep 1
|
|
wait_count=$((wait_count + 1))
|
|
done
|
|
|
|
# Force kill if still running
|
|
if [ $wait_count -ge 3 ]; then
|
|
[ -n "$port_pid" ] && kill -9 "$port_pid" 2>/dev/null
|
|
sleep 1
|
|
fi
|
|
|
|
# If no procd gateway, restart the service
|
|
if [ -z "$gw_pid" ]; then
|
|
/etc/init.d/hermes start >/dev/null 2>&1
|
|
fi
|
|
}
|
|
|
|
status_service() {
|
|
local port pty_port
|
|
port=$(uci -q get hermes.main.port || echo "3000")
|
|
pty_port=$(uci -q get hermes.main.pty_port || echo "3001")
|
|
|
|
echo "=== Hermes Agent Service Status ==="
|
|
|
|
# Python
|
|
if [ -x "$PYTHON_BIN" ]; then
|
|
echo "Python: $($PYTHON_BIN --version 2>/dev/null)"
|
|
else
|
|
echo "Python: Not installed"
|
|
fi
|
|
|
|
# Hermes Agent
|
|
if [ -x "$HERMES_BIN" ]; then
|
|
echo "Hermes: $($HERMES_BIN --version 2>/dev/null | head -1)"
|
|
else
|
|
echo "Hermes: Not installed"
|
|
fi
|
|
|
|
# Gateway
|
|
if port_listening "$port"; then
|
|
echo "Gateway: ✅ Running (:$port)"
|
|
else
|
|
echo "Gateway: ⏹ Stopped"
|
|
fi
|
|
|
|
# PTY
|
|
if port_listening "$pty_port"; then
|
|
echo "Config PTY: ✅ Running (:$pty_port)"
|
|
else
|
|
echo "Config PTY: ⏹ Stopped"
|
|
fi
|
|
|
|
# Config
|
|
echo "Config: $HERMES_CONFIG"
|
|
echo "Data Dir: $HERMES_DATA"
|
|
}
|