mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Mock and real mode shared tmp/dev-config, so toggling between them left residue each way: real connections (and a live local agent) showed in Settings during mock, and mock-cluster alerts lingered in real mode. The toggle script had an isolation flag but it only reached the standalone runtime, never managed-hot-dev. hot-dev.sh now routes mock mode to tmp/mock-data and real mode to tmp/dev-config, choosing the dir from the canonical PULSE_MOCK_MODE flag that toggle-mock writes to tmp/dev-config/.env. The flag is read from that file authoritatively rather than from the environment, because a stale PULSE_MOCK_MODE exported into the hot-dev supervisor from a prior mock run would otherwise keep real mode pinned to the mock dir. toggle-mock status now reports the isolated mock dir for hot-dev runtimes so it matches what the backend actually uses. Verified a full round trip: real -> dev-config (real connections only), mock -> mock-data (mock data only, no real bleed), real -> dev-config (no mock-cluster residue). Trade-off: real metrics history no longer accrues during mock UI sessions, which is the right call for clean dev switching.
869 lines
23 KiB
Bash
Executable file
869 lines
23 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
|
|
# Toggle script for Pulse mock mode.
|
|
#
|
|
# Goals:
|
|
# - `./scripts/toggle-mock.sh` toggles mode (run again toggles back)
|
|
# - Rebuild frontend + backend every switch
|
|
# - Stop and restart the active runtime (systemd, hot-dev, or standalone)
|
|
# - Work on macOS and Linux
|
|
|
|
set -euo pipefail
|
|
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[1;33m'
|
|
BLUE='\033[0;34m'
|
|
NC='\033[0m'
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
ROOT_DIR="$(dirname "$SCRIPT_DIR")"
|
|
HOT_DEV_BG_PATH="${HOT_DEV_BG_PATH:-${ROOT_DIR}/scripts/hot-dev-bg.sh}"
|
|
|
|
DEV_DATA_DIR="${ROOT_DIR}/tmp/dev-config"
|
|
MOCK_DATA_DIR="${ROOT_DIR}/tmp/mock-data"
|
|
DEV_KEY_FILE="${DEV_DATA_DIR}/.encryption.key"
|
|
MOCK_ENV_FILE="${MOCK_ENV_FILE:-${DEV_DATA_DIR}/.env}"
|
|
RUNTIME_MOCK_ENV_FILE="${RUNTIME_MOCK_ENV_FILE:-${MOCK_DATA_DIR}/.env}"
|
|
LEGACY_ROOT_MOCK_ENV_FILE="${LEGACY_ROOT_MOCK_ENV_FILE:-${ROOT_DIR}/mock.env}"
|
|
LEGACY_DEV_MOCK_ENV_FILE="${LEGACY_DEV_MOCK_ENV_FILE:-${DEV_DATA_DIR}/mock.env}"
|
|
LEGACY_RUNTIME_MOCK_ENV_FILE="${LEGACY_RUNTIME_MOCK_ENV_FILE:-${MOCK_DATA_DIR}/mock.env}"
|
|
|
|
HOT_DEV_LOG="/tmp/pulse-hot-dev.log"
|
|
STANDALONE_LOG="/tmp/pulse-standalone.log"
|
|
DEFAULT_PORT=7655
|
|
LOCK_DIR="/tmp/pulse-toggle-mock.lock"
|
|
LOCK_PID_FILE="${LOCK_DIR}/pid"
|
|
|
|
log_info() {
|
|
echo -e "${BLUE}[toggle-mock]${NC} $*"
|
|
}
|
|
|
|
log_warn() {
|
|
echo -e "${YELLOW}[toggle-mock] WARNING:${NC} $*"
|
|
}
|
|
|
|
log_error() {
|
|
echo -e "${RED}[toggle-mock] ERROR:${NC} $*"
|
|
}
|
|
|
|
log_success() {
|
|
echo -e "${GREEN}[toggle-mock]${NC} $*"
|
|
}
|
|
|
|
acquire_lock() {
|
|
if mkdir "${LOCK_DIR}" 2>/dev/null; then
|
|
echo "$$" > "${LOCK_PID_FILE}"
|
|
trap 'release_lock' EXIT
|
|
return
|
|
fi
|
|
|
|
if [[ -f "${LOCK_PID_FILE}" ]]; then
|
|
local owner_pid
|
|
owner_pid="$(cat "${LOCK_PID_FILE}" 2>/dev/null || true)"
|
|
if [[ -n "${owner_pid}" ]] && kill -0 "${owner_pid}" 2>/dev/null; then
|
|
log_error "Another toggle run is in progress (pid: ${owner_pid}). Try again in a few seconds."
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
rm -rf "${LOCK_DIR}" 2>/dev/null || true
|
|
if mkdir "${LOCK_DIR}" 2>/dev/null; then
|
|
echo "$$" > "${LOCK_PID_FILE}"
|
|
trap 'release_lock' EXIT
|
|
return
|
|
fi
|
|
|
|
log_error "Failed to acquire toggle lock at ${LOCK_DIR}"
|
|
exit 1
|
|
}
|
|
|
|
release_lock() {
|
|
rm -rf "${LOCK_DIR}" 2>/dev/null || true
|
|
}
|
|
|
|
launch_detached() {
|
|
local log_file="$1"
|
|
shift
|
|
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 - "$log_file" "$@" <<'PY'
|
|
import subprocess
|
|
import sys
|
|
|
|
log_path = sys.argv[1]
|
|
cmd = sys.argv[2:]
|
|
|
|
with open(log_path, "ab", buffering=0) as log:
|
|
proc = subprocess.Popen(
|
|
cmd,
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=log,
|
|
stderr=subprocess.STDOUT,
|
|
start_new_session=True,
|
|
)
|
|
|
|
print(proc.pid)
|
|
PY
|
|
return
|
|
fi
|
|
|
|
nohup "$@" >"${log_file}" 2>&1 &
|
|
echo $!
|
|
}
|
|
|
|
is_port_listening() {
|
|
local port="$1"
|
|
lsof -nP -iTCP:"${port}" -sTCP:LISTEN >/dev/null 2>&1
|
|
}
|
|
|
|
wait_for_port() {
|
|
local port="$1"
|
|
local timeout_seconds="${2:-20}"
|
|
local checks=$((timeout_seconds * 2))
|
|
|
|
while (( checks > 0 )); do
|
|
if is_port_listening "$port"; then
|
|
return 0
|
|
fi
|
|
sleep 0.5
|
|
checks=$((checks - 1))
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
run_hot_dev_bg() {
|
|
"${HOT_DEV_BG_PATH}" "$@"
|
|
}
|
|
|
|
hot_dev_bg_status_output() {
|
|
run_hot_dev_bg status 2>/dev/null || true
|
|
}
|
|
|
|
hot_dev_bg_is_running() {
|
|
local output
|
|
output="$(hot_dev_bg_status_output)"
|
|
[[ "${output}" == *"[hot-dev-bg] Running"* ]]
|
|
}
|
|
|
|
prefer_hot_dev_runtime() {
|
|
local prefer="${PULSE_TOGGLE_PREFER_HOT_DEV:-auto}"
|
|
case "${prefer}" in
|
|
true) return 0 ;;
|
|
false) return 1 ;;
|
|
auto)
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
;;
|
|
*)
|
|
log_warn "Unknown PULSE_TOGGLE_PREFER_HOT_DEV=${prefer}; using auto"
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
return 0
|
|
fi
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
use_isolated_mock_data_dir() {
|
|
local value="${PULSE_TOGGLE_ISOLATE_MOCK_DATA:-false}"
|
|
case "${value}" in
|
|
true|TRUE|1|yes|YES|on|ON) return 0 ;;
|
|
*) return 1 ;;
|
|
esac
|
|
}
|
|
|
|
verify_mock_state_via_frontend_proxy() {
|
|
local target_mode="$1"
|
|
local expected="false"
|
|
local url="${PULSE_TOGGLE_FRONTEND_VERIFY_URL:-http://127.0.0.1:5173/api/system/mock-mode}"
|
|
local retries=40
|
|
|
|
if [[ "${target_mode}" == "true" ]]; then
|
|
expected="true"
|
|
fi
|
|
|
|
while (( retries > 0 )); do
|
|
local resp
|
|
resp="$(curl -fsS --max-time 2 "${url}" 2>/dev/null || true)"
|
|
if [[ -n "${resp}" ]] && echo "${resp}" | grep -q "\"enabled\":${expected}"; then
|
|
return 0
|
|
fi
|
|
sleep 0.5
|
|
retries=$((retries - 1))
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
sed_inplace() {
|
|
local expr="$1"
|
|
local file="$2"
|
|
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
sed -i '' "$expr" "$file"
|
|
else
|
|
sed -i "$expr" "$file"
|
|
fi
|
|
}
|
|
|
|
load_env_file() {
|
|
local env_file="$1"
|
|
if [[ -f "$env_file" ]]; then
|
|
set +u
|
|
set -a
|
|
# shellcheck disable=SC1090
|
|
source "$env_file"
|
|
set +a
|
|
set -u
|
|
fi
|
|
}
|
|
|
|
mock_default_entries() {
|
|
cat <<'ENVEOF'
|
|
PULSE_MOCK_MODE=false
|
|
PULSE_MOCK_NODES=5
|
|
PULSE_MOCK_VMS_PER_NODE=6
|
|
PULSE_MOCK_LXCS_PER_NODE=8
|
|
PULSE_MOCK_DOCKER_HOSTS=5
|
|
PULSE_MOCK_DOCKER_CONTAINERS=14
|
|
PULSE_MOCK_GENERIC_HOSTS=4
|
|
PULSE_MOCK_K8S_CLUSTERS=3
|
|
PULSE_MOCK_K8S_NODES=5
|
|
PULSE_MOCK_K8S_PODS=40
|
|
PULSE_MOCK_K8S_DEPLOYMENTS=14
|
|
PULSE_MOCK_RANDOM_METRICS=true
|
|
PULSE_MOCK_STOPPED_PERCENT=6
|
|
ENVEOF
|
|
}
|
|
|
|
ensure_parent_dir() {
|
|
mkdir -p "$(dirname "$1")"
|
|
}
|
|
|
|
ensure_env_file_exists() {
|
|
local file="$1"
|
|
ensure_parent_dir "$file"
|
|
if [[ ! -f "$file" ]]; then
|
|
: > "$file"
|
|
fi
|
|
}
|
|
|
|
env_has_key() {
|
|
local file="$1"
|
|
local key="$2"
|
|
[[ -f "$file" ]] && grep -Eq "^[[:space:]]*${key}=" "$file"
|
|
}
|
|
|
|
read_env_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
if [[ ! -f "$file" ]]; then
|
|
return 1
|
|
fi
|
|
|
|
awk -F= -v lookup="$key" '
|
|
$1 ~ "^[[:space:]]*" lookup "$" {
|
|
value = substr($0, index($0, "=") + 1)
|
|
gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
|
|
print value
|
|
}
|
|
' "$file" | tail -n 1
|
|
}
|
|
|
|
set_env_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local value="$3"
|
|
|
|
ensure_env_file_exists "$file"
|
|
if env_has_key "$file" "$key"; then
|
|
sed_inplace "s|^[[:space:]]*${key}=.*|${key}=${value}|" "$file"
|
|
else
|
|
printf '\n%s=%s\n' "$key" "$value" >> "$file"
|
|
fi
|
|
}
|
|
|
|
ensure_env_value() {
|
|
local file="$1"
|
|
local key="$2"
|
|
local value="$3"
|
|
|
|
ensure_env_file_exists "$file"
|
|
if ! env_has_key "$file" "$key"; then
|
|
printf '%s=%s\n' "$key" "$value" >> "$file"
|
|
fi
|
|
}
|
|
|
|
sync_mock_entries_to_file() {
|
|
local file="$1"
|
|
local line key value
|
|
|
|
ensure_env_file_exists "$file"
|
|
while IFS='=' read -r key value; do
|
|
[[ -n "${key}" ]] || continue
|
|
value="$(read_env_value "$MOCK_ENV_FILE" "$key" || printf '%s' "$value")"
|
|
set_env_value "$file" "$key" "$value"
|
|
done < <(mock_default_entries)
|
|
}
|
|
|
|
ensure_mock_env_file() {
|
|
local legacy_file key value
|
|
local created=false
|
|
|
|
if [[ ! -f "$MOCK_ENV_FILE" ]]; then
|
|
ensure_env_file_exists "$MOCK_ENV_FILE"
|
|
created=true
|
|
fi
|
|
|
|
if ! env_has_key "$MOCK_ENV_FILE" "PULSE_MOCK_MODE"; then
|
|
for legacy_file in \
|
|
"$LEGACY_DEV_MOCK_ENV_FILE" \
|
|
"$LEGACY_ROOT_MOCK_ENV_FILE" \
|
|
"$LEGACY_RUNTIME_MOCK_ENV_FILE"
|
|
do
|
|
[[ -f "$legacy_file" ]] || continue
|
|
while IFS='=' read -r key value; do
|
|
[[ "${key}" == PULSE_MOCK_* ]] || continue
|
|
set_env_value "$MOCK_ENV_FILE" "$key" "$value"
|
|
done < <(grep -E '^[[:space:]]*PULSE_MOCK_[A-Z0-9_]*=' "$legacy_file" || true)
|
|
log_info "Migrated legacy mock settings from ${legacy_file} to ${MOCK_ENV_FILE}"
|
|
break
|
|
done
|
|
fi
|
|
|
|
while IFS='=' read -r key value; do
|
|
ensure_env_value "$MOCK_ENV_FILE" "$key" "$value"
|
|
done < <(mock_default_entries)
|
|
|
|
if [[ "$created" == "true" ]]; then
|
|
log_info "Created ${MOCK_ENV_FILE}"
|
|
fi
|
|
}
|
|
|
|
get_mock_mode() {
|
|
ensure_mock_env_file
|
|
if [[ "$(read_env_value "$MOCK_ENV_FILE" "PULSE_MOCK_MODE" || true)" == "true" ]]; then
|
|
echo "true"
|
|
else
|
|
echo "false"
|
|
fi
|
|
}
|
|
|
|
set_mock_mode() {
|
|
local enabled="$1"
|
|
ensure_mock_env_file
|
|
|
|
set_env_value "$MOCK_ENV_FILE" "PULSE_MOCK_MODE" "$enabled"
|
|
sync_mock_entries_to_file "$DEV_DATA_DIR/.env"
|
|
sync_mock_entries_to_file "$RUNTIME_MOCK_ENV_FILE"
|
|
touch "$MOCK_ENV_FILE"
|
|
}
|
|
|
|
detect_runtime_mode() {
|
|
if command -v systemctl >/dev/null 2>&1; then
|
|
if systemctl is-active --quiet pulse-hot-dev 2>/dev/null; then
|
|
echo "systemd-hot-dev"
|
|
return
|
|
fi
|
|
if systemctl is-active --quiet pulse 2>/dev/null; then
|
|
echo "systemd-pulse"
|
|
return
|
|
fi
|
|
fi
|
|
|
|
if hot_dev_bg_is_running; then
|
|
echo "managed-hot-dev"
|
|
return
|
|
fi
|
|
|
|
if pgrep -f "hot-dev.sh" >/dev/null 2>&1; then
|
|
echo "hot-dev"
|
|
return
|
|
fi
|
|
|
|
if pgrep -f "${ROOT_DIR}/pulse|(^|[[:space:]])\./pulse([[:space:]]|$)" >/dev/null 2>&1; then
|
|
echo "standalone"
|
|
return
|
|
fi
|
|
|
|
if pgrep -x pulse >/dev/null 2>&1; then
|
|
echo "pulse-name"
|
|
return
|
|
fi
|
|
|
|
echo "none"
|
|
}
|
|
|
|
kill_with_grace() {
|
|
local pattern="$1"
|
|
local signal="${2:-TERM}"
|
|
|
|
if ! pgrep -f "$pattern" >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
|
|
pkill -"$signal" -f "$pattern" 2>/dev/null || true
|
|
}
|
|
|
|
wait_for_exit() {
|
|
local pattern="$1"
|
|
local retries=20
|
|
|
|
while (( retries > 0 )); do
|
|
if ! pgrep -f "$pattern" >/dev/null 2>&1; then
|
|
return
|
|
fi
|
|
sleep 0.25
|
|
retries=$((retries - 1))
|
|
done
|
|
|
|
pkill -9 -f "$pattern" 2>/dev/null || true
|
|
}
|
|
|
|
stop_hot_dev_runtime() {
|
|
local runtime_mode="${1:-hot-dev}"
|
|
local hot_dev_pattern="${ROOT_DIR}/scripts/hot-dev.sh|scripts/hot-dev.sh|hot-dev.sh"
|
|
local pulse_pattern="${ROOT_DIR}/pulse|(^|[[:space:]])\\./pulse([[:space:]]|$)"
|
|
local vite_pattern="${ROOT_DIR}/frontend-modern.*vite|vite.*${ROOT_DIR}/frontend-modern|vite --config vite.config.ts"
|
|
|
|
if [[ "${runtime_mode}" == "managed-hot-dev" ]] && hot_dev_bg_is_running; then
|
|
log_info "Stopping managed hot-dev runtime..."
|
|
run_hot_dev_bg stop >/dev/null
|
|
return
|
|
fi
|
|
|
|
log_info "Stopping legacy hot-dev runtime..."
|
|
|
|
kill_with_grace "$hot_dev_pattern" TERM
|
|
wait_for_exit "$hot_dev_pattern"
|
|
|
|
kill_with_grace "$pulse_pattern" TERM
|
|
wait_for_exit "$pulse_pattern"
|
|
|
|
kill_with_grace "$vite_pattern" TERM
|
|
wait_for_exit "$vite_pattern"
|
|
}
|
|
|
|
stop_standalone_runtime() {
|
|
local allow_name_kill="${1:-false}"
|
|
local pulse_pattern="${ROOT_DIR}/pulse|(^|[[:space:]])\\./pulse([[:space:]]|$)"
|
|
|
|
log_info "Stopping standalone Pulse runtime..."
|
|
kill_with_grace "$pulse_pattern" TERM
|
|
wait_for_exit "$pulse_pattern"
|
|
|
|
if [[ "$allow_name_kill" == "true" ]] && pgrep -x pulse >/dev/null 2>&1; then
|
|
log_warn "Found remaining 'pulse' process by name; stopping it"
|
|
pkill -TERM -x pulse 2>/dev/null || true
|
|
sleep 1
|
|
pkill -9 -x pulse 2>/dev/null || true
|
|
fi
|
|
}
|
|
|
|
stop_systemd_runtime() {
|
|
local mode="$1"
|
|
|
|
case "$mode" in
|
|
systemd-hot-dev)
|
|
log_info "Stopping systemd service: pulse-hot-dev"
|
|
sudo systemctl stop pulse-hot-dev
|
|
;;
|
|
systemd-pulse)
|
|
log_info "Stopping systemd service: pulse"
|
|
sudo systemctl stop pulse
|
|
;;
|
|
esac
|
|
}
|
|
|
|
has_frontend_artifacts() {
|
|
[[ -f "${ROOT_DIR}/frontend-modern/dist/index.html" ]] && [[ -f "${ROOT_DIR}/internal/api/frontend-modern/dist/index.html" ]]
|
|
}
|
|
|
|
has_backend_artifact() {
|
|
[[ -x "${ROOT_DIR}/pulse" ]]
|
|
}
|
|
|
|
rebuild_frontend_backend() {
|
|
# Rebuild strategy:
|
|
# - auto (default): only rebuild when required artifacts are missing
|
|
# - always: force full frontend+backend rebuild
|
|
# - never: skip rebuild completely
|
|
local rebuild_mode="${PULSE_TOGGLE_REBUILD:-auto}"
|
|
local do_frontend=false
|
|
local do_backend=false
|
|
|
|
case "$rebuild_mode" in
|
|
always)
|
|
do_frontend=true
|
|
do_backend=true
|
|
;;
|
|
never)
|
|
log_info "Skipping rebuild (PULSE_TOGGLE_REBUILD=never)"
|
|
return
|
|
;;
|
|
auto)
|
|
if ! has_frontend_artifacts; then
|
|
do_frontend=true
|
|
# Backend embeds frontend assets, so rebuild backend after frontend build.
|
|
do_backend=true
|
|
fi
|
|
if ! has_backend_artifact; then
|
|
do_backend=true
|
|
fi
|
|
;;
|
|
*)
|
|
log_warn "Unknown PULSE_TOGGLE_REBUILD=${rebuild_mode}; using auto"
|
|
if ! has_frontend_artifacts; then
|
|
do_frontend=true
|
|
do_backend=true
|
|
fi
|
|
if ! has_backend_artifact; then
|
|
do_backend=true
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
if [[ "$do_frontend" == "false" ]] && [[ "$do_backend" == "false" ]]; then
|
|
log_info "Build artifacts already present; skipping rebuild"
|
|
return
|
|
fi
|
|
|
|
(
|
|
cd "$ROOT_DIR"
|
|
if [[ "$do_frontend" == "true" ]]; then
|
|
log_info "Building frontend..."
|
|
make frontend
|
|
fi
|
|
if [[ "$do_backend" == "true" ]]; then
|
|
log_info "Building backend..."
|
|
make backend
|
|
fi
|
|
)
|
|
|
|
log_success "Build complete"
|
|
}
|
|
|
|
ensure_dev_key() {
|
|
mkdir -p "$DEV_DATA_DIR"
|
|
|
|
if [[ ! -f "$DEV_KEY_FILE" ]]; then
|
|
openssl rand -base64 32 > "$DEV_KEY_FILE"
|
|
chmod 600 "$DEV_KEY_FILE"
|
|
log_info "Generated dev encryption key at ${DEV_KEY_FILE}"
|
|
fi
|
|
}
|
|
|
|
sync_production_config() {
|
|
if [[ -x "${ROOT_DIR}/scripts/sync-production-config.sh" ]]; then
|
|
log_info "Syncing production configuration into dev config"
|
|
DEV_DIR="$DEV_DATA_DIR" "${ROOT_DIR}/scripts/sync-production-config.sh"
|
|
else
|
|
log_warn "sync-production-config.sh not found or not executable; skipping sync"
|
|
fi
|
|
}
|
|
|
|
start_hot_dev_runtime() {
|
|
log_info "Starting managed hot-dev runtime..."
|
|
if ! run_hot_dev_bg start --takeover; then
|
|
log_error "Managed hot-dev failed to start"
|
|
return 1
|
|
fi
|
|
log_success "Managed hot-dev runtime is running"
|
|
}
|
|
|
|
start_standalone_runtime() {
|
|
local target_mock_mode="$1"
|
|
local pid
|
|
local startup_timeout="${PULSE_TOGGLE_STANDALONE_START_TIMEOUT:-20}"
|
|
local data_dir="$DEV_DATA_DIR"
|
|
|
|
log_info "Starting standalone Pulse"
|
|
|
|
cd "$ROOT_DIR"
|
|
|
|
load_env_file "${ROOT_DIR}/.env"
|
|
load_env_file "${ROOT_DIR}/.env.local"
|
|
load_env_file "${ROOT_DIR}/.env.dev"
|
|
|
|
if [[ "$target_mock_mode" == "true" ]] && use_isolated_mock_data_dir; then
|
|
data_dir="$MOCK_DATA_DIR"
|
|
fi
|
|
|
|
export PULSE_DATA_DIR="$data_dir"
|
|
mkdir -p "$PULSE_DATA_DIR"
|
|
load_env_file "${PULSE_DATA_DIR}/.env"
|
|
|
|
if [[ "$PULSE_DATA_DIR" == "$MOCK_DATA_DIR" ]]; then
|
|
log_warn "Using isolated mock data dir (${MOCK_DATA_DIR}); production metrics history will pause while mock mode is on"
|
|
else
|
|
ensure_dev_key
|
|
if [[ -z "${PULSE_ENCRYPTION_KEY:-}" ]]; then
|
|
export PULSE_ENCRYPTION_KEY="$(<"$DEV_KEY_FILE")"
|
|
fi
|
|
fi
|
|
# Keep audit persistence inside the selected runtime data directory so
|
|
# local macOS runs don't attempt to write /var/lib/pulse.
|
|
export PULSE_AUDIT_DIR="${PULSE_AUDIT_DIR:-$PULSE_DATA_DIR}"
|
|
|
|
export PORT="${PORT:-$DEFAULT_PORT}"
|
|
export FRONTEND_PORT="${FRONTEND_PORT:-$PORT}"
|
|
|
|
pid="$(launch_detached "${STANDALONE_LOG}" "${ROOT_DIR}/pulse")"
|
|
sleep 2
|
|
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
if ! wait_for_port "${PORT}" "${startup_timeout}"; then
|
|
log_error "Pulse process started but port ${PORT} did not become ready within ${startup_timeout}s"
|
|
return 1
|
|
fi
|
|
log_success "Pulse started (pid: ${pid}, port: ${PORT}, log: ${STANDALONE_LOG})"
|
|
else
|
|
log_error "Pulse failed to start. Check ${STANDALONE_LOG}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
start_systemd_runtime() {
|
|
local mode="$1"
|
|
|
|
case "$mode" in
|
|
systemd-hot-dev)
|
|
log_info "Starting systemd service: pulse-hot-dev"
|
|
sudo systemctl start pulse-hot-dev
|
|
;;
|
|
systemd-pulse)
|
|
log_info "Starting systemd service: pulse"
|
|
sudo systemctl start pulse
|
|
;;
|
|
esac
|
|
}
|
|
|
|
apply_mode() {
|
|
local target_mode="$1"
|
|
local runtime_mode
|
|
local start_mode
|
|
local allow_fallback="${PULSE_TOGGLE_ALLOW_STANDALONE_FALLBACK:-false}"
|
|
|
|
runtime_mode="$(detect_runtime_mode)"
|
|
start_mode="${runtime_mode}"
|
|
|
|
log_info "Detected runtime mode: ${runtime_mode}"
|
|
if [[ "$target_mode" == "true" ]]; then
|
|
log_info "Switching to mock mode"
|
|
if use_isolated_mock_data_dir; then
|
|
mkdir -p "$MOCK_DATA_DIR"
|
|
fi
|
|
else
|
|
log_info "Switching to production-node mode"
|
|
sync_production_config
|
|
fi
|
|
|
|
set_mock_mode "$target_mode"
|
|
|
|
case "$runtime_mode" in
|
|
systemd-hot-dev|systemd-pulse)
|
|
stop_systemd_runtime "$runtime_mode"
|
|
;;
|
|
managed-hot-dev|hot-dev)
|
|
stop_hot_dev_runtime "$runtime_mode"
|
|
;;
|
|
standalone)
|
|
stop_standalone_runtime "false"
|
|
;;
|
|
pulse-name)
|
|
stop_standalone_runtime "true"
|
|
;;
|
|
none)
|
|
stop_standalone_runtime "false"
|
|
;;
|
|
esac
|
|
|
|
# macOS default: keep development on hot-dev so frontend changes at 127.0.0.1:5173
|
|
# are always reflected immediately after toggling.
|
|
if [[ "${runtime_mode}" == "none" ]] || [[ "${runtime_mode}" == "standalone" ]] || [[ "${runtime_mode}" == "pulse-name" ]]; then
|
|
if prefer_hot_dev_runtime; then
|
|
start_mode="managed-hot-dev"
|
|
fi
|
|
fi
|
|
|
|
# hot-dev already handles build/reload on startup; avoid duplicate builds here for fast toggles.
|
|
if [[ "$start_mode" == "managed-hot-dev" ]] || [[ "$start_mode" == "hot-dev" ]] || [[ "$start_mode" == "systemd-hot-dev" ]]; then
|
|
log_info "Skipping pre-rebuild for hot-dev runtime (managed by hot-dev startup)"
|
|
else
|
|
rebuild_frontend_backend
|
|
fi
|
|
|
|
case "$start_mode" in
|
|
systemd-hot-dev|systemd-pulse)
|
|
start_systemd_runtime "$start_mode"
|
|
;;
|
|
managed-hot-dev|hot-dev)
|
|
if ! start_hot_dev_runtime; then
|
|
if [[ "${allow_fallback}" == "true" ]]; then
|
|
log_warn "hot-dev restart failed or is unhealthy; falling back to standalone backend on port ${DEFAULT_PORT}"
|
|
stop_hot_dev_runtime "$start_mode" || true
|
|
start_standalone_runtime "$target_mode"
|
|
start_mode="standalone"
|
|
else
|
|
log_error "hot-dev restart failed; refusing to fallback so frontend dev state stays explicit"
|
|
return 1
|
|
fi
|
|
fi
|
|
;;
|
|
standalone|pulse-name|none)
|
|
start_standalone_runtime "$target_mode"
|
|
;;
|
|
esac
|
|
|
|
if [[ "$start_mode" == "managed-hot-dev" ]] || [[ "$start_mode" == "hot-dev" ]]; then
|
|
if ! verify_mock_state_via_frontend_proxy "$target_mode"; then
|
|
log_error "Frontend proxy at 127.0.0.1:5173 did not report expected mock state (${target_mode})"
|
|
return 1
|
|
fi
|
|
fi
|
|
|
|
if [[ "$target_mode" == "true" ]]; then
|
|
log_success "Mock mode is now enabled"
|
|
else
|
|
log_success "Mock mode is now disabled (real nodes mode)"
|
|
fi
|
|
}
|
|
|
|
show_status() {
|
|
local runtime_mode
|
|
runtime_mode="$(detect_runtime_mode)"
|
|
|
|
ensure_mock_env_file
|
|
set +u
|
|
# shellcheck disable=SC1090
|
|
source "$MOCK_ENV_FILE"
|
|
set -u
|
|
|
|
echo "Mock Data Mode Control for Pulse"
|
|
echo ""
|
|
local status_data_dir="$DEV_DATA_DIR"
|
|
if [[ "${PULSE_MOCK_MODE:-false}" == "true" ]]; then
|
|
# hot-dev runtimes isolate mock data into MOCK_DATA_DIR unconditionally
|
|
# (see scripts/hot-dev.sh); the standalone runtime only isolates when
|
|
# PULSE_TOGGLE_ISOLATE_MOCK_DATA is set.
|
|
case "${runtime_mode}" in
|
|
managed-hot-dev | hot-dev | systemd-hot-dev)
|
|
status_data_dir="$MOCK_DATA_DIR"
|
|
;;
|
|
*)
|
|
use_isolated_mock_data_dir && status_data_dir="$MOCK_DATA_DIR"
|
|
;;
|
|
esac
|
|
fi
|
|
if [[ "${PULSE_MOCK_MODE:-false}" == "true" ]]; then
|
|
echo -e "${GREEN}Mock Mode: ENABLED${NC}"
|
|
echo " Runtime: ${runtime_mode}"
|
|
echo " Nodes: ${PULSE_MOCK_NODES:-0}"
|
|
echo " VMs per node: ${PULSE_MOCK_VMS_PER_NODE:-0}"
|
|
echo " LXCs per node: ${PULSE_MOCK_LXCS_PER_NODE:-0}"
|
|
echo " Generic hosts: ${PULSE_MOCK_GENERIC_HOSTS:-0}"
|
|
echo " Docker hosts: ${PULSE_MOCK_DOCKER_HOSTS:-0}"
|
|
echo " Docker containers/host: ${PULSE_MOCK_DOCKER_CONTAINERS:-0}"
|
|
echo " K8s clusters: ${PULSE_MOCK_K8S_CLUSTERS:-0}"
|
|
echo " Data dir: ${status_data_dir}"
|
|
else
|
|
echo -e "${BLUE}Mock Mode: DISABLED${NC}"
|
|
echo " Runtime: ${runtime_mode}"
|
|
echo " Data dir: ${status_data_dir}"
|
|
fi
|
|
}
|
|
|
|
edit_config() {
|
|
ensure_mock_env_file
|
|
log_info "Opening ${MOCK_ENV_FILE}"
|
|
${EDITOR:-nano} "$MOCK_ENV_FILE"
|
|
}
|
|
|
|
sync_config_only() {
|
|
sync_production_config
|
|
|
|
if [[ "$(detect_runtime_mode)" != "none" ]]; then
|
|
log_info "Runtime detected; restarting current mode to apply synced config"
|
|
apply_mode "$(get_mock_mode)"
|
|
else
|
|
log_info "No active runtime detected; sync complete"
|
|
fi
|
|
}
|
|
|
|
usage() {
|
|
echo "Mock Data Mode Control for Pulse"
|
|
echo ""
|
|
echo "Usage: $0 [toggle|on|off|status|edit|sync]"
|
|
echo ""
|
|
echo " toggle - Toggle between mock mode and production-node mode (default)"
|
|
echo " on - Enable mock mode and restart/rebuild stack"
|
|
echo " off - Disable mock mode, sync real nodes config, restart/rebuild stack"
|
|
echo " status - Show current mock mode + runtime status"
|
|
echo " edit - Edit the runtime .env mock settings"
|
|
echo " sync - Sync production config to dev config and restart if running"
|
|
}
|
|
|
|
main() {
|
|
local cmd="${1:-toggle}"
|
|
local needs_lock=false
|
|
|
|
case "$cmd" in
|
|
on|enable)
|
|
needs_lock=true
|
|
;;
|
|
off|disable)
|
|
needs_lock=true
|
|
;;
|
|
toggle)
|
|
needs_lock=true
|
|
;;
|
|
sync)
|
|
needs_lock=true
|
|
;;
|
|
esac
|
|
|
|
if [[ "$needs_lock" == "true" ]]; then
|
|
acquire_lock
|
|
fi
|
|
|
|
case "$cmd" in
|
|
on|enable)
|
|
apply_mode "true"
|
|
;;
|
|
off|disable)
|
|
apply_mode "false"
|
|
;;
|
|
toggle)
|
|
if [[ "$(get_mock_mode)" == "true" ]]; then
|
|
apply_mode "false"
|
|
else
|
|
apply_mode "true"
|
|
fi
|
|
;;
|
|
status)
|
|
show_status
|
|
;;
|
|
edit)
|
|
edit_config
|
|
;;
|
|
sync)
|
|
sync_config_only
|
|
;;
|
|
-h|--help|help)
|
|
usage
|
|
;;
|
|
*)
|
|
usage
|
|
echo ""
|
|
log_error "Unknown command: $cmd"
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
|
|
main "$@"
|
|
fi
|