#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_KEYS=( DUCK_LLAMA_SERVER_BIN DUCK_MAIN_MODEL_PATH DUCK_MAIN_PORT DUCK_CTX_SIZE DUCK_N_GPU_LAYERS DUCK_LLAMA_DEVICE DUCK_PARALLEL DUCK_LLAMA_PID_FILE DUCK_LLAMA_LOG_FILE DUCK_LLAMA_EXTRA_ARGS DUCK_HOST DUCK_API_HOST DUCK_API_PORT DUCK_API_PID_FILE DUCK_API_LOG_FILE DUCK_API_COMMAND ) declare -A ENV_OVERRIDES=() for key in "${ENV_KEYS[@]}"; do if [[ -v "${key}" ]]; then ENV_OVERRIDES["${key}"]="${!key}" fi done if [[ -f "${ROOT_DIR}/.env" ]]; then set -a # shellcheck disable=SC1091 source "${ROOT_DIR}/.env" set +a fi for key in "${!ENV_OVERRIDES[@]}"; do export "${key}=${ENV_OVERRIDES[${key}]}" done ACTION="${1:-start}" API_PID_FILE="${DUCK_API_PID_FILE:-${ROOT_DIR}/data/duck-api.pid}" API_LOG_FILE="${DUCK_API_LOG_FILE:-${ROOT_DIR}/data/duck-api.log}" API_URL="http://${DUCK_API_HOST:-127.0.0.1}:${DUCK_API_PORT:-8000}" LLAMA_SCRIPT="${ROOT_DIR}/scripts/llama/start_main.sh" usage() { cat <<'EOF' Usage: scripts/duck.sh Commands: start Start llama-server and DuckLM API in the background stop Stop DuckLM API and managed llama-server restart Stop and start the whole local DuckLM stack status Print process and HTTP health status logs Show DuckLM API and llama-server logs; use --follow/-f and --lines N help Show this help Environment: DUCK_API_HOST API bind host, default 127.0.0.1 DUCK_API_PORT API port, default 8000 DUCK_API_PID_FILE API PID file path DUCK_API_LOG_FILE API log file path DUCK_API_COMMAND API command override, default ".venv/bin/python -m duck_core.api" llama-server environment is handled by scripts/llama/start_main.sh. EOF } api_is_running() { [[ -f "${API_PID_FILE}" ]] || return 1 local pid pid="$(cat "${API_PID_FILE}")" [[ "${pid}" =~ ^[0-9]+$ ]] || return 1 kill -0 "${pid}" 2>/dev/null } api_pid_value() { if [[ -f "${API_PID_FILE}" ]]; then cat "${API_PID_FILE}" fi } start_api() { if api_is_running; then echo "DuckLM API already running: pid=$(api_pid_value)" return 0 fi mkdir -p "$(dirname "${API_PID_FILE}")" "$(dirname "${API_LOG_FILE}")" rm -f "${API_PID_FILE}" local command_string="${DUCK_API_COMMAND:-.venv/bin/python -m duck_core.api}" echo "Starting DuckLM API..." echo "Command: ${command_string}" >> "${API_LOG_FILE}" if command -v setsid >/dev/null 2>&1; then ( cd "${ROOT_DIR}" nohup setsid bash -lc "${command_string}" >> "${API_LOG_FILE}" 2>&1 < /dev/null & echo "$!" > "${API_PID_FILE}" ) else ( cd "${ROOT_DIR}" nohup bash -lc "${command_string}" >> "${API_LOG_FILE}" 2>&1 < /dev/null & echo "$!" > "${API_PID_FILE}" ) fi sleep 0.2 if api_is_running; then echo "DuckLM API started: pid=$(api_pid_value)" echo "WebChat: ${API_URL}/" echo "Log: ${API_LOG_FILE}" for _ in {1..20}; do if command -v curl >/dev/null 2>&1 && curl --noproxy "*" -fsS "${API_URL}/health" >/dev/null 2>&1; then echo "DuckLM API health: ok (${API_URL})" return 0 fi sleep 0.25 done echo "DuckLM API health: not ready yet (${API_URL})" return 0 fi echo "DuckLM API failed to start. See ${API_LOG_FILE}" >&2 rm -f "${API_PID_FILE}" return 1 } stop_api() { if ! api_is_running; then rm -f "${API_PID_FILE}" echo "DuckLM API not running" return 0 fi local pid pid="$(api_pid_value)" echo "Stopping DuckLM API: pid=${pid}" kill "${pid}" 2>/dev/null || true for _ in {1..30}; do if ! kill -0 "${pid}" 2>/dev/null; then rm -f "${API_PID_FILE}" echo "DuckLM API stopped" return 0 fi sleep 0.2 done echo "DuckLM API did not stop after SIGTERM; sending SIGKILL" kill -9 "${pid}" 2>/dev/null || true rm -f "${API_PID_FILE}" echo "DuckLM API stopped" } start_stack() { "${LLAMA_SCRIPT}" start start_api echo echo "Status:" status_stack } stop_stack() { stop_api "${LLAMA_SCRIPT}" stop } status_stack() { local rc=0 if api_is_running; then local pid pid="$(api_pid_value)" echo "DuckLM API running: pid=${pid}" if command -v curl >/dev/null 2>&1 && curl --noproxy "*" -fsS "${API_URL}/health" >/dev/null 2>&1; then echo "DuckLM API health: ok (${API_URL})" else echo "DuckLM API health: not ready (${API_URL})" fi else if [[ -f "${API_PID_FILE}" ]]; then echo "DuckLM API not running; removing stale pid file ${API_PID_FILE}" rm -f "${API_PID_FILE}" else echo "DuckLM API not running" fi rc=3 fi local llama_rc=0 "${LLAMA_SCRIPT}" status || llama_rc=$? if [[ "${llama_rc}" != "0" && "${rc}" == "0" ]]; then rc="${llama_rc}" fi return "${rc}" } logs_stack() { local follow=0 local lines=100 shift || true while [[ $# -gt 0 ]]; do case "$1" in -f|--follow) follow=1 shift ;; --lines) lines="${2:?--lines requires a value}" shift 2 ;; *) echo "Unknown logs argument: $1" >&2 return 2 ;; esac done mkdir -p "$(dirname "${API_LOG_FILE}")" touch "${API_LOG_FILE}" if [[ "${follow}" == "1" ]]; then local llama_log_file="${DUCK_LLAMA_LOG_FILE:-${ROOT_DIR}/data/llama-main.log}" mkdir -p "$(dirname "${llama_log_file}")" touch "${llama_log_file}" tail -n "${lines}" -f "${API_LOG_FILE}" "${llama_log_file}" else echo "==> DuckLM API log: ${API_LOG_FILE} <==" tail -n "${lines}" "${API_LOG_FILE}" echo echo "==> llama-server log: ${DUCK_LLAMA_LOG_FILE:-${ROOT_DIR}/data/llama-main.log} <==" "${LLAMA_SCRIPT}" logs --lines "${lines}" fi } case "${ACTION}" in start) start_stack ;; stop) stop_stack ;; restart) stop_stack start_stack ;; status) status_stack ;; logs) logs_stack "$@" ;; help|-h|--help) usage ;; *) echo "Unknown command: ${ACTION}" >&2 usage >&2 exit 2 ;; esac