From ac2a1e11667eb09782fb4e61195dd1da0c5ebd9c Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Tue, 29 Sep 2026 06:03:54 +0300 Subject: [PATCH] fix(lock): other applications can open a drawing read-only while OCS edits it On Windows the drawing was held with an exclusive byte lock, which also refuses reads, so AutoCAD / BricsCAD could not open it at all. The lease now reads through a handle that shares reads but refuses write opens, with a shared lock that refuses writes; the sidecar still keeps a second OCS session out. Unix keeps its advisory lock, which never blocked readers. Discussion #1416, issue #739 Co-Authored-By: Claude Opus 5.5 --- src/io/edit_lock.rs | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/src/io/edit_lock.rs b/src/io/edit_lock.rs index cbe79fcae..bc37ea2a8 100644 --- a/src/io/edit_lock.rs +++ b/src/io/edit_lock.rs @@ -239,8 +239,42 @@ impl Drop for EditLease { } } +/// Open the drawing so other applications may still read it but not write it. +/// +/// Windows: our handle only reads, and its share mode admits other readers +/// (and the atomic save's replace) while refusing any write open; the shared +/// byte lock below refuses writes through handles opened before ours. An +/// exclusive lock would also refuse reads, so another CAD program could not +/// even open the drawing read-only while it is being edited here. +#[cfg(target_os = "windows")] +fn open_drawing_for_lease(path: &Path) -> std::io::Result { + use std::os::windows::fs::OpenOptionsExt; + use windows_sys::Win32::Storage::FileSystem::{FILE_SHARE_DELETE, FILE_SHARE_READ}; + OpenOptions::new() + .read(true) + .share_mode(FILE_SHARE_READ | FILE_SHARE_DELETE) + .open(path) +} + +#[cfg(not(target_os = "windows"))] +fn open_drawing_for_lease(path: &Path) -> std::io::Result { + OpenOptions::new().read(true).write(true).open(path) +} + +/// Windows takes the shared lock (writes refused, reads allowed); elsewhere the +/// lock is advisory and never stops a reader, so it stays exclusive. +#[cfg(target_os = "windows")] +fn lock_drawing(drawing: &File) -> Result<(), TryLockError> { + drawing.try_lock_shared() +} + +#[cfg(not(target_os = "windows"))] +fn lock_drawing(drawing: &File) -> Result<(), TryLockError> { + drawing.try_lock() +} + fn acquire_drawing_lock(path: &Path) -> Result<(Option, Option), EditLeaseError> { - let drawing = match OpenOptions::new().read(true).write(true).open(path) { + let drawing = match open_drawing_for_lease(path) { Ok(file) => file, Err(error) if error_is_lock_conflict(&error) => { return Err(EditLeaseError::Locked(format!( @@ -255,7 +289,7 @@ fn acquire_drawing_lock(path: &Path) -> Result<(Option, Option), E } }; - match drawing.try_lock() { + match lock_drawing(&drawing) { Ok(()) => Ok((Some(drawing), None)), Err(TryLockError::WouldBlock) => Err(EditLeaseError::Locked( "Drawing is locked by another application.".to_string(),