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::{HasSelf, ResourceTable};
13use wasmtime_wasi::{WasiCtx, WasiCtxBuilder, WasiCtxView, 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 = vfs_sync_host::new_s3_storage(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 s3 = Arc::new(s3);
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(
152 s3,
153 vfs_sync_host::HostFs(fs.clone()),
154 metadata_cache,
155 config,
156 ));
157
158 let sync_hooks: Arc<dyn SyncHooks> = Arc::new(S3SyncHooks::new_with_options(
160 sync_manager.clone(),
161 read_from_s3,
162 metadata_sync,
163 ));
164
165 let sync_thread = {
167 let sync = sync_manager.clone();
168 std::thread::spawn(move || {
169 let rt = tokio::runtime::Builder::new_current_thread()
170 .enable_all()
171 .build()
172 .expect("Failed to create tokio runtime for sync thread");
173
174 rt.block_on(async {
175 log::info!("[vfs-host] Background sync thread started");
176 while !sync.is_shutdown() {
177 sync.maybe_sync().await;
178 tokio::time::sleep(Duration::from_millis(100)).await;
179 }
180 if let Err(e) = sync.force_flush().await {
182 log::error!("[vfs-host] Final flush failed: {}", e);
183 }
184 log::info!("[vfs-host] Background sync thread stopped");
185 });
186 })
187 };
188
189 let wasi_ctx = WasiCtxBuilder::new()
190 .inherit_stdio()
191 .inherit_stderr()
192 .build();
193
194 Ok(Self {
195 wasi_ctx,
196 table: ResourceTable::new(),
197 shared_vfs: fs,
198 sync_hooks: Some(sync_hooks),
199 sync_manager: Some(sync_manager),
200 sync_thread: Some(sync_thread),
201 })
202 }
203
204 pub fn clone_shared(&self) -> Self {
207 let wasi_ctx = WasiCtxBuilder::new()
208 .inherit_stdio()
209 .inherit_stderr()
210 .build();
211
212 Self {
213 wasi_ctx,
214 table: ResourceTable::new(),
215 shared_vfs: Arc::clone(&self.shared_vfs),
216 #[cfg(feature = "s3-sync")]
217 sync_hooks: self.sync_hooks.clone(),
218 #[cfg(feature = "s3-sync")]
219 sync_manager: self.sync_manager.clone(),
220 #[cfg(feature = "s3-sync")]
221 sync_thread: None, }
223 }
224
225 pub fn clone_shared_with_args(&self, args: &[&str]) -> Self {
228 let mut builder = WasiCtxBuilder::new();
229 builder.inherit_stdio().inherit_stderr();
230 builder.args(args);
231
232 Self {
233 wasi_ctx: builder.build(),
234 table: ResourceTable::new(),
235 shared_vfs: Arc::clone(&self.shared_vfs),
236 #[cfg(feature = "s3-sync")]
237 sync_hooks: self.sync_hooks.clone(),
238 #[cfg(feature = "s3-sync")]
239 sync_manager: self.sync_manager.clone(),
240 #[cfg(feature = "s3-sync")]
241 sync_thread: None, }
243 }
244
245 pub fn clone_shared_with_env(&self, env_vars: &[(&str, &str)]) -> Self {
248 let mut builder = WasiCtxBuilder::new();
249 builder.inherit_stdio().inherit_stderr();
250
251 for (key, value) in env_vars {
252 builder.env(key, value);
253 }
254
255 Self {
256 wasi_ctx: builder.build(),
257 table: ResourceTable::new(),
258 shared_vfs: Arc::clone(&self.shared_vfs),
259 #[cfg(feature = "s3-sync")]
260 sync_hooks: self.sync_hooks.clone(),
261 #[cfg(feature = "s3-sync")]
262 sync_manager: self.sync_manager.clone(),
263 #[cfg(feature = "s3-sync")]
264 sync_thread: None, }
266 }
267
268 pub fn from_shared_vfs_with_env(shared_vfs: Arc<Fs>, env_vars: &[(&str, &str)]) -> Self {
271 let mut builder = WasiCtxBuilder::new();
272 builder.inherit_stdio().inherit_stderr();
273
274 for (key, value) in env_vars {
275 builder.env(key, value);
276 }
277
278 Self {
279 wasi_ctx: builder.build(),
280 table: ResourceTable::new(),
281 shared_vfs,
282 #[cfg(feature = "s3-sync")]
283 sync_hooks: None,
284 #[cfg(feature = "s3-sync")]
285 sync_manager: None,
286 #[cfg(feature = "s3-sync")]
287 sync_thread: None,
288 }
289 }
290
291 pub fn get_shared_vfs(&self) -> Arc<Fs> {
293 Arc::clone(&self.shared_vfs)
294 }
295
296 #[cfg(feature = "s3-sync")]
298 pub fn shutdown_sync(&mut self) {
299 if let Some(ref sync) = self.sync_manager {
300 log::info!("[vfs-host] Shutting down S3 sync...");
301 sync.shutdown();
302 }
303 if let Some(handle) = self.sync_thread.take() {
304 let _ = handle.join();
305 }
306 }
307}
308
309#[cfg(feature = "s3-sync")]
310impl Drop for VfsHostState {
311 fn drop(&mut self) {
312 self.shutdown_sync();
313 }
314}
315
316impl Default for VfsHostState {
317 fn default() -> Self {
318 Self::new().expect("Failed to create VfsHostState")
319 }
320}
321
322impl WasiView for VfsHostState {
323 fn ctx(&mut self) -> WasiCtxView<'_> {
324 WasiCtxView {
325 ctx: &mut self.wasi_ctx,
326 table: &mut self.table,
327 }
328 }
329}
330
331pub fn convert_fs_error(
333 error: fs_core::FsError,
334) -> wasmtime_wasi::p2::bindings::sync::filesystem::types::ErrorCode {
335 use fs_core::FsError;
336 use wasmtime_wasi::p2::bindings::sync::filesystem::types::ErrorCode;
337
338 match error {
339 FsError::NotFound => ErrorCode::NoEntry,
340 FsError::NotADirectory => ErrorCode::NotDirectory,
341 FsError::IsADirectory => ErrorCode::IsDirectory,
342 FsError::AlreadyExists => ErrorCode::Exist,
343 FsError::NotEmpty => ErrorCode::NotEmpty,
344 FsError::BadFileDescriptor => ErrorCode::BadDescriptor,
345 FsError::PermissionDenied => ErrorCode::Access,
346 FsError::InvalidArgument => ErrorCode::Invalid,
347 }
348}
349
350pub fn add_to_linker_with_vfs(
359 linker: &mut wasmtime::component::Linker<VfsHostState>,
360) -> Result<()> {
361 use wasmtime_wasi::cli::{WasiCli, WasiCliView};
362 use wasmtime_wasi::clocks::{WasiClocks, WasiClocksView};
363 use wasmtime_wasi::p2::bindings;
364 use wasmtime_wasi::random::{WasiRandom, WasiRandomView};
365 use wasmtime_wasi::sockets::{WasiSockets, WasiSocketsView};
366
367 type T = VfsHostState;
368
369 bindings::clocks::wall_clock::add_to_linker::<T, WasiClocks>(linker, T::clocks)?;
371 bindings::clocks::monotonic_clock::add_to_linker::<T, WasiClocks>(linker, T::clocks)?;
372 bindings::random::random::add_to_linker::<T, WasiRandom>(linker, T::random)?;
373 bindings::random::insecure::add_to_linker::<T, WasiRandom>(linker, T::random)?;
374 bindings::random::insecure_seed::add_to_linker::<T, WasiRandom>(linker, T::random)?;
375 bindings::cli::stdin::add_to_linker::<T, WasiCli>(linker, T::cli)?;
376 bindings::cli::stdout::add_to_linker::<T, WasiCli>(linker, T::cli)?;
377 bindings::cli::stderr::add_to_linker::<T, WasiCli>(linker, T::cli)?;
378 bindings::cli::environment::add_to_linker::<T, WasiCli>(linker, T::cli)?;
379 bindings::cli::terminal_input::add_to_linker::<T, WasiCli>(linker, T::cli)?;
380 bindings::cli::terminal_output::add_to_linker::<T, WasiCli>(linker, T::cli)?;
381 bindings::cli::terminal_stdin::add_to_linker::<T, WasiCli>(linker, T::cli)?;
382 bindings::cli::terminal_stdout::add_to_linker::<T, WasiCli>(linker, T::cli)?;
383 bindings::cli::terminal_stderr::add_to_linker::<T, WasiCli>(linker, T::cli)?;
384
385 bindings::sync::sockets::tcp::add_to_linker::<T, WasiSockets>(linker, T::sockets)?;
387 bindings::sync::sockets::udp::add_to_linker::<T, WasiSockets>(linker, T::sockets)?;
388 bindings::sockets::tcp_create_socket::add_to_linker::<T, WasiSockets>(linker, T::sockets)?;
389 bindings::sockets::udp_create_socket::add_to_linker::<T, WasiSockets>(linker, T::sockets)?;
390 bindings::sockets::instance_network::add_to_linker::<T, WasiSockets>(linker, T::sockets)?;
391 bindings::sockets::ip_name_lookup::add_to_linker::<T, WasiSockets>(linker, T::sockets)?;
392
393 wasmtime_wasi_io::bindings::wasi::io::error::add_to_linker::<T, HasSelf<ResourceTable>>(
395 linker,
396 |t| &mut t.table,
397 )?;
398 bindings::sync::io::poll::add_to_linker::<T, HasSelf<ResourceTable>>(linker, |t| &mut t.table)?;
399 bindings::sync::io::streams::add_to_linker::<T, HasSelf<ResourceTable>>(linker, |t| {
400 &mut t.table
401 })?;
402
403 bindings::sync::filesystem::types::add_to_linker::<T, HasSelf<VfsHostState>>(linker, |t| t)?;
405 bindings::sync::filesystem::preopens::add_to_linker::<T, HasSelf<VfsHostState>>(linker, |t| t)?;
406
407 let exit_options = bindings::cli::exit::LinkOptions::default();
409 bindings::cli::exit::add_to_linker::<T, WasiCli>(linker, &exit_options, T::cli)?;
410
411 let network_options = bindings::sockets::network::LinkOptions::default();
412 bindings::sockets::network::add_to_linker::<T, WasiSockets>(
413 linker,
414 &network_options,
415 T::sockets,
416 )?;
417
418 Ok(())
419}