Lint: electric boogaloo
This commit is contained in:
parent
3fd49b2efd
commit
5e5e6f6806
22 changed files with 106 additions and 291 deletions
|
|
@ -52,12 +52,16 @@ Run the test suite:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo test
|
cargo test
|
||||||
|
# as run in CI:
|
||||||
|
cargo test --all-targets
|
||||||
```
|
```
|
||||||
|
|
||||||
Run lint checks:
|
Run lint checks:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo clippy
|
cargo clippy
|
||||||
|
# as run in CI:
|
||||||
|
cargo clippy --all-targets -- -D warnings
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -135,12 +135,6 @@ pub enum Command {
|
||||||
/// Set volume to a specific percentage
|
/// Set volume to a specific percentage
|
||||||
SetVolume { percent: u8 },
|
SetVolume { percent: u8 },
|
||||||
|
|
||||||
/// Increase volume by a step
|
|
||||||
VolumeUp { step: u8 },
|
|
||||||
|
|
||||||
/// Decrease volume by a step
|
|
||||||
VolumeDown { step: u8 },
|
|
||||||
|
|
||||||
/// Toggle mute state
|
/// Toggle mute state
|
||||||
ToggleMute,
|
ToggleMute,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,9 @@
|
||||||
//! Validated policy structures
|
//! 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 crate::validation::{parse_days, parse_time};
|
||||||
use shepherd_api::{EntryKind, WarningSeverity, WarningThreshold};
|
use shepherd_api::{EntryKind, WarningSeverity, WarningThreshold};
|
||||||
use shepherd_util::{DaysOfWeek, EntryId, TimeWindow, WallClock};
|
use shepherd_util::{DaysOfWeek, EntryId, TimeWindow, WallClock};
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -113,8 +113,8 @@ fn validate_entry(entry: &RawEntry, config: &RawConfig) -> Vec<ValidationError>
|
||||||
.or(config.service.default_max_run_seconds);
|
.or(config.service.default_max_run_seconds);
|
||||||
|
|
||||||
// Only validate warnings if max_run is Some and not 0 (unlimited)
|
// Only validate warnings if max_run is Some and not 0 (unlimited)
|
||||||
if let (Some(warnings), Some(max_run)) = (&entry.warnings, max_run) {
|
if let (Some(warnings), Some(max_run)) = (&entry.warnings, max_run)
|
||||||
if max_run > 0 {
|
&& max_run > 0 {
|
||||||
for warning in warnings {
|
for warning in warnings {
|
||||||
if warning.seconds_before >= max_run {
|
if warning.seconds_before >= max_run {
|
||||||
errors.push(ValidationError::WarningExceedsMaxRun {
|
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)
|
// Note: warnings are ignored for unlimited entries (max_run = 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
use chrono::{DateTime, Local};
|
use chrono::{DateTime, Local};
|
||||||
use shepherd_api::{
|
use shepherd_api::{
|
||||||
ServiceStateSnapshot, EntryKindTag, EntryView, ReasonCode, SessionEndReason,
|
ServiceStateSnapshot, EntryView, ReasonCode, SessionEndReason,
|
||||||
WarningSeverity, API_VERSION,
|
WarningSeverity, API_VERSION,
|
||||||
};
|
};
|
||||||
use shepherd_config::{Entry, Policy};
|
use shepherd_config::{Entry, Policy};
|
||||||
|
|
@ -11,7 +11,7 @@ use shepherd_store::{AuditEvent, AuditEventType, Store};
|
||||||
use shepherd_util::{EntryId, MonotonicInstant, SessionId};
|
use shepherd_util::{EntryId, MonotonicInstant, SessionId};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::{ActiveSession, CoreEvent, SessionPlan, StopResult};
|
use crate::{ActiveSession, CoreEvent, SessionPlan, StopResult};
|
||||||
|
|
||||||
|
|
@ -128,23 +128,21 @@ impl CoreEngine {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check cooldown
|
// Check cooldown
|
||||||
if let Ok(Some(until)) = self.store.get_cooldown_until(&entry.id) {
|
if let Ok(Some(until)) = self.store.get_cooldown_until(&entry.id)
|
||||||
if until > now {
|
&& until > now {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
reasons.push(ReasonCode::CooldownActive { available_at: until });
|
reasons.push(ReasonCode::CooldownActive { available_at: until });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check daily quota
|
// Check daily quota
|
||||||
if let Some(quota) = entry.limits.daily_quota {
|
if let Some(quota) = entry.limits.daily_quota {
|
||||||
let today = now.date_naive();
|
let today = now.date_naive();
|
||||||
if let Ok(used) = self.store.get_usage(&entry.id, today) {
|
if let Ok(used) = self.store.get_usage(&entry.id, today)
|
||||||
if used >= quota {
|
&& used >= quota {
|
||||||
enabled = false;
|
enabled = false;
|
||||||
reasons.push(ReasonCode::QuotaExhausted { used, quota });
|
reasons.push(ReasonCode::QuotaExhausted { used, quota });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Calculate max run if enabled (None when disabled, Some(None) flattened for unlimited)
|
// Calculate max run if enabled (None when disabled, Some(None) flattened for unlimited)
|
||||||
let max_run_if_started_now = if enabled {
|
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);
|
let _ = self.store.add_usage(&session.plan.entry_id, today, duration);
|
||||||
|
|
||||||
// Set cooldown if configured
|
// Set cooldown if configured
|
||||||
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id) {
|
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id)
|
||||||
if let Some(cooldown) = entry.limits.cooldown {
|
&& let Some(cooldown) = entry.limits.cooldown {
|
||||||
let until = now + chrono::Duration::from_std(cooldown).unwrap();
|
let until = now + chrono::Duration::from_std(cooldown).unwrap();
|
||||||
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
|
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Log to audit
|
// Log to audit
|
||||||
let _ = self.store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
|
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);
|
let _ = self.store.add_usage(&session.plan.entry_id, today, duration);
|
||||||
|
|
||||||
// Set cooldown if configured
|
// Set cooldown if configured
|
||||||
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id) {
|
if let Some(entry) = self.policy.get_entry(&session.plan.entry_id)
|
||||||
if let Some(cooldown) = entry.limits.cooldown {
|
&& let Some(cooldown) = entry.limits.cooldown {
|
||||||
let until = now + chrono::Duration::from_std(cooldown).unwrap();
|
let until = now + chrono::Duration::from_std(cooldown).unwrap();
|
||||||
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
|
let _ = self.store.set_cooldown_until(&session.plan.entry_id, until);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Log to audit
|
// Log to audit
|
||||||
let _ = self.store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
|
let _ = self.store.append_audit(AuditEvent::new(AuditEventType::SessionEnded {
|
||||||
|
|
@ -510,8 +506,8 @@ impl CoreEngine {
|
||||||
pub fn extend_current(
|
pub fn extend_current(
|
||||||
&mut self,
|
&mut self,
|
||||||
by: Duration,
|
by: Duration,
|
||||||
now_mono: MonotonicInstant,
|
_now_mono: MonotonicInstant,
|
||||||
now: DateTime<Local>,
|
_now: DateTime<Local>,
|
||||||
) -> Option<DateTime<Local>> {
|
) -> Option<DateTime<Local>> {
|
||||||
let session = self.current_session.as_mut()?;
|
let session = self.current_session.as_mut()?;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,7 @@ pub struct VolumeStatus {
|
||||||
impl VolumeStatus {
|
impl VolumeStatus {
|
||||||
/// Get an icon name for the current volume status
|
/// Get an icon name for the current volume status
|
||||||
pub fn icon_name(&self) -> &'static str {
|
pub fn icon_name(&self) -> &'static str {
|
||||||
if self.muted {
|
if self.muted || self.percent == 0 {
|
||||||
"audio-volume-muted-symbolic"
|
|
||||||
} else if self.percent == 0 {
|
|
||||||
"audio-volume-muted-symbolic"
|
"audio-volume-muted-symbolic"
|
||||||
} else if self.percent < 33 {
|
} else if self.percent < 33 {
|
||||||
"audio-volume-low-symbolic"
|
"audio-volume-low-symbolic"
|
||||||
|
|
|
||||||
|
|
@ -126,11 +126,10 @@ impl HostAdapter for LinuxHost {
|
||||||
// followed by any additional args.
|
// followed by any additional args.
|
||||||
let mut argv = vec!["snap".to_string(), "run".to_string(), snap_name.clone()];
|
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 a custom command is specified (different from snap_name), add it
|
||||||
if let Some(cmd) = command {
|
if let Some(cmd) = command
|
||||||
if cmd != snap_name {
|
&& cmd != snap_name {
|
||||||
argv.push(cmd.clone());
|
argv.push(cmd.clone());
|
||||||
}
|
}
|
||||||
}
|
|
||||||
argv.extend(args.clone());
|
argv.extend(args.clone());
|
||||||
(argv, env.clone(), None, Some(snap_name.clone()))
|
(argv, env.clone(), None, Some(snap_name.clone()))
|
||||||
}
|
}
|
||||||
|
|
@ -336,8 +335,8 @@ mod tests {
|
||||||
|
|
||||||
// Process should have exited
|
// Process should have exited
|
||||||
match handle.payload() {
|
match handle.payload() {
|
||||||
HostHandlePayload::Linux { pid, .. } => {
|
HostHandlePayload::Linux { pid: _, .. } => {
|
||||||
let procs = host.processes.lock().unwrap();
|
let _procs = host.processes.lock().unwrap();
|
||||||
// Process may or may not still be tracked depending on monitor timing
|
// Process may or may not still be tracked depending on monitor timing
|
||||||
}
|
}
|
||||||
_ => panic!("Expected Linux handle"),
|
_ => panic!("Expected Linux handle"),
|
||||||
|
|
|
||||||
|
|
@ -264,7 +264,7 @@ impl ManagedProcess {
|
||||||
unsafe {
|
unsafe {
|
||||||
cmd.pre_exec(|| {
|
cmd.pre_exec(|| {
|
||||||
nix::unistd::setsid().map_err(|e| {
|
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(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
@ -304,9 +304,9 @@ impl ManagedProcess {
|
||||||
if let Some(paren_end) = stat.rfind(')') {
|
if let Some(paren_end) = stat.rfind(')') {
|
||||||
let after_comm = &stat[paren_end + 2..];
|
let after_comm = &stat[paren_end + 2..];
|
||||||
let fields: Vec<&str> = after_comm.split_whitespace().collect();
|
let fields: Vec<&str> = after_comm.split_whitespace().collect();
|
||||||
if fields.len() >= 2 {
|
if fields.len() >= 2
|
||||||
if let Ok(ppid) = fields[1].parse::<i32>() {
|
&& let Ok(ppid) = fields[1].parse::<i32>()
|
||||||
if ppid == parent_pid {
|
&& ppid == parent_pid {
|
||||||
descendants.push(pid);
|
descendants.push(pid);
|
||||||
to_check.push(pid);
|
to_check.push(pid);
|
||||||
}
|
}
|
||||||
|
|
@ -316,8 +316,6 @@ impl ManagedProcess {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
descendants
|
descendants
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,12 +147,11 @@ impl LinuxVolumeController {
|
||||||
debug!("pactl get-sink-volume output: {}", stdout.trim());
|
debug!("pactl get-sink-volume output: {}", stdout.trim());
|
||||||
|
|
||||||
// Output: "Volume: front-left: 65536 / 100% / -0.00 dB, front-right: ..."
|
// Output: "Volume: front-left: 65536 / 100% / -0.00 dB, front-right: ..."
|
||||||
if let Some(percent_str) = stdout.split('/').nth(1) {
|
if let Some(percent_str) = stdout.split('/').nth(1)
|
||||||
if let Ok(percent) = percent_str.trim().trim_end_matches('%').parse::<u8>() {
|
&& let Ok(percent) = percent_str.trim().trim_end_matches('%').parse::<u8>() {
|
||||||
status.percent = percent;
|
status.percent = percent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check mute status
|
// Check mute status
|
||||||
if let Ok(output) = Command::new("pactl")
|
if let Ok(output) = Command::new("pactl")
|
||||||
|
|
@ -184,13 +183,11 @@ impl LinuxVolumeController {
|
||||||
for line in stdout.lines() {
|
for line in stdout.lines() {
|
||||||
if line.contains("Playback") && line.contains('%') {
|
if line.contains("Playback") && line.contains('%') {
|
||||||
// Extract percentage: [100%]
|
// Extract percentage: [100%]
|
||||||
if let Some(start) = line.find('[') {
|
if let Some(start) = line.find('[')
|
||||||
if let Some(end) = line[start..].find('%') {
|
&& let Some(end) = line[start..].find('%')
|
||||||
if let Ok(percent) = line[start + 1..start + end].parse::<u8>() {
|
&& let Ok(percent) = line[start + 1..start + end].parse::<u8>() {
|
||||||
status.percent = percent;
|
status.percent = percent;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check mute status: [on] or [off]
|
// Check mute status: [on] or [off]
|
||||||
status.muted = line.contains("[off]");
|
status.muted = line.contains("[off]");
|
||||||
break;
|
break;
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,6 @@ impl HudApp {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Start periodic updates for battery/volume
|
|
||||||
start_metrics_updates(state.clone());
|
|
||||||
|
|
||||||
// Subscribe to state changes
|
// Subscribe to state changes
|
||||||
let window_clone = window.clone();
|
let window_clone = window.clone();
|
||||||
let state_clone = state.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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -34,21 +34,19 @@ impl BatteryStatus {
|
||||||
let name_str = name.to_string_lossy();
|
let name_str = name.to_string_lossy();
|
||||||
|
|
||||||
// Check for battery
|
// Check for battery
|
||||||
if name_str.starts_with("BAT") {
|
if name_str.starts_with("BAT")
|
||||||
if let Some((percent, charging)) = read_battery_info(&path) {
|
&& let Some((percent, charging)) = read_battery_info(&path) {
|
||||||
status.percent = Some(percent);
|
status.percent = Some(percent);
|
||||||
status.charging = charging;
|
status.charging = charging;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Check for AC adapter
|
// Check for AC adapter
|
||||||
if name_str.starts_with("AC") || name_str.contains("ADP") {
|
if (name_str.starts_with("AC") || name_str.contains("ADP"))
|
||||||
if let Some(online) = read_ac_status(&path) {
|
&& let Some(online) = read_ac_status(&path) {
|
||||||
status.ac_connected = online;
|
status.ac_connected = online;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
status
|
status
|
||||||
}
|
}
|
||||||
|
|
@ -70,6 +68,7 @@ impl BatteryStatus {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if battery is critically low
|
/// Check if battery is critically low
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn is_critical(&self) -> bool {
|
pub fn is_critical(&self) -> bool {
|
||||||
matches!(self.percent, Some(p) if p < 10 && !self.charging)
|
matches!(self.percent, Some(p) if p < 10 && !self.charging)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ mod volume;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use gtk4::prelude::*;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! The HUD subscribes to events from shepherdd and tracks session state.
|
//! The HUD subscribes to events from shepherdd and tracks session state.
|
||||||
|
|
||||||
use chrono::Local;
|
use shepherd_api::{Event, EventPayload, VolumeInfo, VolumeRestrictions, WarningSeverity};
|
||||||
use shepherd_api::{Event, EventPayload, SessionEndReason, VolumeInfo, VolumeRestrictions, WarningSeverity};
|
|
||||||
use shepherd_util::{EntryId, SessionId};
|
use shepherd_util::{EntryId, SessionId};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
@ -21,6 +20,7 @@ pub enum SessionState {
|
||||||
entry_name: String,
|
entry_name: String,
|
||||||
started_at: std::time::Instant,
|
started_at: std::time::Instant,
|
||||||
time_limit_secs: Option<u64>,
|
time_limit_secs: Option<u64>,
|
||||||
|
#[allow(dead_code)]
|
||||||
time_remaining_secs: Option<u64>,
|
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
|
/// Shared state for the HUD
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SharedState {
|
pub struct SharedState {
|
||||||
|
|
@ -84,10 +71,6 @@ pub struct SharedState {
|
||||||
session_tx: Arc<watch::Sender<SessionState>>,
|
session_tx: Arc<watch::Sender<SessionState>>,
|
||||||
/// Session state receiver
|
/// Session state receiver
|
||||||
session_rx: watch::Receiver<SessionState>,
|
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 info sender (updated via events, not polling)
|
||||||
volume_tx: Arc<watch::Sender<Option<VolumeInfo>>>,
|
volume_tx: Arc<watch::Sender<Option<VolumeInfo>>>,
|
||||||
/// Volume info receiver
|
/// Volume info receiver
|
||||||
|
|
@ -97,14 +80,11 @@ pub struct SharedState {
|
||||||
impl SharedState {
|
impl SharedState {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let (session_tx, session_rx) = watch::channel(SessionState::NoSession);
|
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);
|
let (volume_tx, volume_rx) = watch::channel(None);
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
session_tx: Arc::new(session_tx),
|
session_tx: Arc::new(session_tx),
|
||||||
session_rx,
|
session_rx,
|
||||||
metrics_tx: Arc::new(metrics_tx),
|
|
||||||
metrics_rx,
|
|
||||||
volume_tx: Arc::new(volume_tx),
|
volume_tx: Arc::new(volume_tx),
|
||||||
volume_rx,
|
volume_rx,
|
||||||
}
|
}
|
||||||
|
|
@ -116,25 +96,16 @@ impl SharedState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to session state changes
|
/// Subscribe to session state changes
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn subscribe_session(&self) -> watch::Receiver<SessionState> {
|
pub fn subscribe_session(&self) -> watch::Receiver<SessionState> {
|
||||||
self.session_rx.clone()
|
self.session_rx.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Subscribe to metrics changes
|
|
||||||
pub fn subscribe_metrics(&self) -> watch::Receiver<SystemMetrics> {
|
|
||||||
self.metrics_rx.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update session state
|
/// Update session state
|
||||||
pub fn set_session_state(&self, state: SessionState) {
|
pub fn set_session_state(&self, state: SessionState) {
|
||||||
let _ = self.session_tx.send(state);
|
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)
|
/// Get current volume info (cached from events)
|
||||||
pub fn volume_info(&self) -> Option<VolumeInfo> {
|
pub fn volume_info(&self) -> Option<VolumeInfo> {
|
||||||
self.volume_rx.borrow().clone()
|
self.volume_rx.borrow().clone()
|
||||||
|
|
@ -165,6 +136,7 @@ impl SharedState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update time remaining for current session
|
/// Update time remaining for current session
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn update_time_remaining(&self, remaining_secs: u64) {
|
pub fn update_time_remaining(&self, remaining_secs: u64) {
|
||||||
self.session_tx.send_modify(|state| {
|
self.session_tx.send_modify(|state| {
|
||||||
if let SessionState::Active {
|
if let SessionState::Active {
|
||||||
|
|
@ -246,8 +218,7 @@ impl SharedState {
|
||||||
entry_name,
|
entry_name,
|
||||||
..
|
..
|
||||||
} = state
|
} = state
|
||||||
{
|
&& sid == session_id {
|
||||||
if sid == session_id {
|
|
||||||
*state = SessionState::Warning {
|
*state = SessionState::Warning {
|
||||||
session_id: session_id.clone(),
|
session_id: session_id.clone(),
|
||||||
entry_id: entry_id.clone(),
|
entry_id: entry_id.clone(),
|
||||||
|
|
@ -258,7 +229,6 @@ impl SharedState {
|
||||||
severity: *severity,
|
severity: *severity,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -275,11 +245,11 @@ impl SharedState {
|
||||||
if let Some(session) = &snapshot.current_session {
|
if let Some(session) = &snapshot.current_session {
|
||||||
let now = shepherd_util::now();
|
let now = shepherd_util::now();
|
||||||
// For unlimited sessions (deadline=None), time_remaining is None
|
// 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 {
|
if d > now {
|
||||||
Some((d - now).num_seconds().max(0) as u64)
|
(d - now).num_seconds().max(0) as u64
|
||||||
} else {
|
} else {
|
||||||
Some(0)
|
0
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
self.set_session_state(SessionState::Active {
|
self.set_session_state(SessionState::Active {
|
||||||
|
|
|
||||||
|
|
@ -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
|
/// Set volume to a specific percentage via shepherdd
|
||||||
pub fn set_volume(percent: u8) -> anyhow::Result<()> {
|
pub fn set_volume(percent: u8) -> anyhow::Result<()> {
|
||||||
let socket_path = get_socket_path();
|
let socket_path = get_socket_path();
|
||||||
|
|
|
||||||
|
|
@ -158,7 +158,7 @@ impl IpcServer {
|
||||||
let client_id_clone = client_id.clone();
|
let client_id_clone = client_id.clone();
|
||||||
|
|
||||||
// Spawn reader task
|
// 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 reader = BufReader::new(read_half);
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
|
|
||||||
|
|
@ -235,8 +235,8 @@ impl IpcServer {
|
||||||
clients.get(&client_id_writer).map(|h| h.subscribed).unwrap_or(false)
|
clients.get(&client_id_writer).map(|h| h.subscribed).unwrap_or(false)
|
||||||
};
|
};
|
||||||
|
|
||||||
if is_subscribed {
|
if is_subscribed
|
||||||
if let Ok(json) = serde_json::to_string(&event) {
|
&& let Ok(json) = serde_json::to_string(&event) {
|
||||||
let mut msg = json;
|
let mut msg = json;
|
||||||
msg.push('\n');
|
msg.push('\n');
|
||||||
if let Err(e) = writer.write_all(msg.as_bytes()).await {
|
if let Err(e) = writer.write_all(msg.as_bytes()).await {
|
||||||
|
|
@ -247,7 +247,6 @@ impl IpcServer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Notify of disconnection
|
// Notify of disconnection
|
||||||
let _ = message_tx_writer.send(ServerMessage::ClientDisconnected {
|
let _ = message_tx_writer.send(ServerMessage::ClientDisconnected {
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,13 @@
|
||||||
|
|
||||||
use gtk4::glib;
|
use gtk4::glib;
|
||||||
use gtk4::prelude::*;
|
use gtk4::prelude::*;
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::rc::Rc;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::runtime::Runtime;
|
use tokio::runtime::Runtime;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tracing::{debug, error, info};
|
use tracing::{debug, error, info};
|
||||||
|
|
||||||
use crate::client::{ClientCommand, CommandClient, ServiceClient};
|
use crate::client::{CommandClient, ServiceClient};
|
||||||
use crate::grid::LauncherGrid;
|
use crate::grid::LauncherGrid;
|
||||||
use crate::state::{LauncherState, SharedState};
|
use crate::state::{LauncherState, SharedState};
|
||||||
|
|
||||||
|
|
@ -166,7 +164,7 @@ impl LauncherApp {
|
||||||
let runtime = Arc::new(Runtime::new().expect("Failed to create tokio runtime"));
|
let runtime = Arc::new(Runtime::new().expect("Failed to create tokio runtime"));
|
||||||
|
|
||||||
// Create command channel
|
// 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
|
// Create command client for sending commands
|
||||||
let command_client = Arc::new(CommandClient::new(&socket_path));
|
let command_client = Arc::new(CommandClient::new(&socket_path));
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,20 @@
|
||||||
//! IPC client wrapper for the launcher UI
|
//! IPC client wrapper for the launcher UI
|
||||||
|
|
||||||
use anyhow::{Context, Result};
|
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_ipc::IpcClient;
|
||||||
use shepherd_util::EntryId;
|
use shepherd_util::EntryId;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio::time::sleep;
|
use tokio::time::sleep;
|
||||||
use tracing::{debug, error, info, warn};
|
use tracing::{error, info, warn};
|
||||||
|
|
||||||
use crate::state::{LauncherState, SharedState};
|
use crate::state::{LauncherState, SharedState};
|
||||||
|
|
||||||
/// Messages from UI to client task
|
/// Messages from UI to client task
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
#[allow(dead_code)]
|
||||||
pub enum ClientCommand {
|
pub enum ClientCommand {
|
||||||
/// Request to launch an entry
|
/// Request to launch an entry
|
||||||
Launch(EntryId),
|
Launch(EntryId),
|
||||||
|
|
@ -98,7 +99,7 @@ impl ServiceClient {
|
||||||
info!("Shutdown requested");
|
info!("Shutdown requested");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
ClientCommand::Launch(entry_id) => {
|
ClientCommand::Launch(_entry_id) => {
|
||||||
// We can't send commands after subscribing since client is consumed
|
// We can't send commands after subscribing since client is consumed
|
||||||
// Need to reconnect for commands
|
// Need to reconnect for commands
|
||||||
warn!("Launch command received but cannot send after subscribe");
|
warn!("Launch command received but cannot send after subscribe");
|
||||||
|
|
@ -222,6 +223,7 @@ impl CommandClient {
|
||||||
}).await.map_err(Into::into)
|
}).await.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn stop_current(&self) -> Result<Response> {
|
pub async fn stop_current(&self) -> Result<Response> {
|
||||||
let mut client = IpcClient::connect(&self.socket_path).await?;
|
let mut client = IpcClient::connect(&self.socket_path).await?;
|
||||||
client.send(Command::StopCurrent {
|
client.send(Command::StopCurrent {
|
||||||
|
|
@ -234,6 +236,7 @@ impl CommandClient {
|
||||||
client.send(Command::GetState).await.map_err(Into::into)
|
client.send(Command::GetState).await.map_err(Into::into)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn list_entries(&self) -> Result<Response> {
|
pub async fn list_entries(&self) -> Result<Response> {
|
||||||
let mut client = IpcClient::connect(&self.socket_path).await?;
|
let mut client = IpcClient::connect(&self.socket_path).await?;
|
||||||
client.send(Command::ListEntries { at_time: None }).await.map_err(Into::into)
|
client.send(Command::ListEntries { at_time: None }).await.map_err(Into::into)
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,12 @@ use crate::tile::LauncherTile;
|
||||||
mod imp {
|
mod imp {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
type LaunchCallback = Rc<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>;
|
||||||
|
|
||||||
pub struct LauncherGrid {
|
pub struct LauncherGrid {
|
||||||
pub flow_box: gtk4::FlowBox,
|
pub flow_box: gtk4::FlowBox,
|
||||||
pub tiles: RefCell<Vec<LauncherTile>>,
|
pub tiles: RefCell<Vec<LauncherTile>>,
|
||||||
pub on_launch: Rc<RefCell<Option<Box<dyn Fn(EntryId) + 'static>>>>,
|
pub on_launch: LaunchCallback,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LauncherGrid {
|
impl Default for LauncherGrid {
|
||||||
|
|
@ -114,11 +116,10 @@ impl LauncherGrid {
|
||||||
// Connect click handler
|
// Connect click handler
|
||||||
let on_launch = imp.on_launch.clone();
|
let on_launch = imp.on_launch.clone();
|
||||||
tile.connect_clicked(move |tile| {
|
tile.connect_clicked(move |tile| {
|
||||||
if let Some(entry_id) = tile.entry_id() {
|
if let Some(entry_id) = tile.entry_id()
|
||||||
if let Some(callback) = on_launch.borrow().as_ref() {
|
&& let Some(callback) = on_launch.borrow().as_ref() {
|
||||||
callback(entry_id);
|
callback(entry_id);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
imp.flow_box.insert(&tile, -1);
|
imp.flow_box.insert(&tile, -1);
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ mod tile;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use gtk4::prelude::*;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,37 +2,36 @@
|
||||||
|
|
||||||
use shepherd_api::{ServiceStateSnapshot, EntryView, Event, EventPayload};
|
use shepherd_api::{ServiceStateSnapshot, EntryView, Event, EventPayload};
|
||||||
use shepherd_util::SessionId;
|
use shepherd_util::SessionId;
|
||||||
use std::sync::Arc;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::watch;
|
use tokio::sync::watch;
|
||||||
|
|
||||||
/// Current state of the launcher UI
|
/// Current state of the launcher UI
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Default)]
|
||||||
pub enum LauncherState {
|
pub enum LauncherState {
|
||||||
/// Not connected to shepherdd
|
/// Not connected to shepherdd
|
||||||
|
#[default]
|
||||||
Disconnected,
|
Disconnected,
|
||||||
/// Connected, waiting for initial state
|
/// Connected, waiting for initial state
|
||||||
Connecting,
|
Connecting,
|
||||||
/// Connected, no session running - show grid
|
/// Connected, no session running - show grid
|
||||||
Idle { entries: Vec<EntryView> },
|
Idle { entries: Vec<EntryView> },
|
||||||
/// Launch requested, waiting for response
|
/// Launch requested, waiting for response
|
||||||
Launching { entry_id: String },
|
Launching {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
entry_id: String
|
||||||
|
},
|
||||||
/// Session is running
|
/// Session is running
|
||||||
SessionActive {
|
SessionActive {
|
||||||
|
#[allow(dead_code)]
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
entry_label: String,
|
entry_label: String,
|
||||||
|
#[allow(dead_code)]
|
||||||
time_remaining: Option<Duration>,
|
time_remaining: Option<Duration>,
|
||||||
},
|
},
|
||||||
/// Error state
|
/// Error state
|
||||||
Error { message: String },
|
Error { message: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for LauncherState {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::Disconnected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Shared state container
|
/// Shared state container
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SharedState {
|
pub struct SharedState {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! SQLite-based store implementation
|
//! SQLite-based store implementation
|
||||||
|
|
||||||
use chrono::{DateTime, Local, NaiveDate, TimeZone};
|
use chrono::{DateTime, Local, NaiveDate};
|
||||||
use rusqlite::{params, Connection, OptionalExtension};
|
use rusqlite::{params, Connection, OptionalExtension};
|
||||||
use shepherd_util::EntryId;
|
use shepherd_util::EntryId;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -8,7 +8,7 @@ use std::sync::Mutex;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{AuditEvent, SessionSnapshot, StateSnapshot, Store, StoreError, StoreResult};
|
use crate::{AuditEvent, StateSnapshot, Store, StoreResult};
|
||||||
|
|
||||||
/// SQLite-based store
|
/// SQLite-based store
|
||||||
pub struct SqliteStore {
|
pub struct SqliteStore {
|
||||||
|
|
|
||||||
|
|
@ -12,22 +12,21 @@
|
||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use shepherd_api::{
|
use shepherd_api::{
|
||||||
Command, ServiceStateSnapshot, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus,
|
Command, ErrorCode, ErrorInfo, Event, EventPayload, HealthStatus,
|
||||||
Response, ResponsePayload, SessionEndReason, StopMode, VolumeInfo, VolumeRestrictions,
|
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_core::{CoreEngine, CoreEvent, LaunchDecision, StopDecision};
|
||||||
use shepherd_host_api::{HostAdapter, HostEvent, StopMode as HostStopMode, VolumeController};
|
use shepherd_host_api::{HostAdapter, HostEvent, StopMode as HostStopMode, VolumeController};
|
||||||
use shepherd_host_linux::{LinuxHost, LinuxVolumeController};
|
use shepherd_host_linux::{LinuxHost, LinuxVolumeController};
|
||||||
use shepherd_ipc::{IpcServer, ServerMessage};
|
use shepherd_ipc::{IpcServer, ServerMessage};
|
||||||
use shepherd_store::{AuditEvent, AuditEventType, SqliteStore, Store};
|
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::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tracing::{debug, error, info, warn, Level};
|
use tracing::{debug, error, info, warn};
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_subscriber::EnvFilter;
|
||||||
|
|
||||||
/// shepherdd - Policy enforcement service for child-focused computing
|
/// shepherdd - Policy enforcement service for child-focused computing
|
||||||
|
|
@ -243,8 +242,8 @@ impl Service {
|
||||||
.and_then(|s| s.host_handle.clone())
|
.and_then(|s| s.host_handle.clone())
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(handle) = handle {
|
if let Some(handle) = handle
|
||||||
if let Err(e) = host
|
&& let Err(e) = host
|
||||||
.stop(
|
.stop(
|
||||||
&handle,
|
&handle,
|
||||||
HostStopMode::Graceful {
|
HostStopMode::Graceful {
|
||||||
|
|
@ -256,7 +255,6 @@ impl Service {
|
||||||
warn!(error = %e, "Failed to stop session gracefully, forcing");
|
warn!(error = %e, "Failed to stop session gracefully, forcing");
|
||||||
let _ = host.stop(&handle, HostStopMode::Force).await;
|
let _ = host.stop(&handle, HostStopMode::Force).await;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
ipc.broadcast_event(Event::new(EventPayload::SessionExpiring {
|
ipc.broadcast_event(Event::new(EventPayload::SessionExpiring {
|
||||||
session_id: session_id.clone(),
|
session_id: session_id.clone(),
|
||||||
|
|
@ -336,13 +334,12 @@ impl Service {
|
||||||
|
|
||||||
info!(has_event = core_event.is_some(), "notify_session_exited result");
|
info!(has_event = core_event.is_some(), "notify_session_exited result");
|
||||||
|
|
||||||
if let Some(event) = core_event {
|
if let Some(CoreEvent::SessionEnded {
|
||||||
if let CoreEvent::SessionEnded {
|
|
||||||
session_id,
|
session_id,
|
||||||
entry_id,
|
entry_id,
|
||||||
reason,
|
reason,
|
||||||
duration,
|
duration,
|
||||||
} = event
|
}) = core_event
|
||||||
{
|
{
|
||||||
info!(
|
info!(
|
||||||
session_id = %session_id,
|
session_id = %session_id,
|
||||||
|
|
@ -367,7 +364,6 @@ impl Service {
|
||||||
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
|
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
HostEvent::WindowReady { handle } => {
|
HostEvent::WindowReady { handle } => {
|
||||||
debug!(session_id = %handle.session_id, "Window ready");
|
debug!(session_id = %handle.session_id, "Window ready");
|
||||||
|
|
@ -443,6 +439,7 @@ impl Service {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
async fn handle_command(
|
async fn handle_command(
|
||||||
engine: &Arc<Mutex<CoreEngine>>,
|
engine: &Arc<Mutex<CoreEngine>>,
|
||||||
host: &Arc<LinuxHost>,
|
host: &Arc<LinuxHost>,
|
||||||
|
|
@ -530,13 +527,12 @@ impl Service {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
// Notify session ended with error and broadcast to subscribers
|
// Notify session ended with error and broadcast to subscribers
|
||||||
let mut eng = engine.lock().await;
|
let mut eng = engine.lock().await;
|
||||||
if let Some(event) = eng.notify_session_exited(Some(-1), now_mono, now) {
|
if let Some(CoreEvent::SessionEnded {
|
||||||
if let CoreEvent::SessionEnded {
|
|
||||||
session_id,
|
session_id,
|
||||||
entry_id,
|
entry_id,
|
||||||
reason,
|
reason,
|
||||||
duration,
|
duration,
|
||||||
} = event
|
}) = eng.notify_session_exited(Some(-1), now_mono, now)
|
||||||
{
|
{
|
||||||
ipc.broadcast_event(Event::new(EventPayload::SessionEnded {
|
ipc.broadcast_event(Event::new(EventPayload::SessionEnded {
|
||||||
session_id,
|
session_id,
|
||||||
|
|
@ -549,7 +545,6 @@ impl Service {
|
||||||
let state = eng.get_state();
|
let state = eng.get_state();
|
||||||
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
|
ipc.broadcast_event(Event::new(EventPayload::StateChanged(state)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Response::error(
|
Response::error(
|
||||||
request_id,
|
request_id,
|
||||||
|
|
@ -629,14 +624,13 @@ impl Service {
|
||||||
|
|
||||||
Command::ReloadConfig => {
|
Command::ReloadConfig => {
|
||||||
// Check permission
|
// Check permission
|
||||||
if let Some(info) = ipc.get_client_info(client_id).await {
|
if let Some(info) = ipc.get_client_info(client_id).await
|
||||||
if !info.role.can_reload_config() {
|
&& !info.role.can_reload_config() {
|
||||||
return Response::error(
|
return Response::error(
|
||||||
request_id,
|
request_id,
|
||||||
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
|
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Reload from original config path
|
// TODO: Reload from original config path
|
||||||
Response::error(
|
Response::error(
|
||||||
|
|
@ -672,14 +666,13 @@ impl Service {
|
||||||
|
|
||||||
Command::ExtendCurrent { by } => {
|
Command::ExtendCurrent { by } => {
|
||||||
// Check permission
|
// Check permission
|
||||||
if let Some(info) = ipc.get_client_info(client_id).await {
|
if let Some(info) = ipc.get_client_info(client_id).await
|
||||||
if !info.role.can_extend() {
|
&& !info.role.can_extend() {
|
||||||
return Response::error(
|
return Response::error(
|
||||||
request_id,
|
request_id,
|
||||||
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
|
ErrorInfo::new(ErrorCode::PermissionDenied, "Admin role required"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
let mut eng = engine.lock().await;
|
let mut eng = engine.lock().await;
|
||||||
match eng.extend_current(by, now_mono, now) {
|
match eng.extend_current(by, now_mono, now) {
|
||||||
|
|
@ -694,7 +687,7 @@ impl Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
Command::GetVolume => {
|
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 {
|
match volume.get_status().await {
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
|
|
@ -722,7 +715,7 @@ impl Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
Command::SetVolume { percent } => {
|
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 {
|
if !restrictions.allow_change {
|
||||||
return Response::success(
|
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 => {
|
Command::ToggleMute => {
|
||||||
let restrictions = Self::get_current_volume_restrictions(&engine).await;
|
let restrictions = Self::get_current_volume_restrictions(engine).await;
|
||||||
|
|
||||||
if !restrictions.allow_mute {
|
if !restrictions.allow_mute {
|
||||||
return Response::success(
|
return Response::success(
|
||||||
|
|
@ -859,7 +780,7 @@ impl Service {
|
||||||
}
|
}
|
||||||
|
|
||||||
Command::SetMute { muted } => {
|
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 {
|
if !restrictions.allow_mute {
|
||||||
return Response::success(
|
return Response::success(
|
||||||
|
|
@ -900,13 +821,11 @@ impl Service {
|
||||||
let eng = engine.lock().await;
|
let eng = engine.lock().await;
|
||||||
|
|
||||||
// Check if there's an active session with volume restrictions
|
// Check if there's an active session with volume restrictions
|
||||||
if let Some(session) = eng.current_session() {
|
if let Some(session) = eng.current_session()
|
||||||
if let Some(entry) = eng.policy().get_entry(&session.plan.entry_id) {
|
&& let Some(entry) = eng.policy().get_entry(&session.plan.entry_id)
|
||||||
if let Some(ref vol_policy) = entry.volume {
|
&& let Some(ref vol_policy) = entry.volume {
|
||||||
return Self::convert_volume_policy(vol_policy);
|
return Self::convert_volume_policy(vol_policy);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to global policy
|
// Fall back to global policy
|
||||||
Self::convert_volume_policy(&eng.policy().volume)
|
Self::convert_volume_policy(&eng.policy().volume)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue