Skip to main content

vfs_host/
lib.rs

1// VFS Host Trait Implementation
2//
3// This library implements wasmtime Host traits using fs-core directly.
4// This enables true dynamic linking where multiple applications can share a single
5// VFS instance at runtime, unlike wasi-virt which creates isolated VFS per app.
6
7use anyhow::Result;
8use std::sync::Arc;
9
10// Re-export fs-core types so users don't need to depend on fs-core directly
11pub use fs_core::{Fs, O_CREAT, O_RDONLY, O_RDWR, O_TRUNC, O_WRONLY};
12use wasmtime::component::ResourceTable;
13use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiView};
14
15pub mod filesystem_preopens;
16pub mod filesystem_types;
17
18// S3 sync support (optional)
19#[cfg(feature = "s3-sync")]
20mod sync_hooks;
21#[cfg(feature = "s3-sync")]
22pub use sync_hooks::{NoOpSyncHooks, S3SyncHooks, SyncHooks};
23#[cfg(feature = "s3-sync")]
24pub use vfs_sync_host::{init_from_s3, HostSyncManager, S3Storage, SyncConfig};
25
26/// Wrapper for fs-core file descriptor stored in ResourceTable.
27/// Contains the fd and optionally the path for directory descriptors.
28#[derive(Clone)]
29pub struct FsDescriptorWrapper {
30    /// fs-core file descriptor
31    pub fd: u32,
32    /// Path for directory descriptors (used for relative path resolution)
33    pub path: Option<String>,
34}
35
36/// Wrapper for directory entry stream stored in ResourceTable.
37/// Caches the directory listing and tracks iteration position.
38#[derive(Clone)]
39pub struct FsDirectoryEntryStreamWrapper {
40    /// Cached directory entries: (name, is_dir)
41    pub entries: Vec<(String, bool)>,
42    /// Current position in the entries list
43    pub position: usize,
44}
45
46/// Host state that uses fs-core directly for filesystem operations.
47/// Multiple instances can share the same VFS via `Arc<Fs>`.
48///
49/// Since fs-core uses DashMap internally with fine-grained locking,
50/// no external lock is required. All Fs methods take `&self`.
51pub struct VfsHostState {
52    /// WASI context for host operations (stdio, env, etc.)
53    pub wasi_ctx: WasiCtx,
54
55    /// Resource table for managing WASM resources
56    pub table: ResourceTable,
57
58    /// Shared VFS: multiple VfsHostState instances can reference the same VFS
59    /// No external lock needed - fs-core uses DashMap for internal thread safety
60    pub shared_vfs: Arc<Fs>,
61
62    /// Optional sync hooks for S3 synchronization
63    #[cfg(feature = "s3-sync")]
64    pub sync_hooks: Option<Arc<dyn SyncHooks>>,
65
66    /// Optional sync manager for S3 synchronization
67    #[cfg(feature = "s3-sync")]
68    pub sync_manager: Option<Arc<HostSyncManager>>,
69
70    /// Background sync thread handle
71    #[cfg(feature = "s3-sync")]
72    sync_thread: Option<std::thread::JoinHandle<()>>,
73}
74
75impl VfsHostState {
76    /// Create a new VfsHostState with a fresh fs-core filesystem
77    pub fn new() -> Result<Self> {
78        // Create host WASI context
79        let wasi_ctx = WasiCtxBuilder::new()
80            .inherit_stdio()
81            .inherit_stderr()
82            .build();
83
84        Ok(Self {
85            wasi_ctx,
86            table: ResourceTable::new(),
87            shared_vfs: Arc::new(Fs::new()),
88            #[cfg(feature = "s3-sync")]
89            sync_hooks: None,
90            #[cfg(feature = "s3-sync")]
91            sync_manager: None,
92            #[cfg(feature = "s3-sync")]
93            sync_thread: None,
94        })
95    }
96
97    /// Create a new VfsHostState with S3 synchronization enabled
98    ///
99    /// This will:
100    /// 1. Initialize S3 storage client
101    /// 2. Load existing files from S3
102    /// 3. Start a background sync thread
103    ///
104    /// # Arguments
105    /// * `bucket` - S3 bucket name
106    /// * `prefix` - S3 key prefix (e.g., "vfs/")
107    #[cfg(feature = "s3-sync")]
108    pub async fn new_with_s3(bucket: String, prefix: String) -> Result<Self> {
109        use std::time::Duration;
110
111        log::info!(
112            "[vfs-host] Initializing with S3 sync: bucket={}, prefix={}",
113            bucket,
114            prefix
115        );
116
117        // Initialize S3 storage
118        let s3 = S3Storage::new(bucket, prefix).await;
119
120        // Load existing files from S3
121        let (fs, metadata_cache) = init_from_s3(&s3)
122            .await
123            .map_err(|e| anyhow::anyhow!("Failed to initialize from S3: {}", e))?;
124
125        let fs = Arc::new(fs);
126        let config = SyncConfig::from_env();
127
128        log::info!("[vfs-host] Sync mode: {:?}", config.mode);
129
130        // Check if read-from-S3 mode is enabled
131        let read_from_s3 = std::env::var("VFS_READ_MODE")
132            .map(|v| v == "s3")
133            .unwrap_or(false);
134        if read_from_s3 {
135            log::info!("[vfs-host] Read mode: S3 (read-through)");
136        } else {
137            log::info!("[vfs-host] Read mode: memory (default)");
138        }
139
140        // Check if metadata sync mode is enabled (like s3fs HEAD requests)
141        let metadata_sync = std::env::var("VFS_METADATA_MODE")
142            .map(|v| v == "s3")
143            .unwrap_or(false);
144        if metadata_sync {
145            log::info!("[vfs-host] Metadata mode: S3 (HEAD on every open, like s3fs)");
146        } else {
147            log::info!("[vfs-host] Metadata mode: memory (default)");
148        }
149
150        // Create sync manager
151        let sync_manager = Arc::new(HostSyncManager::new(s3, fs.clone(), metadata_cache, config));
152
153        // Create sync hooks
154        let sync_hooks: Arc<dyn SyncHooks> = Arc::new(S3SyncHooks::new_with_options(
155            sync_manager.clone(),
156            read_from_s3,
157            metadata_sync,
158        ));
159
160        // Spawn background sync thread
161        let sync_thread = {
162            let sync = sync_manager.clone();
163            std::thread::spawn(move || {
164                let rt = tokio::runtime::Builder::new_current_thread()
165                    .enable_all()
166                    .build()
167                    .expect("Failed to create tokio runtime for sync thread");
168
169                rt.block_on(async {
170                    log::info!("[vfs-host] Background sync thread started");
171                    while !sync.is_shutdown() {
172                        sync.maybe_sync().await;
173                        tokio::time::sleep(Duration::from_millis(100)).await;
174                    }
175                    // Final flush before exit
176                    if let Err(e) = sync.force_flush().await {
177                        log::error!("[vfs-host] Final flush failed: {}", e);
178                    }
179                    log::info!("[vfs-host] Background sync thread stopped");
180                });
181            })
182        };
183
184        let wasi_ctx = WasiCtxBuilder::new()
185            .inherit_stdio()
186            .inherit_stderr()
187            .build();
188
189        Ok(Self {
190            wasi_ctx,
191            table: ResourceTable::new(),
192            shared_vfs: fs,
193            sync_hooks: Some(sync_hooks),
194            sync_manager: Some(sync_manager),
195            sync_thread: Some(sync_thread),
196        })
197    }
198
199    /// Create a new VfsHostState that shares the same VFS core
200    /// This enables multiple applications to access the same VFS concurrently
201    pub fn clone_shared(&self) -> Self {
202        let wasi_ctx = WasiCtxBuilder::new()
203            .inherit_stdio()
204            .inherit_stderr()
205            .build();
206
207        Self {
208            wasi_ctx,
209            table: ResourceTable::new(),
210            shared_vfs: Arc::clone(&self.shared_vfs),
211            #[cfg(feature = "s3-sync")]
212            sync_hooks: self.sync_hooks.clone(),
213            #[cfg(feature = "s3-sync")]
214            sync_manager: self.sync_manager.clone(),
215            #[cfg(feature = "s3-sync")]
216            sync_thread: None, // Don't clone the thread handle
217        }
218    }
219
220    /// Create a new VfsHostState that shares the same VFS core with custom CLI arguments
221    /// This enables passing arguments to WASM applications via WASI
222    pub fn clone_shared_with_args(&self, args: &[&str]) -> Self {
223        let mut builder = WasiCtxBuilder::new();
224        builder.inherit_stdio().inherit_stderr();
225        builder.args(args);
226
227        Self {
228            wasi_ctx: builder.build(),
229            table: ResourceTable::new(),
230            shared_vfs: Arc::clone(&self.shared_vfs),
231            #[cfg(feature = "s3-sync")]
232            sync_hooks: self.sync_hooks.clone(),
233            #[cfg(feature = "s3-sync")]
234            sync_manager: self.sync_manager.clone(),
235            #[cfg(feature = "s3-sync")]
236            sync_thread: None, // Don't clone the thread handle
237        }
238    }
239
240    /// Create a new VfsHostState that shares the same VFS core with custom environment variables
241    /// This enables passing configuration to WASM handlers
242    pub fn clone_shared_with_env(&self, env_vars: &[(&str, &str)]) -> Self {
243        let mut builder = WasiCtxBuilder::new();
244        builder.inherit_stdio().inherit_stderr();
245
246        for (key, value) in env_vars {
247            builder.env(key, value);
248        }
249
250        Self {
251            wasi_ctx: builder.build(),
252            table: ResourceTable::new(),
253            shared_vfs: Arc::clone(&self.shared_vfs),
254            #[cfg(feature = "s3-sync")]
255            sync_hooks: self.sync_hooks.clone(),
256            #[cfg(feature = "s3-sync")]
257            sync_manager: self.sync_manager.clone(),
258            #[cfg(feature = "s3-sync")]
259            sync_thread: None, // Don't clone the thread handle
260        }
261    }
262
263    /// Create a new VfsHostState from an existing shared VFS with custom environment variables
264    /// This is useful when sharing VFS across threads (e.g., in HTTP server handlers)
265    pub fn from_shared_vfs_with_env(shared_vfs: Arc<Fs>, env_vars: &[(&str, &str)]) -> Self {
266        let mut builder = WasiCtxBuilder::new();
267        builder.inherit_stdio().inherit_stderr();
268
269        for (key, value) in env_vars {
270            builder.env(key, value);
271        }
272
273        Self {
274            wasi_ctx: builder.build(),
275            table: ResourceTable::new(),
276            shared_vfs,
277            #[cfg(feature = "s3-sync")]
278            sync_hooks: None,
279            #[cfg(feature = "s3-sync")]
280            sync_manager: None,
281            #[cfg(feature = "s3-sync")]
282            sync_thread: None,
283        }
284    }
285
286    /// Get the shared VFS for external use (e.g., sharing across threads)
287    pub fn get_shared_vfs(&self) -> Arc<Fs> {
288        Arc::clone(&self.shared_vfs)
289    }
290
291    /// Gracefully shutdown S3 sync
292    #[cfg(feature = "s3-sync")]
293    pub fn shutdown_sync(&mut self) {
294        if let Some(ref sync) = self.sync_manager {
295            log::info!("[vfs-host] Shutting down S3 sync...");
296            sync.shutdown();
297        }
298        if let Some(handle) = self.sync_thread.take() {
299            let _ = handle.join();
300        }
301    }
302}
303
304#[cfg(feature = "s3-sync")]
305impl Drop for VfsHostState {
306    fn drop(&mut self) {
307        self.shutdown_sync();
308    }
309}
310
311impl Default for VfsHostState {
312    fn default() -> Self {
313        Self::new().expect("Failed to create VfsHostState")
314    }
315}
316
317impl WasiView for VfsHostState {
318    fn ctx(&mut self) -> &mut WasiCtx {
319        &mut self.wasi_ctx
320    }
321
322    fn table(&mut self) -> &mut ResourceTable {
323        &mut self.table
324    }
325}
326
327/// Helper function to convert fs-core error to WASI error code
328pub fn convert_fs_error(
329    error: fs_core::FsError,
330) -> wasmtime_wasi::bindings::sync::filesystem::types::ErrorCode {
331    use fs_core::FsError;
332    use wasmtime_wasi::bindings::sync::filesystem::types::ErrorCode;
333
334    match error {
335        FsError::NotFound => ErrorCode::NoEntry,
336        FsError::NotADirectory => ErrorCode::NotDirectory,
337        FsError::IsADirectory => ErrorCode::IsDirectory,
338        FsError::AlreadyExists => ErrorCode::Exist,
339        FsError::NotEmpty => ErrorCode::NotEmpty,
340        FsError::BadFileDescriptor => ErrorCode::BadDescriptor,
341        FsError::PermissionDenied => ErrorCode::Access,
342        FsError::InvalidArgument => ErrorCode::Invalid,
343    }
344}
345
346// Public API exports
347// Users can import VfsHostState and related types from vfs_host crate root
348
349// Helper to annotate closure type for lifetime inference (same pattern as wasmtime-wasi)
350fn type_annotate_wasi<F>(val: F) -> F
351where
352    F: Fn(&mut VfsHostState) -> wasmtime_wasi::WasiImpl<&mut VfsHostState>,
353{
354    val
355}
356
357fn type_annotate_identity<F>(val: F) -> F
358where
359    F: Fn(&mut VfsHostState) -> &mut VfsHostState,
360{
361    val
362}
363
364/// Add WASI interfaces to linker with custom VFS filesystem implementation.
365///
366/// This function registers all standard WASI interfaces but replaces the
367/// filesystem implementation with VfsHostState's custom Host trait implementation.
368/// This allows std::fs to work transparently through the VFS.
369pub fn add_to_linker_with_vfs(
370    linker: &mut wasmtime::component::Linker<VfsHostState>,
371) -> Result<()> {
372    use wasmtime_wasi::WasiImpl;
373
374    // Closure for standard WASI implementations (via WasiImpl)
375    let wasi_closure = type_annotate_wasi(|t| WasiImpl(t));
376
377    // Closure for custom filesystem (returns VfsHostState directly)
378    let fs_closure = type_annotate_identity(|t| t);
379
380    // Register standard WASI interfaces (non-filesystem)
381    wasmtime_wasi::bindings::clocks::wall_clock::add_to_linker_get_host(linker, wasi_closure)?;
382    wasmtime_wasi::bindings::clocks::monotonic_clock::add_to_linker_get_host(linker, wasi_closure)?;
383    wasmtime_wasi::bindings::io::error::add_to_linker_get_host(linker, wasi_closure)?;
384    wasmtime_wasi::bindings::sync::io::poll::add_to_linker_get_host(linker, wasi_closure)?;
385    wasmtime_wasi::bindings::sync::io::streams::add_to_linker_get_host(linker, wasi_closure)?;
386    wasmtime_wasi::bindings::random::random::add_to_linker_get_host(linker, wasi_closure)?;
387    wasmtime_wasi::bindings::random::insecure::add_to_linker_get_host(linker, wasi_closure)?;
388    wasmtime_wasi::bindings::random::insecure_seed::add_to_linker_get_host(linker, wasi_closure)?;
389    wasmtime_wasi::bindings::cli::environment::add_to_linker_get_host(linker, wasi_closure)?;
390    wasmtime_wasi::bindings::cli::stdin::add_to_linker_get_host(linker, wasi_closure)?;
391    wasmtime_wasi::bindings::cli::stdout::add_to_linker_get_host(linker, wasi_closure)?;
392    wasmtime_wasi::bindings::cli::stderr::add_to_linker_get_host(linker, wasi_closure)?;
393    wasmtime_wasi::bindings::cli::terminal_input::add_to_linker_get_host(linker, wasi_closure)?;
394    wasmtime_wasi::bindings::cli::terminal_output::add_to_linker_get_host(linker, wasi_closure)?;
395    wasmtime_wasi::bindings::cli::terminal_stdin::add_to_linker_get_host(linker, wasi_closure)?;
396    wasmtime_wasi::bindings::cli::terminal_stdout::add_to_linker_get_host(linker, wasi_closure)?;
397    wasmtime_wasi::bindings::cli::terminal_stderr::add_to_linker_get_host(linker, wasi_closure)?;
398    wasmtime_wasi::bindings::sync::sockets::tcp::add_to_linker_get_host(linker, wasi_closure)?;
399    wasmtime_wasi::bindings::sockets::tcp_create_socket::add_to_linker_get_host(
400        linker,
401        wasi_closure,
402    )?;
403    wasmtime_wasi::bindings::sync::sockets::udp::add_to_linker_get_host(linker, wasi_closure)?;
404    wasmtime_wasi::bindings::sockets::udp_create_socket::add_to_linker_get_host(
405        linker,
406        wasi_closure,
407    )?;
408    wasmtime_wasi::bindings::sockets::instance_network::add_to_linker_get_host(
409        linker,
410        wasi_closure,
411    )?;
412    wasmtime_wasi::bindings::sockets::ip_name_lookup::add_to_linker_get_host(linker, wasi_closure)?;
413
414    // Register custom VFS filesystem implementation
415    // These use VfsHostState's Host trait implementations directly
416    wasmtime_wasi::bindings::sync::filesystem::types::add_to_linker_get_host(linker, fs_closure)?;
417    wasmtime_wasi::bindings::sync::filesystem::preopens::add_to_linker_get_host(
418        linker, fs_closure,
419    )?;
420
421    // cli::exit requires LinkOptions. Use default
422    let exit_options = wasmtime_wasi::bindings::cli::exit::LinkOptions::default();
423    wasmtime_wasi::bindings::cli::exit::add_to_linker_get_host(
424        linker,
425        &exit_options,
426        wasi_closure,
427    )?;
428
429    // sockets::network requires LinkOptions. Use default
430    let network_options = wasmtime_wasi::bindings::sockets::network::LinkOptions::default();
431    wasmtime_wasi::bindings::sockets::network::add_to_linker_get_host(
432        linker,
433        &network_options,
434        wasi_closure,
435    )?;
436
437    Ok(())
438}