# Local process control for LazyBoy. # make build cargo + production web UI (web/dist) # make box Docker computer image (lazyboy-box:local) # make start rebuild, then API :8787 + Vite :5173 # make restart stop, build, start # make stop .DEFAULT_GOAL := help ROOT := $(abspath $(dir $(lastword $(MAKEFILE_LIST)))) RUNDIR := $(ROOT)/.run BIN := $(ROOT)/target/debug/lazyboy WEBDIR := $(ROOT)/web HOST ?= 0.0.0.0 PORT_API ?= 8787 PORT_WEB ?= 5173 PID_SERVE := $(RUNDIR)/serve.pid PID_WEB := $(RUNDIR)/web.pid LOG_SERVE := $(RUNDIR)/serve.log LOG_WEB := $(RUNDIR)/web.log .PHONY: help build build-bin build-ui box start stop restart status logs prepare-ui start-serve start-web stop-serve stop-web # Shared POSIX helpers. Sourced by each recipe. define CTL alive() { pidfile="$$1" [ -f "$$pidfile" ] || return 1 pid=$$(cat "$$pidfile") [ -n "$$pid" ] && kill -0 "$$pid" 2>/dev/null } kill_tree() { pid="$$1" [ -n "$$pid" ] || return 0 for child in $$(pgrep -P "$$pid" 2>/dev/null); do kill_tree "$$child" done kill -TERM "$$pid" 2>/dev/null || true } kill_tree_hard() { pid="$$1" [ -n "$$pid" ] || return 0 for child in $$(pgrep -P "$$pid" 2>/dev/null); do kill_tree_hard "$$child" done kill -KILL "$$pid" 2>/dev/null || true } free_port() { port="$$1" extra=$$(lsof -nP -tiTCP:"$$port" -sTCP:LISTEN 2>/dev/null || true) [ -n "$$extra" ] || return 0 echo "freeing :$$port ($$extra)" kill -TERM $$extra 2>/dev/null || true sleep 0.2 extra=$$(lsof -nP -tiTCP:"$$port" -sTCP:LISTEN 2>/dev/null || true) [ -n "$$extra" ] && kill -KILL $$extra 2>/dev/null || true } stop_one() { name="$$1" pidfile="$$2" port="$$3" if alive "$$pidfile"; then pid=$$(cat "$$pidfile") echo "stopping $$name pid=$$pid" kill_tree "$$pid" n=0 while [ $$n -lt 15 ] && kill -0 "$$pid" 2>/dev/null; do sleep 0.2 n=$$((n + 1)) done kill_tree_hard "$$pid" elif [ -f "$$pidfile" ]; then echo "$$name not running (stale pidfile)" else echo "$$name not running" fi rm -f "$$pidfile" free_port "$$port" } wait_alive() { pidfile="$$1" name="$$2" log="$$3" i=0 while [ $$i -lt 15 ]; do if alive "$$pidfile"; then return 0 fi sleep 0.1 i=$$((i + 1)) done echo "$$name exited immediately. Last log:" tail -n 40 "$$log" 2>/dev/null || true rm -f "$$pidfile" return 1 } wait_listen() { pidfile="$$1" port="$$2" name="$$3" log="$$4" i=0 while [ $$i -lt 50 ]; do if lsof -nP -iTCP:"$$port" -sTCP:LISTEN >/dev/null 2>&1; then return 0 fi if [ -f "$$pidfile" ]; then pid=$$(cat "$$pidfile") if [ -n "$$pid" ] && ! kill -0 "$$pid" 2>/dev/null; then echo "$$name died before listening on :$$port. Last log:" tail -n 40 "$$log" 2>/dev/null || true rm -f "$$pidfile" return 1 fi fi sleep 0.2 i=$$((i + 1)) done echo "$$name did not listen on :$$port. Last log:" tail -n 40 "$$log" 2>/dev/null || true return 1 } status_one() { name="$$1" pidfile="$$2" port="$$3" if alive "$$pidfile"; then pid=$$(cat "$$pidfile") listen=no lsof -nP -iTCP:"$$port" -sTCP:LISTEN >/dev/null 2>&1 && listen=yes echo "$$name running pid=$$pid :$$port listen=$$listen" elif [ -f "$$pidfile" ]; then echo "$$name stopped (stale pidfile)" else echo "$$name stopped" fi } load_env() { if [ -f "$(ROOT)/.env" ]; then set -a . "$(ROOT)/.env" set +a fi } endef export CTL help: @echo "make build Compile API + production web UI (web/dist; phone uses :$(PORT_API))" @echo "make box Build Docker computer image lazyboy-box:local (first time / after box/)" @echo "make start Rebuild and start API (:$(PORT_API)) and frontend (:$(PORT_WEB))" @echo "make stop Stop both" @echo "make restart Stop, rebuild, start" @echo "make status Show PIDs and ports" @echo "make logs Tail both logs (Ctrl-C to leave)" build: build-bin build-ui @echo "built $(BIN) and $(WEBDIR)/dist" build-bin: @echo "building lazyboy…" @cargo build -p lazyboy build-ui: @if [ ! -d "$(WEBDIR)/node_modules" ]; then \ echo "npm install…"; \ (cd "$(WEBDIR)" && npm install) || exit 1; \ fi @echo "building web UI…" @(cd "$(WEBDIR)" && npm run build) || exit 1 box: @echo "building computer image lazyboy-box:local…" @bash "$(ROOT)/box/build.sh" start: build @$(MAKE) start-serve @$(MAKE) start-web @echo @echo "API http://127.0.0.1:$(PORT_API)" @echo "UI http://127.0.0.1:$(PORT_WEB)" @lan=$$(awk '/手機同一個 Wi-Fi:/ {sub("http://", "", $$NF); sub(/:[0-9]+$$/, "", $$NF); print $$NF; exit}' "$(LOG_SERVE)"); \ if [ -n "$$lan" ]; then \ echo "Phone http://$$lan:$(PORT_API)"; \ fi restart: stop @$(MAKE) start stop: stop-web stop-serve @echo "stopped" status: @eval "$$CTL"; \ status_one serve "$(PID_SERVE)" "$(PORT_API)"; \ status_one web "$(PID_WEB)" "$(PORT_WEB)" logs: @mkdir -p "$(RUNDIR)" @touch "$(LOG_SERVE)" "$(LOG_WEB)" @echo "=== serve $(LOG_SERVE) ===" @echo "=== web $(LOG_WEB) ===" @tail -f "$(LOG_SERVE)" "$(LOG_WEB)" prepare-ui: build-ui start-serve: @mkdir -p "$(RUNDIR)" @eval "$$CTL"; \ if alive "$(PID_SERVE)"; then \ echo "restarting serve with rebuilt binary"; \ stop_one serve "$(PID_SERVE)" "$(PORT_API)"; \ fi; \ load_env; \ LAZYBOY_WEB_HOST="$(HOST)" LAZYBOY_WEB_PORT="$(PORT_API)" nohup "$(BIN)" serve > "$(LOG_SERVE)" 2>&1 < /dev/null & echo $$! > "$(PID_SERVE)"; \ wait_alive "$(PID_SERVE)" serve "$(LOG_SERVE)" || exit 1; \ wait_listen "$(PID_SERVE)" "$(PORT_API)" serve "$(LOG_SERVE)" || exit 1; \ echo "started serve pid=$$(cat "$(PID_SERVE)") :$(PORT_API) log=$(LOG_SERVE)" start-web: @mkdir -p "$(RUNDIR)" @eval "$$CTL"; \ if alive "$(PID_WEB)"; then \ stop_one web "$(PID_WEB)" "$(PORT_WEB)"; \ fi; \ if [ ! -d "$(WEBDIR)/node_modules" ]; then \ echo "npm install…"; \ (cd "$(WEBDIR)" && npm install) || exit 1; \ fi; \ LAZYBOY_WEB_PORT="$(PORT_API)" nohup npm --prefix "$(WEBDIR)" run dev -- --host "$(HOST)" --port "$(PORT_WEB)" > "$(LOG_WEB)" 2>&1 < /dev/null & echo $$! > "$(PID_WEB)"; \ wait_alive "$(PID_WEB)" web "$(LOG_WEB)" || exit 1; \ wait_listen "$(PID_WEB)" "$(PORT_WEB)" web "$(LOG_WEB)" || exit 1; \ echo "started web pid=$$(cat "$(PID_WEB)") :$(PORT_WEB) log=$(LOG_WEB)" stop-serve: @eval "$$CTL"; stop_one serve "$(PID_SERVE)" "$(PORT_API)" stop-web: @eval "$$CTL"; stop_one web "$(PID_WEB)" "$(PORT_WEB)"