#!/usr/bin/env bash set -euo pipefail # Unified background agent script for YAZE (macOS launchd, Linux systemd) # Subcommands: # install [--build-type X] [--use-inotify] [--ubuntu-bootstrap] [--enable-linger] # uninstall # run-once # one-shot build & test # watch # linux: inotify-based watch loop SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" BUILD_DIR_DEFAULT="${PROJECT_ROOT}/build" BUILD_TYPE_DEFAULT="Debug" usage() { cat < [options] Subcommands: install Install background agent --build-type X CMake build type (default: ${BUILD_TYPE_DEFAULT}) --use-inotify Linux: use long-lived inotify watcher --ubuntu-bootstrap Install Ubuntu deps via apt (sudo) --enable-linger Enable user lingering (Linux) uninstall Remove installed background agent run-once One-shot build and test env: BUILD_DIR, BUILD_TYPE watch Linux: inotify-based watch loop env: BUILD_DIR, BUILD_TYPE, INTERVAL_SECONDS (fallback) Environment: BUILD_DIR Build directory (default: ${BUILD_DIR_DEFAULT}) BUILD_TYPE CMake build type (default: ${BUILD_TYPE_DEFAULT}) EOF } build_and_test() { local build_dir="${BUILD_DIR:-${BUILD_DIR_DEFAULT}}" local build_type="${BUILD_TYPE:-${BUILD_TYPE_DEFAULT}}" local log_file="${LOG_FILE:-"${build_dir}/yaze_agent.log"}" mkdir -p "${build_dir}" { echo "==== $(date '+%Y-%m-%d %H:%M:%S') | yaze agent run (type=${build_type}) ====" echo "Project: ${PROJECT_ROOT}" cmake -S "${PROJECT_ROOT}" -B "${build_dir}" -DCMAKE_BUILD_TYPE="${build_type}" cmake --build "${build_dir}" --config "${build_type}" -j if [[ -x "${build_dir}/bin/yaze_test" ]]; then "${build_dir}/bin/yaze_test" else if command -v ctest >/dev/null 2>&1; then ctest --test-dir "${build_dir}" --output-on-failure else echo "ctest not found and test binary missing. Skipping tests." >&2 exit 1 fi fi } >>"${log_file}" 2>&1 } sub_run_once() { build_and_test } sub_watch() { local build_dir="${BUILD_DIR:-${BUILD_DIR_DEFAULT}}" local build_type="${BUILD_TYPE:-${BUILD_TYPE_DEFAULT}}" local interval="${INTERVAL_SECONDS:-5}" mkdir -p "${build_dir}" if command -v inotifywait >/dev/null 2>&1; then echo "[agent] using inotifywait watcher" while true; do BUILD_DIR="${build_dir}" BUILD_TYPE="${build_type}" build_and_test || true inotifywait -r -e modify,create,delete,move \ "${PROJECT_ROOT}/src" \ "${PROJECT_ROOT}/test" \ "${PROJECT_ROOT}/CMakeLists.txt" \ "${PROJECT_ROOT}/src/CMakeLists.txt" >/dev/null 2>&1 || true done else echo "[agent] inotifywait not found; running periodic loop (${interval}s)" while true; do BUILD_DIR="${build_dir}" BUILD_TYPE="${build_type}" build_and_test || true sleep "${interval}" done fi } ensure_exec() { if [[ ! -x "$1" ]]; then chmod +x "$1" fi } ubuntu_bootstrap() { if command -v apt-get >/dev/null 2>&1; then sudo apt-get update sudo apt-get install -y inotify-tools build-essential cmake ninja-build pkg-config \ libsdl2-dev libpng-dev libglew-dev libwavpack-dev libabsl-dev \ libboost-all-dev libboost-python-dev python3-dev libpython3-dev else echo "apt-get not found; skipping Ubuntu bootstrap" >&2 fi } enable_linger_linux() { if command -v loginctl >/dev/null 2>&1; then if sudo -n true 2>/dev/null; then sudo loginctl enable-linger "$USER" || true else echo "Note: enabling linger may require sudo: sudo loginctl enable-linger $USER" >&2 fi fi } # Wrapper to run systemctl --user with a session bus in headless shells systemctl_user() { # Only apply on Linux if [[ "$(uname -s)" != "Linux" ]]; then systemctl "$@" return fi local uid uid="$(id -u)" export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/${uid}}" export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=${XDG_RUNTIME_DIR}/bus}" if [[ ! -S "${XDG_RUNTIME_DIR}/bus" ]]; then echo "[agent] Warning: user bus not found at ${XDG_RUNTIME_DIR}/bus. If this fails, run: sudo loginctl enable-linger $USER" >&2 fi systemctl --user "$@" } is_systemd_available() { # True if systemd is PID 1 and systemctl exists and a user bus likely available if [[ ! -d /run/systemd/system ]]; then return 1 fi if ! command -v systemctl >/dev/null 2>&1; then return 1 fi return 0 } start_userland_agent() { local build_type="${1}" local build_dir="${2}" local agent_home="$HOME/.local/share/yaze-agent" mkdir -p "${agent_home}" local log_file="${agent_home}/agent.log" nohup env BUILD_TYPE="${build_type}" BUILD_DIR="${build_dir}" "${SCRIPT_DIR}/agent.sh" watch >>"${log_file}" 2>&1 & echo $! > "${agent_home}/agent.pid" echo "Userland agent started (PID $(cat "${agent_home}/agent.pid")) - logs: ${log_file}" } stop_userland_agent() { local agent_home="$HOME/.local/share/yaze-agent" local pid_file="${agent_home}/agent.pid" if [[ -f "${pid_file}" ]]; then local pid pid="$(cat "${pid_file}")" if kill -0 "${pid}" >/dev/null 2>&1; then kill "${pid}" || true fi rm -f "${pid_file}" echo "Stopped userland agent" fi } install_macos() { local build_type="${1}" local build_dir="${2}" local label="com.yaze.watchtest" local plist_path="$HOME/Library/LaunchAgents/${label}.plist" mkdir -p "${build_dir}" cat >"$plist_path" < Label ${label} RunAtLoad ProgramArguments /bin/zsh -lc "${SCRIPT_DIR}/agent.sh" run-once WorkingDirectory ${PROJECT_ROOT} WatchPaths ${PROJECT_ROOT}/src ${PROJECT_ROOT}/test ${PROJECT_ROOT}/CMakeLists.txt ${PROJECT_ROOT}/src/CMakeLists.txt StandardOutPath ${build_dir}/yaze_agent.out.log StandardErrorPath ${build_dir}/yaze_agent.err.log EnvironmentVariables BUILD_TYPE${build_type} BUILD_DIR${build_dir} PLIST launchctl unload -w "$plist_path" 2>/dev/null || true launchctl load -w "$plist_path" echo "LaunchAgent installed and loaded: ${plist_path}" } install_linux() { local build_type="${1}" local build_dir="${2}" local use_inotify="${3}" local systemd_dir="$HOME/.config/systemd/user" local service_name="yaze-watchtest.service" local path_name="yaze-watchtest.path" mkdir -p "${systemd_dir}" "${build_dir}" if ! is_systemd_available; then echo "[agent] systemd not available; installing userland background agent" start_userland_agent "${build_type}" "${build_dir}" return fi if [[ "${use_inotify}" == "1" ]]; then cat >"${systemd_dir}/yaze-watchtest-inotify.service" <"${systemd_dir}/${service_name}" <"${systemd_dir}/${path_name}" <&2; usage; exit 1 ;; esac done case "$(uname -s)" in Darwin) install_macos "${build_type}" "${build_dir}" ;; Linux) if (( do_bootstrap == 1 )); then ubuntu_bootstrap; fi if (( do_linger == 1 )); then enable_linger_linux; fi install_linux "${build_type}" "${build_dir}" "${use_inotify}" ;; *) echo "Unsupported platform" >&2; exit 1 ;; esac } sub_uninstall() { case "$(uname -s)" in Darwin) local label="com.yaze.watchtest" local plist_path="$HOME/Library/LaunchAgents/${label}.plist" launchctl unload -w "$plist_path" 2>/dev/null || true rm -f "$plist_path" echo "Removed LaunchAgent ${label}" ;; Linux) local systemd_dir="$HOME/.config/systemd/user" if is_systemd_available; then systemctl_user stop yaze-watchtest.path 2>/dev/null || true systemctl_user disable yaze-watchtest.path 2>/dev/null || true systemctl_user stop yaze-watchtest.service 2>/dev/null || true systemctl_user stop yaze-watchtest-inotify.service 2>/dev/null || true systemctl_user disable yaze-watchtest-inotify.service 2>/dev/null || true rm -f "${systemd_dir}/yaze-watchtest.service" "${systemd_dir}/yaze-watchtest.path" "${systemd_dir}/yaze-watchtest-inotify.service" systemctl_user daemon-reload || true echo "Removed systemd user units" fi stop_userland_agent ;; *) echo "Unsupported platform" >&2; exit 1 ;; esac } main() { ensure_exec "$0" local subcmd="${1:-}"; shift || true case "${subcmd}" in install) sub_install "$@" ;; uninstall) sub_uninstall ;; run-once) sub_run_once ;; watch) sub_watch ;; -h|--help|help|"") usage ;; *) echo "Unknown subcommand: ${subcmd}" >&2; usage; exit 1 ;; esac } main "$@"