From 00daab5a86500299f0395ea5f5304786051dd9eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Tue, 19 May 2026 00:08:25 +0800 Subject: [PATCH] Make local Web UI startup repeatable Developers currently start the API server and Vite frontend by hand with fixed ports. This script keeps that workflow fast while avoiding bind failures when a previous run or another service already owns one of the defaults. Constraint: Web UI must receive the actual backend URL whenever the backend port changes Rejected: Reuse Vite auto-port behavior alone | it would not check or report the backend port and could leave the browser pointed at 3456 Confidence: high Scope-risk: narrow Directive: Keep this script dependency-light so it remains usable on a default macOS developer machine Tested: bash -n scripts/start-web-ui.sh Tested: ./scripts/start-web-ui.sh --help Tested: occupied preferred ports 45678 and 45679, script switched to 45680 and 45681 and reached Web UI ready Not-tested: full bun run verify, script-only developer workflow change --- scripts/start-web-ui.sh | 206 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 206 insertions(+) create mode 100755 scripts/start-web-ui.sh diff --git a/scripts/start-web-ui.sh b/scripts/start-web-ui.sh new file mode 100755 index 00000000..3b553e95 --- /dev/null +++ b/scripts/start-web-ui.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DESKTOP_DIR="${ROOT_DIR}/desktop" +HOST="${HOST:-127.0.0.1}" +SERVER_PORT_START="${SERVER_PORT:-3456}" +WEB_PORT_START="${WEB_PORT:-2024}" +MAX_PORT_SCAN="${MAX_PORT_SCAN:-100}" +RUN_ID="$(date +%s)-$RANDOM" +LOG_DIR="${LOG_DIR:-/tmp/cc-haha-web-ui-${RUN_ID}}" +SERVER_LOG="${LOG_DIR}/server.log" +WEB_LOG="${LOG_DIR}/web.log" + +SERVER_PID="" +WEB_PID="" + +usage() { + cat <<'EOF' +Usage: scripts/start-web-ui.sh + +Environment overrides: + SERVER_PORT=3456 preferred backend port + WEB_PORT=2024 preferred Web UI port + HOST=127.0.0.1 bind host for both processes + LOG_DIR=/tmp/... directory for server.log and web.log + +The script checks the preferred ports first and scans upward when a port is busy. +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +require_command() { + local command_name="$1" + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "Missing required command: ${command_name}" >&2 + exit 1 + fi +} + +is_port_in_use() { + local port="$1" + + if command -v lsof >/dev/null 2>&1; then + lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1 + return $? + fi + + if command -v nc >/dev/null 2>&1; then + nc -z "${HOST}" "${port}" >/dev/null 2>&1 + return $? + fi + + echo "Missing lsof or nc; cannot check whether port ${port} is available." >&2 + exit 1 +} + +find_available_port() { + local start_port="$1" + local port="${start_port}" + local end_port=$((start_port + MAX_PORT_SCAN)) + + while (( port <= end_port )); do + if ! is_port_in_use "${port}"; then + printf '%s\n' "${port}" + return 0 + fi + port=$((port + 1)) + done + + echo "No available port found in range ${start_port}-${end_port}" >&2 + exit 1 +} + +urlencode() { + bun -e 'console.log(encodeURIComponent(process.argv[1]))' "$1" +} + +wait_for_http() { + local url="$1" + local log_file="$2" + + for _ in $(seq 1 120); do + if curl -fsS "${url}" >/dev/null 2>&1; then + return 0 + fi + + if [[ -n "${SERVER_PID}" ]] && ! kill -0 "${SERVER_PID}" >/dev/null 2>&1; then + echo "Server process exited before ${url} became ready. Recent log:" >&2 + tail -n 40 "${SERVER_LOG}" >&2 || true + exit 1 + fi + + if [[ -n "${WEB_PID}" ]] && ! kill -0 "${WEB_PID}" >/dev/null 2>&1; then + echo "Web UI process exited before ${url} became ready. Recent log:" >&2 + tail -n 40 "${WEB_LOG}" >&2 || true + exit 1 + fi + + sleep 1 + done + + echo "Timed out waiting for ${url}. Recent log:" >&2 + tail -n 40 "${log_file}" >&2 || true + exit 1 +} + +cleanup() { + local exit_code=$? + + if [[ -n "${WEB_PID}" ]]; then + kill "${WEB_PID}" >/dev/null 2>&1 || true + wait "${WEB_PID}" >/dev/null 2>&1 || true + fi + + if [[ -n "${SERVER_PID}" ]]; then + kill "${SERVER_PID}" >/dev/null 2>&1 || true + wait "${SERVER_PID}" >/dev/null 2>&1 || true + fi + + if [[ "${exit_code}" -ne 0 && "${exit_code}" -ne 130 && "${exit_code}" -ne 143 ]]; then + echo "Logs kept at ${LOG_DIR}" >&2 + fi +} + +trap cleanup EXIT +trap 'exit 130' INT +trap 'exit 143' TERM + +require_command bun +require_command curl + +if [[ ! -d "${DESKTOP_DIR}" ]]; then + echo "Desktop directory not found: ${DESKTOP_DIR}" >&2 + exit 1 +fi + +mkdir -p "${LOG_DIR}" + +SERVER_PORT_RESOLVED="$(find_available_port "${SERVER_PORT_START}")" +WEB_PORT_RESOLVED="$(find_available_port "${WEB_PORT_START}")" +if [[ "${WEB_PORT_RESOLVED}" == "${SERVER_PORT_RESOLVED}" ]]; then + WEB_PORT_RESOLVED="$(find_available_port "$((WEB_PORT_RESOLVED + 1))")" +fi + +SERVER_URL="http://${HOST}:${SERVER_PORT_RESOLVED}" +WEB_URL="http://${HOST}:${WEB_PORT_RESOLVED}/?serverUrl=$(urlencode "${SERVER_URL}")" + +if [[ "${SERVER_PORT_RESOLVED}" != "${SERVER_PORT_START}" ]]; then + echo "Server port ${SERVER_PORT_START} is busy; using ${SERVER_PORT_RESOLVED}." +fi + +if [[ "${WEB_PORT_RESOLVED}" != "${WEB_PORT_START}" ]]; then + echo "Web UI port ${WEB_PORT_START} is busy; using ${WEB_PORT_RESOLVED}." +fi + +echo "Starting server: ${SERVER_URL}" +( + cd "${ROOT_DIR}" + SERVER_PORT="${SERVER_PORT_RESOLVED}" bun run src/server/index.ts --host "${HOST}" --port "${SERVER_PORT_RESOLVED}" +) >"${SERVER_LOG}" 2>&1 & +SERVER_PID=$! + +wait_for_http "${SERVER_URL}/health" "${SERVER_LOG}" + +echo "Starting Web UI: http://${HOST}:${WEB_PORT_RESOLVED}" +( + cd "${DESKTOP_DIR}" + VITE_DESKTOP_SERVER_URL="${SERVER_URL}" bun run dev -- --host "${HOST}" --port "${WEB_PORT_RESOLVED}" --strictPort +) >"${WEB_LOG}" 2>&1 & +WEB_PID=$! + +wait_for_http "http://${HOST}:${WEB_PORT_RESOLVED}" "${WEB_LOG}" + +cat </dev/null 2>&1; then + wait "${SERVER_PID}" + exit $? + fi + + if ! kill -0 "${WEB_PID}" >/dev/null 2>&1; then + wait "${WEB_PID}" + exit $? + fi + + sleep 1 +done