1use anyhow::Result;
8use std::sync::Arc;
9
10pub 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#[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#[derive(Clone)]
29pub struct FsDescriptorWrapper {
30 pub fd: u32,
32 pub path: Option<String>,
34}
35
36#[derive(Clone)]
39pub struct FsDirectoryEntryStreamWrapper {
40 pub entries: Vec<(String, bool)>,
42 pub position: usize,
44}
45
46pub struct VfsHostState {
52 pub wasi_ctx: WasiCtx,
54
55 pub table: ResourceTable,
57
58 pub shared_vfs: Arc<Fs>,
61
62 #[cfg(feature = "s3-sync")]
64 pub sync_hooks: Option<Arc<dyn SyncHooks>>,
65
66 #[cfg(feature = "s3-sync")]
68 pub sync_manager: Option<Arc<HostSyncManager>>,
69
70 #[cfg(feature = "s3-sync")]
72 sync_thread: Option<std::thread::JoinHandle<()>>,
73}
74
75impl VfsHostState {
76 pub fn new() -> Result<Self> {
78 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 #[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 let s3 = S3Storage::new(bucket, prefix).await;
119
120 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 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 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 let sync_manager = Arc::new(HostSyncManager::new(s3, fs.clone(), metadata_cache, config));
152
153 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 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 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 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, }
218 }
219
220 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, }
238 }
239
240 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, }
261 }
262
263 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 pub fn get_shared_vfs(&self) -> Arc<Fs> {
288 Arc::clone(&self.shared_vfs)
289 }
290
291 #[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
327pub 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
346fn 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
364pub fn add_to_linker_with_vfs(
370 linker: &mut wasmtime::component::Linker<VfsHostState>,
371) -> Result<()> {
372 use wasmtime_wasi::WasiImpl;
373
374 let wasi_closure = type_annotate_wasi(|t| WasiImpl(t));
376
377 let fs_closure = type_annotate_identity(|t| t);
379
380 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 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 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 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}