record-daemon: initial commit
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user