Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 1 | //! Lazy workspaces on Windows: the workspace folder is a sync root of the |
| 2 | //! Cloud Files API, the one OneDrive's files on demand use. It's built into |
| 3 | //! Windows 10 (1709) and later: nothing to install, no admin rights. Every |
| 4 | //! file of the tree is a placeholder in the folder from the start, at its |
| 5 |
| 6 | //! --serve`, which fetches it from the host. From then on it's an ordinary |
| 7 | //! file, and edits stay in the folder. The workspace's own `.onset` sits in |
| 8 | //! the folder as in any workspace, and the backing (ONSET_HOME/lazy/<id>) |
| 9 | //! keeps only the tree's index and the server's log. |
| 10 | //! |
| 11 | //! Each placeholder is made with its content's ID and its path, and |
| 12 | //! Windows keeps it "in sync" until something changes it, so a save can |
| 13 | //! tell a file that's still as it was made without reading (and so |
| 14 | //! fetching) it. After a restart the placeholders are still there; ones not |
| 15 | //! fetched yet open once the next `on` command (or `on mount`) has started |
| 16 | //! the server again. |
| 17 | //! |
| 18 | //! Git Bash's `mv` can't move a file that isn't fetched yet, and nothing |
| 19 | //! here can change that. Windows marks every such placeholder |
| 20 | //! FILE_ATTRIBUTE_RECALL_ON_DATA_ACCESS and OFFLINE (setting the |
| 21 | //! attributes doesn't take them off). Git Bash's runtime (MSYS2's, as |
| 22 | //! Cygwin's) sees placeholders as they are (PHCM_EXPOSE_PLACEHOLDERS), |
| 23 | //! doesn't read a marked file's reparse tag, and its rename() then answers |
| 24 | //! EBUSY for any reparse point that isn't a symlink, taking it for a volume |
| 25 | //! mount point. That happens before Windows is asked to move anything, so |
| 26 | //! no callback or policy of the sync root's comes into it, and the only |
| 27 | //! callback an open gives (NOTIFY_FILE_OPEN_COMPLETION) doesn't hold the |
| 28 | //! open up. Once fetched, the file isn't marked: the runtime reads its tag, |
| 29 | //! doesn't know it, and moves it. Windows' own moves work, as do Git |
| 30 | //! Bash's `rm` and its `mv` of a folder (a folder isn't marked). |
| 31 |
| 32 | use std::collections::BTreeMap; |
| 33 | use std::ffi::{OsStr, c_void}; |
| 34 | use std::os::windows::ffi::OsStrExt; |
| 35 | use std::path::Path; |
| 36 |
| 37 | use anyhow::{Context, Result, anyhow}; |
| 38 | use onset_core::Id; |
| 39 | use onset_core::tree::{self, EntryKind}; |
| 40 | use windows_sys::Win32::Foundation::{CloseHandle, E_FAIL, HANDLE, NTSTATUS, STATUS_CLOUD_FILE_UNSUCCESSFUL, STATUS_SUCCESS}; |
| 41 | use windows_sys::Win32::Storage::CloudFilters::*; |
| 42 | use windows_sys::Win32::Storage::FileSystem::{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_BASIC_INFO, FILE_READ_ATTRIBUTES, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE}; |
| 43 | use windows_sys::Win32::System::LibraryLoader::{GetProcAddress, LOAD_LIBRARY_SEARCH_SYSTEM32, LoadLibraryExW}; |
| 44 | use windows_sys::Win32::System::Threading::{CreateEventW, EVENT_MODIFY_STATE, INFINITE, OpenEventW, SYNCHRONIZATION_SYNCHRONIZE, SetEvent, WaitForSingleObject}; |
| 45 | use windows_sys::core::{GUID, HRESULT, PCWSTR}; |
| 46 |
| 47 | use crate::app::App; |
| 48 | use crate::db::Workspace; |
| 49 | use crate::lazy::{MTIME_NS, backing, workspace_at}; |
| 50 | use crate::objects::Store; |
| 51 | use crate::remote::Remote; |
| 52 | use crate::ws::{self, Index}; |
| 53 |
| 54 | /// What Windows knows the sync roots by. |
| 55 | const PROVIDER: &str = "Onset"; |
| 56 | const PROVIDER_ID: GUID = GUID::from_u128(0x5f0c_2a7e_93d4_4b61_8a2e_0d7c_4f19_b3a6); |
| 57 | /// 100-ns ticks from 1601, where Windows' file times start, to 1970. |
| 58 | const TICKS_TO_1970: i64 = 116_444_736_000_000_000; |
| 59 | /// How much of a file goes to Windows at once while it's fetched. |
| 60 | const CHUNK: usize = 1 << 20; |
| 61 |
| 62 | /// The Cloud Files API's functions, from cldapi.dll the first time a lazy |
| 63 | /// workspace needs one, so `on` still starts on a Windows without it (10 |
| 64 | /// before version 1709). There each answers ERROR_NOT_SUPPORTED. |
| 65 | macro_rules! cloud_files { |
| 66 | ($($name:ident($($arg:ident: $ty:ty),*);)*) => { |
| 67 | #[allow(non_snake_case)] |
| 68 | struct Api { |
| 69 | $($name: unsafe extern "system" fn($($ty),*) -> HRESULT,)* |
| 70 | } |
| 71 |
| 72 | fn api() -> Option<&'static Api> { |
| 73 | static API: std::sync::OnceLock<Option<Api>> = std::sync::OnceLock::new(); |
| 74 | API.get_or_init(|| { |
| 75 | // SAFETY: a system DLL by name, and functions of its with these signatures. |
| 76 | unsafe { |
| 77 | let lib = LoadLibraryExW(wide("cldapi.dll").as_ptr(), std::ptr::null_mut(), LOAD_LIBRARY_SEARCH_SYSTEM32); |
| 78 | if lib.is_null() { |
| 79 | return None; |
| 80 | } |
| 81 | Some(Api { |
| 82 | $($name: std::mem::transmute::<unsafe extern "system" fn() -> isize, unsafe extern "system" fn($($ty),*) -> HRESULT>( |
| 83 | GetProcAddress(lib, concat!(stringify!($name), "\0").as_ptr())?, |
| 84 | ),)* |
| 85 | }) |
| 86 | } |
| 87 | }) |
| 88 | .as_ref() |
| 89 | } |
| 90 |
| 91 | $( |
| 92 | #[allow(non_snake_case)] |
| 93 | unsafe fn $name($($arg: $ty),*) -> HRESULT { |
| 94 | match api() { |
| 95 | // SAFETY: the caller's, as for the function itself. |
| 96 | Some(api) => unsafe { (api.$name)($($arg),*) }, |
| 97 | None => from_win32(50), |
| 98 | } |
| 99 | } |
| 100 | )* |
| 101 | }; |
| 102 | } |
| 103 |
| 104 | cloud_files! { |
| 105 | CfRegisterSyncRoot(path: PCWSTR, registration: *const CF_SYNC_REGISTRATION, policies: *const CF_SYNC_POLICIES, flags: CF_REGISTER_FLAGS); |
| 106 | CfUnregisterSyncRoot(path: PCWSTR); |
| 107 | CfGetSyncRootInfoByPath(path: PCWSTR, class: CF_SYNC_ROOT_INFO_CLASS, buffer: *mut c_void, length: u32, returned: *mut u32); |
| 108 | CfCreatePlaceholders(base: PCWSTR, items: *mut CF_PLACEHOLDER_CREATE_INFO, count: u32, flags: CF_CREATE_FLAGS, done: *mut u32); |
| 109 | CfGetPlaceholderInfo(file: HANDLE, class: CF_PLACEHOLDER_INFO_CLASS, buffer: *mut c_void, length: u32, returned: *mut u32); |
| 110 | CfConnectSyncRoot(path: PCWSTR, callbacks: *const CF_CALLBACK_REGISTRATION, context: *const c_void, flags: CF_CONNECT_FLAGS, key: *mut CF_CONNECTION_KEY); |
| 111 | CfDisconnectSyncRoot(key: CF_CONNECTION_KEY); |
| 112 | CfExecute(op: *const CF_OPERATION_INFO, params: *mut CF_OPERATION_PARAMETERS); |
| 113 | } |
| 114 |
| 115 | /// A string or path as Windows' API takes it: UTF-16, NUL-terminated. Long |
| 116 | /// paths get the `\\?\` prefix that lifts the 260-character limit. |
| 117 | fn wide(s: impl AsRef<OsStr>) -> Vec<u16> { |
| 118 | s.as_ref().encode_wide().chain(Some(0)).collect() |
| 119 | } |
| 120 |
| 121 | fn wide_path(path: &Path) -> Vec<u16> { |
| 122 | if path.as_os_str().len() < 248 || path.to_string_lossy().starts_with(r"\\") { |
| 123 | return wide(path); |
| 124 | } |
| 125 | let mut long = std::ffi::OsString::from(r"\\?\"); |
| 126 | long.push(path.as_os_str()); |
| 127 | wide(long) |
| 128 | } |
| 129 |
| 130 | /// The `len` bytes `offset` bytes into a buffer Windows filled, if they're inside it. |
| 131 | fn tail(buf: &[u64], offset: usize, len: usize) -> Option<&[u8]> { |
| 132 | // SAFETY: the buffer's bytes, as bytes. |
| 133 | let bytes = unsafe { std::slice::from_raw_parts(buf.as_ptr() as *const u8, buf.len() * 8) }; |
| 134 | bytes.get(offset..offset.checked_add(len)?) |
| 135 | } |
| 136 |
| 137 | /// An HRESULT as words: one that carries a Windows error reads as that error. |
| 138 | fn os_error(hr: HRESULT) -> String { |
| 139 | if (hr as u32) >> 16 == 0x8007 { std::io::Error::from_raw_os_error(hr & 0xFFFF).to_string() } else { format!("error {:#010x}", hr as u32) } |
| 140 | } |
| 141 |
| 142 | const fn from_win32(code: u32) -> HRESULT { |
| 143 | (0x8007_0000 | code) as HRESULT |
| 144 | } |
| 145 |
| 146 | /// A time as Windows keeps file times. |
| 147 | fn ticks(ns: i64) -> i64 { |
| 148 | ns / 100 + TICKS_TO_1970 |
| 149 | } |
| 150 |
| 151 | /// What a placeholder is made with: its content's ID and its path. |
| 152 | fn identity(id: &Id, path: &str) -> Vec<u8> { |
| 153 | format!("{}\n{path}", id.hex()).into_bytes() |
| 154 | } |
| 155 |
| 156 | /// The named event a running server holds: it exists while the server |
| 157 | /// does, and setting it stops the server. |
| 158 | fn event_name(workspace: &str) -> Vec<u16> { |
| 159 | wide(format!("Local\\onset-lazy-{workspace}")) |
| 160 | } |
| 161 |
| 162 | /// Whether `root` is a lazy workspace's folder: a sync root of Onset's for |
| 163 | /// the workspace there, its placeholders in place (the server may be |
| 164 | /// stopped). |
| 165 | pub fn mounted(root: &Path) -> bool { |
| 166 | let Ok(id) = ws::read_id(root) else { return false }; |
| 167 | if !crate::lazy::is_lazy(&id) { |
| 168 | return false; |
| 169 | } |
| 170 | // The info, its provider's name, and room for the workspace's ID after it. |
| 171 | let mut buf = vec![0u64; 512]; |
| 172 | let size = (buf.len() * 8) as u32; |
| 173 | let mut got = 0u32; |
| 174 | // SAFETY: a NUL-terminated path, and a buffer of `size` bytes to fill. |
| 175 | if unsafe { CfGetSyncRootInfoByPath(wide_path(root).as_ptr(), CF_SYNC_ROOT_INFO_STANDARD, buf.as_mut_ptr().cast(), size, &mut got) } < 0 { |
| 176 | return false; |
| 177 | } |
| 178 | // SAFETY: the buffer is aligned for, and holds, a CF_SYNC_ROOT_STANDARD_INFO. |
| 179 | let info = unsafe { &*(buf.as_ptr() as *const CF_SYNC_ROOT_STANDARD_INFO) }; |
| 180 | let name = String::from_utf16_lossy(&info.ProviderName[..info.ProviderName.iter().position(|c| *c == 0).unwrap_or(info.ProviderName.len())]); |
| 181 | let identity = tail(&buf, std::mem::offset_of!(CF_SYNC_ROOT_STANDARD_INFO, SyncRootIdentity), info.SyncRootIdentityLength as usize); |
| 182 | name == PROVIDER && identity == Some(id.as_bytes()) |
| 183 | } |
| 184 |
| 185 | /// Whether the server for the workspace at `root` is running. |
| 186 | fn running(root: &Path) -> bool { |
| 187 | let Ok(id) = ws::read_id(root) else { return false }; |
| 188 | // SAFETY: a NUL-terminated name; the handle is closed at once. |
| 189 | unsafe { |
| 190 | let event = OpenEventW(SYNCHRONIZATION_SYNCHRONIZE, 0, event_name(&id).as_ptr()); |
| 191 | if event.is_null() { |
| 192 | return false; |
| 193 | } |
| 194 | CloseHandle(event); |
| 195 | } |
| 196 | true |
| 197 | } |
| 198 |
| 199 | /// Makes `root` a sync root of Onset's, for workspace `id`. |
| 200 | fn register(root: &Path, id: &str) -> Result<()> { |
| 201 | let name = wide(PROVIDER); |
| 202 | let version = wide(crate::VERSION); |
| 203 | let registration = CF_SYNC_REGISTRATION { |
| 204 | StructSize: std::mem::size_of::<CF_SYNC_REGISTRATION>() as u32, |
| 205 | ProviderName: name.as_ptr(), |
| 206 | ProviderVersion: version.as_ptr(), |
| 207 | SyncRootIdentity: id.as_ptr().cast(), |
| 208 | SyncRootIdentityLength: id.len() as u32, |
| 209 | FileIdentity: std::ptr::null(), |
| 210 | FileIdentityLength: 0, |
| 211 | ProviderId: PROVIDER_ID, |
| 212 | }; |
| 213 | let policies = CF_SYNC_POLICIES { |
| 214 | StructSize: std::mem::size_of::<CF_SYNC_POLICIES>() as u32, |
| 215 | // A file comes whole the first time it's read. |
| 216 | Hydration: CF_HYDRATION_POLICY { Primary: CF_HYDRATION_POLICY_FULL, Modifier: CF_HYDRATION_POLICY_MODIFIER_NONE }, |
| 217 | // Every folder's placeholders are made up front. |
| 218 | Population: CF_POPULATION_POLICY { Primary: CF_POPULATION_POLICY_ALWAYS_FULL, Modifier: CF_POPULATION_POLICY_MODIFIER_NONE }, |
| 219 | // Any change to a file, its times or attributes takes it out of sync. |
| 220 | InSync: CF_INSYNC_POLICY_TRACK_ALL, |
| 221 | HardLink: CF_HARDLINK_POLICY_NONE, |
| 222 | PlaceholderManagement: CF_PLACEHOLDER_MANAGEMENT_POLICY_DEFAULT, |
| 223 | }; |
| 224 | let flags = CF_REGISTER_FLAG_DISABLE_ON_DEMAND_POPULATION_ON_ROOT | CF_REGISTER_FLAG_MARK_IN_SYNC_ON_ROOT; |
| 225 | let path = wide_path(root); |
| 226 | // SAFETY: the registration's strings and identity outlive the calls. |
| 227 | let mut hr = unsafe { CfRegisterSyncRoot(path.as_ptr(), ®istration, &policies, flags) }; |
| 228 | if hr < 0 { |
| 229 | // Registered already (by Onset, as something else saw it): it's registered again. |
| 230 | hr = unsafe { CfRegisterSyncRoot(path.as_ptr(), ®istration, &policies, flags | CF_REGISTER_FLAG_UPDATE) }; |
| 231 | } |
| 232 | if hr < 0 { |
| 233 | return Err(anyhow!( |
| 234 | "couldn't make {} a lazy workspace: {} (it needs a folder on an NTFS disk, outside OneDrive and other synced folders)", |
| 235 | root.display(), |
| 236 | os_error(hr) |
| 237 | )); |
| 238 | } |
| 239 | Ok(()) |
| 240 | } |
| 241 |
| 242 | /// A file or folder a placeholder is made for. |
| 243 | enum Item { |
| 244 | Folder, |
| 245 | /// Content, size, time shown (ns). |
| 246 | File(Id, u64, i64), |
| 247 | } |
| 248 |
| 249 | /// Makes a placeholder for every file and folder of the tree `base` |
| 250 | /// indexes that isn't in the folder yet, each at the size and time the |
| 251 | /// index has for it. |
| 252 | fn place(root: &Path, base: &Index) -> Result<()> { |
| 253 | let split = |p: &str| -> (String, String) { |
| 254 | match p.rsplit_once('/') { |
| 255 | Some((dir, name)) => (dir.to_string(), name.to_string()), |
| 256 | None => (String::new(), p.to_string()), |
| 257 | } |
| 258 | }; |
| 259 | let mut dirs: BTreeMap<String, BTreeMap<String, Item>> = BTreeMap::new(); |
| 260 | for (path, e) in &base.entries { |
| 261 | // Never Onset's or git's own folders, or a name this system can't have. |
| 262 | if !tree::valid_path(path) || !path.split('/').all(crate::sys::fits) { |
| 263 | return Err(anyhow!("{path} can't be written here: this system can't have a file by that name")); |
| 264 | } |
| 265 | let item = if e.entry.kind == EntryKind::Gitlink { Item::Folder } else { Item::File(e.entry.id, e.stat.size, e.stat.mtime_ns) }; |
| 266 | let (dir, name) = split(path); |
| 267 | dirs.entry(dir.clone()).or_default().insert(name, item); |
| 268 | // Its folders, up to the root. |
| 269 | let mut d = dir; |
| 270 | while !d.is_empty() { |
| 271 | let (up, name) = split(&d); |
| 272 | let kids = dirs.entry(up.clone()).or_default(); |
| 273 | if kids.contains_key(&name) { |
| 274 | break; |
| 275 | } |
| 276 | kids.insert(name, Item::Folder); |
| 277 | d = up; |
| 278 | } |
| 279 | } |
| 280 | let empty = onset_core::object::Object::blob(Vec::new()).id(); |
| 281 | // A folder before what's in it: "a" sorts before "a/b". |
| 282 | for (dir, all) in &dirs { |
| 283 | let at = if dir.is_empty() { root.to_path_buf() } else { root.join(dir) }; |
| 284 | let full = |name: &str| if dir.is_empty() { name.to_string() } else { format!("{dir}/{name}") }; |
| 285 | // Empty files are made as they are: there's nothing to fetch. |
| 286 | let (empties, kids): (Vec<_>, Vec<_>) = all.iter().partition(|(_, item)| matches!(item, Item::File(id, 0, _) if *id == empty)); |
| 287 | for (name, item) in empties { |
| 288 | let file = at.join(name); |
| 289 | if let Item::File(_, _, ns) = item |
| 290 | && !file.exists() |
| 291 | { |
| 292 | std::fs::File::create(&file)?.set_modified(std::time::UNIX_EPOCH + std::time::Duration::from_nanos(*ns as u64))?; |
| 293 | } |
| 294 | } |
| 295 | if kids.is_empty() { |
| 296 | continue; |
| 297 | } |
| 298 | let names: Vec<Vec<u16>> = kids.iter().map(|(name, _)| wide(name)).collect(); |
| 299 | let identities: Vec<Vec<u8>> = kids |
| 300 | .iter() |
| 301 | .map(|(name, item)| match *item { |
| 302 | Item::File(id, ..) => identity(id, &full(name)), |
| 303 | Item::Folder => format!("folder\n{}", full(name)).into_bytes(), |
| 304 | }) |
| 305 | .collect(); |
| 306 | let mut infos: Vec<CF_PLACEHOLDER_CREATE_INFO> = kids |
| 307 | .iter() |
| 308 | .enumerate() |
| 309 | .map(|(i, (_, item))| { |
| 310 | let (attributes, size, time, flags) = match *item { |
| 311 | Item::Folder => (FILE_ATTRIBUTE_DIRECTORY, 0, ticks(MTIME_NS), CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC | CF_PLACEHOLDER_CREATE_FLAG_DISABLE_ON_DEMAND_POPULATION), |
| 312 | Item::File(_, size, ns) => (FILE_ATTRIBUTE_NORMAL, *size as i64, ticks(*ns), CF_PLACEHOLDER_CREATE_FLAG_MARK_IN_SYNC), |
| 313 | }; |
| 314 | CF_PLACEHOLDER_CREATE_INFO { |
| 315 | RelativeFileName: names[i].as_ptr(), |
| 316 | FsMetadata: CF_FS_METADATA { |
| 317 | BasicInfo: FILE_BASIC_INFO { CreationTime: time, LastAccessTime: time, LastWriteTime: time, ChangeTime: time, FileAttributes: attributes }, |
| 318 | FileSize: size, |
| 319 | }, |
| 320 | FileIdentity: identities[i].as_ptr().cast(), |
| 321 | FileIdentityLength: identities[i].len() as u32, |
| 322 | Flags: flags, |
| 323 | // Until Windows says otherwise. |
| 324 | Result: E_FAIL, |
| 325 | CreateUsn: 0, |
| 326 | } |
| 327 | }) |
| 328 | .collect(); |
| 329 | let mut done = 0u32; |
| 330 | // SAFETY: each entry's name and identity outlive the call. |
| 331 | let hr = unsafe { CfCreatePlaceholders(wide_path(&at).as_ptr(), infos.as_mut_ptr(), infos.len() as u32, CF_CREATE_FLAG_NONE, &mut done) }; |
| 332 | for ((name, _), info) in kids.iter().zip(&infos) { |
| 333 | // Already there (a folder copied from another workspace, say): it stays. |
| 334 | if info.Result < 0 && info.Result != from_win32(183) && info.Result != from_win32(80) { |
| 335 | let why = if info.Result == E_FAIL && hr < 0 { os_error(hr) } else { os_error(info.Result) }; |
| 336 | return Err(anyhow!("couldn't make {} in the lazy workspace: {why}", full(name))); |
| 337 | } |
| 338 | } |
| 339 | } |
| 340 | Ok(()) |
| 341 | } |
| 342 |
| 343 | /// Starts the server for a lazy workspace, if it isn't running: the folder |
| 344 | /// becomes a sync root with every file's placeholder the first time, then |
| 345 | /// `on mount --serve` runs in the background, and this is back once it |
| 346 | /// answers. |
| 347 | pub fn mount(w: &Workspace) -> Result<()> { |
| 348 | if running(&w.path) { |
| 349 | return Ok(()); |
| 350 | } |
| 351 | if api().is_none() { |
| 352 | return Err(anyhow!("lazy workspaces need Windows 10 version 1709 or later, which has the Cloud Files API")); |
| 353 | } |
| 354 | let dir = backing(&w.id); |
| 355 | if !ws::is_workspace(&w.path) { |
| 356 | return Err(anyhow!("{} has lost its {} folder, so its files can't come back; on get <project> <folder> --lazy makes a new one", w.path.display(), ws::DIR)); |
| 357 | } |
| 358 | if !mounted(&w.path) { |
| 359 | register(&w.path, &w.id)?; |
| 360 | } |
| 361 | let placed = dir.join(ws::DIR).join("placed"); |
| 362 | if !placed.exists() { |
| 363 | let base = Index::load_from(&dir, "lazy-base").ok_or_else(|| anyhow!("{} has no lazy index", dir.display()))?; |
| 364 | place(&w.path, &base)?; |
| 365 | std::fs::write(&placed, "")?; |
| 366 | } |
| 367 | let log = dir.join(ws::DIR).join("mount.log"); |
| 368 | let out = std::fs::File::create(&log)?; |
| 369 | let mut command = std::process::Command::new(std::env::current_exe()?); |
| 370 | command.arg("mount").arg("--serve").arg(&w.path).current_dir(&dir).stdin(std::process::Stdio::null()).stdout(out.try_clone()?).stderr(out); |
| 371 | crate::sys::detach(&mut command); |
| 372 | let mut child = command.spawn()?; |
| 373 | for _ in 0..200 { |
| 374 | if running(&w.path) { |
| 375 | return Ok(()); |
| 376 | } |
| 377 | if let Ok(Some(status)) = child.try_wait() { |
| 378 | let said = std::fs::read_to_string(&log).unwrap_or_default(); |
| 379 | return Err(anyhow!("the mount at {} stopped ({status}): {}", w.path.display(), said.trim())); |
| 380 | } |
| 381 | std::thread::sleep(std::time::Duration::from_millis(50)); |
| 382 | } |
| 383 | Err(anyhow!("the mount at {} didn't start; see {}", w.path.display(), log.display())) |
| 384 | } |
| 385 |
| 386 | /// Stops a lazy workspace's server. Its placeholders stay; files not |
| 387 | /// fetched yet can't be opened until it runs again. |
| 388 | pub fn unmount(root: &Path) -> Result<()> { |
| 389 | if !running(root) { |
| 390 | return Ok(()); |
| 391 | } |
| 392 | let id = ws::read_id(root)?; |
| 393 | // SAFETY: a NUL-terminated name; the handle is closed at once. |
| 394 | unsafe { |
| 395 | let event = OpenEventW(EVENT_MODIFY_STATE, 0, event_name(&id).as_ptr()); |
| 396 | if !event.is_null() { |
| 397 | SetEvent(event); |
| 398 | CloseHandle(event); |
| 399 | } |
| 400 | } |
| 401 | for _ in 0..200 { |
| 402 | if !running(root) { |
| 403 | return Ok(()); |
| 404 | } |
| 405 | std::thread::sleep(std::time::Duration::from_millis(50)); |
| 406 | } |
| 407 | Err(anyhow!("couldn't stop the mount at {} (is something still using it?)", root.display())) |
| 408 | } |
| 409 |
| 410 | /// Lets go of a lazy workspace's folder before it's deleted: it's no longer |
| 411 | /// a sync root, and files not fetched yet go with that. |
| 412 | pub fn forget(root: &Path) { |
| 413 | // SAFETY: a NUL-terminated path. |
| 414 | unsafe { CfUnregisterSyncRoot(wide_path(root).as_ptr()) }; |
| 415 | } |
| 416 |
| 417 | /// Whether file `path` of the workspace at `root` is a placeholder just as |
| 418 | /// it was made for that path with `id`'s content: Windows says it's in sync |
| 419 | /// (nothing has changed it), and it was made for that path and content. |
| 420 | /// Opening it for its attributes never fetches it. Only lazy workspaces |
| 421 | /// are asked; whether a folder is one is remembered for the process. |
| 422 | pub fn untouched(root: &Path, path: &str, id: &Id) -> bool { |
| 423 | use std::os::windows::fs::OpenOptionsExt; |
| 424 | use std::os::windows::io::AsRawHandle; |
| 425 | static LAZY: std::sync::Mutex<BTreeMap<std::path::PathBuf, bool>> = std::sync::Mutex::new(BTreeMap::new()); |
| 426 | let lazy = *LAZY |
| 427 | .lock() |
| 428 | .unwrap_or_else(|e| e.into_inner()) |
| 429 | .entry(root.to_path_buf()) |
| 430 | .or_insert_with(|| ws::read_id(root).is_ok_and(|id| crate::lazy::is_lazy(&id))); |
| 431 | if !lazy { |
| 432 | return false; |
| 433 | } |
| 434 | let Ok(file) = std::fs::OpenOptions::new().access_mode(FILE_READ_ATTRIBUTES).share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE).open(root.join(path)) else { |
| 435 | return false; |
| 436 | }; |
| 437 | // The info and an identity of up to 4 KiB. |
| 438 | let mut buf = vec![0u64; 640]; |
| 439 | let size = (buf.len() * 8) as u32; |
| 440 | let mut got = 0u32; |
| 441 | // SAFETY: an open handle, and a buffer of `size` bytes to fill. |
| 442 | if unsafe { CfGetPlaceholderInfo(file.as_raw_handle() as HANDLE, CF_PLACEHOLDER_INFO_BASIC, buf.as_mut_ptr().cast(), size, &mut got) } < 0 { |
| 443 | return false; |
| 444 | } |
| 445 | // SAFETY: the buffer is aligned for, and holds, a CF_PLACEHOLDER_BASIC_INFO. |
| 446 | let info = unsafe { &*(buf.as_ptr() as *const CF_PLACEHOLDER_BASIC_INFO) }; |
| 447 | let made = tail(&buf, std::mem::offset_of!(CF_PLACEHOLDER_BASIC_INFO, FileIdentity), info.FileIdentityLength as usize); |
| 448 | info.InSyncState == CF_IN_SYNC_STATE_IN_SYNC && made == Some(identity(id, path).as_slice()) |
| 449 | } |
| 450 |
| 451 | /// What the server needs to fetch a file. |
| 452 | struct Server { |
| 453 | tree: Id, |
| 454 | store: Store, |
| 455 | remote: Remote, |
| 456 | } |
| 457 |
| 458 | impl Server { |
| 459 | /// A file's content from the store, fetched from the host first if it isn't here. |
| 460 | fn content(&self, id: &Id, path: &str) -> Result<Vec<u8>> { |
| 461 | if let Ok(bytes) = self.store.read_file(id) { |
| 462 | return Ok(bytes); |
| 463 | } |
| 464 | let bundle = self.remote.post_for_bytes(&format!("/trees/{}/files", self.tree), &serde_json::json!({ "paths": [path] }))?; |
| 465 | self.store.ingest(&bundle)?; |
| 466 | self.store.read_file(id) |
| 467 | } |
| 468 | } |
| 469 |
| 470 | /// `on mount --serve <folder>`: answers Windows' requests for the |
| 471 | /// workspace's files until `on mount --stop`. |
| 472 | pub fn serve(app: &App, root: &Path) -> Result<()> { |
| 473 | let w = workspace_at(app, root)?.filter(|w| w.path == root).ok_or_else(|| anyhow!("{} isn't a lazy workspace on this machine", root.display()))?; |
| 474 | let project = app.db.project(&w.project)?.ok_or_else(|| anyhow!("the workspace's project isn't known here"))?; |
| 475 | let dir = backing(&w.id); |
| 476 | let base = Index::load_from(&dir, "lazy-base").ok_or_else(|| anyhow!("{} has no lazy index", dir.display()))?; |
| 477 | let tree = base.tree.ok_or_else(|| anyhow!("{} has no tree", dir.display()))?; |
| 478 | // Windows calls back on threads of its own for as long as this runs. |
| 479 | let server: &'static Server = |
| 480 | Box::leak(Box::new(Server { tree, store: Store::open(&crate::paths::objects()).context("opening the object store")?, remote: Remote::for_project(&project) })); |
| 481 | // Only fetches: moves and deletes need no say from the server. |
| 482 | let callbacks = [ |
| 483 | CF_CALLBACK_REGISTRATION { Type: CF_CALLBACK_TYPE_FETCH_DATA, Callback: Some(fetch_data) }, |
| 484 | CF_CALLBACK_REGISTRATION { Type: CF_CALLBACK_TYPE_NONE, Callback: None }, |
| 485 | ]; |
| 486 | let mut key: CF_CONNECTION_KEY = 0; |
| 487 | // SAFETY: a NUL-terminated path, a callback table ending in NONE, and a |
| 488 | // context that lives as long as the process. |
| 489 | let hr = unsafe { CfConnectSyncRoot(wide_path(root).as_ptr(), callbacks.as_ptr(), server as *const Server as *const c_void, CF_CONNECT_FLAG_NONE, &mut key) }; |
| 490 | if hr == from_win32(378) { |
| 491 | // Another server has it already. |
| 492 | return Ok(()); |
| 493 | } |
| 494 | if hr < 0 { |
| 495 | return Err(anyhow!("couldn't serve {}: {}", root.display(), os_error(hr))); |
| 496 | } |
| 497 | // SAFETY: a NUL-terminated name; the event is waited on, then closed. |
| 498 | unsafe { |
| 499 | let stop = CreateEventW(std::ptr::null(), 1, 0, event_name(&w.id).as_ptr()); |
| 500 | if stop.is_null() { |
| 501 | CfDisconnectSyncRoot(key); |
| 502 | return Err(anyhow!("couldn't serve {}: {}", root.display(), std::io::Error::last_os_error())); |
| 503 | } |
| 504 | WaitForSingleObject(stop, INFINITE); |
| 505 | CfDisconnectSyncRoot(key); |
| 506 | CloseHandle(stop); |
| 507 | } |
| 508 | Ok(()) |
| 509 | } |
| 510 |
| 511 | /// Windows wants a placeholder's content: the whole file goes to it, from |
| 512 | /// the start of the range it asked for. |
| 513 | unsafe extern "system" fn fetch_data(info: *const CF_CALLBACK_INFO, params: *const CF_CALLBACK_PARAMETERS) { |
| 514 | // A panic can't cross into Windows; the fetch fails instead. |
| 515 | let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { |
| 516 | // SAFETY: Windows passes valid info and parameters for the call's length. |
| 517 | let (info, fetch) = unsafe { (&*info, (*params).Anonymous.FetchData) }; |
| 518 | // Transfers start on a 4 KiB boundary; failing one names the rest of the file. |
| 519 | let start = fetch.RequiredFileOffset & !4095; |
| 520 | let rest = info.FileSize - start; |
| 521 | // SAFETY: Windows' context is the server, and its identity is the placeholder's. |
| 522 | let server = unsafe { &*(info.CallbackContext as *const Server) }; |
| 523 | let made: &[u8] = |
| 524 | if info.FileIdentity.is_null() { &[] } else { unsafe { std::slice::from_raw_parts(info.FileIdentity as *const u8, info.FileIdentityLength as usize) } }; |
| 525 | let found = std::str::from_utf8(made).ok().and_then(|t| t.split_once('\n')).and_then(|(id, path)| Some((Id::parse(id)?, path))); |
| 526 | let Some((id, path)) = found else { |
| 527 | eprintln!("on mount: a placeholder Onset didn't make was asked for"); |
| 528 | return transfer(info, start, None, rest); |
| 529 | }; |
| 530 | match server.content(&id, path) { |
| 531 | Ok(bytes) if bytes.len() as i64 == info.FileSize => { |
| 532 | if bytes.is_empty() { |
| 533 | return transfer(info, 0, Some(&bytes), 0); |
| 534 | } |
| 535 | let mut at = start; |
| 536 | while at < info.FileSize { |
| 537 | let end = (at + CHUNK as i64).min(info.FileSize); |
| 538 | let hr = transfer(info, at, Some(&bytes[at as usize..end as usize]), end - at); |
| 539 | if hr < 0 { |
| 540 | eprintln!("on mount: couldn't hand over {path}: {}", os_error(hr)); |
| 541 | return hr; |
| 542 | } |
| 543 | at = end; |
| 544 | } |
| 545 | 0 |
| 546 | } |
| 547 | Ok(bytes) => { |
| 548 | eprintln!("on mount: {path} is {} bytes, not the {} its placeholder says", bytes.len(), info.FileSize); |
| 549 | transfer(info, start, None, rest) |
| 550 | } |
| 551 | Err(e) => { |
| 552 | eprintln!("on mount: couldn't fetch {path}: {e:#}"); |
| 553 | transfer(info, start, None, rest) |
| 554 | } |
| 555 | } |
| 556 | })); |
| 557 | } |
| 558 |
| 559 | /// Gives Windows `len` bytes of a placeholder from `at`, or (with no bytes) |
| 560 | /// fails its fetch. |
| 561 | fn transfer(info: &CF_CALLBACK_INFO, at: i64, bytes: Option<&[u8]>, len: i64) -> HRESULT { |
| 562 | let op = CF_OPERATION_INFO { |
| 563 | StructSize: std::mem::size_of::<CF_OPERATION_INFO>() as u32, |
| 564 | Type: CF_OPERATION_TYPE_TRANSFER_DATA, |
| 565 | ConnectionKey: info.ConnectionKey, |
| 566 | TransferKey: info.TransferKey, |
| 567 | CorrelationVector: info.CorrelationVector, |
| 568 | SyncStatus: std::ptr::null(), |
| 569 | RequestKey: info.RequestKey, |
| 570 | }; |
| 571 | let status: NTSTATUS = if bytes.is_some() { STATUS_SUCCESS } else { STATUS_CLOUD_FILE_UNSUCCESSFUL }; |
| 572 | let mut params = CF_OPERATION_PARAMETERS { |
| 573 | ParamSize: (std::mem::offset_of!(CF_OPERATION_PARAMETERS, Anonymous) + std::mem::size_of::<CF_OPERATION_PARAMETERS_0_0>()) as u32, |
| 574 | ..Default::default() |
| 575 | }; |
| 576 | params.Anonymous.TransferData = CF_OPERATION_PARAMETERS_0_0 { |
| 577 | Flags: CF_OPERATION_TRANSFER_DATA_FLAG_NONE, |
| 578 | CompletionStatus: status, |
| 579 | Buffer: bytes.map_or(std::ptr::null(), |b| b.as_ptr().cast()), |
| 580 | Offset: at, |
| 581 | Length: len, |
| 582 | }; |
| 583 | // SAFETY: the operation's keys are the callback's, and `bytes` outlives the call. |
| 584 | unsafe { CfExecute(&op, &mut params) } |
| 585 | } |
| 586 |