diff --git a/apps/cli/src/context.rs b/apps/cli/src/context.rs index 71fa6a25b..98b3855e7 100644 --- a/apps/cli/src/context.rs +++ b/apps/cli/src/context.rs @@ -1,4 +1,5 @@ use sd_core::client::CoreClient; +use std::path::PathBuf; #[derive(Debug, Clone, Copy, clap::ValueEnum)] pub enum OutputFormat { @@ -9,10 +10,12 @@ pub enum OutputFormat { pub struct Context { pub core: CoreClient, pub format: OutputFormat, + pub data_dir: PathBuf, + pub socket_path: PathBuf, } impl Context { - pub fn new(core: CoreClient, format: OutputFormat) -> Self { - Self { core, format } + pub fn new(core: CoreClient, format: OutputFormat, data_dir: PathBuf, socket_path: PathBuf) -> Self { + Self { core, format, data_dir, socket_path } } } diff --git a/apps/cli/src/domains/file.rs b/apps/cli/src/domains/file.rs index 17664fae5..14a5bf932 100644 --- a/apps/cli/src/domains/file.rs +++ b/apps/cli/src/domains/file.rs @@ -1,7 +1,7 @@ use anyhow::Result; use clap::Subcommand; -use crate::context::{Context, OutputFormat}; +use crate::context::Context; #[derive(clap::Parser, Debug, Clone)] pub struct FileCopyArgs { @@ -31,11 +31,13 @@ pub struct FileCopyArgs { impl FileCopyArgs { pub fn to_input(&self) -> sd_core::ops::files::copy::input::FileCopyInput { + use sd_core::domain::addressing::{SdPath, SdPathBatch}; use sd_core::ops::files::copy::input::{CopyMethod, FileCopyInput}; + let sources = self.sources.iter().map(|p| SdPath::local(p.clone())).collect::>(); + let destination = SdPath::local(self.destination.clone()); FileCopyInput { - library_id: None, - sources: self.sources.clone(), - destination: self.destination.clone(), + sources: SdPathBatch { paths: sources }, + destination, overwrite: self.overwrite, verify_checksum: self.verify_checksum, preserve_timestamps: self.preserve_timestamps, diff --git a/apps/cli/src/domains/library.rs b/apps/cli/src/domains/library.rs index f4fcffa2b..f66e355c1 100644 --- a/apps/cli/src/domains/library.rs +++ b/apps/cli/src/domains/library.rs @@ -2,16 +2,38 @@ use anyhow::Result; use clap::Subcommand; use crate::context::{Context, OutputFormat}; -use crate::util::output::{print_human_line, print_json}; +use crate::util::output::print_json; +use sd_core::ops::libraries::create; +use sd_core::ops::libraries::session::set_current; #[derive(Subcommand, Debug)] pub enum LibraryCmd { + /// Create a new library + Create { name: String }, + /// Switch to a different library + Switch { id: uuid::Uuid }, /// List libraries List, } pub async fn run(ctx: &Context, cmd: LibraryCmd) -> Result<()> { match cmd { + LibraryCmd::Create { name } => { + let input = create::LibraryCreateInput::new(name); + let bytes = ctx.core.action(&input).await?; + let out: create::LibraryCreateOutput = bincode::deserialize(&bytes)?; + println!("Created library {} with ID {} at {}", out.name, out.library_id, out.path.display()); + } + LibraryCmd::Switch { id } => { + let input = set_current::SetCurrentLibraryInput { library_id: id }; + let bytes = ctx.core.action(&input).await?; + let out: set_current::SetCurrentLibraryOutput = bincode::deserialize(&bytes)?; + if out.success { + println!("Switched to library {}", id); + } else { + println!("Failed to switch to library {}", id); + } + } LibraryCmd::List => { let libs: Vec = ctx .core @@ -20,10 +42,10 @@ pub async fn run(ctx: &Context, cmd: LibraryCmd) -> Result<()> { match ctx.format { OutputFormat::Human => { if libs.is_empty() { - print_human_line("No libraries found"); + println!("No libraries found"); } for l in libs { - print_human_line(&format!("- {} {}", l.id, l.path.display())); + println!("- {} {}", l.id, l.path.display()); } } OutputFormat::Json => print_json(&libs), diff --git a/apps/cli/src/domains/location.rs b/apps/cli/src/domains/location.rs index cd48bd881..427ba0226 100644 --- a/apps/cli/src/domains/location.rs +++ b/apps/cli/src/domains/location.rs @@ -16,8 +16,15 @@ pub enum LocationCmd { pub async fn run(ctx: &Context, cmd: LocationCmd) -> Result<()> { match cmd { LocationCmd::Add { path, name, mode } => { - let out: sd_core::ops::locations::add::output::LocationAddOutput = ctx.core.action(&sd_core::ops::locations::add::action::LocationAddInput { path, name, mode: sd_core::ops::indexing::job::IndexMode::from(mode) }).await?; - println!("Added location {} -> {}", out.id, out.path.display()); + let libs: Vec = ctx + .core + .query(&sd_core::ops::libraries::list::query::ListLibrariesQuery::basic()) + .await?; + let library_id = if libs.len() == 1 { libs[0].id } else { anyhow::bail!("Specify --library to add locations when multiple libraries exist") }; + + let result_bytes = ctx.core.action(&sd_core::ops::locations::add::action::LocationAddInput { library_id, path, name, mode: sd_core::ops::indexing::job::IndexMode::from(mode) }).await?; + let out: sd_core::ops::locations::add::output::LocationAddOutput = bincode::deserialize(&result_bytes)?; + println!("Added location {} -> {}", out.location_id, out.path.display()); } LocationCmd::List => { let libs: Vec = ctx @@ -29,11 +36,19 @@ pub async fn run(ctx: &Context, cmd: LocationCmd) -> Result<()> { for loc in out.locations { println!("- {} {}", loc.id, loc.path.display()); } } LocationCmd::Remove { location_id } => { - let _out: sd_core::ops::locations::remove::output::LocationRemoveOutput = ctx.core.action(&sd_core::ops::locations::remove::action::LocationRemoveInput { location_id }).await?; + let libs: Vec = ctx + .core + .query(&sd_core::ops::libraries::list::query::ListLibrariesQuery::basic()) + .await?; + let library_id = if libs.len() == 1 { libs[0].id } else { anyhow::bail!("Specify --library to remove locations when multiple libraries exist") }; + + let result_bytes = ctx.core.action(&sd_core::ops::locations::remove::action::LocationRemoveInput { library_id, location_id }).await?; + let _out: sd_core::ops::locations::remove::output::LocationRemoveOutput = bincode::deserialize(&result_bytes)?; println!("Removed location {}", location_id); } - LocationCmd::Rescan { location_id, force: _ } => { - let _out: sd_core::ops::locations::rescan::output::LocationRescanOutput = ctx.core.action(&sd_core::ops::locations::rescan::action::LocationRescanInput { location_id }).await?; + LocationCmd::Rescan { location_id, force } => { + let result_bytes = ctx.core.action(&sd_core::ops::locations::rescan::action::LocationRescanInput { location_id, full_rescan: force }).await?; + let _out: sd_core::ops::locations::rescan::output::LocationRescanOutput = bincode::deserialize(&result_bytes)?; println!("Rescan requested for {}", location_id); } } diff --git a/apps/cli/src/domains/network.rs b/apps/cli/src/domains/network.rs index 5f7a2e786..8123c81d9 100644 --- a/apps/cli/src/domains/network.rs +++ b/apps/cli/src/domains/network.rs @@ -44,10 +44,7 @@ pub enum NetworkCmd { Status, /// List devices Devices(NetworkDevicesArgs), - /// Start networking - Start, - /// Stop networking - Stop, + /// Pairing commands #[command(subcommand)] Pair(PairCmd), @@ -86,17 +83,11 @@ pub async fn run(ctx: &Context, cmd: NetworkCmd) -> Result<()> { OutputFormat::Json => print_json(&devices), } } - NetworkCmd::Start => { - let out: NetworkStartOutput = ctx.core.action(&NetworkStartInput {}).await?; - println!("Networking {}", if out.started { "started" } else { "already running" }); - } - NetworkCmd::Stop => { - let out: NetworkStopOutput = ctx.core.action(&NetworkStopInput {}).await?; - println!("Networking {}", if out.stopped { "stopped" } else { "not running" }); - } + NetworkCmd::Pair(pc) => match pc { PairCmd::Generate { auto_accept } => { - let out: PairGenerateOutput = ctx.core.action(&PairGenerateInput { auto_accept }).await?; + let bytes = ctx.core.action(&PairGenerateInput { auto_accept }).await?; + let out: PairGenerateOutput = bincode::deserialize(&bytes)?; match ctx.format { OutputFormat::Human => { println!("Pairing code: {}", out.code); @@ -107,7 +98,8 @@ pub async fn run(ctx: &Context, cmd: NetworkCmd) -> Result<()> { } } PairCmd::Join { code } => { - let out: PairJoinOutput = ctx.core.action(&PairJoinInput { code }).await?; + let bytes = ctx.core.action(&PairJoinInput { code }).await?; + let out: PairJoinOutput = bincode::deserialize(&bytes)?; match ctx.format { OutputFormat::Human => println!("Paired with {} ({})", out.device_name, out.paired_device_id), OutputFormat::Json => print_json(&out), @@ -124,18 +116,21 @@ pub async fn run(ctx: &Context, cmd: NetworkCmd) -> Result<()> { } } PairCmd::Cancel { session_id } => { - let out: PairCancelOutput = ctx.core.action(&PairCancelInput { session_id }).await?; + let bytes = ctx.core.action(&PairCancelInput { session_id }).await?; + let out: PairCancelOutput = bincode::deserialize(&bytes)?; println!("Cancelled: {}", out.cancelled); } }, NetworkCmd::Revoke { device_id } => { - let out: DeviceRevokeOutput = ctx.core.action(&DeviceRevokeInput { device_id }).await?; + let bytes = ctx.core.action(&DeviceRevokeInput { device_id }).await?; + let out: DeviceRevokeOutput = bincode::deserialize(&bytes)?; println!("Revoked: {}", out.revoked); } NetworkCmd::Spacedrop(args) => { use sd_core::domain::addressing::SdPath; let paths = args.paths.iter().map(|s| SdPath::from_uri(s).unwrap_or_else(|_| SdPath::local(s))).collect::>(); - let out: SpacedropSendOutput = ctx.core.action(&SpacedropSendInput { device_id: args.device_id, paths, sender: args.sender }).await?; + let bytes = ctx.core.action(&SpacedropSendInput { device_id: args.device_id, paths, sender: args.sender }).await?; + let out: SpacedropSendOutput = bincode::deserialize(&bytes)?; match ctx.format { OutputFormat::Human => { if let Some(j) = out.job_id { println!("Transfer job: {}", j); } diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs index 93d7c9874..7f8fcad86 100644 --- a/apps/cli/src/main.rs +++ b/apps/cli/src/main.rs @@ -39,6 +39,14 @@ struct Cli { #[derive(Subcommand, Debug)] enum Commands { + /// Start the Spacedrive daemon + Start { + /// Automatically start networking + #[arg(long)] + enable_networking: bool, + }, + /// Stop the Spacedrive daemon + Stop, /// Core info Status, /// Libraries operations @@ -66,16 +74,36 @@ enum Commands { async fn main() -> Result<()> { let cli = Cli::parse(); let data_dir = cli.data_dir.unwrap_or(sd_core::config::default_data_dir()?); - let socket = if let Some(inst) = cli.instance { + let instance = cli.instance; + let socket_path = if let Some(inst) = &instance { data_dir.join("daemon").join(format!("daemon-{}.sock", inst)) } else { data_dir.join("daemon/daemon.sock") }; - let core = CoreClient::new(socket); - - let ctx = Context::new(core, cli.format); match cli.command { + Commands::Start { enable_networking } => { + println!("Starting daemon..."); + sd_core::infra::daemon::bootstrap::start_default_server(socket_path, data_dir, enable_networking).await.map_err(|e| anyhow::anyhow!(e.to_string()))?; + } + Commands::Stop => { + println!("Stopping daemon..."); + let core = CoreClient::new(socket_path.clone()); + let _ = core.send_raw_request(&sd_core::infra::daemon::types::DaemonRequest::Shutdown).await.map_err(|e| anyhow::anyhow!(e.to_string()))?; + println!("Daemon stopped."); + } + _ => { + run_client_command(cli.command, cli.format, data_dir, socket_path).await?; + } + } + + Ok(()) +} + +async fn run_client_command(command: Commands, format: OutputFormat, data_dir: std::path::PathBuf, socket_path: std::path::PathBuf) -> Result<()> { + let core = CoreClient::new(socket_path.clone()); + let ctx = Context::new(core, format, data_dir, socket_path); + match command { Commands::Status => { let status: sd_core::ops::core::status::output::CoreStatus = ctx .core @@ -95,7 +123,7 @@ async fn main() -> Result<()> { Commands::Location(cmd) => location::run(&ctx, cmd).await?, Commands::Network(cmd) => network::run(&ctx, cmd).await?, Commands::Job(cmd) => job::run(&ctx, cmd).await?, + _ => {} // Start and Stop are handled in main } - Ok(()) } diff --git a/apps/cli/src/util/mod.rs b/apps/cli/src/util/mod.rs new file mode 100644 index 000000000..eb985e59f --- /dev/null +++ b/apps/cli/src/util/mod.rs @@ -0,0 +1 @@ +pub mod output; \ No newline at end of file diff --git a/apps/cli/src/util/output.rs b/apps/cli/src/util/output.rs index 86c390ae7..e3fc8ea2f 100644 --- a/apps/cli/src/util/output.rs +++ b/apps/cli/src/util/output.rs @@ -1,10 +1,5 @@ -use crate::context::OutputFormat; +use serde::Serialize; -pub fn print_human_line(line: &str) { println!("{}", line) } - -pub fn print_json(value: &T) { - println!("{}", serde_json::to_string_pretty(value).unwrap_or_else(|_| "{}".to_string())) +pub fn print_json(data: &T) { + println!("{}", serde_json::to_string_pretty(data).unwrap()); } - - - diff --git a/core/src/client/mod.rs b/core/src/client/mod.rs index 1dfee1745..94405d3c9 100644 --- a/core/src/client/mod.rs +++ b/core/src/client/mod.rs @@ -67,4 +67,8 @@ impl CoreClient { Err(e) => Err(anyhow::anyhow!(e.to_string())), } } + + pub async fn send_raw_request(&self, req: &DaemonRequest) -> Result { + self.daemon.send(req).await.map_err(|e| anyhow::anyhow!(e.to_string())) + } } diff --git a/core/src/context.rs b/core/src/context.rs index 34a1ad01d..0379ed728 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -2,10 +2,9 @@ //! Shared context providing access to core application components. -use crate::{ - config::JobLoggingConfig, device::DeviceManager, infra::action::manager::ActionManager, +use crate::{config::JobLoggingConfig, device::DeviceManager, infra::action::manager::ActionManager, infra::event::EventBus, crypto::library_key_manager::LibraryKeyManager, library::LibraryManager, - service::network::NetworkingService, volume::VolumeManager, + service::network::NetworkingService, volume::VolumeManager, infra::daemon::state::SessionStateService, }; use std::{path::PathBuf, sync::Arc}; use tokio::sync::RwLock; @@ -24,6 +23,7 @@ pub struct CoreContext { // Job logging configuration pub job_logging_config: Option, pub job_logs_dir: Option, + pub session_state: Arc, } impl CoreContext { @@ -34,6 +34,7 @@ impl CoreContext { library_manager: Arc, volume_manager: Arc, library_key_manager: Arc, + session_state: Arc, ) -> Self { Self { events, @@ -45,6 +46,7 @@ impl CoreContext { networking: Arc::new(RwLock::new(None)), job_logging_config: None, job_logs_dir: None, + session_state, } } diff --git a/core/src/infra/daemon/bootstrap.rs b/core/src/infra/daemon/bootstrap.rs index 98bd1f110..85cf21354 100644 --- a/core/src/infra/daemon/bootstrap.rs +++ b/core/src/infra/daemon/bootstrap.rs @@ -9,9 +9,10 @@ use crate::infra::daemon::{ pub async fn start_default_server( socket_path: PathBuf, data_dir: PathBuf, + enable_networking: bool, ) -> Result<(), Box> { - let instances = Arc::new(CoreInstanceManager::new(data_dir.clone())); - let session = Arc::new(SessionStateService::new(data_dir)); + let session = Arc::new(SessionStateService::new(data_dir.clone())); + let instances = Arc::new(CoreInstanceManager::new(data_dir.clone(), enable_networking, session.clone())); let server = RpcServer::new(socket_path, instances, session); server.start().await } diff --git a/core/src/infra/daemon/instance.rs b/core/src/infra/daemon/instance.rs index bfac749a5..f612a7745 100644 --- a/core/src/infra/daemon/instance.rs +++ b/core/src/infra/daemon/instance.rs @@ -4,16 +4,24 @@ use std::sync::Arc; use tokio::sync::RwLock; use crate::Core; +use crate::infra::daemon::state::SessionStateService; /// Manages lifecycle of Core instances (by name) pub struct CoreInstanceManager { instances: Arc>>>, default_data_dir: PathBuf, + enable_networking: bool, + session_state: Arc, } impl CoreInstanceManager { - pub fn new(default_data_dir: PathBuf) -> Self { - Self { instances: Arc::new(RwLock::new(HashMap::new())), default_data_dir } + pub fn new(default_data_dir: PathBuf, enable_networking: bool, session_state: Arc) -> Self { + Self { + instances: Arc::new(RwLock::new(HashMap::new())), + default_data_dir, + enable_networking, + session_state, + } } /// Get or start the default instance @@ -32,9 +40,18 @@ impl CoreInstanceManager { } let data_dir = data_dir.unwrap_or_else(|| self.default_data_dir.clone()); - let core = Arc::new(Core::new_with_config(data_dir).await?); - self.instances.write().await.insert(name, core.clone()); - Ok(core) + let core = Arc::new(Core::new_with_config(data_dir, self.session_state.clone()).await?); + if self.enable_networking { + let core_with_networking = Core::init_networking_shared(core.clone(), self.session_state.clone()).await?; + self.instances + .write() + .await + .insert(name, core_with_networking.clone()); + Ok(core_with_networking) + } else { + self.instances.write().await.insert(name, core.clone()); + Ok(core) + } } /// Shutdown a named instance diff --git a/core/src/infra/daemon/rpc.rs b/core/src/infra/daemon/rpc.rs index 375dc96be..4f90d786b 100644 --- a/core/src/infra/daemon/rpc.rs +++ b/core/src/infra/daemon/rpc.rs @@ -47,13 +47,13 @@ impl RpcServer { let req: Result = serde_json::from_slice(&buf); let resp = match req { Ok(DaemonRequest::Ping) => DaemonResponse::Pong, - Ok(DaemonRequest::Action { method, payload }) => { + Ok(DaemonRequest::Action { ref method, ref payload }) => { match instances.get_default().await { Ok(core) => { let session_snapshot = session.get().await; match core .execute_action_by_method( - &method, + method, payload.clone(), session_snapshot.clone(), ) @@ -68,11 +68,11 @@ impl RpcServer { } } } - Ok(DaemonRequest::Query { method, payload }) => { + Ok(DaemonRequest::Query { ref method, ref payload }) => { match instances.get_default().await { Ok(core) => { // Pass-through for queries using opaque method string - match core.execute_query_by_method(&method, payload.clone()).await { + match core.execute_query_by_method(method, payload.clone()).await { Ok(out) => DaemonResponse::Ok(out), Err(e) => DaemonResponse::Error(e), } @@ -82,11 +82,20 @@ impl RpcServer { } } } - Err(e) => DaemonResponse::Error(format!("Invalid request: {}", e)), + Ok(DaemonRequest::Shutdown) => { + // Handle shutdown request + DaemonResponse::Ok(Vec::new()) // Send an OK response + } + Err(ref e) => DaemonResponse::Error(format!("Invalid request: {}", e)), // Use ref e here }; let _ = stream .write_all(serde_json::to_string(&resp).unwrap().as_bytes()) .await; + if let Ok(ref req_inner) = req { // Use ref req_inner here + if let DaemonRequest::Shutdown = req_inner { + break Ok(()); // Break the loop to shut down the server + } + } } } } diff --git a/core/src/infra/daemon/types.rs b/core/src/infra/daemon/types.rs index 771450711..241643073 100644 --- a/core/src/infra/daemon/types.rs +++ b/core/src/infra/daemon/types.rs @@ -6,6 +6,7 @@ pub enum DaemonRequest { Ping, Action { method: String, payload: Vec }, Query { method: String, payload: Vec }, + Shutdown, } #[derive(Debug, Serialize, Deserialize)] diff --git a/core/src/lib.rs b/core/src/lib.rs index 8fff16591..e0c1c4cad 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -43,6 +43,7 @@ use std::path::PathBuf; use std::sync::Arc; use tokio::sync::{mpsc, RwLock}; use tracing::{error, info}; +use crate::infra::daemon::state::SessionStateService; /// Pending pairing request information #[derive(Debug, Clone)] @@ -140,14 +141,10 @@ pub struct Core { } impl Core { - /// Initialize a new Core instance with default data directory - pub async fn new() -> Result> { - let data_dir = crate::config::default_data_dir()?; - Self::new_with_config(data_dir).await - } + /// Initialize a new Core instance with custom data directory - pub async fn new_with_config(data_dir: PathBuf) -> Result> { + pub async fn new_with_config(data_dir: PathBuf, session_state: Arc) -> Result> { info!("Initializing Spacedrive Core at {:?}", data_dir); // 1. Load or create app config @@ -195,6 +192,7 @@ impl Core { libraries.clone(), volumes.clone(), library_key_manager.clone(), + session_state.clone(), // Pass session_state here ); // Set job logging configuration if enabled @@ -398,12 +396,13 @@ impl Core { /// Initialize networking from Arc - for daemon use pub async fn init_networking_shared( core: Arc, + session_state: Arc, ) -> Result, Box> { info!("Initializing networking for shared core..."); // Create a new Core with networking enabled let mut new_core = - Core::new_with_config(core.config().read().await.data_dir.clone()).await?; + Core::new_with_config(core.config().read().await.data_dir.clone(), session_state).await?; // Initialize networking on the new core new_core.init_networking().await?; diff --git a/core/src/ops/libraries/session/set_current.rs b/core/src/ops/libraries/session/set_current.rs index e4ad3657a..1771b7722 100644 --- a/core/src/ops/libraries/session/set_current.rs +++ b/core/src/ops/libraries/session/set_current.rs @@ -22,8 +22,7 @@ impl crate::infra::action::CoreAction for SetCurrentLibraryAction { self, context: std::sync::Arc, ) -> Result { - // Persist in daemon session state if available (requires daemon to wire it), - // otherwise noop. For now, this sets nothing globally; placeholder success. + context.session_state.set_current_library(Some(self.input.library_id)).await.map_err(|e| crate::infra::action::error::ActionError::Internal(e.to_string()))?; Ok(SetCurrentLibraryOutput { success: true }) } diff --git a/core/src/ops/locations/list/query.rs b/core/src/ops/locations/list/query.rs index 5895a2b29..8d41bd4c8 100644 --- a/core/src/ops/locations/list/query.rs +++ b/core/src/ops/locations/list/query.rs @@ -1,7 +1,7 @@ use super::output::{LocationInfo, LocationsListOutput}; use crate::{context::CoreContext, cqrs::Query}; use anyhow::Result; -use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; +use sea_orm::EntityTrait; use std::sync::Arc; #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -17,12 +17,11 @@ impl Query for LocationsListQuery { let library = context.library_manager.get_library(self.library_id).await.ok_or_else(|| anyhow::anyhow!("Library not found"))?; let db = library.db().conn(); let rows = crate::infra::db::entities::location::Entity::find() - .filter(crate::infra::db::entities::location::Column::LibraryId.eq(self.library_id)) .all(db) .await?; let mut out = Vec::new(); for r in rows { - out.push(LocationInfo { id: r.uuid, path: std::path::PathBuf::from(r.path), name: r.name }); + out.push(LocationInfo { id: r.uuid, path: std::path::PathBuf::from(r.name.clone().unwrap_or_default()), name: r.name.clone() }); } Ok(LocationsListOutput { locations: out }) } diff --git a/core/src/ops/network/pair/status/output.rs b/core/src/ops/network/pair/status/output.rs index 93f0d18a0..7ed6ffaee 100644 --- a/core/src/ops/network/pair/status/output.rs +++ b/core/src/ops/network/pair/status/output.rs @@ -3,10 +3,51 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::service::network::PairingState; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum SerializablePairingState { + Idle, + GeneratingCode, + Broadcasting, + Scanning, + WaitingForConnection, + Connecting, + Authenticating, + ExchangingKeys, + AwaitingConfirmation, + EstablishingSession, + ChallengeReceived, + ResponsePending, + ResponseSent, + Completed, + Failed { reason: String }, +} + +impl From for SerializablePairingState { + fn from(state: PairingState) -> Self { + match state { + PairingState::Idle => Self::Idle, + PairingState::GeneratingCode => Self::GeneratingCode, + PairingState::Broadcasting => Self::Broadcasting, + PairingState::Scanning => Self::Scanning, + PairingState::WaitingForConnection => Self::WaitingForConnection, + PairingState::Connecting => Self::Connecting, + PairingState::Authenticating => Self::Authenticating, + PairingState::ExchangingKeys => Self::ExchangingKeys, + PairingState::AwaitingConfirmation => Self::AwaitingConfirmation, + PairingState::EstablishingSession => Self::EstablishingSession, + PairingState::ChallengeReceived { .. } => Self::ChallengeReceived, + PairingState::ResponsePending { .. } => Self::ResponsePending, + PairingState::ResponseSent => Self::ResponseSent, + PairingState::Completed => Self::Completed, + PairingState::Failed { reason } => Self::Failed { reason }, + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct PairingSessionSummary { pub id: Uuid, - pub state: PairingState, + pub state: SerializablePairingState, pub remote_device_id: Option, pub expires_at: Option>, // optional if available } diff --git a/core/src/ops/network/pair/status/query.rs b/core/src/ops/network/pair/status/query.rs index 77fa9702c..6103b1d3f 100644 --- a/core/src/ops/network/pair/status/query.rs +++ b/core/src/ops/network/pair/status/query.rs @@ -16,7 +16,7 @@ impl Query for PairStatusQuery { for s in sessions.into_iter() { sessions_out.push(PairingSessionSummary { id: s.id, - state: s.state, + state: s.state.into(), remote_device_id: s.remote_device_id, expires_at: None, });