Initial GuruConnect implementation - Phase 1 MVP

- Agent: DXGI/GDI screen capture, mouse/keyboard input, WebSocket transport
- Server: Axum relay, session management, REST API
- Dashboard: React viewer components with TypeScript
- Protocol: Protobuf definitions for all message types

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
AZ Computer Guru
2025-12-21 17:18:05 -07:00
commit 33893ea73b
38 changed files with 7724 additions and 0 deletions

194
agent/src/session/mod.rs Normal file
View File

@@ -0,0 +1,194 @@
//! 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<()> {
let transport = self.transport.as_mut()
.ok_or_else(|| anyhow::anyhow!("Not connected"))?;
self.state = SessionState::Active;
// Get primary display
let display = capture::primary_display()?;
tracing::info!("Using display: {} ({}x{})", display.name, display.width, display.height);
// Create capturer
let mut capturer = capture::create_capturer(
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)
while let Some(msg) = transport.try_recv()? {
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)),
};
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(())
}
}