Skip to main content

arcbox_fs/
server.rs

1//! Filesystem server implementation.
2//!
3//! This module provides the FUSE server that handles filesystem requests
4//! from the guest VM. On Darwin, the native VZ framework handles virtio-fs,
5//! so this is primarily used for Linux KVM.
6//!
7//! The [`FsServer`] implements [`arcbox_virtio::fs::FuseRequestHandler`] to
8//! integrate with the VirtIO-FS device.
9
10use std::sync::Arc;
11
12use arcbox_virtio::fs::{FuseRequestHandler, FuseSession};
13
14use crate::FsConfig;
15use crate::dispatcher::{DispatcherConfig, FuseDispatcher};
16use crate::error::{FsError, Result};
17use crate::passthrough::PassthroughFs;
18
19/// Filesystem server state.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ServerState {
22    /// Server created but not started.
23    Created,
24    /// Server is running.
25    Running,
26    /// Server is stopped.
27    Stopped,
28}
29
30/// Filesystem server.
31///
32/// Manages the virtiofs server lifecycle and request handling.
33/// Integrates `PassthroughFs` for host filesystem access and
34/// `FuseDispatcher` for FUSE protocol handling.
35pub struct FsServer {
36    config: FsConfig,
37    state: ServerState,
38    /// Passthrough filesystem backend.
39    fs: Option<Arc<PassthroughFs>>,
40    /// FUSE request dispatcher.
41    dispatcher: Option<FuseDispatcher>,
42}
43
44impl FsServer {
45    /// Creates a new filesystem server.
46    #[must_use]
47    pub fn new(config: FsConfig) -> Self {
48        Self {
49            config,
50            state: ServerState::Created,
51            fs: None,
52            dispatcher: None,
53        }
54    }
55
56    /// Returns the current server state.
57    #[must_use]
58    pub const fn state(&self) -> ServerState {
59        self.state
60    }
61
62    /// Returns the filesystem tag.
63    #[must_use]
64    pub fn tag(&self) -> &str {
65        &self.config.tag
66    }
67
68    /// Returns the source directory path.
69    #[must_use]
70    pub fn source(&self) -> &str {
71        &self.config.source
72    }
73
74    /// Sets the DAX mapper for direct host page mapping.
75    /// Must be called after `start()`.
76    pub fn set_dax_mapper(&mut self, mapper: Arc<dyn crate::DaxMapper>) {
77        if let Some(ref mut dispatcher) = self.dispatcher {
78            dispatcher.set_dax_mapper(mapper);
79        }
80    }
81
82    /// Starts the server.
83    ///
84    /// Initializes the passthrough filesystem and FUSE dispatcher.
85    ///
86    /// # Errors
87    ///
88    /// Returns an error if the filesystem cannot be initialized.
89    pub fn start(&mut self) -> Result<()> {
90        if self.state == ServerState::Running {
91            return Ok(());
92        }
93
94        // Initialize passthrough filesystem with configured cache TTL
95        let pt_config = crate::passthrough::PassthroughConfig {
96            negative_cache_timeout: std::time::Duration::from_secs(self.config.negative_cache_ttl),
97            ..crate::passthrough::PassthroughConfig::default()
98        };
99        let fs = PassthroughFs::with_config(&self.config.source, pt_config)
100            .map_err(|e| FsError::not_found(format!("Failed to initialize filesystem: {}", e)))?;
101        let fs = Arc::new(fs);
102
103        // Initialize dispatcher using cache profile timeouts
104        let dispatcher_config = DispatcherConfig {
105            entry_timeout: self.config.cache_profile.entry_timeout().as_secs(),
106            attr_timeout: self.config.cache_profile.attr_timeout().as_secs(),
107        };
108        let dispatcher = FuseDispatcher::new(Arc::clone(&fs), dispatcher_config);
109
110        self.fs = Some(fs);
111        self.dispatcher = Some(dispatcher);
112        self.state = ServerState::Running;
113
114        tracing::info!(
115            "Started filesystem server: tag='{}', source='{}'",
116            self.config.tag,
117            self.config.source
118        );
119
120        Ok(())
121    }
122
123    /// Stops the server.
124    ///
125    /// Releases filesystem resources.
126    ///
127    /// # Errors
128    ///
129    /// Returns an error if cleanup fails.
130    pub fn stop(&mut self) -> Result<()> {
131        if self.state != ServerState::Running {
132            return Ok(());
133        }
134
135        self.dispatcher = None;
136        self.fs = None;
137        self.state = ServerState::Stopped;
138
139        tracing::info!("Stopped filesystem server: tag='{}'", self.config.tag);
140
141        Ok(())
142    }
143
144    /// Handles a FUSE request and returns the response.
145    ///
146    /// This method is called by the virtio-fs device when it receives
147    /// a request from the guest.
148    ///
149    /// # Errors
150    ///
151    /// Returns an error if the request cannot be processed.
152    pub fn handle_request(&self, request: &[u8]) -> Result<Vec<u8>> {
153        let dispatcher = self
154            .dispatcher
155            .as_ref()
156            .ok_or_else(|| FsError::Fuse("Server not started".to_string()))?;
157
158        dispatcher.dispatch(request)
159    }
160
161    /// Returns a reference to the passthrough filesystem.
162    #[must_use]
163    pub fn filesystem(&self) -> Option<&Arc<PassthroughFs>> {
164        self.fs.as_ref()
165    }
166
167    /// Returns a reference to the dispatcher.
168    #[must_use]
169    pub fn dispatcher(&self) -> Option<&FuseDispatcher> {
170        self.dispatcher.as_ref()
171    }
172}
173
174impl FuseRequestHandler for FsServer {
175    fn handle_request(&self, request: &[u8]) -> arcbox_virtio::Result<Vec<u8>> {
176        self.handle_request(request)
177            .map_err(|e| arcbox_virtio::VirtioError::DeviceError {
178                device: "fs".to_string(),
179                message: e.to_string(),
180            })
181    }
182
183    fn on_init(&self, session: &FuseSession) {
184        tracing::info!(
185            "FsServer received FUSE_INIT: version {}.{}, max_readahead={}, max_write={}",
186            session.major(),
187            session.minor(),
188            session.max_readahead(),
189            session.max_write()
190        );
191    }
192
193    fn on_destroy(&self) {
194        tracing::info!("FsServer received FUSE_DESTROY");
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::CacheProfile;
202    use tempfile::TempDir;
203
204    #[test]
205    fn test_server_lifecycle() {
206        let temp = TempDir::new().unwrap();
207        let config = FsConfig {
208            tag: "test".to_string(),
209            source: temp.path().to_string_lossy().to_string(),
210            num_threads: 1,
211            writeback_cache: false,
212            cache_profile: CacheProfile::Dynamic,
213            cache_timeout: 1,
214            negative_cache_ttl: 1,
215        };
216
217        let mut server = FsServer::new(config);
218        assert_eq!(server.state(), ServerState::Created);
219        assert!(server.filesystem().is_none());
220
221        // Start
222        server.start().unwrap();
223        assert_eq!(server.state(), ServerState::Running);
224        assert!(server.filesystem().is_some());
225        assert!(server.dispatcher().is_some());
226
227        // Stop
228        server.stop().unwrap();
229        assert_eq!(server.state(), ServerState::Stopped);
230        assert!(server.filesystem().is_none());
231    }
232
233    #[test]
234    fn test_server_handle_request() {
235        use crate::fuse::{
236            FUSE_KERNEL_MINOR_VERSION, FUSE_KERNEL_VERSION, FuseInHeader, FuseInitIn, FuseOpcode,
237        };
238        use std::mem::size_of;
239
240        let temp = TempDir::new().unwrap();
241        let config = FsConfig {
242            tag: "test".to_string(),
243            source: temp.path().to_string_lossy().to_string(),
244            num_threads: 1,
245            writeback_cache: false,
246            cache_profile: CacheProfile::Dynamic,
247            cache_timeout: 1,
248            negative_cache_ttl: 1,
249        };
250
251        let mut server = FsServer::new(config);
252        server.start().unwrap();
253
254        // Build INIT request
255        let init_in = FuseInitIn {
256            major: FUSE_KERNEL_VERSION,
257            minor: FUSE_KERNEL_MINOR_VERSION,
258            max_readahead: 128 * 1024,
259            flags: 0,
260        };
261
262        let header = FuseInHeader {
263            len: (FuseInHeader::SIZE + size_of::<FuseInitIn>()) as u32,
264            opcode: FuseOpcode::Init as u32,
265            unique: 1,
266            nodeid: 0,
267            uid: 0,
268            gid: 0,
269            pid: 0,
270            padding: 0,
271        };
272
273        let mut request = Vec::new();
274        #[allow(clippy::ptr_as_ptr, clippy::borrow_as_ptr, clippy::ref_as_ptr)]
275        request.extend_from_slice(unsafe {
276            std::slice::from_raw_parts(&header as *const _ as *const u8, FuseInHeader::SIZE)
277        });
278        #[allow(clippy::ptr_as_ptr, clippy::borrow_as_ptr, clippy::ref_as_ptr)]
279        request.extend_from_slice(unsafe {
280            std::slice::from_raw_parts(&init_in as *const _ as *const u8, size_of::<FuseInitIn>())
281        });
282
283        // Handle request
284        let response = server.handle_request(&request).unwrap();
285        assert!(!response.is_empty());
286    }
287
288    #[test]
289    fn test_server_not_started_error() {
290        let config = FsConfig {
291            tag: "test".to_string(),
292            source: "/tmp".to_string(),
293            num_threads: 1,
294            writeback_cache: false,
295            cache_profile: CacheProfile::Dynamic,
296            cache_timeout: 1,
297            negative_cache_ttl: 1,
298        };
299
300        let server = FsServer::new(config);
301        let result = server.handle_request(&[]);
302        assert!(result.is_err());
303    }
304}