Lint: electric boogaloo

This commit is contained in:
Albert Armea 2025-12-29 17:51:55 -05:00
parent 3fd49b2efd
commit 5e5e6f6806
22 changed files with 106 additions and 291 deletions

View file

@ -52,12 +52,16 @@ Run the test suite:
```sh
cargo test
# as run in CI:
cargo test --all-targets
```
Run lint checks:
```sh
cargo clippy
# as run in CI:
cargo clippy --all-targets -- -D warnings
```

View file

@ -135,12 +135,6 @@ pub enum Command {
/// Set volume to a specific percentage
SetVolume { percent: u8 },
/// Increase volume by a step
VolumeUp { step: u8 },
/// Decrease volume by a step
VolumeDown { step: u8 },
/// Toggle mute state
ToggleMute,

View file

@ -1,10 +1,9 @@
//! Validated policy structures
use crate::schema::{RawConfig, RawDays, RawEntry, RawEntryKind, RawVolumeConfig, RawServiceConfig, RawWarningThreshold};
use crate::schema::{RawConfig, RawEntry, RawEntryKind, RawVolumeConfig, RawServiceConfig, RawWarningThreshold};
use crate::validation::{parse_days, parse_time};
use shepherd_api::{EntryKind, WarningSeverity, WarningThreshold};
use shepherd_util::{DaysOfWeek, EntryId, TimeWindow, WallClock};
use std::collections::HashMap;
use std::path::PathBuf;
use std::time::Duration;

View file

@ -113,8 +113,8 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
.or(config.service.default_max_run_seconds);
// Only validate warnings if max_run is Some and not 0 (unlimited)
if let (Some(warnings), Some(max_run)) = (&entry.warnings, max_run) {
if max_run > 0 {
if let (Some(warnings), Some(max_run)) = (&entry.warnings, max_run)
&& max_run > 0 {
for warning in warnings {
if warning.seconds_before >= max_run {
errors.push(ValidationError::WarningExceedsMaxRun {
@ -124,7 +124,6 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
});
}
}
}
// Note: warnings are ignored for unlimited entries (max_run = 0)
}

View file

@ -2,7 +2,7 @@
use chrono::{DateTime, Local};
use shepherd_api::{
ServiceStateSnapshot, EntryKindTag, EntryView, ReasonCode, SessionEndReason,
ServiceStateSnapshot, EntryView, ReasonCode, SessionEndReason,
WarningSeverity, API_VERSION,
};
use shepherd_config::{Entry, Policy};
@ -11,7 +11,7 @@ use shepherd_store::{AuditEvent, AuditEventType, Store};
use shepherd_util::{EntryId, MonotonicInstant, SessionId};
use std::sync::Arc;
use std::time::Duration;
use tracing::{debug, info, warn};
use tracing::{debug, info};
use crate::{ActiveSession, CoreEvent, SessionPlan, StopResult};
@ -128,23 +128,21 @@ impl CoreEngine {
}
// Check cooldown
if let Ok(Some(until)) = self.store.get_cooldown_until(&entry.id) {
if until > now {
if let Ok(Some(until)) = self.store.get_cooldown_until(&entry.id)
&& until > now {
enabled = false;
reasons.push(ReasonCode::CooldownActive { available_at: until });
}
}
// Check daily quota
if let Some(quota) = entry.limits.daily_quota {
let today = now.date_naive();
if let Ok(used) = self.store.get_usage(&entry.id, today) {
if used >= quota {
if let Ok(used) = self.store.get_usage(&entry.id, today)
&& used >= quota {
enabled = false;
reasons.push(ReasonCode::QuotaExhausted { used, quota });
}
}
}
// Calculate max run if enabled (None when disabled, Some(None) flattened for unlimited)
let max_run_if_started_now = if enabled {
@ -393,12 +391,11 @@ impl CoreEngine {
let _ = self.store.add_usage(&session.plan.entry_id, today, duration);
// Set cooldown if configured
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id) {
if let Some(cooldown) = entry.limits.cooldown {
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id)
&& let Some(cooldown) = entry.limits.cooldown {
let until = now + chrono::Duration::from_std(cooldown).unwrap();
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
}
}
// Log to audit
let _ = self.store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
@ -443,12 +440,11 @@ impl CoreEngine {
let _ = self.store.add_usage(&session.plan.entry_id, today, duration);
// Set cooldown if configured
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id) {
if let Some(cooldown) = entry.limits.cooldown {
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id)
&& let Some(cooldown) = entry.limits.cooldown {
let until = now + chrono::Duration::from_std(cooldown).unwrap();
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
}
}
// Log to audit
let _ = self.store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
@ -510,8 +506,8 @@ impl CoreEngine {
pub fn extend_current(
&mut self,
by: Duration,
now_mono: MonotonicInstant,
now: DateTime<Local>,
_now_mono: MonotonicInstant,
_now: DateTime<Local>,
) -> Option<DateTime<Local>> {
let session = self.current_session.as_mut()?;

View file

@ -37,9 +37,7 @@ pub struct VolumeStatus {
impl VolumeStatus {
/// Get an icon name for the current volume status
pub fn icon_name(&self) -> &'static str {
if self.muted {
"audio-volume-muted-symbolic"
} else if self.percent == 0 {
if self.muted || self.percent == 0 {
"audio-volume-muted-symbolic"
} else if self.percent < 33 {
"audio-volume-low-symbolic"

View file

@ -126,11 +126,10 @@ impl HostAdapter for LinuxHost {
// followed by any additional args.
let mut argv = vec!["snap".to_string(), "run".to_string(), snap_name.clone()];
// If a custom command is specified (different from snap_name), add it
if let Some(cmd) = command {
if cmd != snap_name {
if let Some(cmd) = command
&& cmd != snap_name {
argv.push(cmd.clone());
}
}
argv.extend(args.clone());
(argv, env.clone(), None, Some(snap_name.clone()))
}
@ -336,8 +335,8 @@ mod tests {
// Process should have exited
match handle.payload() {
HostHandlePayload::Linux { pid, .. } => {
let procs = host.processes.lock().unwrap();
HostHandlePayload::Linux { pid: _, .. } => {
let _procs = host.processes.lock().unwrap();
// Process may or may not still be tracked depending on monitor timing
}
_ => panic!("Expected Linux handle"),

View file

@ -264,7 +264,7 @@ impl ManagedProcess {
unsafe {
cmd.pre_exec(|| {
nix::unistd::setsid().map_err(|e| {
std::io::Error::new(std::io::ErrorKind::Other, e.to_string())
std::io::Error::other(e.to_string())
})?;
Ok(())
});
@ -304,9 +304,9 @@ impl ManagedProcess {
if let Some(paren_end) = stat.rfind(')') {
let after_comm = &stat[paren_end + 2..];
let fields: Vec<&str> = after_comm.split_whitespace().collect();
if fields.len() >= 2 {
if let Ok(ppid) = fields[1].parse::<i32>() {
if ppid == parent_pid {
if fields.len() >= 2
&& let Ok(ppid) = fields[1].parse::<i32>()
&& ppid == parent_pid {
descendants.push(pid);
to_check.push(pid);
}
@ -316,8 +316,6 @@ impl ManagedProcess {
}
}
}
}
}
descendants
}

View file

@ -147,12 +147,11 @@ impl LinuxVolumeController {
debug!("pactl get-sink-volume output: {}", stdout.trim());
// Output: "Volume: front-left: 65536 / 100% / -0.00 dB, front-right: ..."
if let Some(percent_str) = stdout.split('/').nth(1) {
if let Ok(percent) = percent_str.trim().trim_end_matches('%').parse::<u8>() {
if let Some(percent_str) = stdout.split('/').nth(1)
&& let Ok(percent) = percent_str.trim().trim_end_matches('%').parse::<u8>() {
status.percent = percent;
}
}
}
// Check mute status
if let Ok(output) = Command::new("pactl")
@ -184,13 +183,11 @@ impl LinuxVolumeController {
for line in stdout.lines() {
if line.contains("Playback") && line.contains('%') {
// Extract percentage: [100%]
if let Some(start) = line.find('[') {
if let Some(end) = line[start..].find('%') {
if let Ok(percent) = line[start + 1..start + end].parse::<u8>() {
if let Some(start) = line.find('[')
&& let Some(end) = line[start..].find('%')
&& let Ok(percent) = line[start + 1..start + end].parse::<u8>() {
status.percent = percent;
}
}
}
// Check mute status: [on] or [off]
status.muted = line.contains("[off]");
break;

View file

@ -56,9 +56,6 @@ impl HudApp {
}
});
// Start periodic updates for battery/volume
start_metrics_updates(state.clone());
// Subscribe to state changes
let window_clone = window.clone();
let state_clone = state.clone();
@ -727,9 +724,3 @@ fn run_event_loop(socket_path: PathBuf, state: SharedState) -> anyhow::Result<()
}
})
}
fn start_metrics_updates(_state: SharedState) {
// Battery and volume are now updated in the main UI loop
// This function could be used for more expensive operations
// that don't need to run as frequently
}

View file

@ -34,21 +34,19 @@ impl BatteryStatus {
let name_str = name.to_string_lossy();
// Check for battery
if name_str.starts_with("BAT") {
if let Some((percent, charging)) = read_battery_info(&path) {
if name_str.starts_with("BAT")
&& let Some((percent, charging)) = read_battery_info(&path) {
status.percent = Some(percent);
status.charging = charging;
}
}
// Check for AC adapter
if name_str.starts_with("AC") || name_str.contains("ADP") {
if let Some(online) = read_ac_status(&path) {
if (name_str.starts_with("AC") || name_str.contains("ADP"))
&& let Some(online) = read_ac_status(&path) {
status.ac_connected = online;
}
}
}
}
status
}
@ -70,6 +68,7 @@ impl BatteryStatus {
}
/// Check if battery is critically low
#[allow(dead_code)]
pub fn is_critical(&self) -> bool {
matches!(self.percent, Some(p) if p < 10 && !self.charging)
}

View file

@ -11,7 +11,6 @@ mod volume;
use anyhow::Result;
use clap::Parser;
use gtk4::prelude::*;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;

View file

@ -2,8 +2,7 @@
//!
//! The HUD subscribes to events from shepherdd and tracks session state.
use chrono::Local;
use shepherd_api::{Event, EventPayload, SessionEndReason, VolumeInfo, VolumeRestrictions, WarningSeverity};
use shepherd_api::{Event, EventPayload, VolumeInfo, VolumeRestrictions, WarningSeverity};
use shepherd_util::{EntryId, SessionId};
use std::sync::Arc;
use tokio::sync::watch;
@ -21,6 +20,7 @@ pub enum SessionState {
entry_name: String,
started_at: std::time::Instant,
time_limit_secs: Option<u64>,
#[allow(dead_code)]
time_remaining_secs: Option<u64>,
},
@ -64,19 +64,6 @@ impl SessionState {
}
}
/// System metrics for display
#[derive(Debug, Clone, Default)]
pub struct SystemMetrics {
/// Battery percentage (0-100)
pub battery_percent: Option<u8>,
/// Whether battery is charging
pub battery_charging: bool,
/// Volume percentage (0-100)
pub volume_percent: Option<u8>,
/// Whether volume is muted
pub volume_muted: bool,
}
/// Shared state for the HUD
#[derive(Clone)]
pub struct SharedState {
@ -84,10 +71,6 @@ pub struct SharedState {
session_tx: Arc<watch::Sender<SessionState>>,
/// Session state receiver
session_rx: watch::Receiver<SessionState>,
/// System metrics sender
metrics_tx: Arc<watch::Sender<SystemMetrics>>,
/// System metrics receiver
metrics_rx: watch::Receiver<SystemMetrics>,
/// Volume info sender (updated via events, not polling)
volume_tx: Arc<watch::Sender<Option<VolumeInfo>>>,
/// Volume info receiver
@ -97,14 +80,11 @@ pub struct SharedState {
impl SharedState {
pub fn new() -> Self {
let (session_tx, session_rx) = watch::channel(SessionState::NoSession);
let (metrics_tx, metrics_rx) = watch::channel(SystemMetrics::default());
let (volume_tx, volume_rx) = watch::channel(None);
Self {
session_tx: Arc::new(session_tx),
session_rx,
metrics_tx: Arc::new(metrics_tx),
metrics_rx,
volume_tx: Arc::new(volume_tx),
volume_rx,
}
@ -116,25 +96,16 @@ impl SharedState {
}
/// Subscribe to session state changes
#[allow(dead_code)]
pub fn subscribe_session(&self) -> watch::Receiver<SessionState> {
self.session_rx.clone()
}
/// Subscribe to metrics changes
pub fn subscribe_metrics(&self) -> watch::Receiver<SystemMetrics> {
self.metrics_rx.clone()
}
/// Update session state
pub fn set_session_state(&self, state: SessionState) {
let _ = self.session_tx.send(state);
}
/// Update system metrics
pub fn set_metrics(&self, metrics: SystemMetrics) {
let _ = self.metrics_tx.send(metrics);
}
/// Get current volume info (cached from events)
pub fn volume_info(&self) -> Option<VolumeInfo> {
self.volume_rx.borrow().clone()
@ -165,6 +136,7 @@ impl SharedState {
}
/// Update time remaining for current session
#[allow(dead_code)]
pub fn update_time_remaining(&self, remaining_secs: u64) {
self.session_tx.send_modify(|state| {
if let SessionState::Active {
@ -246,8 +218,7 @@ impl SharedState {
entry_name,
..
} = state
{
if sid == session_id {
&& sid == session_id {
*state = SessionState::Warning {
session_id: session_id.clone(),
entry_id: entry_id.clone(),
@ -258,7 +229,6 @@ impl SharedState {
severity: *severity,
};
}
}
});
}
@ -275,11 +245,11 @@ impl SharedState {
if let Some(session) = &snapshot.current_session {
let now = shepherd_util::now();
// For unlimited sessions (deadline=None), time_remaining is None
let time_remaining = session.deadline.and_then(|d| {
let time_remaining = session.deadline.map(|d| {
if d > now {
Some((d - now).num_seconds().max(0) as u64)
(d - now).num_seconds().max(0) as u64
} else {
Some(0)
0
}
});
self.set_session_state(SessionState::Active {

View file

@ -75,52 +75,6 @@ pub fn toggle_mute() -> anyhow::Result<()> {
})
}
/// Increase volume by a step via shepherdd
pub fn volume_up(step: u8) -> anyhow::Result<()> {
let socket_path = get_socket_path();
let rt = Runtime::new()?;
rt.block_on(async {
let mut client = IpcClient::connect(&socket_path).await?;
let response = client.send(Command::VolumeUp { step }).await?;
match response.result {
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeSet) => Ok(()),
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeDenied { reason }) => {
Err(anyhow::anyhow!("Volume denied: {}", reason))
}
shepherd_api::ResponseResult::Err(e) => {
Err(anyhow::anyhow!("Error: {}", e.message))
}
_ => Err(anyhow::anyhow!("Unexpected response")),
}
})
}
/// Decrease volume by a step via shepherdd
pub fn volume_down(step: u8) -> anyhow::Result<()> {
let socket_path = get_socket_path();
let rt = Runtime::new()?;
rt.block_on(async {
let mut client = IpcClient::connect(&socket_path).await?;
let response = client.send(Command::VolumeDown { step }).await?;
match response.result {
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeSet) => Ok(()),
shepherd_api::ResponseResult::Ok(ResponsePayload::VolumeDenied { reason }) => {
Err(anyhow::anyhow!("Volume denied: {}", reason))
}
shepherd_api::ResponseResult::Err(e) => {
Err(anyhow::anyhow!("Error: {}", e.message))
}
_ => Err(anyhow::anyhow!("Unexpected response")),
}
})
}
/// Set volume to a specific percentage via shepherdd
pub fn set_volume(percent: u8) -> anyhow::Result<()> {
let socket_path = get_socket_path();

View file

@ -158,7 +158,7 @@ impl IpcServer {
let client_id_clone = client_id.clone();
// Spawn reader task
let reader_handle = tokio::spawn(async move {
let _reader_handle = tokio::spawn(async move {
let mut reader = BufReader::new(read_half);
let mut line = String::new();
@ -235,8 +235,8 @@ impl IpcServer {
clients.get(&client_id_writer).map(|h| h.subscribed).unwrap_or(false)
};
if is_subscribed {
if let Ok(json) = serde_json::to_string(&event) {
if is_subscribed
&& let Ok(json) = serde_json::to_string(&event) {
let mut msg = json;
msg.push('\n');
if let Err(e) = writer.write_all(msg.as_bytes()).await {
@ -247,7 +247,6 @@ impl IpcServer {
}
}
}
}
// Notify of disconnection
let _ = message_tx_writer.send(ServerMessage::ClientDisconnected {

View file

@ -2,15 +2,13 @@
use gtk4::glib;
use gtk4::prelude::*;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use tokio::runtime::Runtime;
use tokio::sync::mpsc;
use tracing::{debug, error, info};
use crate::client::{ClientCommand, CommandClient, ServiceClient};
use crate::client::{CommandClient, ServiceClient};
use crate::grid::LauncherGrid;
use crate::state::{LauncherState, SharedState};
@ -166,7 +164,7 @@ impl LauncherApp {
let runtime = Arc::new(Runtime::new().expect("Failed to create tokio runtime"));
// Create command channel
let (command_tx, command_rx) = mpsc::unbounded_channel();
let (_command_tx, command_rx) = mpsc::unbounded_channel();
// Create command client for sending commands
let command_client = Arc::new(CommandClient::new(&socket_path));

View file

@ -1,19 +1,20 @@
//! IPC client wrapper for the launcher UI
use anyhow::{Context, Result};
use shepherd_api::{Command, Event, ReasonCode, Response, ResponsePayload, ResponseResult};
use shepherd_api::{Command, ReasonCode, Response, ResponsePayload, ResponseResult};
use shepherd_ipc::IpcClient;
use shepherd_util::EntryId;
use std::path::Path;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::sleep;
use tracing::{debug, error, info, warn};
use tracing::{error, info, warn};
use crate::state::{LauncherState, SharedState};
/// Messages from UI to client task
#[derive(Debug)]
#[allow(dead_code)]
pub enum ClientCommand {
/// Request to launch an entry
Launch(EntryId),
@ -98,7 +99,7 @@ impl ServiceClient {
info!("Shutdown requested");
return Ok(());
}
ClientCommand::Launch(entry_id) => {
ClientCommand::Launch(_entry_id) => {
// We can't send commands after subscribing since client is consumed
// Need to reconnect for commands
warn!("Launch command received but cannot send after subscribe");
@ -222,6 +223,7 @@ impl CommandClient {
}).await.map_err(Into::into)
}
#[allow(dead_code)]
pub async fn stop_current(&self) -> Result<Response> {
let mut client = IpcClient::connect(&self.socket_path).await?;
client.send(Command::StopCurrent {
@ -234,6 +236,7 @@ impl CommandClient {
client.send(Command::GetState).await.map_err(Into::into)
}
#[allow(dead_code)]
pub async fn list_entries(&self) -> Result<Response> {
let mut client = IpcClient::connect(&self.socket_path).await?;
client.send(Command::ListEntries { at_time: None }).await.map_err(Into::into)

View file

@ -13,10 +13,12 @@ use crate::tile::LauncherTile;
mod imp {
use super::*;
type LaunchCallback = Rc<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>;
pub struct LauncherGrid {
pub flow_box: gtk4::FlowBox,
pub tiles: RefCell<Vec<LauncherTile>>,
pub on_launch: Rc<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>,
pub on_launch: LaunchCallback,
}
impl Default for LauncherGrid {
@ -114,11 +116,10 @@ impl LauncherGrid {
// Connect click handler
let on_launch = imp.on_launch.clone();
tile.connect_clicked(move |tile| {
if let Some(entry_id) = tile.entry_id() {
if let Some(callback) = on_launch.borrow().as_ref() {
if let Some(entry_id) = tile.entry_id()
&& let Some(callback) = on_launch.borrow().as_ref() {
callback(entry_id);
}
}
});
imp.flow_box.insert(&tile, -1);

View file

@ -11,7 +11,6 @@ mod tile;
use anyhow::Result;
use clap::Parser;
use gtk4::prelude::*;
use std::path::PathBuf;
use tracing_subscriber::EnvFilter;

View file

@ -2,37 +2,36 @@
use shepherd_api::{ServiceStateSnapshot, EntryView, Event, EventPayload};
use shepherd_util::SessionId;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::watch;
/// Current state of the launcher UI
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub enum LauncherState {
/// Not connected to shepherdd
#[default]
Disconnected,
/// Connected, waiting for initial state
Connecting,
/// Connected, no session running - show grid
Idle { entries: Vec<EntryView> },
/// Launch requested, waiting for response
Launching { entry_id: String },
Launching {
#[allow(dead_code)]
entry_id: String
},
/// Session is running
SessionActive {
#[allow(dead_code)]
session_id: SessionId,
entry_label: String,
#[allow(dead_code)]
time_remaining: Option<Duration>,
},
/// Error state
Error { message: String },
}
impl Default for LauncherState {
fn default() -> Self {
Self::Disconnected
}
}
/// Shared state container
#[derive(Clone)]
pub struct SharedState {

View file

@ -1,6 +1,6 @@
//! SQLite-based store implementation
use chrono::{DateTime, Local, NaiveDate, TimeZone};
use chrono::{DateTime, Local, NaiveDate};
use rusqlite::{params, Connection, OptionalExtension};
use shepherd_util::EntryId;
use std::path::Path;
@ -8,7 +8,7 @@ use std::sync::Mutex;
use std::time::Duration;
use tracing::{debug, warn};
use crate::{AuditEvent, SessionSnapshot, StateSnapshot, Store, StoreError, StoreResult};
use crate::{AuditEvent, StateSnapshot, Store, StoreResult};
/// SQLite-based store
pub struct SqliteStore {

View file

@ -12,22 +12,21 @@
use anyhow::{Context, Result};
use clap::Parser;
use shepherd_api::{
Command, ServiceStateSnapshot, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus,
Command, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus,
Response, ResponsePayload, SessionEndReason, StopMode, VolumeInfo, VolumeRestrictions,
API_VERSION,
};
use shepherd_config::{load_config, Policy, VolumePolicy};
use shepherd_config::{load_config, VolumePolicy};
use shepherd_core::{CoreEngine, CoreEvent, LaunchDecision, StopDecision};
use shepherd_host_api::{HostAdapter, HostEvent, StopMode as HostStopMode, VolumeController};
use shepherd_host_linux::{LinuxHost, LinuxVolumeController};
use shepherd_ipc::{IpcServer, ServerMessage};
use shepherd_store::{AuditEvent, AuditEventType, SqliteStore, Store};
use shepherd_util::{ClientId, EntryId, MonotonicInstant, RateLimiter};
use shepherd_util::{ClientId, MonotonicInstant, RateLimiter};
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use tracing::{debug, error, info, warn, Level};
use tracing::{debug, error, info, warn};
use tracing_subscriber::EnvFilter;
/// shepherdd - Policy enforcement service for child-focused computing
@ -243,8 +242,8 @@ impl Service {
.and_then(|s| s.host_handle.clone())
};
if let Some(handle) = handle {
if let Err(e) = host
if let Some(handle) = handle
&& let Err(e) = host
.stop(
&handle,
HostStopMode::Graceful {
@ -256,7 +255,6 @@ impl Service {
warn!(error = %e, "Failed to stop session gracefully, forcing");
let _ = host.stop(&handle, HostStopMode::Force).await;
}
}
ipc.broadcast_event(Event::new(EventPayload::SessionExpiring {
session_id: session_id.clone(),
@ -336,13 +334,12 @@ impl Service {
info!(has_event = core_event.is_some(), "notify_session_exited result");
if let Some(event) = core_event {
if let CoreEvent::SessionEnded {
if let Some(CoreEvent::SessionEnded {
session_id,
entry_id,
reason,
duration,
} = event
}) = core_event
{
info!(
session_id = %session_id,
@ -367,7 +364,6 @@ impl Service {
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
}
}
}
HostEvent::WindowReady { handle } => {
debug!(session_id = %handle.session_id, "Window ready");
@ -443,6 +439,7 @@ impl Service {
}
}
#[allow(clippy::too_many_arguments)]
async fn handle_command(
engine: &Arc<Mutex<CoreEngine>>,
host: &Arc<LinuxHost>,
@ -530,13 +527,12 @@ impl Service {
Err(e) => {
// Notify session ended with error and broadcast to subscribers
let mut eng = engine.lock().await;
if let Some(event) = eng.notify_session_exited(Some(-1), now_mono, now) {
if let CoreEvent::SessionEnded {
if let Some(CoreEvent::SessionEnded {
session_id,
entry_id,
reason,
duration,
} = event
}) = eng.notify_session_exited(Some(-1), now_mono, now)
{
ipc.broadcast_event(Event::new(EventPayload::SessionEnded {
session_id,
@ -549,7 +545,6 @@ impl Service {
let state = eng.get_state();
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
}
}
Response::error(
request_id,
@ -629,14 +624,13 @@ impl Service {
Command::ReloadConfig => {
// Check permission
if let Some(info) = ipc.get_client_info(client_id).await {
if !info.role.can_reload_config() {
if let Some(info) = ipc.get_client_info(client_id).await
&& !info.role.can_reload_config() {
return Response::error(
request_id,
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
);
}
}
// TODO: Reload from original config path
Response::error(
@ -672,14 +666,13 @@ impl Service {
Command::ExtendCurrent { by } => {
// Check permission
if let Some(info) = ipc.get_client_info(client_id).await {
if !info.role.can_extend() {
if let Some(info) = ipc.get_client_info(client_id).await
&& !info.role.can_extend() {
return Response::error(
request_id,
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
);
}
}
let mut eng = engine.lock().await;
match eng.extend_current(by, now_mono, now) {
@ -694,7 +687,7 @@ impl Service {
}
Command::GetVolume => {
let restrictions = Self::get_current_volume_restrictions(&engine).await;
let restrictions = Self::get_current_volume_restrictions(engine).await;
match volume.get_status().await {
Ok(status) => {
@ -722,7 +715,7 @@ impl Service {
}
Command::SetVolume { percent } => {
let restrictions = Self::get_current_volume_restrictions(&engine).await;
let restrictions = Self::get_current_volume_restrictions(engine).await;
if !restrictions.allow_change {
return Response::success(
@ -755,80 +748,8 @@ impl Service {
}
}
Command::VolumeUp { step } => {
let restrictions = Self::get_current_volume_restrictions(&engine).await;
if !restrictions.allow_change {
return Response::success(
request_id,
ResponsePayload::VolumeDenied {
reason: "Volume changes are not allowed".into(),
},
);
}
// Get current volume and check if we'd exceed max
let current = volume.get_status().await.map(|s| s.percent).unwrap_or(0);
let target = current.saturating_add(step);
let clamped = restrictions.clamp_volume(target);
match volume.set_volume(clamped).await {
Ok(()) => {
if let Ok(status) = volume.get_status().await {
ipc.broadcast_event(Event::new(EventPayload::VolumeChanged {
percent: status.percent,
muted: status.muted,
}));
}
Response::success(request_id, ResponsePayload::VolumeSet)
}
Err(e) => Response::success(
request_id,
ResponsePayload::VolumeDenied {
reason: e.to_string(),
},
),
}
}
Command::VolumeDown { step } => {
let restrictions = Self::get_current_volume_restrictions(&engine).await;
if !restrictions.allow_change {
return Response::success(
request_id,
ResponsePayload::VolumeDenied {
reason: "Volume changes are not allowed".into(),
},
);
}
// Get current volume and check if we'd go below min
let current = volume.get_status().await.map(|s| s.percent).unwrap_or(0);
let target = current.saturating_sub(step);
let clamped = restrictions.clamp_volume(target);
match volume.set_volume(clamped).await {
Ok(()) => {
if let Ok(status) = volume.get_status().await {
ipc.broadcast_event(Event::new(EventPayload::VolumeChanged {
percent: status.percent,
muted: status.muted,
}));
}
Response::success(request_id, ResponsePayload::VolumeSet)
}
Err(e) => Response::success(
request_id,
ResponsePayload::VolumeDenied {
reason: e.to_string(),
},
),
}
}
Command::ToggleMute => {
let restrictions = Self::get_current_volume_restrictions(&engine).await;
let restrictions = Self::get_current_volume_restrictions(engine).await;
if !restrictions.allow_mute {
return Response::success(
@ -859,7 +780,7 @@ impl Service {
}
Command::SetMute { muted } => {
let restrictions = Self::get_current_volume_restrictions(&engine).await;
let restrictions = Self::get_current_volume_restrictions(engine).await;
if !restrictions.allow_mute {
return Response::success(
@ -900,13 +821,11 @@ impl Service {
let eng = engine.lock().await;
// Check if there's an active session with volume restrictions
if let Some(session) = eng.current_session() {
if let Some(entry) = eng.policy().get_entry(&session.plan.entry_id) {
if let Some(ref vol_policy) = entry.volume {
if let Some(session) = eng.current_session()
&& let Some(entry) = eng.policy().get_entry(&session.plan.entry_id)
&& let Some(ref vol_policy) = entry.volume {
return Self::convert_volume_policy(vol_policy);
}
}
}
// Fall back to global policy
Self::convert_volume_policy(&eng.policy().volume)