use std::collections::BTreeMap; use std::path::PathBuf; use std::sync::mpsc::{self, SyncSender}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle, ThreadId}; use std::time::Duration; use prost_types::Any; use crate::client::{ClientBuilder, KiCadClient}; use crate::error::KiCadError; use crate::model::board::*; use crate::model::common::*; const BLOCKING_QUEUE_CAPACITY: usize = 64; type Job = Box; #[derive(Debug)] struct BlockingCore { job_tx: Mutex>>, worker_thread_id: ThreadId, worker_join: Mutex>>, } impl BlockingCore { fn start() -> Result, KiCadError> { let (job_tx, job_rx) = mpsc::sync_channel::(BLOCKING_QUEUE_CAPACITY); let (init_tx, init_rx) = mpsc::sync_channel::>(1); let worker_name = format!("kicad-ipc-blocking-runtime-{}", std::process::id()); let worker_join = thread::Builder::new() .name(worker_name) .spawn(move || { let runtime = match tokio::runtime::Builder::new_current_thread() .enable_time() .build() { Ok(runtime) => runtime, Err(err) => { let _ = init_tx.send(Err(KiCadError::RuntimeJoin(err.to_string()))); return; } }; let _ = init_tx.send(Ok(thread::current().id())); for job in job_rx { job(&runtime); } }) .map_err(|err| KiCadError::RuntimeJoin(err.to_string()))?; let worker_thread_id = init_rx .recv() .map_err(|_| KiCadError::BlockingRuntimeClosed)??; Ok(Arc::new(Self { job_tx: Mutex::new(Some(job_tx)), worker_thread_id, worker_join: Mutex::new(Some(worker_join)), })) } fn shutdown(&self) { if let Ok(mut tx_guard) = self.job_tx.lock() { tx_guard.take(); } let handle = match self.worker_join.lock() { Ok(mut guard) => guard.take(), Err(_) => None, }; if let Some(handle) = handle { if thread::current().id() != self.worker_thread_id { let _ = handle.join(); } } } fn call(&self, f: F) -> Result where T: Send + 'static, F: FnOnce(&tokio::runtime::Runtime) -> Result + Send + 'static, { let sender = { let guard = self .job_tx .lock() .map_err(|_| KiCadError::BlockingRuntimeClosed)?; guard .as_ref() .cloned() .ok_or(KiCadError::BlockingRuntimeClosed)? }; let (result_tx, result_rx) = mpsc::sync_channel::>(1); sender .send(Box::new(move |runtime| { let result = f(runtime); let _ = result_tx.send(result); })) .map_err(|_| KiCadError::BlockingRuntimeClosed)?; result_rx .recv() .map_err(|_| KiCadError::BlockingRuntimeClosed)? } } impl Drop for BlockingCore { fn drop(&mut self) { self.shutdown(); } } #[derive(Clone, Debug)] pub struct KiCadClientBlocking { inner: KiCadClient, core: Arc, } #[derive(Clone, Debug)] pub struct KiCadClientBlockingBuilder { inner: ClientBuilder, } impl KiCadClientBlockingBuilder { pub fn new() -> Self { Self { inner: ClientBuilder::new(), } } pub fn timeout(mut self, timeout: Duration) -> Self { self.inner = self.inner.timeout(timeout); self } pub fn socket_path(mut self, socket_path: impl Into) -> Self { self.inner = self.inner.socket_path(socket_path); self } pub fn token(mut self, token: impl Into) -> Self { self.inner = self.inner.token(token); self } pub fn client_name(mut self, client_name: impl Into) -> Self { self.inner = self.inner.client_name(client_name); self } pub fn connect(self) -> Result { let core = BlockingCore::start()?; let inner_builder = self.inner; let inner = core.call(move |runtime| runtime.block_on(inner_builder.connect()))?; Ok(KiCadClientBlocking { inner, core }) } } impl Default for KiCadClientBlockingBuilder { fn default() -> Self { Self::new() } } macro_rules! blocking_methods { ( $(fn $name:ident(&self $(, $arg:ident : $arg_ty:ty)*) -> $ret:ty;)+ ) => { $( pub fn $name(&self, $($arg: $arg_ty),*) -> $ret { let client = self.inner.clone(); self.core.call(move |runtime| runtime.block_on(async move { client.$name($($arg),*).await })) } )+ #[cfg(test)] pub(crate) const GENERATED_BLOCKING_METHOD_NAMES: &'static [&'static str] = &[ $(stringify!($name),)+ ]; }; } impl KiCadClientBlocking { pub fn builder() -> KiCadClientBlockingBuilder { KiCadClientBlockingBuilder::new() } pub fn connect() -> Result { KiCadClientBlockingBuilder::new().connect() } pub fn timeout(&self) -> Duration { self.inner.timeout() } pub fn socket_uri(&self) -> &str { self.inner.socket_uri() } pub fn inner(&self) -> &KiCadClient { &self.inner } pub fn run_action_raw(&self, action: impl Into) -> Result { let action = action.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client.run_action_raw(action).await }) }) } pub fn run_action(&self, action: impl Into) -> Result { let action = action.into(); let client = self.inner.clone(); self.core .call(move |runtime| runtime.block_on(async move { client.run_action(action).await })) } pub fn get_kicad_binary_path_raw( &self, binary_name: impl Into, ) -> Result { let binary_name = binary_name.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client.get_kicad_binary_path_raw(binary_name).await }) }) } pub fn get_kicad_binary_path( &self, binary_name: impl Into, ) -> Result { let binary_name = binary_name.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client.get_kicad_binary_path(binary_name).await }) }) } pub fn get_plugin_settings_path_raw( &self, identifier: impl Into, ) -> Result { let identifier = identifier.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client.get_plugin_settings_path_raw(identifier).await }) }) } pub fn get_plugin_settings_path( &self, identifier: impl Into, ) -> Result { let identifier = identifier.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client.get_plugin_settings_path(identifier).await }) }) } pub fn end_commit_raw( &self, session: CommitSession, action: CommitAction, message: impl Into, ) -> Result { let message = message.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client.end_commit_raw(session, action, message).await }) }) } pub fn end_commit( &self, session: CommitSession, action: CommitAction, message: impl Into, ) -> Result<(), KiCadError> { let message = message.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client.end_commit(session, action, message).await }) }) } pub fn parse_and_create_items_from_string_raw( &self, contents: impl Into, ) -> Result { let contents = contents.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client .parse_and_create_items_from_string_raw(contents) .await }) }) } pub fn parse_and_create_items_from_string( &self, contents: impl Into, ) -> Result, KiCadError> { let contents = contents.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime .block_on(async move { client.parse_and_create_items_from_string(contents).await }) }) } pub fn inject_drc_error_raw( &self, severity: DrcSeverity, message: impl Into, position: Option, item_ids: Vec, ) -> Result { let message = message.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client .inject_drc_error_raw(severity, message, position, item_ids) .await }) }) } pub fn inject_drc_error( &self, severity: DrcSeverity, message: impl Into, position: Option, item_ids: Vec, ) -> Result, KiCadError> { let message = message.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client .inject_drc_error(severity, message, position, item_ids) .await }) }) } pub fn save_copy_of_document_raw( &self, path: impl Into, overwrite: bool, include_project: bool, ) -> Result { let path = path.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client .save_copy_of_document_raw(path, overwrite, include_project) .await }) }) } pub fn save_copy_of_document( &self, path: impl Into, overwrite: bool, include_project: bool, ) -> Result<(), KiCadError> { let path = path.into(); let client = self.inner.clone(); self.core.call(move |runtime| { runtime.block_on(async move { client .save_copy_of_document(path, overwrite, include_project) .await }) }) } blocking_methods! { fn ping(&self) -> Result<(), KiCadError>; fn refresh_editor(&self, frame: EditorFrameType) -> Result<(), KiCadError>; fn get_version(&self) -> Result; fn get_open_documents(&self, document_type: DocumentType) -> Result, KiCadError>; fn get_net_classes_raw(&self) -> Result; fn get_net_classes(&self) -> Result, KiCadError>; fn set_net_classes_raw(&self, net_classes: Vec, merge_mode: MapMergeMode) -> Result; fn set_net_classes(&self, net_classes: Vec, merge_mode: MapMergeMode) -> Result, KiCadError>; fn get_text_variables_raw(&self) -> Result; fn get_text_variables(&self) -> Result, KiCadError>; fn set_text_variables_raw(&self, variables: BTreeMap, merge_mode: MapMergeMode) -> Result; fn set_text_variables(&self, variables: BTreeMap, merge_mode: MapMergeMode) -> Result, KiCadError>; fn expand_text_variables_raw(&self, text: Vec) -> Result; fn expand_text_variables(&self, text: Vec) -> Result, KiCadError>; fn get_text_extents_raw(&self, text: TextSpec) -> Result; fn get_text_extents(&self, text: TextSpec) -> Result; fn get_text_as_shapes_raw(&self, text: Vec) -> Result; fn get_text_as_shapes(&self, text: Vec) -> Result, KiCadError>; fn get_current_project_path(&self) -> Result; fn has_open_board(&self) -> Result; fn begin_commit_raw(&self) -> Result; fn begin_commit(&self) -> Result; fn create_items_raw(&self, items: Vec, container_id: Option) -> Result; fn create_items(&self, items: Vec, container_id: Option) -> Result, KiCadError>; fn update_items_raw(&self, items: Vec) -> Result; fn update_items(&self, items: Vec) -> Result, KiCadError>; fn delete_items_raw(&self, item_ids: Vec) -> Result; fn delete_items(&self, item_ids: Vec) -> Result, KiCadError>; fn get_nets(&self) -> Result, KiCadError>; fn get_board_enabled_layers(&self) -> Result; fn set_board_enabled_layers(&self, copper_layer_count: u32, layer_ids: Vec) -> Result; fn get_active_layer(&self) -> Result; fn set_active_layer(&self, layer_id: i32) -> Result<(), KiCadError>; fn get_visible_layers(&self) -> Result, KiCadError>; fn set_visible_layers(&self, layer_ids: Vec) -> Result<(), KiCadError>; fn get_board_layer_name(&self, layer_id: i32) -> Result; fn get_board_origin(&self, kind: BoardOriginKind) -> Result; fn set_board_origin(&self, kind: BoardOriginKind, origin: Vector2Nm) -> Result<(), KiCadError>; fn get_selection_summary(&self, type_codes: Vec) -> Result; fn get_selection_raw(&self, type_codes: Vec) -> Result, KiCadError>; fn get_selection_details(&self, type_codes: Vec) -> Result, KiCadError>; fn get_selection(&self, type_codes: Vec) -> Result, KiCadError>; fn add_to_selection_raw(&self, item_ids: Vec) -> Result, KiCadError>; fn add_to_selection(&self, item_ids: Vec) -> Result; fn clear_selection_raw(&self) -> Result, KiCadError>; fn clear_selection(&self) -> Result; fn remove_from_selection_raw(&self, item_ids: Vec) -> Result, KiCadError>; fn remove_from_selection(&self, item_ids: Vec) -> Result; fn get_pad_netlist(&self) -> Result, KiCadError>; fn get_vias_raw(&self) -> Result, KiCadError>; fn get_vias(&self) -> Result, KiCadError>; fn get_items_raw_by_type_codes(&self, type_codes: Vec) -> Result, KiCadError>; fn get_items_details_by_type_codes(&self, type_codes: Vec) -> Result, KiCadError>; fn get_items_by_type_codes(&self, type_codes: Vec) -> Result, KiCadError>; fn get_all_pcb_items_raw(&self) -> Result)>, KiCadError>; fn get_all_pcb_items_details(&self) -> Result)>, KiCadError>; fn get_all_pcb_items(&self) -> Result)>, KiCadError>; fn get_items_by_net_raw(&self, type_codes: Vec, net_codes: Vec) -> Result, KiCadError>; fn get_items_by_net(&self, type_codes: Vec, net_codes: Vec) -> Result, KiCadError>; fn get_items_by_net_class_raw(&self, type_codes: Vec, net_classes: Vec) -> Result, KiCadError>; fn get_items_by_net_class(&self, type_codes: Vec, net_classes: Vec) -> Result, KiCadError>; fn get_netclass_for_nets_raw(&self, nets: Vec) -> Result; fn get_netclass_for_nets(&self, nets: Vec) -> Result, KiCadError>; fn refill_zones(&self, zone_ids: Vec) -> Result<(), KiCadError>; fn get_pad_shape_as_polygon_raw(&self, pad_ids: Vec, layer_id: i32) -> Result, KiCadError>; fn get_pad_shape_as_polygon(&self, pad_ids: Vec, layer_id: i32) -> Result, KiCadError>; fn check_padstack_presence_on_layers_raw(&self, item_ids: Vec, layer_ids: Vec) -> Result, KiCadError>; fn check_padstack_presence_on_layers(&self, item_ids: Vec, layer_ids: Vec) -> Result, KiCadError>; fn get_board_stackup_raw(&self) -> Result; fn get_board_stackup(&self) -> Result; fn update_board_stackup_raw(&self, stackup: BoardStackup) -> Result; fn update_board_stackup(&self, stackup: BoardStackup) -> Result; fn get_graphics_defaults_raw(&self) -> Result; fn get_graphics_defaults(&self) -> Result; fn get_board_editor_appearance_settings_raw(&self) -> Result; fn get_board_editor_appearance_settings(&self) -> Result; fn set_board_editor_appearance_settings(&self, settings: BoardEditorAppearanceSettings) -> Result; fn interactive_move_items_raw(&self, item_ids: Vec) -> Result; fn interactive_move_items(&self, item_ids: Vec) -> Result<(), KiCadError>; fn get_title_block_info(&self) -> Result; fn save_document_raw(&self) -> Result; fn save_document(&self) -> Result<(), KiCadError>; fn revert_document_raw(&self) -> Result; fn revert_document(&self) -> Result<(), KiCadError>; fn get_board_as_string(&self) -> Result; fn get_selection_as_string(&self) -> Result; fn get_items_by_id_raw(&self, item_ids: Vec) -> Result, KiCadError>; fn get_items_by_id_details(&self, item_ids: Vec) -> Result, KiCadError>; fn get_items_by_id(&self, item_ids: Vec) -> Result, KiCadError>; fn get_item_bounding_boxes(&self, item_ids: Vec, include_child_text: bool) -> Result, KiCadError>; fn hit_test_item(&self, item_id: String, position: Vector2Nm, tolerance_nm: i32) -> Result; } #[cfg(test)] pub(crate) const MANUAL_BLOCKING_METHOD_NAMES: &'static [&'static str] = &[ "connect", "run_action_raw", "run_action", "get_kicad_binary_path_raw", "get_kicad_binary_path", "get_plugin_settings_path_raw", "get_plugin_settings_path", "end_commit_raw", "end_commit", "parse_and_create_items_from_string_raw", "parse_and_create_items_from_string", "inject_drc_error_raw", "inject_drc_error", "save_copy_of_document_raw", "save_copy_of_document", ]; } #[cfg(test)] mod tests { use super::*; use std::collections::BTreeSet; use std::sync::mpsc as std_mpsc; use std::time::{Duration, Instant}; #[test] fn blocking_core_executes_job_and_returns_result() { let core = BlockingCore::start().expect("blocking core must start"); let value = core .call(|_| Ok::<_, KiCadError>(1234)) .expect("blocking job should execute"); assert_eq!(value, 1234); } #[test] fn blocking_core_handles_concurrent_submitters() { let core = BlockingCore::start().expect("blocking core must start"); let mut handles = Vec::new(); for idx in 0..8 { let core = Arc::clone(&core); handles.push(thread::spawn(move || { core.call(move |_| Ok::<_, KiCadError>(idx * 2)) .expect("job should return"); })); } for handle in handles { handle.join().expect("submitter thread must join"); } } #[test] fn blocking_core_shutdown_drains_inflight_jobs() { let core = BlockingCore::start().expect("blocking core must start"); let (started_tx, started_rx) = std_mpsc::sync_channel::<()>(1); let core_for_call = Arc::clone(&core); let worker = thread::spawn(move || { core_for_call .call(move |_| { let _ = started_tx.send(()); thread::sleep(Duration::from_millis(120)); Ok::<_, KiCadError>(()) }) .expect("in-flight job should complete"); }); started_rx .recv_timeout(Duration::from_secs(1)) .expect("job should begin"); let begin = Instant::now(); core.shutdown(); let elapsed = begin.elapsed(); assert!( elapsed >= Duration::from_millis(80), "shutdown should wait for in-flight job; elapsed: {elapsed:?}" ); worker.join().expect("worker submitter should join"); } #[test] fn blocking_core_returns_closed_error_after_shutdown() { let core = BlockingCore::start().expect("blocking core must start"); core.shutdown(); let err = core .call(|_| Ok::<_, KiCadError>(())) .expect_err("closed core should reject calls"); assert!(matches!(err, KiCadError::BlockingRuntimeClosed)); } #[test] fn sync_wrapper_covers_async_method_names() { let mut async_methods = BTreeSet::new(); for line in include_str!("client.rs").lines() { let trimmed = line.trim_start(); if let Some(rest) = trimmed.strip_prefix("pub async fn ") { if let Some(name) = rest.split('(').next() { async_methods.insert(name.trim().to_string()); } } } let blocking_methods: BTreeSet = KiCadClientBlocking::GENERATED_BLOCKING_METHOD_NAMES .iter() .chain(KiCadClientBlocking::MANUAL_BLOCKING_METHOD_NAMES.iter()) .map(|name| (*name).to_string()) .collect(); let missing: Vec = async_methods .into_iter() .filter(|name| !blocking_methods.contains(name)) .collect(); assert!( missing.is_empty(), "missing blocking wrappers for async methods: {:?}", missing ); } #[test] fn impl_into_string_wrapper_signatures_accept_str() { fn assert_signatures(client: &KiCadClientBlocking) { let _ = client.run_action_raw("pcbnew.Refresh"); let _ = client.run_action("pcbnew.Refresh"); let _ = client.get_kicad_binary_path_raw("kicad-cli"); let _ = client.get_kicad_binary_path("kicad-cli"); let _ = client.get_plugin_settings_path_raw("kicad-ipc-rs"); let _ = client.get_plugin_settings_path("kicad-ipc-rs"); let _ = client.end_commit_raw( CommitSession { id: "commit-id".to_string(), }, CommitAction::Drop, "test", ); let _ = client.end_commit( CommitSession { id: "commit-id".to_string(), }, CommitAction::Drop, "test", ); let _ = client.parse_and_create_items_from_string_raw("(kicad_pcb)"); let _ = client.parse_and_create_items_from_string("(kicad_pcb)"); let _ = client.inject_drc_error_raw(DrcSeverity::Warning, "marker", None, Vec::new()); let _ = client.inject_drc_error(DrcSeverity::Warning, "marker", None, Vec::new()); let _ = client.save_copy_of_document_raw("/tmp/example.kicad_pcb", false, false); let _ = client.save_copy_of_document("/tmp/example.kicad_pcb", false, false); } let _ = assert_signatures as fn(&KiCadClientBlocking); } #[test] fn blocking_smoke_live_when_socket_env_is_set() { if std::env::var("KICAD_API_SOCKET").is_err() { return; } let client = KiCadClientBlocking::connect().expect("blocking client should connect"); client.ping().expect("ping should succeed"); let _ = client.get_version().expect("version should succeed"); let _ = client .get_open_documents(DocumentType::Pcb) .expect("open docs should succeed"); let _ = client .get_visible_layers() .expect("board read method should succeed"); } }