From 4d4deb51a0c6c251f7d211e9f1f1342a3ede7650 Mon Sep 17 00:00:00 2001 From: oliver Date: Mon, 1 Jun 2026 11:23:59 +0800 Subject: [PATCH] chore(scripts): add Linux start/stop helpers Add bash scripts to start/stop netx API (8890) and Vite web (8055) with PID/log files under scripts/.run. Co-authored-by: Cursor --- scripts/start_netx.sh | 145 ++++++++++++++++++++++++++++++++++++++++++ scripts/stop_netx.sh | 112 ++++++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+) create mode 100644 scripts/start_netx.sh create mode 100644 scripts/stop_netx.sh diff --git a/scripts/start_netx.sh b/scripts/start_netx.sh new file mode 100644 index 0000000..155dae3 --- /dev/null +++ b/scripts/start_netx.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +set -euo pipefail + +# netx Linux start script (API + Web) +# - API: python -m netx_api.main (default 8890) +# - Web: Vite dev server (default 8055, external) +# +# PID/log files are stored in: scripts/.run/ + +usage() { + cat <<'EOF' +Usage: ./scripts/start_netx.sh [options] + +Options: + --bind-host API bind host (sets NETX_HOST). Default: 127.0.0.1 + --port API port (sets NETX_PORT). Default: 8890 + --web-host Web bind host. Default: 0.0.0.0 + --web-port Web port. Default: 8055 + --skip-install Skip pip/npm install steps + -h, --help Show help + +Notes: + - If repo root has a .env file, it will be sourced (exported). + - Web uses Vite dev proxy (see web/vite.config.ts) to forward /v1 -> API. +EOF +} + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RUN_DIR="${ROOT_DIR}/scripts/.run" +mkdir -p "${RUN_DIR}" + +PID_FILE="${RUN_DIR}/netx.pid" +LOG_FILE="${RUN_DIR}/netx.out.log" +ERR_FILE="${RUN_DIR}/netx.err.log" +WEB_PID_FILE="${RUN_DIR}/web.pid" +WEB_LOG_FILE="${RUN_DIR}/web.out.log" +WEB_ERR_FILE="${RUN_DIR}/web.err.log" + +API_BIND_HOST="127.0.0.1" +API_PORT="8890" +WEB_BIND_HOST="0.0.0.0" +WEB_PORT="8055" +SKIP_INSTALL="0" + +while [[ $# -gt 0 ]]; do + case "$1" in + --bind-host) API_BIND_HOST="${2:-}"; shift 2 ;; + --port) API_PORT="${2:-}"; shift 2 ;; + --web-host) WEB_BIND_HOST="${2:-}"; shift 2 ;; + --web-port) WEB_PORT="${2:-}"; shift 2 ;; + --skip-install) SKIP_INSTALL="1"; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "[ERR] Unknown arg: $1" >&2; usage; exit 2 ;; + esac +done + +cd "${ROOT_DIR}" + +if [[ -f "${PID_FILE}" ]] || [[ -f "${WEB_PID_FILE}" ]]; then + echo "[INFO] Existing PID file(s) found under ${RUN_DIR}." + echo " If services are not actually running, remove PID files or run ./scripts/stop_netx.sh" +fi + +if [[ -f "${ROOT_DIR}/.env" ]]; then + set -a + # shellcheck disable=SC1091 + source "${ROOT_DIR}/.env" + set +a +fi + +export NETX_HOST="${API_BIND_HOST}" +export NETX_PORT="${API_PORT}" + +VENV_PY="${ROOT_DIR}/.venv/bin/python" +if [[ ! -x "${VENV_PY}" ]]; then + echo "==> .venv not found, creating virtual environment" + python3 -m venv "${ROOT_DIR}/.venv" +fi +if [[ ! -x "${VENV_PY}" ]]; then + echo "[ERR] failed_to_create_venv" >&2 + exit 1 +fi + +echo "==> Project root: ${ROOT_DIR}" +echo "==> Using python: ${VENV_PY}" + +if [[ "${SKIP_INSTALL}" != "1" ]]; then + echo "==> Installing backend dependencies" + "${VENV_PY}" -m pip install -r "${ROOT_DIR}/requirements.txt" +else + echo "==> Skip dependency install" +fi + +if ! command -v npm >/dev/null 2>&1; then + echo "[ERR] npm_not_found: install Node.js (includes npm)" >&2 + exit 1 +fi + +WEB_ROOT="${ROOT_DIR}/web" +if [[ ! -d "${WEB_ROOT}" ]]; then + echo "[ERR] web/ not found: ${WEB_ROOT}" >&2 + exit 1 +fi + +if [[ "${SKIP_INSTALL}" != "1" ]] || [[ ! -d "${WEB_ROOT}/node_modules" ]]; then + echo "==> Installing web dependencies (npm install)" + (cd "${WEB_ROOT}" && npm install) +else + echo "==> Skip web dependency install (node_modules exists)" +fi + +BASE_URL="http://${API_BIND_HOST}:${API_PORT}" +WEB_URL="http://${WEB_BIND_HOST}:${WEB_PORT}" + +echo "" +echo "==> netx API URL" +echo "Base: ${BASE_URL}/" +echo "Health: ${BASE_URL}/health" +echo "Integrations: ${BASE_URL}/v1/integrations/status" +echo "" +echo "==> netx UI URL" +echo "Vite Dev UI: ${WEB_URL}/" +echo "" + +echo "==> Starting netx API in background" +nohup "${VENV_PY}" -m netx_api.main >"${LOG_FILE}" 2>"${ERR_FILE}" & +API_PID="$!" +echo "${API_PID}" > "${PID_FILE}" +echo "netx.pid = ${PID_FILE}" +echo "PID = ${API_PID}" +echo "Log = ${LOG_FILE}" +echo "Err = ${ERR_FILE}" + +echo "==> Starting Vite dev server in background" +nohup bash -lc "cd \"${WEB_ROOT}\" && npm run dev -- --host \"${WEB_BIND_HOST}\" --port \"${WEB_PORT}\"" \ + >"${WEB_LOG_FILE}" 2>"${WEB_ERR_FILE}" & +WEB_PID="$!" +echo "${WEB_PID}" > "${WEB_PID_FILE}" +echo "web.pid = ${WEB_PID_FILE}" +echo "PID = ${WEB_PID}" +echo "Log = ${WEB_LOG_FILE}" +echo "Err = ${WEB_ERR_FILE}" + +echo "" +echo "==> Background services started; API/web keep running." diff --git a/scripts/stop_netx.sh b/scripts/stop_netx.sh new file mode 100644 index 0000000..18951f5 --- /dev/null +++ b/scripts/stop_netx.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +set -euo pipefail + +# netx Linux stop script (API + Web) +# Stops by PID file first, then falls back to killing listeners on ports. + +usage() { + cat <<'EOF' +Usage: ./scripts/stop_netx.sh [options] + +Options: + --port API port to stop by port scan. Default: 8890 + --web-port Web port to stop by port scan. Default: 8055 + --force Use SIGKILL (immediate) + -h, --help Show help +EOF +} + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RUN_DIR="${ROOT_DIR}/scripts/.run" + +PID_FILE="${RUN_DIR}/netx.pid" +WEB_PID_FILE="${RUN_DIR}/web.pid" + +API_PORT="8890" +WEB_PORT="8055" +FORCE="0" + +while [[ $# -gt 0 ]]; do + case "$1" in + --port) API_PORT="${2:-}"; shift 2 ;; + --web-port) WEB_PORT="${2:-}"; shift 2 ;; + --force) FORCE="1"; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "[ERR] Unknown arg: $1" >&2; usage; exit 2 ;; + esac +done + +kill_pid() { + local pid="$1" + local name="$2" + if [[ -z "${pid}" ]] || ! [[ "${pid}" =~ ^[0-9]+$ ]]; then + return 0 + fi + if ! kill -0 "${pid}" 2>/dev/null; then + echo "[INFO] ${name} PID not running: ${pid}" + return 0 + fi + + if [[ "${FORCE}" == "1" ]]; then + kill -KILL "${pid}" 2>/dev/null || true + echo "Stopped ${name} PID=${pid} (SIGKILL)" + return 0 + fi + + kill -TERM "${pid}" 2>/dev/null || true + for _ in {1..30}; do + if ! kill -0 "${pid}" 2>/dev/null; then + echo "Stopped ${name} PID=${pid}" + return 0 + fi + sleep 0.2 + done + kill -KILL "${pid}" 2>/dev/null || true + echo "Stopped ${name} PID=${pid} (SIGKILL after timeout)" +} + +pids_by_port() { + local port="$1" + # Try lsof first (best), then ss fallback. + if command -v lsof >/dev/null 2>&1; then + lsof -tiTCP:"${port}" -sTCP:LISTEN 2>/dev/null || true + return 0 + fi + if command -v ss >/dev/null 2>&1; then + # Example: LISTEN ... users:(("python",pid=1234,fd=3)) + ss -ltnp 2>/dev/null \ + | awk -v p=":${port}" '$4 ~ p {print $0}' \ + | sed -n 's/.*pid=\([0-9]\+\).*/\1/p' \ + | sort -u + return 0 + fi + return 0 +} + +echo "==> Stopping netx" + +if [[ -f "${PID_FILE}" ]]; then + API_PID="$(head -n 1 "${PID_FILE}" | tr -d '[:space:]' || true)" + kill_pid "${API_PID}" "api" + rm -f "${PID_FILE}" || true +else + echo "[INFO] No API PID file: ${PID_FILE}" +fi + +if [[ -f "${WEB_PID_FILE}" ]]; then + WEB_PID="$(head -n 1 "${WEB_PID_FILE}" | tr -d '[:space:]' || true)" + kill_pid "${WEB_PID}" "web" + rm -f "${WEB_PID_FILE}" || true +else + echo "[INFO] No Web PID file: ${WEB_PID_FILE}" +fi + +echo "==> Fallback: stopping by listening ports (${API_PORT}, ${WEB_PORT})" +for pid in $(pids_by_port "${API_PORT}"); do + kill_pid "${pid}" "api(port:${API_PORT})" +done +for pid in $(pids_by_port "${WEB_PORT}"); do + kill_pid "${pid}" "web(port:${WEB_PORT})" +done + +echo "==> netx stop finished"