mirror of
https://github.com/ruvnet/RuView.git
synced 2026-04-26 13:10:40 +00:00
Merge pull request #402 from voidborne-d/fix/docker-entrypoint-and-model-path
fix: Docker entrypoint arg handling + configurable model directory
This commit is contained in:
commit
b816292ead
6 changed files with 225 additions and 16 deletions
|
|
@ -50,7 +50,15 @@ ENV RUST_LOG=info
|
|||
# Override at runtime: docker run -e CSI_SOURCE=esp32 ...
|
||||
ENV CSI_SOURCE=auto
|
||||
|
||||
ENTRYPOINT ["/bin/sh", "-c"]
|
||||
# Shell-form CMD allows $CSI_SOURCE to be substituted at container start.
|
||||
# The ENV default above (CSI_SOURCE=auto) applies when the variable is unset.
|
||||
CMD ["/app/sensing-server --source ${CSI_SOURCE} --tick-ms 100 --ui-path /app/ui --http-port 3000 --ws-port 3001"]
|
||||
# MODELS_DIR controls where the server scans for .rvf model files.
|
||||
# Mount a host directory here to make models visible to the API:
|
||||
# docker run -v /path/to/models:/app/models -e MODELS_DIR=/app/models ...
|
||||
ENV MODELS_DIR=data/models
|
||||
|
||||
COPY docker/docker-entrypoint.sh /app/docker-entrypoint.sh
|
||||
|
||||
# Exec-form ENTRYPOINT so Docker appends user arguments correctly.
|
||||
# Pass flags directly: docker run <image> --source esp32 --tick-ms 500
|
||||
# Or use env vars: docker run -e CSI_SOURCE=esp32 <image>
|
||||
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||
CMD []
|
||||
|
|
|
|||
|
|
@ -18,8 +18,13 @@ services:
|
|||
# wifi — use host Wi-Fi RSSI/scan data (Windows netsh)
|
||||
# simulated — generate synthetic CSI data (no hardware required)
|
||||
- CSI_SOURCE=${CSI_SOURCE:-auto}
|
||||
# command is passed as arguments to ENTRYPOINT (/bin/sh -c), so $CSI_SOURCE is expanded by the shell.
|
||||
command: ["/app/sensing-server --source ${CSI_SOURCE:-auto} --tick-ms 100 --ui-path /app/ui --http-port 3000 --ws-port 3001"]
|
||||
# MODELS_DIR controls where the server scans for .rvf model files.
|
||||
# Mount a host directory and set this to make models visible:
|
||||
# volumes: ["/path/to/models:/app/models"]
|
||||
# MODELS_DIR=/app/models
|
||||
- MODELS_DIR=${MODELS_DIR:-data/models}
|
||||
# No explicit command needed — docker-entrypoint.sh uses CSI_SOURCE.
|
||||
# Override with: command: ["--source", "esp32", "--tick-ms", "500"]
|
||||
|
||||
python-sensing:
|
||||
build:
|
||||
|
|
|
|||
32
docker/docker-entrypoint.sh
Executable file
32
docker/docker-entrypoint.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/sh
|
||||
# Docker entrypoint for WiFi-DensePose sensing server.
|
||||
#
|
||||
# Supports two usage patterns:
|
||||
#
|
||||
# 1. No arguments — use defaults from environment:
|
||||
# docker run -e CSI_SOURCE=esp32 ruvnet/wifi-densepose:latest
|
||||
#
|
||||
# 2. Pass CLI flags directly:
|
||||
# docker run ruvnet/wifi-densepose:latest --source esp32 --tick-ms 500
|
||||
# docker run ruvnet/wifi-densepose:latest --model /app/models/my.rvf
|
||||
#
|
||||
# Environment variables:
|
||||
# CSI_SOURCE — data source: auto (default), esp32, wifi, simulated
|
||||
# MODELS_DIR — directory to scan for .rvf model files (default: data/models)
|
||||
set -e
|
||||
|
||||
# If the first argument looks like a flag (starts with -), prepend the
|
||||
# server binary so users can just pass flags:
|
||||
# docker run <image> --source esp32 --tick-ms 500
|
||||
if [ "${1#-}" != "$1" ] || [ -z "$1" ]; then
|
||||
set -- /app/sensing-server \
|
||||
--source "${CSI_SOURCE:-auto}" \
|
||||
--tick-ms 100 \
|
||||
--ui-path /app/ui \
|
||||
--http-port 3000 \
|
||||
--ws-port 3001 \
|
||||
--bind-addr 0.0.0.0 \
|
||||
"$@"
|
||||
fi
|
||||
|
||||
exec "$@"
|
||||
|
|
@ -2797,7 +2797,7 @@ async fn delete_model(
|
|||
if safe_id.is_empty() || safe_id != id {
|
||||
return Json(serde_json::json!({ "error": "invalid model id", "success": false }));
|
||||
}
|
||||
let path = PathBuf::from("data/models").join(format!("{}.rvf", safe_id));
|
||||
let path = effective_models_dir().join(format!("{}.rvf", safe_id));
|
||||
if path.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&path) {
|
||||
warn!("Failed to delete model file {:?}: {}", path, e);
|
||||
|
|
@ -2842,9 +2842,18 @@ async fn activate_lora_profile(
|
|||
Json(serde_json::json!({ "success": true, "profile": profile }))
|
||||
}
|
||||
|
||||
/// Scan `data/models/` for `.rvf` files and return metadata.
|
||||
/// Return the effective models directory, respecting the `MODELS_DIR`
|
||||
/// environment variable. Defaults to `data/models`.
|
||||
fn effective_models_dir() -> PathBuf {
|
||||
PathBuf::from(
|
||||
std::env::var("MODELS_DIR").unwrap_or_else(|_| "data/models".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Scan the models directory for `.rvf` files and return metadata.
|
||||
/// Respects the `MODELS_DIR` environment variable.
|
||||
fn scan_model_files() -> Vec<serde_json::Value> {
|
||||
let dir = PathBuf::from("data/models");
|
||||
let dir = effective_models_dir();
|
||||
let mut models = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(&dir) {
|
||||
for entry in entries.flatten() {
|
||||
|
|
@ -2874,9 +2883,10 @@ fn scan_model_files() -> Vec<serde_json::Value> {
|
|||
models
|
||||
}
|
||||
|
||||
/// Scan `data/models/` for `.lora.json` LoRA profile files.
|
||||
/// Scan the models directory for `.lora.json` LoRA profile files.
|
||||
/// Respects the `MODELS_DIR` environment variable.
|
||||
fn scan_lora_profiles() -> Vec<serde_json::Value> {
|
||||
let dir = PathBuf::from("data/models");
|
||||
let dir = effective_models_dir();
|
||||
let mut profiles = Vec::new();
|
||||
if let Ok(entries) = std::fs::read_dir(&dir) {
|
||||
for entry in entries.flatten() {
|
||||
|
|
@ -4604,7 +4614,8 @@ async fn main() {
|
|||
}
|
||||
|
||||
// Ensure data directories exist for models and recordings
|
||||
let _ = std::fs::create_dir_all("data/models");
|
||||
let models_dir = effective_models_dir();
|
||||
let _ = std::fs::create_dir_all(&models_dir);
|
||||
let _ = std::fs::create_dir_all("data/recordings");
|
||||
|
||||
// Discover model and recording files on startup
|
||||
|
|
|
|||
|
|
@ -30,8 +30,19 @@ use crate::rvf_container::RvfReader;
|
|||
|
||||
// ── Models data directory ────────────────────────────────────────────────────
|
||||
|
||||
/// Base directory for RVF model files.
|
||||
pub const MODELS_DIR: &str = "data/models";
|
||||
/// Default base directory for RVF model files.
|
||||
///
|
||||
/// Overridden at runtime by the `MODELS_DIR` environment variable so that
|
||||
/// Docker users can point to a mounted volume without rebuilding:
|
||||
/// docker run -v /path/to/models:/app/models -e MODELS_DIR=/app/models ...
|
||||
pub const MODELS_DIR_DEFAULT: &str = "data/models";
|
||||
|
||||
/// Return the effective models directory, respecting `MODELS_DIR` env var.
|
||||
pub fn models_dir() -> PathBuf {
|
||||
PathBuf::from(
|
||||
std::env::var("MODELS_DIR").unwrap_or_else(|_| MODELS_DIR_DEFAULT.to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
// ── Types ────────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -110,7 +121,7 @@ pub type AppState = Arc<RwLock<super::AppStateInner>>;
|
|||
|
||||
/// Scan the models directory and build `ModelInfo` for each `.rvf` file.
|
||||
async fn scan_models() -> Vec<ModelInfo> {
|
||||
let dir = PathBuf::from(MODELS_DIR);
|
||||
let dir = models_dir();
|
||||
let mut models = Vec::new();
|
||||
|
||||
let mut entries = match tokio::fs::read_dir(&dir).await {
|
||||
|
|
@ -204,7 +215,7 @@ async fn scan_models() -> Vec<ModelInfo> {
|
|||
|
||||
/// Load a model from disk by ID and return its `LoadedModelState`.
|
||||
fn load_model_from_disk(model_id: &str) -> Result<LoadedModelState, String> {
|
||||
let file_path = PathBuf::from(MODELS_DIR).join(format!("{model_id}.rvf"));
|
||||
let file_path = models_dir().join(format!("{model_id}.rvf"));
|
||||
let reader = RvfReader::from_file(&file_path)?;
|
||||
|
||||
let manifest = reader.manifest().unwrap_or_default();
|
||||
|
|
|
|||
142
tests/test_docker_entrypoint.sh
Executable file
142
tests/test_docker_entrypoint.sh
Executable file
|
|
@ -0,0 +1,142 @@
|
|||
#!/bin/bash
|
||||
# Regression tests for docker-entrypoint.sh
|
||||
#
|
||||
# Validates that the entrypoint script correctly handles:
|
||||
# 1. No arguments → uses env var defaults
|
||||
# 2. Flag arguments → prepends sensing-server binary
|
||||
# 3. Explicit binary path → passes through unchanged
|
||||
# 4. CSI_SOURCE env var substitution
|
||||
# 5. MODELS_DIR env var propagation
|
||||
#
|
||||
# These tests use a stub sensing-server that just prints its args.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
ENTRYPOINT="$SCRIPT_DIR/../docker/docker-entrypoint.sh"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
assert_contains() {
|
||||
local test_name="$1"
|
||||
local haystack="$2"
|
||||
local needle="$3"
|
||||
if printf '%s\n' "$haystack" | grep -qF -- "$needle"; then
|
||||
PASS=$((PASS + 1))
|
||||
echo " ✓ $test_name"
|
||||
else
|
||||
FAIL=$((FAIL + 1))
|
||||
echo " ✗ $test_name"
|
||||
echo " expected to contain: $needle"
|
||||
echo " got: $haystack"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_not_contains() {
|
||||
local test_name="$1"
|
||||
local haystack="$2"
|
||||
local needle="$3"
|
||||
if printf '%s\n' "$haystack" | grep -qF -- "$needle"; then
|
||||
FAIL=$((FAIL + 1))
|
||||
echo " ✗ $test_name"
|
||||
echo " expected NOT to contain: $needle"
|
||||
echo " got: $haystack"
|
||||
else
|
||||
PASS=$((PASS + 1))
|
||||
echo " ✓ $test_name"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create a temporary stub for /app/sensing-server that just prints args
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap "rm -rf $TMPDIR" EXIT
|
||||
|
||||
STUB="$TMPDIR/sensing-server"
|
||||
cat > "$STUB" << 'EOF'
|
||||
#!/bin/sh
|
||||
echo "EXEC_ARGS: $@"
|
||||
EOF
|
||||
chmod +x "$STUB"
|
||||
|
||||
# We'll modify the entrypoint to use our stub path for testing
|
||||
TEST_ENTRYPOINT="$TMPDIR/docker-entrypoint.sh"
|
||||
sed "s|/app/sensing-server|$STUB|g" "$ENTRYPOINT" > "$TEST_ENTRYPOINT"
|
||||
chmod +x "$TEST_ENTRYPOINT"
|
||||
|
||||
echo "=== Docker entrypoint tests ==="
|
||||
|
||||
# Test 1: No arguments — should use CSI_SOURCE default (auto)
|
||||
echo ""
|
||||
echo "Test 1: No arguments (default CSI_SOURCE=auto)"
|
||||
OUT=$(CSI_SOURCE=auto "$TEST_ENTRYPOINT" 2>&1)
|
||||
assert_contains "includes --source auto" "$OUT" "--source auto"
|
||||
assert_contains "includes --tick-ms 100" "$OUT" "--tick-ms 100"
|
||||
assert_contains "includes --ui-path" "$OUT" "--ui-path /app/ui"
|
||||
assert_contains "includes --http-port 3000" "$OUT" "--http-port 3000"
|
||||
assert_contains "includes --ws-port 3001" "$OUT" "--ws-port 3001"
|
||||
assert_contains "includes --bind-addr 0.0.0.0" "$OUT" "--bind-addr 0.0.0.0"
|
||||
|
||||
# Test 2: CSI_SOURCE=esp32 — should substitute
|
||||
echo ""
|
||||
echo "Test 2: CSI_SOURCE=esp32"
|
||||
OUT=$(CSI_SOURCE=esp32 "$TEST_ENTRYPOINT" 2>&1)
|
||||
assert_contains "includes --source esp32" "$OUT" "--source esp32"
|
||||
|
||||
# Test 3: Flag arguments — should prepend binary
|
||||
echo ""
|
||||
echo "Test 3: User passes --source wifi --tick-ms 500"
|
||||
OUT=$(CSI_SOURCE=auto "$TEST_ENTRYPOINT" --source wifi --tick-ms 500 2>&1)
|
||||
assert_contains "includes --source wifi" "$OUT" "--source wifi"
|
||||
assert_contains "includes --tick-ms 500" "$OUT" "--tick-ms 500"
|
||||
|
||||
# Test 4: No CSI_SOURCE set — should default to auto
|
||||
echo ""
|
||||
echo "Test 4: CSI_SOURCE unset"
|
||||
OUT=$(unset CSI_SOURCE; "$TEST_ENTRYPOINT" 2>&1)
|
||||
assert_contains "includes --source auto (default)" "$OUT" "--source auto"
|
||||
|
||||
# Test 5: User passes --model flag — should be appended
|
||||
echo ""
|
||||
echo "Test 5: User passes --model /app/models/my.rvf"
|
||||
OUT=$(CSI_SOURCE=esp32 "$TEST_ENTRYPOINT" --model /app/models/my.rvf 2>&1)
|
||||
assert_contains "includes --model" "$OUT" "--model /app/models/my.rvf"
|
||||
assert_contains "also includes default flags" "$OUT" "--source esp32"
|
||||
|
||||
# Test 6: CSI_SOURCE=simulated
|
||||
echo ""
|
||||
echo "Test 6: CSI_SOURCE=simulated"
|
||||
OUT=$(CSI_SOURCE=simulated "$TEST_ENTRYPOINT" 2>&1)
|
||||
assert_contains "includes --source simulated" "$OUT" "--source simulated"
|
||||
|
||||
# Test 7: Explicit binary path passed (e.g., docker run <image> /bin/sh)
|
||||
# First arg does NOT start with -, so entrypoint should exec it directly
|
||||
echo ""
|
||||
echo "Test 7: Explicit command (echo hello)"
|
||||
OUT=$("$TEST_ENTRYPOINT" echo hello 2>&1)
|
||||
assert_contains "passes through explicit command" "$OUT" "hello"
|
||||
assert_not_contains "does not inject sensing-server flags" "$OUT" "--source"
|
||||
|
||||
# Test 8: MODELS_DIR env var is passed through to the process
|
||||
echo ""
|
||||
echo "Test 8: MODELS_DIR env var propagation"
|
||||
# Create a stub that prints MODELS_DIR
|
||||
ENV_STUB="$TMPDIR/env-sensing-server"
|
||||
cat > "$ENV_STUB" << 'ENVEOF'
|
||||
#!/bin/sh
|
||||
echo "MODELS_DIR=${MODELS_DIR:-unset}"
|
||||
ENVEOF
|
||||
chmod +x "$ENV_STUB"
|
||||
ENV_ENTRYPOINT="$TMPDIR/env-entrypoint.sh"
|
||||
sed "s|/app/sensing-server|$ENV_STUB|g" "$ENTRYPOINT" > "$ENV_ENTRYPOINT"
|
||||
chmod +x "$ENV_ENTRYPOINT"
|
||||
|
||||
OUT=$(MODELS_DIR=/app/models CSI_SOURCE=auto "$ENV_ENTRYPOINT" 2>&1)
|
||||
assert_contains "MODELS_DIR is visible" "$OUT" "MODELS_DIR=/app/models"
|
||||
|
||||
OUT=$(unset MODELS_DIR; CSI_SOURCE=auto "$ENV_ENTRYPOINT" 2>&1)
|
||||
assert_contains "MODELS_DIR defaults to unset" "$OUT" "MODELS_DIR=unset"
|
||||
|
||||
echo ""
|
||||
echo "=== Results: $PASS passed, $FAIL failed ==="
|
||||
[ "$FAIL" -eq 0 ] || exit 1
|
||||
Loading…
Add table
Add a link
Reference in a new issue