Files
guru-connect/agent/src/session/mod.rs
AZ Computer Guru d2c8cf1c0b Fix remaining API issues and borrow conflicts
- Rename display param to target_display to avoid tracing conflict
- Refactor session loop to avoid holding borrow across handle_message
2025-12-28 12:43:54 -07:00

207 lines
6.8 KiB
Rust

//! Session management for the agent
//!
//! Handles the lifecycle of a remote session including:
//! - Connection to server
//! - Authentication
//! - Frame capture and encoding loop
//! - Input event handling
use crate::capture::{self, Capturer, Display};
use crate::config::Config;
use crate::encoder::{self, Encoder};
use crate::input::InputController;
use crate::proto::{Message, message};
use crate::transport::WebSocketTransport;
use anyhow::Result;
use std::sync::Arc;
use std::time::{Duration, Instant};
use tokio::sync::mpsc;
/// Session manager handles the remote control session
pub struct SessionManager {
config: Config,
transport: Option<WebSocketTransport>,
state: SessionState,
}
#[derive(Debug, Clone, PartialEq)]
enum SessionState {
Disconnected,
Connecting,
Connected,
Active,
}
impl SessionManager {
/// Create a new session manager
pub fn new(config: Config) -> Self {
Self {
config,
transport: None,
state: SessionState::Disconnected,
}
}
/// Connect to the server
pub async fn connect(&mut self) -> Result<()> {
self.state = SessionState::Connecting;
let transport = WebSocketTransport::connect(
&self.config.server_url,
&self.config.api_key,
).await?;
self.transport = Some(transport);
self.state = SessionState::Connected;
Ok(())
}
/// Run the session main loop
pub async fn run(&mut self) -> Result<()> {
if self.transport.is_none() {
anyhow::bail!("Not connected");
}
self.state = SessionState::Active;
// Get primary display
let primary_display = capture::primary_display()?;
tracing::info!("Using display: {} ({}x{})", primary_display.name, primary_display.width, primary_display.height);
// Create capturer
let mut capturer = capture::create_capturer(
primary_display.clone(),
self.config.capture.use_dxgi,
self.config.capture.gdi_fallback,
)?;
// Create encoder
let mut encoder = encoder::create_encoder(
&self.config.encoding.codec,
self.config.encoding.quality,
)?;
// Create input controller
let mut input = InputController::new()?;
// Calculate frame interval
let frame_interval = Duration::from_millis(1000 / self.config.capture.fps as u64);
let mut last_frame_time = Instant::now();
// Main loop
loop {
// Check for incoming messages (non-blocking)
// Collect messages first, then release borrow before handling
let messages: Vec<Message> = {
let transport = self.transport.as_mut().unwrap();
let mut msgs = Vec::new();
while let Some(msg) = transport.try_recv()? {
msgs.push(msg);
}
msgs
};
for msg in messages {
self.handle_message(&mut input, msg)?;
}
// Capture and send frame if interval elapsed
if last_frame_time.elapsed() >= frame_interval {
last_frame_time = Instant::now();
if let Some(frame) = capturer.capture()? {
let encoded = encoder.encode(&frame)?;
// Skip empty frames (no changes)
if encoded.size > 0 {
let msg = Message {
payload: Some(message::Payload::VideoFrame(encoded.frame)),
};
let transport = self.transport.as_mut().unwrap();
transport.send(msg).await?;
}
}
}
// Small sleep to prevent busy loop
tokio::time::sleep(Duration::from_millis(1)).await;
// Check if still connected
if !transport.is_connected() {
tracing::warn!("Connection lost");
break;
}
}
self.state = SessionState::Disconnected;
Ok(())
}
/// Handle incoming message from server
fn handle_message(&mut self, input: &mut InputController, msg: Message) -> Result<()> {
match msg.payload {
Some(message::Payload::MouseEvent(mouse)) => {
// Handle mouse event
use crate::proto::MouseEventType;
use crate::input::MouseButton;
match MouseEventType::try_from(mouse.event_type).unwrap_or(MouseEventType::MouseMove) {
MouseEventType::MouseMove => {
input.mouse_move(mouse.x, mouse.y)?;
}
MouseEventType::MouseDown => {
input.mouse_move(mouse.x, mouse.y)?;
if let Some(ref buttons) = mouse.buttons {
if buttons.left { input.mouse_click(MouseButton::Left, true)?; }
if buttons.right { input.mouse_click(MouseButton::Right, true)?; }
if buttons.middle { input.mouse_click(MouseButton::Middle, true)?; }
}
}
MouseEventType::MouseUp => {
if let Some(ref buttons) = mouse.buttons {
if buttons.left { input.mouse_click(MouseButton::Left, false)?; }
if buttons.right { input.mouse_click(MouseButton::Right, false)?; }
if buttons.middle { input.mouse_click(MouseButton::Middle, false)?; }
}
}
MouseEventType::MouseWheel => {
input.mouse_scroll(mouse.wheel_delta_x, mouse.wheel_delta_y)?;
}
}
}
Some(message::Payload::KeyEvent(key)) => {
// Handle keyboard event
input.key_event(key.vk_code as u16, key.down)?;
}
Some(message::Payload::SpecialKey(special)) => {
use crate::proto::SpecialKey;
match SpecialKey::try_from(special.key).ok() {
Some(SpecialKey::CtrlAltDel) => {
input.send_ctrl_alt_del()?;
}
_ => {}
}
}
Some(message::Payload::Heartbeat(_)) => {
// Respond to heartbeat
// TODO: Send heartbeat ack
}
Some(message::Payload::Disconnect(disc)) => {
tracing::info!("Disconnect requested: {}", disc.reason);
return Err(anyhow::anyhow!("Disconnect: {}", disc.reason));
}
_ => {
// Ignore unknown messages
}
}
Ok(())
}
}