record-daemon: initial commit

This commit is contained in:
2026-03-19 17:48:07 +01:00
commit d6c0334369
30 changed files with 9486 additions and 0 deletions
+355
View File
@@ -0,0 +1,355 @@
//! Record Daemon entry point.
use std::sync::Arc;
use clap::Parser;
use parking_lot::RwLock;
use tokio::sync::broadcast;
use tracing::{debug, error, info, warn};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use futures::StreamExt;
use record_daemon::{
config::{self, Settings},
error::Result,
ipc::{self, IpcHandlers, IpcServer, IpcServerConfig},
lqp::{GameEvent, LockfileWatcher, LqpClient},
recording::RecordingEngine,
state::{DaemonStateMachine, DaemonStatus, StateTransition},
timeline::{EventMapper, TimelineStore},
};
/// Record Daemon - League of Legends recording daemon.
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Path to configuration file.
#[arg(short, long, value_name = "PATH")]
config: Option<std::path::PathBuf>,
/// Log level (trace, debug, info, warn, error).
#[arg(short, long, default_value = "info")]
log_level: String,
/// Run in foreground (don't daemonize).
#[arg(short, long)]
foreground: bool,
/// Socket path for IPC.
#[arg(short, long)]
socket: Option<std::path::PathBuf>,
}
/// Main daemon structure.
struct Daemon {
/// Configuration.
settings: Arc<RwLock<Settings>>,
/// State machine.
state_machine: Arc<DaemonStateMachine>,
/// LQP client.
lqp_client: Arc<LqpClient>,
/// Recording engine.
recording_engine: Arc<RwLock<Option<RecordingEngine>>>,
/// Timeline store.
timeline_store: Arc<RwLock<TimelineStore>>,
/// Event mapper.
event_mapper: Arc<RwLock<EventMapper>>,
/// IPC server.
ipc_server: Option<IpcServer>,
/// Shutdown signal.
shutdown_tx: broadcast::Sender<()>,
}
impl Daemon {
/// Create a new daemon instance.
fn new(settings: Settings) -> Self {
let (shutdown_tx, _) = broadcast::channel(1);
Self {
settings: Arc::new(RwLock::new(settings.clone())),
state_machine: Arc::new(DaemonStateMachine::new()),
lqp_client: Arc::new(LqpClient::new()),
recording_engine: Arc::new(RwLock::new(None)),
timeline_store: Arc::new(RwLock::new(TimelineStore::new())),
event_mapper: Arc::new(RwLock::new(EventMapper::new())),
ipc_server: None,
shutdown_tx,
}
}
/// Initialize the daemon.
async fn init(&mut self) -> Result<()> {
info!("Initializing record daemon v{}", record_daemon::VERSION);
// Initialize recording engine
let settings = self.settings.read().clone();
let mut engine = RecordingEngine::new(settings);
engine.initialize()?;
*self.recording_engine.write() = Some(engine);
// Load existing recordings from disk
self.timeline_store.read().load_from_disk()?;
// Initialize IPC server
let ipc_config = IpcServerConfig {
socket_path: self
.settings
.read()
.daemon
.socket_path
.clone()
.unwrap_or_else(ipc::default_socket_path),
..Default::default()
};
let handlers = IpcHandlers::new(
self.settings.clone(),
self.recording_engine.clone(),
self.timeline_store.clone(),
Arc::new(RwLock::new(DaemonStatus::Idle)),
Arc::new(RwLock::new(false)),
);
let mut ipc_server = IpcServer::new(ipc_config, handlers);
ipc_server.start().await?;
self.ipc_server = Some(ipc_server);
info!("Daemon initialized successfully");
Ok(())
}
/// Run the main daemon loop.
async fn run(&mut self) -> Result<()> {
info!("Starting main daemon loop");
let mut shutdown_rx = self.shutdown_tx.subscribe();
let mut lockfile_watcher = LockfileWatcher::new();
// Spawn IPC server task - take ownership
if let Some(ipc_server) = self.ipc_server.take() {
tokio::spawn(async move {
if let Err(e) = ipc_server.run().await {
error!("IPC server error: {}", e);
}
});
}
// Subscribe to LQP events before the loop
let mut event_rx = self.lqp_client.subscribe();
// Main event loop
loop {
tokio::select! {
// Shutdown signal
_ = shutdown_rx.recv() => {
info!("Shutdown signal received");
break;
}
// Check for League Client
result = self.check_client(&mut lockfile_watcher) => {
if let Err(e) = result {
warn!("Client check error: {}", e);
}
}
// Process LQP events
event = event_rx.recv() => {
if let Ok(event) = event {
if let Err(e) = self.handle_game_event(event).await {
warn!("Event handling error: {}", e);
}
}
}
}
}
info!("Daemon loop ended");
Ok(())
}
/// Check for League Client connection.
async fn check_client(&self, watcher: &mut LockfileWatcher) -> Result<()> {
let poll_interval =
std::time::Duration::from_millis(self.settings.read().daemon.poll_interval_ms);
match watcher.check()? {
Some(true) => {
// Client started
info!("League Client detected");
self.state_machine
.transition(StateTransition::ClientStarted);
if let Some(creds) = watcher.credentials() {
self.lqp_client.connect(creds.clone()).await?;
self.lqp_client.start_event_listener().await?;
}
}
Some(false) => {
// Client stopped
info!("League Client stopped");
self.state_machine
.transition(StateTransition::ClientStopped);
self.lqp_client.disconnect().await;
}
None => {}
}
tokio::time::sleep(poll_interval).await;
Ok(())
}
/// Handle a game event.
async fn handle_game_event(&self, event: GameEvent) -> Result<()> {
debug!("Game event: {:?}", event);
// Process state transitions
if let Some(transition) = self.state_machine.process_event(&event) {
self.state_machine.transition(transition.clone());
// Handle recording start/stop
match transition {
StateTransition::GameStarted { game_id, champion } => {
self.start_recording(game_id, champion.as_deref()).await?;
}
StateTransition::GameEnded => {
self.stop_recording().await?;
}
_ => {}
}
}
// Record event to timeline if recording
if self.state_machine.is_recording() {
if let Some((video_ts, game_ts)) = self.event_mapper.write().handle_event(&event) {
// Event would be added to timeline here
debug!(
"Event mapped: video_ts={:?}, game_ts={:?}",
video_ts, game_ts
);
}
}
Ok(())
}
/// Start recording.
async fn start_recording(&self, game_id: u64, champion: Option<&str>) -> Result<()> {
info!("Starting recording for game {} ({:?})", game_id, champion);
let mut engine_guard = self.recording_engine.write();
if let Some(ref mut engine) = *engine_guard {
engine.start_recording(Some(game_id), champion)?;
self.event_mapper.write().start();
}
Ok(())
}
/// Stop recording.
async fn stop_recording(&self) -> Result<()> {
info!("Stopping recording");
let mut engine_guard = self.recording_engine.write();
if let Some(ref mut engine) = *engine_guard {
let result = engine.stop_recording()?;
self.event_mapper.write().stop();
// Save to timeline
self.timeline_store.write().add_recording(result)?;
}
Ok(())
}
/// Shutdown the daemon.
async fn shutdown(&mut self) -> Result<()> {
info!("Shutting down daemon");
// Stop recording if active
if self.state_machine.is_recording() {
self.stop_recording().await?;
}
// Stop IPC server
if let Some(ref mut ipc_server) = self.ipc_server {
ipc_server.stop().await?;
}
// Shutdown recording engine
if let Some(ref mut engine) = *self.recording_engine.write() {
engine.shutdown()?;
}
info!("Daemon shutdown complete");
Ok(())
}
}
/// Initialize logging.
fn init_logging(level: &str) {
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level));
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.init();
}
#[tokio::main]
async fn main() -> Result<()> {
let args = Args::parse();
// Initialize logging
init_logging(&args.log_level);
info!("Record Daemon v{} starting", record_daemon::VERSION);
// Load configuration
let settings = if let Some(config_path) = args.config {
config::ConfigPersistence::new(config_path).load()?
} else {
config::load_config()?
};
// Create and run daemon
let mut daemon = Daemon::new(settings);
// Handle shutdown signals
let shutdown_tx = daemon.shutdown_tx.clone();
#[cfg(unix)]
{
use signal_hook::consts::signal::*;
use signal_hook_tokio::Signals;
let mut signals = Signals::new([SIGTERM, SIGINT, SIGQUIT])?;
let handle = signals.handle();
let tx = shutdown_tx.clone();
tokio::spawn(async move {
if let Some(signal) = signals.next().await {
info!("Received signal {:?}", signal);
let _ = tx.send(());
}
});
}
// Initialize and run
if let Err(e) = daemon.init().await {
error!("Failed to initialize daemon: {}", e);
return Err(e);
}
// Run main loop
let result = daemon.run().await;
// Cleanup
if let Err(e) = daemon.shutdown().await {
error!("Error during shutdown: {}", e);
}
result
}