Add lazy Muninn telemetry organ
This commit is contained in:
parent
6ad39e776e
commit
b63f8e4241
21 changed files with 863 additions and 44 deletions
9
Cargo.lock
generated
9
Cargo.lock
generated
|
|
@ -494,6 +494,15 @@ version = "2.8.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
|
||||
|
||||
[[package]]
|
||||
name = "muninn-daemon"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cultmesh-rs",
|
||||
"odin-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"crates/idunn-daemon",
|
||||
"crates/muninn-daemon",
|
||||
"crates/odin-core",
|
||||
]
|
||||
resolver = "3"
|
||||
|
|
|
|||
|
|
@ -306,7 +306,10 @@ fn run_operator_alarm_command(options: &Options, alarm: &IdunnOperatorAlarmRecor
|
|||
|
||||
match output {
|
||||
Ok(output) if output.status.success() => {
|
||||
println!("Idunn operator alarm command completed for {}.", alarm.daemon_id);
|
||||
println!(
|
||||
"Idunn operator alarm command completed for {}.",
|
||||
alarm.daemon_id
|
||||
);
|
||||
}
|
||||
Ok(output) => {
|
||||
eprintln!(
|
||||
|
|
|
|||
15
crates/muninn-daemon/Cargo.toml
Normal file
15
crates/muninn-daemon/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "muninn-daemon"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish.workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "muninn"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
cultmesh-rs = { path = "E:/Projects/EpiphanyAgent/vendor/cultmesh-rs" }
|
||||
odin-core = { path = "../odin-core" }
|
||||
578
crates/muninn-daemon/src/main.rs
Normal file
578
crates/muninn-daemon/src/main.rs
Normal file
|
|
@ -0,0 +1,578 @@
|
|||
use anyhow::{Context, Result, anyhow};
|
||||
use cultmesh_rs::{CultMesh, CultMeshNodeOptions};
|
||||
use odin_core::{MuninnCaptureStreamRecord, MuninnTelemetrySurfaceRecord, OdinDocuments};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::process::{Child, Command, Stdio};
|
||||
use std::thread;
|
||||
use std::time::{Duration, SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum Mode {
|
||||
Serve,
|
||||
Activate,
|
||||
Health,
|
||||
DryRun,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct Options {
|
||||
mode: Mode,
|
||||
store_path: PathBuf,
|
||||
surface_id: String,
|
||||
stream_id: String,
|
||||
host_id: String,
|
||||
target_host: String,
|
||||
port: u16,
|
||||
obs_target_host: Option<String>,
|
||||
obs_port: u16,
|
||||
width: u32,
|
||||
height: u32,
|
||||
framerate: u32,
|
||||
ddagrab_output_index: u32,
|
||||
audio_device: String,
|
||||
audio_sample_rate: u32,
|
||||
audio_channels: u32,
|
||||
ffmpeg_path: String,
|
||||
loopback_script: PathBuf,
|
||||
log_root: PathBuf,
|
||||
interval_seconds: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
struct MuxPlan {
|
||||
command_line: String,
|
||||
command_file: PathBuf,
|
||||
targets: Vec<String>,
|
||||
}
|
||||
|
||||
trait ProcessSpawner {
|
||||
fn spawn_mux(&self, plan: &MuxPlan) -> Result<Child>;
|
||||
}
|
||||
|
||||
struct CmdSpawner;
|
||||
|
||||
impl ProcessSpawner for CmdSpawner {
|
||||
fn spawn_mux(&self, plan: &MuxPlan) -> Result<Child> {
|
||||
Command::new("cmd.exe")
|
||||
.arg("/d")
|
||||
.arg("/c")
|
||||
.arg(&plan.command_file)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.spawn()
|
||||
.with_context(|| format!("starting mux command {}", plan.command_file.display()))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
let options = Options::parse(env::args().skip(1))?;
|
||||
match options.mode {
|
||||
Mode::Serve => serve(options),
|
||||
Mode::Activate => activate(options, CmdSpawner),
|
||||
Mode::Health => health_check(&options),
|
||||
Mode::DryRun => {
|
||||
let plan = build_mux_plan(&options, "dry-run".to_string());
|
||||
println!("{}", plan.command_line);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serve(options: Options) -> Result<()> {
|
||||
ensure_state_dirs(&options)?;
|
||||
let mut node = open_node(&options, "muninn-daemon")?;
|
||||
|
||||
loop {
|
||||
publish_surface(&mut node, &options, "idle", &[])?;
|
||||
if let Some(interval) = options.interval_seconds {
|
||||
thread::sleep(Duration::from_secs(interval));
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn activate(options: Options, spawner: impl ProcessSpawner) -> Result<()> {
|
||||
ensure_state_dirs(&options)?;
|
||||
let mut node = open_node(&options, "muninn-activation")?;
|
||||
let plan = build_mux_plan(&options, timestamp()?);
|
||||
write_command_file(&plan)?;
|
||||
|
||||
let supervisor_pid = std::process::id();
|
||||
let mut restart_count = 0;
|
||||
|
||||
loop {
|
||||
publish_surface(
|
||||
&mut node,
|
||||
&options,
|
||||
"active",
|
||||
std::slice::from_ref(&options.stream_id),
|
||||
)?;
|
||||
|
||||
let mut child = match spawner.spawn_mux(&plan) {
|
||||
Ok(child) => child,
|
||||
Err(error) => {
|
||||
publish_stream(
|
||||
&mut node,
|
||||
&options,
|
||||
&plan,
|
||||
"failed",
|
||||
supervisor_pid,
|
||||
None,
|
||||
restart_count,
|
||||
&format!("could not start mux: {error:#}"),
|
||||
)?;
|
||||
return Err(error);
|
||||
}
|
||||
};
|
||||
|
||||
publish_stream(
|
||||
&mut node,
|
||||
&options,
|
||||
&plan,
|
||||
"running",
|
||||
supervisor_pid,
|
||||
Some(child.id()),
|
||||
restart_count,
|
||||
"requested stream is active",
|
||||
)?;
|
||||
|
||||
let status = child.wait().context("waiting for mux process")?;
|
||||
restart_count += 1;
|
||||
publish_stream(
|
||||
&mut node,
|
||||
&options,
|
||||
&plan,
|
||||
"restarting",
|
||||
supervisor_pid,
|
||||
None,
|
||||
restart_count,
|
||||
&format!("mux exited with {status}"),
|
||||
)?;
|
||||
thread::sleep(Duration::from_secs((2 + restart_count).min(30) as u64));
|
||||
}
|
||||
}
|
||||
|
||||
fn publish_surface(
|
||||
node: &mut cultmesh_rs::CultMeshNode,
|
||||
options: &Options,
|
||||
state: &str,
|
||||
active_streams: &[String],
|
||||
) -> Result<()> {
|
||||
let record = MuninnTelemetrySurfaceRecord {
|
||||
surface_id: options.surface_id.clone(),
|
||||
host_id: options.host_id.clone(),
|
||||
state: state.to_string(),
|
||||
available_sources: available_sources(options),
|
||||
stream_affordances: vec![
|
||||
"screen.capture.ddagrab".to_string(),
|
||||
"audio.loopback.wasapi".to_string(),
|
||||
"audio.input.enumeration.pending".to_string(),
|
||||
"video.input.enumeration.pending".to_string(),
|
||||
],
|
||||
active_streams: active_streams.to_vec(),
|
||||
activation_authority: "muninn.activate.explicit-request".to_string(),
|
||||
detail: if active_streams.is_empty() {
|
||||
"Muninn is idle; no capture streams are consuming resources.".to_string()
|
||||
} else {
|
||||
"Muninn has explicit active stream requests.".to_string()
|
||||
},
|
||||
updated_at: timestamp()?,
|
||||
};
|
||||
node.put("latest", &record)?;
|
||||
node.put(&record.surface_id, &record)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn available_sources(options: &Options) -> Vec<String> {
|
||||
vec![
|
||||
format!("screen:ddagrab:output_idx={}", options.ddagrab_output_index),
|
||||
format!(
|
||||
"audio-loopback:wasapi:{}:{}ch@{}",
|
||||
options.audio_device, options.audio_channels, options.audio_sample_rate
|
||||
),
|
||||
"sensor:microphone:enumeration-pending".to_string(),
|
||||
"sensor:camera:enumeration-pending".to_string(),
|
||||
]
|
||||
}
|
||||
|
||||
fn publish_stream(
|
||||
node: &mut cultmesh_rs::CultMeshNode,
|
||||
options: &Options,
|
||||
plan: &MuxPlan,
|
||||
state: &str,
|
||||
supervisor_pid: u32,
|
||||
mux_pid: Option<u32>,
|
||||
restart_count: u32,
|
||||
detail: &str,
|
||||
) -> Result<()> {
|
||||
let record = MuninnCaptureStreamRecord {
|
||||
stream_id: options.stream_id.clone(),
|
||||
host_id: options.host_id.clone(),
|
||||
state: state.to_string(),
|
||||
video_source: format!(
|
||||
"ddagrab:output_idx={}:{}x{}@{}",
|
||||
options.ddagrab_output_index, options.width, options.height, options.framerate
|
||||
),
|
||||
audio_source: format!(
|
||||
"wasapi-loopback:{}:{}ch@{}",
|
||||
options.audio_device, options.audio_channels, options.audio_sample_rate
|
||||
),
|
||||
transport: "srt".to_string(),
|
||||
targets: plan.targets.clone(),
|
||||
command_witness: plan.command_file.display().to_string(),
|
||||
supervisor_pid: Some(supervisor_pid),
|
||||
mux_pid,
|
||||
restart_count,
|
||||
detail: detail.to_string(),
|
||||
updated_at: timestamp()?,
|
||||
};
|
||||
node.put("latest-stream", &record)?;
|
||||
node.put(&record.stream_id, &record)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn health_check(options: &Options) -> Result<()> {
|
||||
let node = open_node(options, "muninn-health")?;
|
||||
let surface = node
|
||||
.get_required::<MuninnTelemetrySurfaceRecord>("latest")
|
||||
.context("Muninn telemetry surface is unavailable")?;
|
||||
if surface.state == "idle" || surface.state == "active" {
|
||||
println!(
|
||||
"Muninn healthy: {} on {} ({})",
|
||||
surface.surface_id, surface.host_id, surface.state
|
||||
);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"Muninn telemetry surface is {}: {}",
|
||||
surface.state,
|
||||
surface.detail
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn build_mux_plan(options: &Options, timestamp: String) -> MuxPlan {
|
||||
let command_file = options.log_root.join(format!("muninn-{timestamp}.cmd"));
|
||||
let targets = build_targets(options);
|
||||
|
||||
let loopback = vec![
|
||||
"powershell.exe".to_string(),
|
||||
"-NoProfile".to_string(),
|
||||
"-ExecutionPolicy".to_string(),
|
||||
"Bypass".to_string(),
|
||||
"-File".to_string(),
|
||||
quote_cmd(&options.loopback_script.display().to_string()),
|
||||
"-Output".to_string(),
|
||||
quote_cmd("stdout"),
|
||||
"-SampleRate".to_string(),
|
||||
options.audio_sample_rate.to_string(),
|
||||
"-Channels".to_string(),
|
||||
options.audio_channels.to_string(),
|
||||
"-Device".to_string(),
|
||||
quote_cmd(&options.audio_device),
|
||||
]
|
||||
.join(" ");
|
||||
|
||||
let tee_targets = targets
|
||||
.iter()
|
||||
.map(|target| format!("[f=mpegts]{target}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("|");
|
||||
|
||||
let ffmpeg_args = vec![
|
||||
"-hide_banner".to_string(),
|
||||
"-loglevel".to_string(),
|
||||
"warning".to_string(),
|
||||
"-thread_queue_size".to_string(),
|
||||
"1024".to_string(),
|
||||
"-f".to_string(),
|
||||
"lavfi".to_string(),
|
||||
"-i".to_string(),
|
||||
format!(
|
||||
"ddagrab=framerate={}:output_idx={}:draw_mouse=1",
|
||||
options.framerate, options.ddagrab_output_index
|
||||
),
|
||||
"-thread_queue_size".to_string(),
|
||||
"1024".to_string(),
|
||||
"-f".to_string(),
|
||||
"f32le".to_string(),
|
||||
"-ar".to_string(),
|
||||
options.audio_sample_rate.to_string(),
|
||||
"-ac".to_string(),
|
||||
options.audio_channels.to_string(),
|
||||
"-i".to_string(),
|
||||
"pipe:0".to_string(),
|
||||
"-map".to_string(),
|
||||
"0:v:0".to_string(),
|
||||
"-map".to_string(),
|
||||
"1:a:0".to_string(),
|
||||
"-c:v".to_string(),
|
||||
"h264_nvenc".to_string(),
|
||||
"-preset".to_string(),
|
||||
"p4".to_string(),
|
||||
"-tune".to_string(),
|
||||
"ll".to_string(),
|
||||
"-b:v".to_string(),
|
||||
"12000k".to_string(),
|
||||
"-maxrate".to_string(),
|
||||
"12000k".to_string(),
|
||||
"-bufsize".to_string(),
|
||||
"24000k".to_string(),
|
||||
"-g".to_string(),
|
||||
"60".to_string(),
|
||||
"-c:a".to_string(),
|
||||
"aac".to_string(),
|
||||
"-b:a".to_string(),
|
||||
"192k".to_string(),
|
||||
"-ar".to_string(),
|
||||
options.audio_sample_rate.to_string(),
|
||||
"-ac".to_string(),
|
||||
options.audio_channels.to_string(),
|
||||
"-f".to_string(),
|
||||
"tee".to_string(),
|
||||
tee_targets,
|
||||
];
|
||||
let command_line = format!(
|
||||
"{} | {} {}",
|
||||
loopback,
|
||||
quote_cmd(&options.ffmpeg_path),
|
||||
ffmpeg_args
|
||||
.into_iter()
|
||||
.map(|arg| quote_cmd(&arg))
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ")
|
||||
);
|
||||
|
||||
MuxPlan {
|
||||
command_line,
|
||||
command_file,
|
||||
targets,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_targets(options: &Options) -> Vec<String> {
|
||||
let mut targets = vec![srt_endpoint(&options.target_host, options.port)];
|
||||
if let Some(host) = &options.obs_target_host {
|
||||
targets.push(srt_endpoint(host, options.obs_port));
|
||||
}
|
||||
targets
|
||||
}
|
||||
|
||||
fn srt_endpoint(host: &str, port: u16) -> String {
|
||||
format!("srt://{host}:{port}?mode=caller&latency=120000&timeout=30000000")
|
||||
}
|
||||
|
||||
fn write_command_file(plan: &MuxPlan) -> Result<()> {
|
||||
fs::write(
|
||||
&plan.command_file,
|
||||
format!("@echo off\r\n{}\r\n", plan.command_line),
|
||||
)
|
||||
.with_context(|| format!("writing {}", plan.command_file.display()))
|
||||
}
|
||||
|
||||
fn quote_cmd(value: &str) -> String {
|
||||
format!("\"{}\"", value.replace('"', "\"\""))
|
||||
}
|
||||
|
||||
fn ensure_state_dirs(options: &Options) -> Result<()> {
|
||||
fs::create_dir_all(&options.log_root)
|
||||
.with_context(|| format!("creating {}", options.log_root.display()))?;
|
||||
if let Some(parent) = options.store_path.parent() {
|
||||
fs::create_dir_all(parent).with_context(|| format!("creating {}", parent.display()))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_node(options: &Options, runtime_id: &str) -> Result<cultmesh_rs::CultMeshNode> {
|
||||
CultMesh::create_node(
|
||||
&options.store_path,
|
||||
OdinDocuments,
|
||||
CultMeshNodeOptions {
|
||||
runtime_id: runtime_id.to_string(),
|
||||
pull_on_start: true,
|
||||
},
|
||||
)
|
||||
.context("opening Muninn CultMesh store")
|
||||
}
|
||||
|
||||
impl Options {
|
||||
fn parse(args: impl Iterator<Item = String>) -> Result<Self> {
|
||||
let mut options = Options {
|
||||
mode: Mode::Serve,
|
||||
store_path: PathBuf::from("C:/Meta/Odin/state/muninn.telemetry.cc"),
|
||||
surface_id: "muninn.telemetry.local".to_string(),
|
||||
stream_id: "muninn.raven.av.srt".to_string(),
|
||||
host_id: "raven".to_string(),
|
||||
target_host: "10.77.0.2".to_string(),
|
||||
port: 5200,
|
||||
obs_target_host: Some("10.77.0.2".to_string()),
|
||||
obs_port: 5204,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
framerate: 30,
|
||||
ddagrab_output_index: 0,
|
||||
audio_device: "Realtek".to_string(),
|
||||
audio_sample_rate: 48000,
|
||||
audio_channels: 2,
|
||||
ffmpeg_path: "ffmpeg".to_string(),
|
||||
loopback_script: PathBuf::from("scripts/wasapi-loopback-capture.ps1"),
|
||||
log_root: PathBuf::from("C:/Meta/Odin/logs/muninn"),
|
||||
interval_seconds: None,
|
||||
};
|
||||
|
||||
let mut args = args.peekable();
|
||||
while let Some(arg) = args.next() {
|
||||
match arg.as_str() {
|
||||
"serve" => options.mode = Mode::Serve,
|
||||
"activate" => options.mode = Mode::Activate,
|
||||
"--health" => options.mode = Mode::Health,
|
||||
"--dry-run" => options.mode = Mode::DryRun,
|
||||
"--store" => options.store_path = PathBuf::from(take_value(&mut args, "--store")?),
|
||||
"--surface" => options.surface_id = take_value(&mut args, "--surface")?,
|
||||
"--stream" => options.stream_id = take_value(&mut args, "--stream")?,
|
||||
"--host" => options.host_id = take_value(&mut args, "--host")?,
|
||||
"--target-host" => options.target_host = take_value(&mut args, "--target-host")?,
|
||||
"--port" => options.port = take_value(&mut args, "--port")?.parse()?,
|
||||
"--obs-target-host" => {
|
||||
options.obs_target_host = Some(take_value(&mut args, "--obs-target-host")?)
|
||||
}
|
||||
"--no-obs-target" => options.obs_target_host = None,
|
||||
"--obs-port" => options.obs_port = take_value(&mut args, "--obs-port")?.parse()?,
|
||||
"--width" => options.width = take_value(&mut args, "--width")?.parse()?,
|
||||
"--height" => options.height = take_value(&mut args, "--height")?.parse()?,
|
||||
"--framerate" => {
|
||||
options.framerate = take_value(&mut args, "--framerate")?.parse()?
|
||||
}
|
||||
"--ddagrab-output-index" => {
|
||||
options.ddagrab_output_index =
|
||||
take_value(&mut args, "--ddagrab-output-index")?.parse()?
|
||||
}
|
||||
"--audio-device" => options.audio_device = take_value(&mut args, "--audio-device")?,
|
||||
"--audio-sample-rate" => {
|
||||
options.audio_sample_rate =
|
||||
take_value(&mut args, "--audio-sample-rate")?.parse()?
|
||||
}
|
||||
"--audio-channels" => {
|
||||
options.audio_channels = take_value(&mut args, "--audio-channels")?.parse()?
|
||||
}
|
||||
"--ffmpeg" => options.ffmpeg_path = take_value(&mut args, "--ffmpeg")?,
|
||||
"--loopback-script" => {
|
||||
options.loopback_script =
|
||||
PathBuf::from(take_value(&mut args, "--loopback-script")?)
|
||||
}
|
||||
"--log-root" => {
|
||||
options.log_root = PathBuf::from(take_value(&mut args, "--log-root")?)
|
||||
}
|
||||
"--interval-seconds" => {
|
||||
options.interval_seconds = Some(
|
||||
take_value(&mut args, "--interval-seconds")?
|
||||
.parse()
|
||||
.context("--interval-seconds must be a positive integer")?,
|
||||
)
|
||||
}
|
||||
"--help" | "-h" => return Err(anyhow!(help_text())),
|
||||
other => {
|
||||
return Err(anyhow!(
|
||||
"unknown Muninn argument: {other}\n\n{}",
|
||||
help_text()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if options.interval_seconds == Some(0) {
|
||||
return Err(anyhow!("--interval-seconds must be greater than zero"));
|
||||
}
|
||||
Ok(options)
|
||||
}
|
||||
}
|
||||
|
||||
fn take_value(args: &mut impl Iterator<Item = String>, name: &str) -> Result<String> {
|
||||
args.next()
|
||||
.ok_or_else(|| anyhow!("{name} requires a value"))
|
||||
}
|
||||
|
||||
fn timestamp() -> Result<String> {
|
||||
let seconds = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.context("system clock is before Unix epoch")?
|
||||
.as_secs();
|
||||
Ok(format!("unix-{seconds}"))
|
||||
}
|
||||
|
||||
fn help_text() -> &'static str {
|
||||
"Usage: muninn [serve|activate] [--store <path>] [--target-host <host>] [--port <port>] [--obs-target-host <host>] [--obs-port <port>] [--loopback-script <path>] [--ffmpeg <path>] [--dry-run] [--health]\n\nMuninn is Odin's portable telemetry Verse assembler. serve publishes cheap typed telemetry affordances; activate starts an explicitly requested local stream."
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn serve_is_default_and_does_not_activate_streams() {
|
||||
let options = Options::parse([].into_iter()).unwrap();
|
||||
|
||||
assert_eq!(options.mode, Mode::Serve);
|
||||
assert!(options.interval_seconds.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_two_srt_targets_for_explicit_activation() {
|
||||
let options = Options::parse(
|
||||
[
|
||||
"activate",
|
||||
"--target-host",
|
||||
"10.77.0.2",
|
||||
"--port",
|
||||
"5200",
|
||||
"--obs-target-host",
|
||||
"10.77.0.2",
|
||||
"--obs-port",
|
||||
"5204",
|
||||
]
|
||||
.into_iter()
|
||||
.map(String::from),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let plan = build_mux_plan(&options, "test".to_string());
|
||||
|
||||
assert_eq!(plan.targets.len(), 2);
|
||||
assert!(plan.command_line.contains("ddagrab=framerate=30"));
|
||||
assert!(plan.command_line.contains("srt://10.77.0.2:5200"));
|
||||
assert!(plan.command_line.contains("srt://10.77.0.2:5204"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_disable_obs_target_for_partial_activation_smoke() {
|
||||
let options = Options::parse(
|
||||
["activate", "--no-obs-target"]
|
||||
.into_iter()
|
||||
.map(String::from),
|
||||
)
|
||||
.unwrap();
|
||||
let plan = build_mux_plan(&options, "test".to_string());
|
||||
|
||||
assert_eq!(
|
||||
plan.targets,
|
||||
vec!["srt://10.77.0.2:5200?mode=caller&latency=120000&timeout=30000000"]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_surface_advertises_affordances_without_active_streams() {
|
||||
let options = Options::parse([].into_iter()).unwrap();
|
||||
let sources = available_sources(&options);
|
||||
|
||||
assert!(sources.iter().any(|source| source.starts_with("screen:")));
|
||||
assert!(
|
||||
sources
|
||||
.iter()
|
||||
.any(|source| source.starts_with("audio-loopback:"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -17,6 +17,8 @@ pub const IDUNN_KEEPALIVE_DECISION_SCHEMA: &str = "idunn.keepalive_decision.v1";
|
|||
pub const IDUNN_RESTART_REQUEST_SCHEMA: &str = "idunn.restart_request.v1";
|
||||
pub const IDUNN_RESTART_RESULT_SCHEMA: &str = "idunn.restart_result.v1";
|
||||
pub const IDUNN_OPERATOR_ALARM_SCHEMA: &str = "idunn.operator_alarm.v1";
|
||||
pub const MUNINN_TELEMETRY_SURFACE_SCHEMA: &str = "muninn.telemetry_surface.v1";
|
||||
pub const MUNINN_CAPTURE_STREAM_SCHEMA: &str = "muninn.capture_stream.v1";
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "odin.snapshot", schema = "odin.snapshot.v1")]
|
||||
|
|
@ -288,6 +290,63 @@ pub struct IdunnOperatorAlarmRecord {
|
|||
pub raised_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(type = "muninn.capture_stream", schema = "muninn.capture_stream.v1")]
|
||||
pub struct MuninnCaptureStreamRecord {
|
||||
#[cultcache(key = 0)]
|
||||
pub stream_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub host_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub state: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub video_source: String,
|
||||
#[cultcache(key = 4)]
|
||||
pub audio_source: String,
|
||||
#[cultcache(key = 5)]
|
||||
pub transport: String,
|
||||
#[cultcache(key = 6)]
|
||||
pub targets: Vec<String>,
|
||||
#[cultcache(key = 7)]
|
||||
pub command_witness: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub supervisor_pid: Option<u32>,
|
||||
#[cultcache(key = 9)]
|
||||
pub mux_pid: Option<u32>,
|
||||
#[cultcache(key = 10)]
|
||||
pub restart_count: u32,
|
||||
#[cultcache(key = 11)]
|
||||
pub detail: String,
|
||||
#[cultcache(key = 12)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)]
|
||||
#[cultcache(
|
||||
type = "muninn.telemetry_surface",
|
||||
schema = "muninn.telemetry_surface.v1"
|
||||
)]
|
||||
pub struct MuninnTelemetrySurfaceRecord {
|
||||
#[cultcache(key = 0)]
|
||||
pub surface_id: String,
|
||||
#[cultcache(key = 1)]
|
||||
pub host_id: String,
|
||||
#[cultcache(key = 2)]
|
||||
pub state: String,
|
||||
#[cultcache(key = 3)]
|
||||
pub available_sources: Vec<String>,
|
||||
#[cultcache(key = 4)]
|
||||
pub stream_affordances: Vec<String>,
|
||||
#[cultcache(key = 5)]
|
||||
pub active_streams: Vec<String>,
|
||||
#[cultcache(key = 6)]
|
||||
pub activation_authority: String,
|
||||
#[cultcache(key = 7)]
|
||||
pub detail: String,
|
||||
#[cultcache(key = 8)]
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
cultmesh_rs::cultmesh_documents!(OdinDocuments {
|
||||
OdinSnapshotRecord => ODIN_SNAPSHOT_SCHEMA,
|
||||
OdinVerseRecord => ODIN_VERSE_SCHEMA,
|
||||
|
|
@ -305,6 +364,8 @@ cultmesh_rs::cultmesh_documents!(OdinDocuments {
|
|||
IdunnRestartRequestRecord => IDUNN_RESTART_REQUEST_SCHEMA,
|
||||
IdunnRestartResultRecord => IDUNN_RESTART_RESULT_SCHEMA,
|
||||
IdunnOperatorAlarmRecord => IDUNN_OPERATOR_ALARM_SCHEMA,
|
||||
MuninnTelemetrySurfaceRecord => MUNINN_TELEMETRY_SURFACE_SCHEMA,
|
||||
MuninnCaptureStreamRecord => MUNINN_CAPTURE_STREAM_SCHEMA,
|
||||
});
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
|
|
|
|||
|
|
@ -97,8 +97,24 @@ schema truth, provider dashboards, identity grants, Discord delivery, owner-DM
|
|||
delivery, or renderer layout. Keepalive loops belong in Idunn, not Odin's
|
||||
coordinator or individual daemons.
|
||||
|
||||
Muninn is the portable local telemetry Verse assembler. Its Rust body lives in
|
||||
`crates/muninn-daemon` and publishes `muninn.telemetry_surface.v1` through
|
||||
CultMesh/CultCache. Muninn may run on Raven, Nightwing, Starfire, or any future
|
||||
device body. It names locally accessible telemetry affordances: screen capture,
|
||||
loopback audio, microphones, cameras, and future sensors. Muninn does not start
|
||||
expensive capture streams merely because the daemon is alive. The default
|
||||
`serve` posture publishes an idle typed surface; explicit activation, such as
|
||||
`muninn activate` for Raven A/V over SRT, is the only path that starts FFmpeg,
|
||||
WASAPI loopback, video capture, or similar resource-consuming workers.
|
||||
|
||||
Muninn owns local telemetry discovery and stream activation boundaries. It does
|
||||
not own Mimir's normalized ingest ledger, OBS rendering, Gjallar composition,
|
||||
Odin discovery truth, or Idunn keepalive policy. Active stream records such as
|
||||
`muninn.capture_stream.v1` are evidence of requested streams, not permission for
|
||||
startup to burn capture resources.
|
||||
|
||||
Bifrost is the bridge for Persona speech and other public/owner-facing
|
||||
crossings. When a Persona interpreter decides a Face speaks, the accepted
|
||||
crossings. When a Persona interpreter decides a Persona speaks, the accepted
|
||||
side effect is a Bifrost CultMesh command or document that names actor,
|
||||
authority, target surface, context, policy result, and receipt path. VoidBot
|
||||
observes Discord, preserves room cognition, moderates, and may provide
|
||||
|
|
@ -124,6 +140,8 @@ Verse announcement
|
|||
- Odin owns the accepted registry of known Verses.
|
||||
- A Verse owns its own schemas and authority model; Odin indexes and translates, it does not silently rewrite local truth.
|
||||
- Device clients own sensor and media capture; Mimir owns the normalized ingest ledger; Odin owns the aggregate operator projection.
|
||||
- Muninn advertises local telemetry affordances cheaply and starts capture only
|
||||
after an explicit activation request.
|
||||
- Translation paths must name source schema, target schema, lossiness, authority, and version.
|
||||
- Service presentation flows are CultMesh/Eve/CultUI interface projections. Odin aggregates those projection graphs; it does not replace them with nameplate summaries.
|
||||
- Renderers lower surfaces only. If a renderer fixes network truth, the machine is split-brained.
|
||||
|
|
|
|||
|
|
@ -49,9 +49,8 @@ Idunn now shares Odin's Rust body:
|
|||
- `src/Idunn/README.md` is the user-facing introduction for developers,
|
||||
operators, and daemon authors.
|
||||
- `scripts/start-idunn-local.ps1` starts resident local watchdogs, including
|
||||
VoidBot through `health-voidbot.cmd` and `restart-voidbot.cmd`, and Raven's
|
||||
capture mux through `health-raven-capture-mux.cmd` and
|
||||
`restart-raven-capture-mux.cmd`.
|
||||
VoidBot through `health-voidbot.cmd` and `restart-voidbot.cmd`, and Muninn
|
||||
through `health-muninn.cmd` and `restart-muninn.cmd`.
|
||||
- `scripts/notify-idunn-operator-alarm.ps1` is the local operator crossing:
|
||||
Idunn invokes it only after raising an operator alarm, and it asks Bifrost to
|
||||
publish a typed `gamecult.operator_dm_request.v1` CultMesh command document
|
||||
|
|
@ -104,10 +103,13 @@ idunn.operator_alarm.v1
|
|||
environment variables to `E:\Projects\Bifrost\tools\operator-notification.mjs
|
||||
publish-idunn-alarm`. Bifrost publishes the typed CultMesh command document;
|
||||
Idunn only decides that an alarm exists.
|
||||
- Raven capture continuity is a remote keepalive path. Idunn probes
|
||||
`http://127.0.0.1:8801/health` through SSH to Raven's WireGuard address
|
||||
`10.77.0.4` and restarts through Mimir's `start-raven-daemon.ps1` with
|
||||
Mimir and OBS SRT targets declared separately.
|
||||
- Muninn continuity on Raven is a remote keepalive path. Idunn probes the
|
||||
deployed Odin `muninn.exe --health` through the `raven` SSH alias and
|
||||
restarts Muninn's idle `serve` posture when needed. Raven is the host/body;
|
||||
Muninn is the local telemetry Verse assembler. Idunn must not activate
|
||||
screen/audio streams as part of keepalive. Raven A/V over SRT is an explicit
|
||||
activation path through `activate-muninn-raven-av-srt.ps1`, not daemon
|
||||
startup behavior.
|
||||
|
||||
## First Runtime Direction
|
||||
|
||||
|
|
|
|||
40
docs/muninn.md
Normal file
40
docs/muninn.md
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Muninn
|
||||
|
||||
Muninn is Odin's portable local telemetry Verse assembler.
|
||||
|
||||
It runs on a device body and publishes a typed CultMesh/CultCache surface for
|
||||
what that body can sense or expose locally: screen capture, loopback audio,
|
||||
microphones, cameras, and future sensors.
|
||||
|
||||
## Authority
|
||||
|
||||
- Owner: Muninn owns local telemetry discovery, stream affordance publication,
|
||||
and explicit activation of requested local streams.
|
||||
- Inputs: cheap local probes, operator or Verse activation requests, and local
|
||||
capture tools such as FFmpeg or WASAPI helpers.
|
||||
- Outputs: `muninn.telemetry_surface.v1` and active `muninn.capture_stream.v1`
|
||||
records.
|
||||
- Forbidden writers: daemon startup, Idunn keepalive, Mimir ingest, OBS, Odin,
|
||||
Gjallar, and renderer bodies must not start capture by implication.
|
||||
|
||||
## Runtime
|
||||
|
||||
```powershell
|
||||
cargo build -p muninn-daemon
|
||||
muninn serve --store C:\Meta\Odin\state\muninn.telemetry.cc --interval-seconds 15
|
||||
muninn --health --store C:\Meta\Odin\state\muninn.telemetry.cc
|
||||
```
|
||||
|
||||
`serve` is cheap and idle. It publishes affordances and keeps the local Verse
|
||||
surface fresh without starting FFmpeg, screen capture, microphone capture, or
|
||||
loopback capture.
|
||||
|
||||
Activation is explicit:
|
||||
|
||||
```powershell
|
||||
.\scripts\activate-muninn-raven-av-srt.ps1
|
||||
```
|
||||
|
||||
That Raven activation starts the requested screen and Realtek loopback stream
|
||||
and publishes `muninn.capture_stream.v1`. Mimir and OBS are stream consumers;
|
||||
they do not own Muninn.
|
||||
|
|
@ -11,6 +11,8 @@
|
|||
"gjallar:start": "cargo run -p gjallar-daemon --",
|
||||
"idunn:build": "cargo build -p idunn-daemon",
|
||||
"idunn:start": "cargo run -p idunn-daemon --",
|
||||
"muninn:build": "cargo build -p muninn-daemon",
|
||||
"muninn:start": "cargo run -p muninn-daemon --",
|
||||
"rust:test": "cargo test --workspace",
|
||||
"check": "node scripts/check-syntax.cjs",
|
||||
"start": "node src/odin-coordinator.cjs"
|
||||
|
|
|
|||
2
scripts/activate-muninn-raven-av-srt.cmd
Normal file
2
scripts/activate-muninn-raven-av-srt.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\activate-muninn-raven-av-srt.ps1
|
||||
45
scripts/activate-muninn-raven-av-srt.ps1
Normal file
45
scripts/activate-muninn-raven-av-srt.ps1
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
param(
|
||||
[string] $RavenHost = "raven",
|
||||
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe",
|
||||
[string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc",
|
||||
[string] $LoopbackScript = "C:\Meta\Odin\Muninn\scripts\wasapi-loopback-capture.ps1",
|
||||
[string] $LogRoot = "C:\Meta\Odin\logs\muninn",
|
||||
[string] $Ffmpeg = "C:\Users\Madman's Lullaby\AppData\Local\Microsoft\WinGet\Links\ffmpeg.exe",
|
||||
[string] $TargetHost = "10.77.0.2",
|
||||
[int] $Port = 5200,
|
||||
[string] $ObsTargetHost = "10.77.0.2",
|
||||
[int] $ObsPort = 5204,
|
||||
[string] $AudioDevice = "Realtek"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$remoteScript = @"
|
||||
`$ErrorActionPreference = "Stop"
|
||||
if (-not (Test-Path -LiteralPath "$MuninnExe")) {
|
||||
throw "Muninn executable not found at $MuninnExe"
|
||||
}
|
||||
if (-not (Test-Path -LiteralPath "$LoopbackScript")) {
|
||||
throw "Muninn loopback script not found at $LoopbackScript"
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path "$LogRoot" | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent "$StorePath") | Out-Null
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { (`$_.Name -ieq "muninn.exe" -and `$_.CommandLine -like "*activate*") -or (`$_.Name -ieq "ffmpeg.exe" -and `$_.CommandLine -like "*srt://$TargetHost*") -or (`$_.Name -ieq "powershell.exe" -and `$_.CommandLine -like "*wasapi-loopback-capture.ps1*") } |
|
||||
ForEach-Object { taskkill.exe /PID `$_.ProcessId /T /F | Out-Null }
|
||||
`$muninnDir = Split-Path -Parent "$MuninnExe"
|
||||
`$launcher = Join-Path `$muninnDir "activate-raven-av-srt.cmd"
|
||||
`$lines = @(
|
||||
"@echo off",
|
||||
"cd /d ""`$muninnDir""",
|
||||
"""$MuninnExe"" activate --store ""$StorePath"" --host raven --stream muninn.raven.av.srt --target-host ""$TargetHost"" --port $Port --obs-target-host ""$ObsTargetHost"" --obs-port $ObsPort --audio-device ""$AudioDevice"" --ffmpeg ""$Ffmpeg"" --loopback-script ""$LoopbackScript"" --log-root ""$LogRoot"" 1>>""$LogRoot\muninn-activate.out.log"" 2>>""$LogRoot\muninn-activate.err.log"""
|
||||
)
|
||||
Set-Content -LiteralPath `$launcher -Value `$lines -Encoding ASCII
|
||||
cmd /c "schtasks /Delete /TN GameCult-Muninn-Activate /F 2>NUL"
|
||||
cmd /c schtasks /Create /TN GameCult-Muninn-Activate /SC ONCE /ST 23:59 /TR `$launcher /IT /RL HIGHEST /F
|
||||
cmd /c schtasks /Run /TN GameCult-Muninn-Activate
|
||||
"@
|
||||
|
||||
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteScript))
|
||||
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $RavenHost "powershell.exe -NoProfile -EncodedCommand $encoded"
|
||||
exit $LASTEXITCODE
|
||||
2
scripts/health-muninn.cmd
Normal file
2
scripts/health-muninn.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-muninn.ps1
|
||||
25
scripts/health-muninn.ps1
Normal file
25
scripts/health-muninn.ps1
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
param(
|
||||
[string] $RavenHost = "raven",
|
||||
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe",
|
||||
[string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$remoteScript = @"
|
||||
`$ErrorActionPreference = "Stop"
|
||||
if (-not (Test-Path -LiteralPath "$MuninnExe")) {
|
||||
throw "Muninn executable not found at $MuninnExe"
|
||||
}
|
||||
`$process = Get-CimInstance Win32_Process |
|
||||
Where-Object { `$_.Name -ieq "muninn.exe" -and `$_.CommandLine -like "*serve*" } |
|
||||
Select-Object -First 1
|
||||
if (`$null -eq `$process) {
|
||||
throw "Muninn serve process is not running"
|
||||
}
|
||||
& "$MuninnExe" --health --store "$StorePath"
|
||||
"@
|
||||
|
||||
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteScript))
|
||||
& ssh.exe -o BatchMode=yes -o ConnectTimeout=5 $RavenHost "powershell.exe -NoProfile -EncodedCommand $encoded"
|
||||
exit $LASTEXITCODE
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-raven-capture-mux.ps1
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\health-muninn.ps1
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
param(
|
||||
[string] $RavenHost = "10.77.0.4",
|
||||
[string] $HealthUrl = "http://127.0.0.1:8801/health"
|
||||
[string] $RavenHost = "raven"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
& ssh.exe -o BatchMode=yes -o ConnectTimeout=5 $RavenHost "curl.exe -fsS $HealthUrl >NUL"
|
||||
$script = Join-Path $PSScriptRoot "health-muninn.ps1"
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script -RavenHost $RavenHost
|
||||
exit $LASTEXITCODE
|
||||
|
|
|
|||
2
scripts/restart-muninn.cmd
Normal file
2
scripts/restart-muninn.cmd
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\restart-muninn.ps1
|
||||
37
scripts/restart-muninn.ps1
Normal file
37
scripts/restart-muninn.ps1
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
param(
|
||||
[string] $RavenHost = "raven",
|
||||
[string] $MuninnExe = "C:\Meta\Odin\Muninn\muninn.exe",
|
||||
[string] $StorePath = "C:\Meta\Odin\state\muninn.telemetry.cc",
|
||||
[string] $LogRoot = "C:\Meta\Odin\logs\muninn"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$remoteScript = @"
|
||||
`$ErrorActionPreference = "Stop"
|
||||
if (-not (Test-Path -LiteralPath "$MuninnExe")) {
|
||||
throw "Muninn executable not found at $MuninnExe"
|
||||
}
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { `$_.Name -ieq "muninn.exe" -and `$_.CommandLine -like "*serve*" } |
|
||||
ForEach-Object {
|
||||
& taskkill.exe /PID `$_.ProcessId /T /F | Out-Null
|
||||
}
|
||||
New-Item -ItemType Directory -Force -Path "$LogRoot" | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Split-Path -Parent "$StorePath") | Out-Null
|
||||
`$muninnDir = Split-Path -Parent "$MuninnExe"
|
||||
`$launcher = Join-Path `$muninnDir "start-muninn-serve.cmd"
|
||||
`$lines = @(
|
||||
"@echo off",
|
||||
"cd /d ""`$muninnDir""",
|
||||
"""$MuninnExe"" serve --store ""$StorePath"" --log-root ""$LogRoot"" --host raven --interval-seconds 15 1>>""$LogRoot\muninn-serve.out.log"" 2>>""$LogRoot\muninn-serve.err.log"""
|
||||
)
|
||||
Set-Content -LiteralPath `$launcher -Value `$lines -Encoding ASCII
|
||||
cmd /c "schtasks /Delete /TN GameCult-Muninn /F 2>NUL"
|
||||
cmd /c schtasks /Create /TN GameCult-Muninn /SC ONCE /ST 23:59 /TR `$launcher /IT /RL HIGHEST /F
|
||||
cmd /c schtasks /Run /TN GameCult-Muninn
|
||||
"@
|
||||
|
||||
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteScript))
|
||||
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $RavenHost "powershell.exe -NoProfile -EncodedCommand $encoded"
|
||||
exit $LASTEXITCODE
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
@echo off
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\restart-raven-capture-mux.ps1
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File E:\Projects\Odin\scripts\restart-muninn.ps1
|
||||
|
|
|
|||
|
|
@ -1,28 +1,7 @@
|
|||
param(
|
||||
[string] $RavenHost = "10.77.0.4",
|
||||
[string] $MimirRepo = "E:\Projects\Mimir",
|
||||
[string] $TargetHost = "10.77.0.2",
|
||||
[int] $Port = 5200,
|
||||
[string] $ObsTargetHost = "10.77.0.2",
|
||||
[int] $ObsPort = 5204
|
||||
[string] $RavenHost = "raven"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$remoteScript = @"
|
||||
`$ErrorActionPreference = "Stop"
|
||||
`$startScript = Join-Path "$MimirRepo" "scripts\start-raven-daemon.ps1"
|
||||
if (-not (Test-Path -LiteralPath `$startScript)) {
|
||||
throw "Raven Mimir start script not found at `$startScript"
|
||||
}
|
||||
Get-CimInstance Win32_Process |
|
||||
Where-Object { `$_.Name -ieq "dotnet.exe" -and `$_.CommandLine -like "*Mimir.RavenDaemon*" } |
|
||||
ForEach-Object {
|
||||
& taskkill.exe /PID `$_.ProcessId /T /F | Out-Null
|
||||
}
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File `$startScript -TargetHost "$TargetHost" -Port $Port -ObsTargetHost "$ObsTargetHost" -ObsPort $ObsPort
|
||||
"@
|
||||
|
||||
$encoded = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($remoteScript))
|
||||
& ssh.exe -o BatchMode=yes -o ConnectTimeout=10 $RavenHost "powershell.exe -NoProfile -EncodedCommand $encoded"
|
||||
$script = Join-Path $PSScriptRoot "restart-muninn.ps1"
|
||||
& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $script -RavenHost $RavenHost
|
||||
exit $LASTEXITCODE
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ $watchdogs = @(
|
|||
IntervalSeconds = 300
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "raven-capture-mux"
|
||||
Name = "Raven capture mux"
|
||||
Id = "muninn"
|
||||
Name = "Muninn telemetry Verse assembler"
|
||||
Verse = "raven.local"
|
||||
Health = "$repoRoot\scripts\health-raven-capture-mux.cmd"
|
||||
Restart = "$repoRoot\scripts\restart-raven-capture-mux.cmd"
|
||||
Health = "$repoRoot\scripts\health-muninn.cmd"
|
||||
Restart = "$repoRoot\scripts\restart-muninn.cmd"
|
||||
},
|
||||
[pscustomobject]@{
|
||||
Id = "nightwing-gjallar"
|
||||
|
|
|
|||
Loading…
Reference in a new issue