Skip to content

Commit 0487b0a

Browse files
committed
chore: make local startup output more graceful
1 parent 2ad52f8 commit 0487b0a

2 files changed

Lines changed: 144 additions & 16 deletions

File tree

scripts/start.sh

Lines changed: 125 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export OLLAMA_EMBED_MODEL="qwen3-embedding:8b"
1010

1111
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
1212
CLIENT_DIR="${ROOT_DIR}/client"
13+
LOG_DIR="${ROOT_DIR}/.logs/start"
1314

1415
if ! command -v uv >/dev/null 2>&1; then
1516
echo "uv is required. Install it from https://docs.astral.sh/uv/." >&2
@@ -21,22 +22,135 @@ if ! command -v npm >/dev/null 2>&1; then
2122
exit 1
2223
fi
2324

24-
echo "Starting data server (port 8000)..."
25-
uv run --directory "${ROOT_DIR}" python server_api/scripts/serve_data.py &
25+
mkdir -p "${LOG_DIR}"
2626

27-
echo "Starting API server (port 4242)..."
28-
PYTHONDONTWRITEBYTECODE=1 uv run --directory "${ROOT_DIR}" python -m server_api.main &
27+
STARTED_PIDS=()
2928

30-
echo "Starting PyTC server (port 4243)..."
31-
uv run --directory "${ROOT_DIR}" python -m server_pytc.main &
29+
relative_log_path() {
30+
local path="$1"
31+
if [[ "${path}" == "${ROOT_DIR}"/* ]]; then
32+
echo "${path#${ROOT_DIR}/}"
33+
else
34+
echo "${path}"
35+
fi
36+
}
37+
38+
port_is_listening() {
39+
local port="$1"
40+
lsof -tiTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
41+
}
42+
43+
wait_for_http() {
44+
local name="$1"
45+
local url="$2"
46+
local max_attempts="${3:-30}"
47+
local attempt=1
48+
49+
while [[ ${attempt} -le ${max_attempts} ]]; do
50+
if curl -sf "${url}" >/dev/null 2>&1; then
51+
return 0
52+
fi
53+
attempt=$((attempt + 1))
54+
sleep 1
55+
done
56+
57+
echo "ERROR: ${name} did not become ready at ${url}" >&2
58+
return 1
59+
}
60+
61+
start_service() {
62+
local name="$1"
63+
local port="$2"
64+
local health_url="$3"
65+
local log_file="$4"
66+
shift 4
67+
68+
if port_is_listening "${port}"; then
69+
if wait_for_http "${name}" "${health_url}" 5; then
70+
echo "${name} already running on :${port}; reusing existing service."
71+
return 0
72+
fi
73+
echo "ERROR: Port ${port} is already in use, but ${name} did not respond at ${health_url}." >&2
74+
return 1
75+
fi
76+
77+
: >"${log_file}"
78+
"$@" >"${log_file}" 2>&1 &
79+
local pid=$!
80+
STARTED_PIDS+=("${pid}")
81+
echo "${name} starting on :${port} (pid ${pid}; log: $(relative_log_path "${log_file}"))"
82+
83+
if ! wait_for_http "${name}" "${health_url}" 30; then
84+
echo "Recent ${name} log output:" >&2
85+
tail -n 40 "${log_file}" >&2 || true
86+
return 1
87+
fi
88+
89+
echo "${name} ready on :${port}"
90+
}
91+
92+
cleanup() {
93+
local exit_code=$?
94+
local pid
95+
for pid in "${STARTED_PIDS[@]:-}"; do
96+
if ps -p "${pid}" >/dev/null 2>&1; then
97+
kill "${pid}" >/dev/null 2>&1 || true
98+
fi
99+
done
100+
wait || true
101+
exit "${exit_code}"
102+
}
103+
104+
trap cleanup EXIT INT TERM
105+
106+
start_service \
107+
"Data server" \
108+
8000 \
109+
"http://localhost:8000/" \
110+
"${LOG_DIR}/data-server.log" \
111+
uv run --directory "${ROOT_DIR}" python server_api/scripts/serve_data.py
112+
113+
start_service \
114+
"API server" \
115+
4242 \
116+
"http://localhost:4242/health" \
117+
"${LOG_DIR}/api-server.log" \
118+
env PYTHONDONTWRITEBYTECODE=1 uv run --directory "${ROOT_DIR}" python -m server_api.main
119+
120+
start_service \
121+
"PyTC server" \
122+
4243 \
123+
"http://localhost:4243/hello" \
124+
"${LOG_DIR}/pytc-server.log" \
125+
uv run --directory "${ROOT_DIR}" python -m server_pytc.main
32126

33-
echo "Starting React server (port 3000)..."
34127
pushd "${CLIENT_DIR}" >/dev/null
35128
if [[ "${SKIP_CLIENT_BUILD:-0}" != "1" ]]; then
36-
echo "Building React client (set SKIP_CLIENT_BUILD=1 to skip)..."
37-
npm run build
129+
BUILD_LOG="${LOG_DIR}/react-build.log"
130+
echo "Building React client (log: $(relative_log_path "${BUILD_LOG}"))..."
131+
if npm run build >"${BUILD_LOG}" 2>&1; then
132+
echo "React build complete"
133+
else
134+
echo "ERROR: React build failed. Recent log output:" >&2
135+
tail -n 60 "${BUILD_LOG}" >&2 || true
136+
exit 1
137+
fi
138+
fi
139+
140+
REACT_LOG="${LOG_DIR}/react-dev.log"
141+
if port_is_listening 3000; then
142+
if curl -sf http://localhost:3000 >/dev/null 2>&1; then
143+
echo "React server already running on :3000; reusing existing service."
144+
else
145+
echo "ERROR: Port 3000 is already in use, but React did not respond." >&2
146+
exit 1
147+
fi
148+
else
149+
: >"${REACT_LOG}"
150+
echo "React server starting on :3000 (log: $(relative_log_path "${REACT_LOG}"))"
151+
BROWSER=none npm start >"${REACT_LOG}" 2>&1 &
152+
STARTED_PIDS+=("$!")
38153
fi
39-
BROWSER=none npm start >/dev/null 2>&1 &
40154

41155
wait_for_react() {
42156
local max_attempts=60
@@ -55,6 +169,7 @@ wait_for_react() {
55169
}
56170

57171
if wait_for_react; then
172+
echo "Startup logs are under $(relative_log_path "${LOG_DIR}")"
58173
echo "Starting Electron client..."
59174
ENVIRONMENT=development npm run electron
60175
else

server_api/scripts/serve_data.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import http.server
2-
import socketserver
32
import os
3+
import socketserver
44
import sys
55

66
PORT = 8000
@@ -21,6 +21,10 @@ def translate_path(self, path):
2121
return super().translate_path(path)
2222

2323

24+
class ReusableTCPServer(socketserver.TCPServer):
25+
allow_reuse_address = True
26+
27+
2428
if __name__ == "__main__":
2529
# Change to the directory we want to serve
2630
target_dir = os.path.join(os.getcwd(), DIRECTORY)
@@ -32,8 +36,17 @@ def translate_path(self, path):
3236

3337
print(f"Serving directory {target_dir} at http://localhost:{PORT}")
3438

35-
with socketserver.TCPServer(("", PORT), CORSRequestHandler) as httpd:
36-
try:
37-
httpd.serve_forever()
38-
except KeyboardInterrupt:
39-
pass
39+
try:
40+
with ReusableTCPServer(("", PORT), CORSRequestHandler) as httpd:
41+
try:
42+
httpd.serve_forever()
43+
except KeyboardInterrupt:
44+
pass
45+
except OSError as exc:
46+
if exc.errno in (48, 98):
47+
print(
48+
f"Data server could not start: port {PORT} is already in use.",
49+
file=sys.stderr,
50+
)
51+
sys.exit(1)
52+
raise

0 commit comments

Comments
 (0)