From ee2898095e690703a59b95df558e38884cbb6906 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 15 Oct 2026 12:54:02 +0300 Subject: [PATCH] fix(plugins): Python plugins see the host document again Plugins read the host's document through a shared reference across the dynamic-library boundary, and the entity index hashed with foldhash's per-process seed, so every handle lookup from the plugin missed. The codec now uses a fixed hasher (opencadcodec f385f75; kernel 9841749, graph e9c23ad). The Python IPC suite also caught up with intended changes: ATTDEF renders its tag, DXF now keeps the lock-position flag, and the modify audit turns associative arrays off since it checks loose copies. The rate-limiter test waits for the host's check instead of a fixed sleep that raced on macOS, and ENV_LOCK no longer cascades a poisoned lock into every later test. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 8 ++++---- Cargo.toml | 8 ++++---- crates/ocs_plugin_api/Cargo.toml | 4 ++-- crates/ocs_plugin_api/src/ipc/v4/client.rs | 4 ++-- crates/ocs_plugin_api/src/manifest.rs | 10 ++++----- crates/ocs_plugin_api/src/process.rs | 8 ++++---- crates/ocs_plugin_api/src/process/v4.rs | 24 ++++++++++++---------- crates/ocs_save/Cargo.toml | 2 +- crates/ocs_web_worker/Cargo.toml | 2 +- plugins/opencad-python/Cargo.lock | 2 +- src/app/plugin_host.rs | 13 ++++++------ 11 files changed, 43 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 04bd0a5e3..a259be614 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4373,7 +4373,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=d36331d#d36331d9e4f01576cdd29e8420caeb6503675c2c" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=f385f75#f385f75decc311b7caeefb22d4e7435d97e12edb" dependencies = [ "anyhow", "bitflags 2.13.1", @@ -4396,7 +4396,7 @@ dependencies = [ [[package]] name = "opencadgraph" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=74aac56#74aac563a09388489834a1cbafc1b3893a739802" +source = "git+https://github.com/HakanSeven12/opencadgraph.git?rev=e9c23ad#e9c23adb0facdd00d043ca9aeb6a5dacf0208779" dependencies = [ "opencadkernel", "serde_json", @@ -4405,7 +4405,7 @@ dependencies = [ [[package]] name = "opencadkernel" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=60f086b#60f086bd6200924f9f96f31de4722d1216bb29b9" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=9841749#9841749422c8fb0fa1f68205ac725d27f13e13bd" dependencies = [ "cavalier_contours", "opencadcodec", @@ -4416,7 +4416,7 @@ dependencies = [ [[package]] name = "opencadkernel-constraints" version = "0.1.0" -source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=60f086b#60f086bd6200924f9f96f31de4722d1216bb29b9" +source = "git+https://github.com/HakanSeven12/opencadkernel.git?rev=9841749#9841749422c8fb0fa1f68205ac725d27f13e13bd" dependencies = [ "nalgebra", ] diff --git a/Cargo.toml b/Cargo.toml index 0469252a2..832fec9bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,10 +37,10 @@ rfd = "0.17" clap = { version = "4", features = ["derive", "string"] } env_logger = "0.11" log = "0.4" -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "d36331d", features = ["serde"] } -kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "60f086b", features = ["acis", "offset"] } -kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "60f086b" } -graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "74aac56" } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "f385f75", features = ["serde"] } +kernel = { package = "opencadkernel", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "9841749", features = ["acis", "offset"] } +kernel-constraints = { package = "opencadkernel-constraints", git = "https://github.com/HakanSeven12/opencadkernel.git", rev = "9841749" } +graph = { package = "opencadgraph", git = "https://github.com/HakanSeven12/opencadgraph.git", rev = "e9c23ad" } dwg-thumbnailer = { path = "crates/dwg-thumbnailer" } # Shared SAVE serialization dispatch + web-worker wire format: one # implementation for the inline (fallback/native) path and the worker path. diff --git a/crates/ocs_plugin_api/Cargo.toml b/crates/ocs_plugin_api/Cargo.toml index dd1f363c3..b61a1fba7 100644 --- a/crates/ocs_plugin_api/Cargo.toml +++ b/crates/ocs_plugin_api/Cargo.toml @@ -15,7 +15,7 @@ serde_json = { version = "1", optional = true } # Pulled in only by the `host` feature, which adds the `opencadcodec`-typed # `HostApi` runtime surface. The default crate stays dependency-free so engine # crates and external tooling can depend on the manifest/ribbon contract cheaply. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "d36331d", optional = true, features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "f385f75", optional = true, features = ["serde"] } # Runtime IPC and serialization (host feature only). interprocess = { version = "2", optional = true } @@ -39,7 +39,7 @@ serde_json = "1" serde = { version = "1", features = ["derive"] } cargo-lock = "11" # opencadcodec is scanned at build time to generate the embedded type registry. -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "d36331d", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "f385f75", features = ["serde"] } [dev-dependencies] serde_json = "1" diff --git a/crates/ocs_plugin_api/src/ipc/v4/client.rs b/crates/ocs_plugin_api/src/ipc/v4/client.rs index aaf2f448f..8adf8eb44 100644 --- a/crates/ocs_plugin_api/src/ipc/v4/client.rs +++ b/crates/ocs_plugin_api/src/ipc/v4/client.rs @@ -1210,7 +1210,7 @@ mod tests { #[test] fn v4_client_notification_queue_size_env_is_honored() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let prev = std::env::var("OCS_PLUGIN_NOTIFY_QUEUE_SIZE").ok(); std::env::set_var("OCS_PLUGIN_NOTIFY_QUEUE_SIZE", "7"); assert_eq!(notify_queue_size(), 7); @@ -1222,7 +1222,7 @@ mod tests { #[test] fn v4_client_notification_queue_drops_newest_on_overflow() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let prev = std::env::var("OCS_PLUGIN_NOTIFY_QUEUE_SIZE").ok(); std::env::set_var("OCS_PLUGIN_NOTIFY_QUEUE_SIZE", "1"); let (host_stream, mut runner_stream) = connect_pair(); diff --git a/crates/ocs_plugin_api/src/manifest.rs b/crates/ocs_plugin_api/src/manifest.rs index 27c371d73..8fb07e446 100644 --- a/crates/ocs_plugin_api/src/manifest.rs +++ b/crates/ocs_plugin_api/src/manifest.rs @@ -108,14 +108,14 @@ mod tests { #[test] fn effective_max_defaults_to_api_version() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::remove_var(MAX_API_VERSION_ENV); assert_eq!(effective_max_api_version(), API_VERSION); } #[test] fn effective_max_can_disable_v4() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::set_var(MAX_API_VERSION_ENV, "3"); assert_eq!(effective_max_api_version(), 3); assert!(!host_accepts_plugin_version(4)); @@ -126,7 +126,7 @@ mod tests { #[test] fn effective_max_below_minimum_is_v3_only_mode() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::set_var(MAX_API_VERSION_ENV, "2"); assert_eq!(effective_max_api_version(), 3); assert!(!host_accepts_plugin_version(4)); @@ -137,7 +137,7 @@ mod tests { #[test] fn effective_max_clamps_to_supported_range() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::set_var(MAX_API_VERSION_ENV, "99"); assert_eq!(effective_max_api_version(), API_VERSION); std::env::set_var(MAX_API_VERSION_ENV, "1"); @@ -147,7 +147,7 @@ mod tests { #[test] fn host_accepts_supported_range() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::remove_var(MAX_API_VERSION_ENV); assert!(host_accepts_plugin_version(API_VERSION_MIN_SUPPORTED)); assert!(host_accepts_plugin_version(API_VERSION)); diff --git a/crates/ocs_plugin_api/src/process.rs b/crates/ocs_plugin_api/src/process.rs index 117ed330d..052ccf780 100644 --- a/crates/ocs_plugin_api/src/process.rs +++ b/crates/ocs_plugin_api/src/process.rs @@ -1497,7 +1497,7 @@ mod timeout_tests { #[test] fn runner_handshake_correct_token_is_accepted() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::remove_var("OCS_PLUGIN_MAX_API_VERSION"); let result = verify_runner_handshake( RunnerHandshake::Token("correct-token".to_string()), @@ -1512,7 +1512,7 @@ mod timeout_tests { #[test] fn runner_handshake_tokenv4_selects_v4_path() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::remove_var("OCS_PLUGIN_MAX_API_VERSION"); let result = verify_runner_handshake( RunnerHandshake::TokenV4 { @@ -1530,7 +1530,7 @@ mod timeout_tests { #[test] fn runner_handshake_tokenv4_mismatched_version_is_rejected() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::remove_var("OCS_PLUGIN_MAX_API_VERSION"); let result = verify_runner_handshake( RunnerHandshake::TokenV4 { @@ -1547,7 +1547,7 @@ mod timeout_tests { #[test] fn runner_handshake_tokenv4_rejected_when_v4_disabled() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); std::env::set_var("OCS_PLUGIN_MAX_API_VERSION", "3"); let result = verify_runner_handshake( RunnerHandshake::TokenV4 { diff --git a/crates/ocs_plugin_api/src/process/v4.rs b/crates/ocs_plugin_api/src/process/v4.rs index 868bd2288..4517e86f3 100644 --- a/crates/ocs_plugin_api/src/process/v4.rs +++ b/crates/ocs_plugin_api/src/process/v4.rs @@ -482,7 +482,7 @@ mod tests { #[test] fn execute_code_timeout_floor_is_at_least_60s_and_env_override() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); let prev_call_timeout = std::env::var("OCS_PLUGIN_CALL_TIMEOUT_SECS").ok(); let prev_test_floor = std::env::var("OCS_PLUGIN_TEST_FLOOR_SECS").ok(); let prev_execute_timeout = std::env::var("OCS_PLUGIN_EXECUTE_TIMEOUT_SECS").ok(); @@ -519,7 +519,7 @@ mod tests { #[test] fn v4_connection_request_response() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_test_env(); let (host_stream, mut runner_stream) = connect_pair(); let handler: Arc, PluginNotification) + Send + Sync> = @@ -565,7 +565,7 @@ mod tests { #[test] fn v4_connection_handles_nested_plugin_request() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_test_env(); let (host_stream, mut runner_stream) = connect_pair(); let handler: Arc, PluginNotification) + Send + Sync> = @@ -635,7 +635,7 @@ mod tests { #[test] fn v4_connection_routes_entity_transaction_over_nested_ipc() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_test_env(); let (host_stream, mut runner_stream) = connect_pair(); let handler: Arc, PluginNotification) + Send + Sync> = @@ -705,7 +705,7 @@ mod tests { #[test] fn v4_connection_notification_invokes_handler() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_test_env(); use crate::host::LogLevel; @@ -744,7 +744,7 @@ mod tests { #[test] fn v4_connection_handler_panic_is_isolated() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_test_env(); use crate::host::LogLevel; @@ -792,7 +792,7 @@ mod tests { #[test] fn v4_connection_notification_rate_limiter_drops_excess() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_test_env(); std::env::set_var("OCS_PLUGIN_NOTIFY_RATE_LIMIT", "2"); use crate::host::LogLevel; @@ -805,6 +805,7 @@ mod tests { *received2.lock().unwrap() += 1; }); let conn = V4Connection::new(host_stream, handler).unwrap(); + let (checked_tx, checked_rx) = mpsc::channel::<()>(); let runner = thread::spawn(move || { for _ in 0..10 { @@ -820,9 +821,9 @@ mod tests { ) .unwrap(); } - // Keep the stream open long enough for the host to verify the - // reader thread is still alive after the flood. - std::thread::sleep(Duration::from_millis(300)); + // Keep the stream open until the host has checked the reader + // thread survived the flood; a fixed sleep raced on slow runners. + let _ = checked_rx.recv_timeout(Duration::from_secs(30)); }); std::thread::sleep(Duration::from_millis(200)); @@ -835,6 +836,7 @@ mod tests { conn.is_alive(), "connection should survive rate-limited flood" ); + checked_tx.send(()).unwrap(); runner.join().unwrap(); drop(conn); std::env::remove_var("OCS_PLUGIN_NOTIFY_RATE_LIMIT"); @@ -843,7 +845,7 @@ mod tests { #[test] fn execute_code_returns_delayed_result_with_tab_index() { - let _guard = ENV_LOCK.lock().unwrap(); + let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()); set_test_env(); let (host_stream, mut runner_stream) = connect_pair(); diff --git a/crates/ocs_save/Cargo.toml b/crates/ocs_save/Cargo.toml index 19da41e25..c7e1a17e9 100644 --- a/crates/ocs_save/Cargo.toml +++ b/crates/ocs_save/Cargo.toml @@ -10,6 +10,6 @@ publish = false # exactly one implementation. No UI/app dependencies — only the codec + serde. [dependencies] -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "d36331d", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "f385f75", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } diff --git a/crates/ocs_web_worker/Cargo.toml b/crates/ocs_web_worker/Cargo.toml index f6d056778..24b9b090c 100644 --- a/crates/ocs_web_worker/Cargo.toml +++ b/crates/ocs_web_worker/Cargo.toml @@ -8,7 +8,7 @@ publish = false crate-type = ["cdylib"] [dependencies] -codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "d36331d", features = ["serde"] } +codec = { package = "opencadcodec", git = "https://github.com/HakanSeven12/opencadcodec.git", rev = "f385f75", features = ["serde"] } bincode = "1.3" serde = { version = "1", features = ["derive"] } console_error_panic_hook = "0.1" diff --git a/plugins/opencad-python/Cargo.lock b/plugins/opencad-python/Cargo.lock index 0bb1cb4b1..af00d623b 100644 --- a/plugins/opencad-python/Cargo.lock +++ b/plugins/opencad-python/Cargo.lock @@ -1264,7 +1264,7 @@ dependencies = [ [[package]] name = "opencadcodec" version = "0.6.0" -source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=d36331d#d36331d9e4f01576cdd29e8420caeb6503675c2c" +source = "git+https://github.com/HakanSeven12/opencadcodec.git?rev=f385f75#f385f75decc311b7caeefb22d4e7435d97e12edb" dependencies = [ "anyhow", "bitflags", diff --git a/src/app/plugin_host.rs b/src/app/plugin_host.rs index 0195ab66b..3088a9e07 100644 --- a/src/app/plugin_host.rs +++ b/src/app/plugin_host.rs @@ -5199,7 +5199,7 @@ mod tests { assert!(strokes.iter().any(|stroke| stroke .run .as_ref() - .is_some_and(|run| run.text.contains("PN-002")))); + .is_some_and(|run| run.text.contains("PART_NO")))); for (label, patch, message) in [ ("Reject tag", "'tag':'BAD TAG'", "whitespace"), @@ -8644,12 +8644,8 @@ mod tests { expect_created: "PART_NO|Part number|PN-001 ins1.0,2.0 al3.0,4.0 h2.5 r0.25 wf1.25 ob0.10 Center/Top f1010 fl12 tg2 lock1", expect_edited: "PART_NO|Serial|PN-002 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock1", expect_reedited: "PART_NO|Serial|PN-003 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock1", - // BLOCKER (opencadcodec): the DXF ATTDEF writer emits no group 280 at - // all — neither the version byte nor the lock-position flag the - // reader looks for after it — so `lock_position` reopens false. Every - // other field round-trips since opencadcodec dd1d7bf. - expect_edited_dxf: "PART_NO|Serial|PN-002 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock0", - expect_reedited_dxf: "PART_NO|Serial|PN-003 ins4.0,5.0 al3.0,4.0 h3.0 r0.50 wf0.80 ob0.20 Right/Middle f0101 fl20 tg2 lock0", + expect_edited_dxf: "", + expect_reedited_dxf: "", expect_edited_dwg: "", expect_reedited_dwg: "", }); @@ -12126,6 +12122,9 @@ LT.delete('Temp') let Some(plugin_path) = std::env::var_os("OCS_TEST_PYTHON_PLUGIN") else { return; }; + // The array assertions below check loose copies; ARRAYRECT/ARRAYPOLAR + // build associative arrays by default (thread-local, this thread only). + crate::modules::draw::defaults::set_array_assoc(0.0); let mut app = OpenCADStudio::new_for_test(); app.tabs[0].is_start = false; let mut host = HostSession::new(&mut app, 0);