mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-10 00:14:38 +00:00
Mock mode seeded rich in-memory chart history but passed a nil store to the seeder and the live tick, so the sqlite metrics store held almost nothing: vmware/truenas/docker resources got one row per poll from the platform ingestion paths and mock Proxmox guests got nothing at all (the unified sync skips every resource in mock mode). Performance reports rendered "Data Points: 0" for PVE guests and "Samples: 1" elsewhere, with no charts. Re-enable store seeding behind an explicit PULSE_MOCK_SEED_METRICS_STORE opt-in that scripts/hot-dev.sh and scripts/toggle-mock.sh export exactly where they point PULSE_DATA_DIR at the isolated tmp/mock-data dir. Without the opt-in (a production install flipping PULSE_MOCK_MODE on its real data dir) the store stays untouched, which is what the old nil guard protected. Replace the dormant dense seeding policy (every in-memory timestamp written to both hourly and daily tiers, ~11M rows at current fixture scale) with tier-correct backfill driven by the deterministic mock.SampleMetric runtime: daily 30d at 4h spacing, hourly 7d at 2h, minute 24h at 15m, raw left to the live tick (now also store-connected under the opt-in). Timestamps sit on the spacing grid and a new Store.MaxTimestampsForTier coverage query fills only the gap since the previous boot, so restarts neither duplicate rows nor re-pay the seed: a fresh seed wrote 264,960 rows in 8.4s (71MB) and a restart wrote only the gap plus fixture series whose mock IDs are not boot-stable (k8s pod names, ceph FSID, a pre-existing generator defect). Verified live in mock mode: 30-day PDF reports for a mock PVE VM (checkout-web-01), a vSphere VM, the TrueNAS host, and a Docker container now show 1260/1260/1260/540 data points with rendered time-series charts.
882 lines
24 KiB
Bash
Executable file
882 lines
24 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
|
|
}
|
|
|
|
managed_hot_dev_reports_mock_state() {
|
|
local target_mode="$1"
|
|
|
|
hot_dev_bg_is_running || return 1
|
|
verify_mock_state_via_frontend_proxy "$target_mode"
|
|
}
|
|
|
|
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_warn "Managed hot-dev start command did not complete cleanly"
|
|
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
|
|
# The isolated mock data dir holds no real history, so the backend may
|
|
# safely backfill mock metrics history into its metrics.db (powers
|
|
# reports and long-range charts in mock mode).
|
|
export PULSE_MOCK_SEED_METRICS_STORE=true
|
|
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 managed_hot_dev_reports_mock_state "$target_mode"; then
|
|
log_warn "Managed hot-dev reported a restart problem, but the browser entrypoint is serving the requested mock state; continuing"
|
|
elif [[ "${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
|