#!/usr/bin/env bash # KPAA 백엔드 + Open WebUI 통합 관리 스크립트. # # 사용법: # ./manage.sh start KPAA + Open WebUI 백그라운드 기동, ready 대기까지 # ./manage.sh stop 양쪽 모두 정확한 종료 (PID 파일 기반) # ./manage.sh restart stop → start # ./manage.sh status 실행 여부 + PID + 포트 (양쪽) # ./manage.sh logs KPAA 로그 tail # ./manage.sh logs-owui Open WebUI 로그 tail # # 환경변수 (KPAA): # KPAA_HOST 기본 127.0.0.1 # KPAA_PORT 기본 8000 # KPAA_LOG_FILE 기본 /tmp/kpaa_serve.log # KPAA_PID_FILE 기본 ./.run/kpaa.pid # KPAA_READY_TIMEOUT 기본 90 (초) # # 환경변수 (Open WebUI): # KPAA_OPENWEBUI_ENABLED 기본 1 (0 으로 비활성화) # KPAA_OPENWEBUI_BIN 기본 ~/.kpaa-owui/bin/open-webui (PATH 폴백) # KPAA_OPENWEBUI_HOST 기본 127.0.0.1 # KPAA_OPENWEBUI_PORT 기본 8080 # KPAA_OPENWEBUI_LOG_FILE 기본 /tmp/kpaa_openwebui.log # KPAA_OPENWEBUI_READY_TIMEOUT 기본 120 (초, 첫 부팅 시 모델 다운로드 등 변수) # KPAA_OPENWEBUI_OPENAI_BASE_URLS 기본 http://${KPAA_HOST}:${KPAA_PORT}/v1 # (`;` 구분으로 다중 endpoint 가능) # KPAA_OPENWEBUI_OPENAI_KEYS 기본 local # KPAA_OPENWEBUI_DEFAULT_MODELS 기본 kpaa-privacy-ko (UI 첫 default 모델) # KPAA_OPENWEBUI_NAME 기본 "KPAA — 개인정보보호법 상담" # KPAA_OPENWEBUI_WEBUI_AUTH 기본 false — true 로 두면 본인 이메일/비밀번호 가입·로그인. # 기본은 admin@localhost/admin 자동 생성·로그인 (로컬 전용). set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$ROOT_DIR" # ── KPAA ───────────────────────────────────────────────────────────── HOST="${KPAA_HOST:-127.0.0.1}" PORT="${KPAA_PORT:-8000}" LOG_FILE="${KPAA_LOG_FILE:-/tmp/kpaa_serve.log}" PID_DIR="${ROOT_DIR}/.run" PID_FILE="${KPAA_PID_FILE:-${PID_DIR}/kpaa.pid}" VENV_BIN="${ROOT_DIR}/.venv/bin" HEALTH_URL="http://${HOST}:${PORT}/v1/models" READY_TIMEOUT_S="${KPAA_READY_TIMEOUT:-90}" # ── Open WebUI ──────────────────────────────────────────────────────── OWUI_ENABLED="${KPAA_OPENWEBUI_ENABLED:-1}" OWUI_BIN_DEFAULT="${HOME}/.kpaa-owui/bin/open-webui" OWUI_BIN="${KPAA_OPENWEBUI_BIN:-$OWUI_BIN_DEFAULT}" if [[ ! -x "$OWUI_BIN" ]] && command -v open-webui >/dev/null 2>&1; then OWUI_BIN="$(command -v open-webui)" fi OWUI_HOST="${KPAA_OPENWEBUI_HOST:-127.0.0.1}" OWUI_PORT="${KPAA_OPENWEBUI_PORT:-8080}" OWUI_LOG_FILE="${KPAA_OPENWEBUI_LOG_FILE:-/tmp/kpaa_openwebui.log}" OWUI_PID_FILE="${PID_DIR}/openwebui.pid" OWUI_HEALTH_URL="http://${OWUI_HOST}:${OWUI_PORT}/health" OWUI_READY_TIMEOUT_S="${KPAA_OPENWEBUI_READY_TIMEOUT:-120}" C_GREEN='\033[0;32m'; C_YELLOW='\033[0;33m'; C_RED='\033[0;31m'; C_RESET='\033[0m' ok() { printf "${C_GREEN}✓${C_RESET} %s\n" "$*"; } warn() { printf "${C_YELLOW}!${C_RESET} %s\n" "$*"; } err() { printf "${C_RED}✗${C_RESET} %s\n" "$*" >&2; } # ── 공통 PID 헬퍼 ──────────────────────────────────────────────────── is_alive() { local pid="$1" [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null } read_pid() { local f="$1" [[ -f "$f" ]] && cat "$f" 2>/dev/null || true } current_pid() { # PID 파일 우선. 없거나 죽었으면 빈값. 포트로 *추론하지 않음* — Chrome 등 # 무관한 클라이언트 프로세스를 잘못 죽이는 사고 방지. local pid; pid="$(read_pid "$1")" if is_alive "$pid"; then echo "$pid" else echo "" fi } wait_health() { local url="$1" timeout="$2" label="$3" local deadline=$(( $(date +%s) + timeout )) while (( $(date +%s) < deadline )); do if curl -fsS --max-time 2 "$url" > /dev/null 2>&1; then return 0 fi sleep 1 done return 1 } # ── KPAA 시작 ──────────────────────────────────────────────────────── start_kpaa() { local existing; existing="$(current_pid "$PID_FILE")" if [[ -n "$existing" ]]; then warn "[kpaa] 이미 기동 중 (pid=$existing)" return 0 fi if [[ ! -x "${VENV_BIN}/kpaa" ]]; then err "${VENV_BIN}/kpaa 가 없습니다. 가상환경을 먼저 활성화/설치하세요." return 1 fi if lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then err "[kpaa] 포트 ${PORT} 이미 점유." lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN >&2 return 1 fi mkdir -p "$PID_DIR" : > "$LOG_FILE" nohup "${VENV_BIN}/kpaa" serve --host "$HOST" --port "$PORT" \ >> "$LOG_FILE" 2>&1 & local pid=$! echo "$pid" > "$PID_FILE" ok "[kpaa] 기동 시도 (pid=$pid, log=$LOG_FILE)" if wait_health "$HEALTH_URL" "$READY_TIMEOUT_S" "kpaa"; then ok "[kpaa] 준비 완료 — ${HEALTH_URL}" return 0 fi err "[kpaa] ${READY_TIMEOUT_S}초 내 ready 응답 없음. tail -n 80 $LOG_FILE" return 1 } # ── Open WebUI 시작 ────────────────────────────────────────────────── start_owui() { if [[ "$OWUI_ENABLED" != "1" ]]; then warn "[owui] 비활성화 (KPAA_OPENWEBUI_ENABLED=$OWUI_ENABLED) — 건너뜀" return 0 fi if [[ ! -x "$OWUI_BIN" ]]; then warn "[owui] 실행 파일을 찾지 못함: $OWUI_BIN" warn " 설치: pip install open-webui (전용 venv 권장: ${OWUI_BIN_DEFAULT%/bin/*})" warn " 이 기능 영구 비활성화: export KPAA_OPENWEBUI_ENABLED=0" return 0 fi local existing; existing="$(current_pid "$OWUI_PID_FILE")" if [[ -n "$existing" ]]; then warn "[owui] 이미 기동 중 (pid=$existing)" return 0 fi if lsof -nP -iTCP:"${OWUI_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then err "[owui] 포트 ${OWUI_PORT} 이미 점유." lsof -nP -iTCP:"${OWUI_PORT}" -sTCP:LISTEN >&2 return 1 fi mkdir -p "$PID_DIR" : > "$OWUI_LOG_FILE" # OpenWebUI 가 KPAA backend 의 OpenAI 호환 endpoint 를 자동으로 부르도록 # 환경변수 주입 — docker-compose.yml 의 동일 패턴. # # KPAA 는 llama-cpp-python 임베드 단일 경로라, OpenWebUI 의 외부 추론 데몬 # 자동 감지(`ENABLE_OLLAMA_API`)는 항상 차단 — KPAA 백엔드만 사용. local owui_envs=() owui_envs+=("OPENAI_API_BASE_URLS=${KPAA_OPENWEBUI_OPENAI_BASE_URLS:-http://${HOST}:${PORT}/v1}") owui_envs+=("OPENAI_API_KEYS=${KPAA_OPENWEBUI_OPENAI_KEYS:-local}") owui_envs+=("DEFAULT_MODELS=${KPAA_OPENWEBUI_DEFAULT_MODELS:-kpaa-privacy-ko}") owui_envs+=("WEBUI_NAME=${KPAA_OPENWEBUI_NAME:-KPAA — 개인정보보호법 상담}") owui_envs+=("ENABLE_OLLAMA_API=false") # 로컬 단일 사용자 — 기본 false 시 OpenWebUI 가 admin@localhost/admin 자동 생성·로그인. # 본인 이메일/비밀번호로 운영하려면 export KPAA_OPENWEBUI_WEBUI_AUTH=true 후 재시작. # 자세한 내용 README "🔐 인증 모드" 참고. owui_envs+=("WEBUI_AUTH=${KPAA_OPENWEBUI_WEBUI_AUTH:-false}") # `open-webui serve` 인자: --host / --port 지원. 0.5+ 기준. # `env -S` 대신 nohup 앞에 환경변수 prefix — bash 표준 호환. nohup env "${owui_envs[@]}" "$OWUI_BIN" serve --host "$OWUI_HOST" --port "$OWUI_PORT" \ >> "$OWUI_LOG_FILE" 2>&1 & local pid=$! echo "$pid" > "$OWUI_PID_FILE" ok "[owui] 기동 시도 (pid=$pid, log=$OWUI_LOG_FILE)" ok " OPENAI_API_BASE_URLS=${KPAA_OPENWEBUI_OPENAI_BASE_URLS:-http://${HOST}:${PORT}/v1}" if wait_health "$OWUI_HEALTH_URL" "$OWUI_READY_TIMEOUT_S" "owui"; then ok "[owui] 준비 완료 — ${OWUI_HEALTH_URL}" return 0 fi warn "[owui] ${OWUI_READY_TIMEOUT_S}초 내 ready 응답 없음. tail -n 80 $OWUI_LOG_FILE" warn " KPAA backend 는 정상 동작 — OpenAI-호환 API 직접 호출 가능: http://${HOST}:${PORT}/v1" return 0 # owui 실패는 *전체 start 실패* 로 보지 않음 — KPAA 는 동작 } # ── 종료 헬퍼 ──────────────────────────────────────────────────────── stop_one() { local label="$1" pid_file="$2" local pid; pid="$(current_pid "$pid_file")" if [[ -z "$pid" ]]; then warn "[$label] 실행 중 아님" [[ -f "$pid_file" ]] && rm -f "$pid_file" return 0 fi ok "[$label] SIGTERM (pid=$pid)" kill "$pid" 2>/dev/null || true local deadline=$(( $(date +%s) + 15 )) while is_alive "$pid" && (( $(date +%s) < deadline )); do sleep 1 done if is_alive "$pid"; then warn "[$label] SIGTERM 무응답. SIGKILL." kill -9 "$pid" 2>/dev/null || true sleep 1 fi if is_alive "$pid"; then err "[$label] 여전히 살아있음 (pid=$pid). 수동 점검 필요." return 1 fi rm -f "$pid_file" ok "[$label] 종료 완료" } # ── 통합 명령 ──────────────────────────────────────────────────────── cmd_start() { start_kpaa || return 1 start_owui || true # owui 실패가 KPAA 부팅을 막지 않게 } cmd_stop() { # 종료 순서: owui 먼저 (자식 클라이언트 → 백엔드 순으로 깨끗) stop_one owui "$OWUI_PID_FILE" || true stop_one kpaa "$PID_FILE" || true } cmd_restart() { cmd_stop || true cmd_start } cmd_status() { local kpid opid kpid="$(current_pid "$PID_FILE")" opid="$(current_pid "$OWUI_PID_FILE")" if [[ -n "$kpid" ]]; then ok "[kpaa] RUNNING (pid=$kpid, port=$PORT)" if curl -fsS --max-time 2 "$HEALTH_URL" >/dev/null 2>&1; then ok "[kpaa] 헬스체크 OK — $HEALTH_URL" else warn "[kpaa] 프로세스 alive 이나 헬스체크 미응답 (부팅 중일 수 있음)" fi else warn "[kpaa] STOPPED" if lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN >/dev/null 2>&1; then warn " 다만 포트 ${PORT} 다른 프로세스가 점유:" lsof -nP -iTCP:"${PORT}" -sTCP:LISTEN fi fi if [[ "$OWUI_ENABLED" != "1" ]]; then warn "[owui] 비활성화 (KPAA_OPENWEBUI_ENABLED=$OWUI_ENABLED)" elif [[ -n "$opid" ]]; then ok "[owui] RUNNING (pid=$opid, port=$OWUI_PORT)" if curl -fsS --max-time 2 "$OWUI_HEALTH_URL" >/dev/null 2>&1; then ok "[owui] 헬스체크 OK — $OWUI_HEALTH_URL" else warn "[owui] 프로세스 alive 이나 헬스체크 미응답 (부팅 중일 수 있음)" fi else warn "[owui] STOPPED" if lsof -nP -iTCP:"${OWUI_PORT}" -sTCP:LISTEN >/dev/null 2>&1; then warn " 다만 포트 ${OWUI_PORT} 다른 프로세스가 점유:" lsof -nP -iTCP:"${OWUI_PORT}" -sTCP:LISTEN fi fi echo "logs:" echo " kpaa : $LOG_FILE" echo " owui : $OWUI_LOG_FILE" } cmd_logs() { if [[ ! -f "$LOG_FILE" ]]; then err "로그 파일 없음: $LOG_FILE" return 1 fi exec tail -n 200 -f "$LOG_FILE" } cmd_logs_owui() { if [[ ! -f "$OWUI_LOG_FILE" ]]; then err "로그 파일 없음: $OWUI_LOG_FILE" return 1 fi exec tail -n 200 -f "$OWUI_LOG_FILE" } usage() { cat <