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    /// Starts the server.
75    ///
76    /// Initializes the passthrough filesystem and FUSE dispatcher.
77    ///
78    /// # Errors
79    ///
80    /// Returns an error if the filesystem cannot be initialized.
81    pub fn start(&mut self) -> Result<()> {
82        if self.state == ServerState::Running {
83            return Ok(());
84        }
85
86        // Initialize passthrough filesystem
87        let fs = PassthroughFs::new(&self.config.source)
88            .map_err(|e| FsError::not_found(format!("Failed to initialize filesystem: {}", e)))?;
89        let fs = Arc::new(fs);
90
91        // Initialize dispatcher
92        let dispatcher_config = DispatcherConfig {
93            entry_timeout: self.config.cache_timeout,
94            attr_timeout: self.config.cache_timeout,
95        };
96        let dispatcher = FuseDispatcher::new(Arc::clone(&fs), dispatcher_config);
97
98        self.fs = Some(fs);
99        self.dispatcher = Some(dispatcher);
100        self.state = ServerState::Running;
101
102        tracing::info!(
103            "Started filesystem server: tag='{}', source='{}'",
104            self.config.tag,
105            self.config.source
106        );
107
108        Ok(())
109    }
110
111    /// Stops the server.
112    ///
113    /// Releases filesystem resources.
114    ///
115    /// # Errors
116    ///
117    /// Returns an error if cleanup fails.
118    pub fn stop(&mut self) -> Result<()> {
119        if self.state != ServerState::Running {
120            return Ok(());
121        }
122
123        self.dispatcher = None;
124        self.fs = None;
125        self.state = ServerState::Stopped;
126
127        tracing::info!("Stopped filesystem server: tag='{}'", self.config.tag);
128
129        Ok(())
130    }
131
132    /// Handles a FUSE request and returns the response.
133    ///
134    /// This method is called by the virtio-fs device when it receives
135    /// a request from the guest.
136    ///
137    /// # Errors
138    ///
139    /// Returns an error if the request cannot be processed.
140    pub fn handle_request(&self, request: &[u8]) -> Result<Vec<u8>> {
141        let dispatcher = self
142            .dispatcher
143            .as_ref()
144            .ok_or_else(|| FsError::Fuse("Server not started".to_string()))?;
145
146        dispatcher.dispatch(request)
147    }
148
149    /// Returns a reference to the passthrough filesystem.
150    #[must_use]
151    pub fn filesystem(&self) -> Option<&Arc<PassthroughFs>> {
152        self.fs.as_ref()
153    }
154
155    /// Returns a reference to the dispatcher.
156    #[must_use]
157    pub fn dispatcher(&self) -> Option<&FuseDispatcher> {
158        self.dispatcher.as_ref()
159    }
160}
161
162// ============================================================================
163// FuseRequestHandler Implementation
164// ============================================================================
165
166impl FuseRequestHandler for FsServer {
167    fn handle_request(&self, request: &[u8]) -> arcbox_virtio::Result<Vec<u8>> {
168        self.handle_request(request)
169            .map_err(|e| arcbox_virtio::VirtioError::DeviceError {
170                device: "fs".to_string(),
171                message: e.to_string(),
172            })
173    }
174
175    fn on_init(&self, session: &FuseSession) {
176        tracing::info!(
177            "FsServer received FUSE_INIT: version {}.{}, max_readahead={}, max_write={}",
178            session.major(),
179            session.minor(),
180            session.max_readahead(),
181            session.max_write()
182        );
183    }
184
185    fn on_destroy(&self) {
186        tracing::info!("FsServer received FUSE_DESTROY");
187    }
188}
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use tempfile::TempDir;
194
195    #[test]
196    fn test_server_lifecycle() {
197        let temp = TempDir::new().unwrap();
198        let config = FsConfig {
199            tag: "test".to_string(),
200            source: temp.path().to_string_lossy().to_string(),
201            num_threads: 1,
202            writeback_cache: false,
203            cache_timeout: 1,
204        };
205
206        let mut server = FsServer::new(config);
207        assert_eq!(server.state(), ServerState::Created);
208        assert!(server.filesystem().is_none());
209
210        // Start
211        server.start().unwrap();
212        assert_eq!(server.state(), ServerState::Running);
213        assert!(server.filesystem().is_some());
214        assert!(server.dispatcher().is_some());
215
216        // Stop
217        server.stop().unwrap();
218        assert_eq!(server.state(), ServerState::Stopped);
219        assert!(server.filesystem().is_none());
220    }
221
222    #[test]
223    fn test_server_handle_request() {
224        use crate::fuse::{
225            FUSE_KERNEL_MINOR_VERSION, FUSE_KERNEL_VERSION, FuseInHeader, FuseInitIn, FuseOpcode,
226        };
227        use std::mem::size_of;
228
229        let temp = TempDir::new().unwrap();
230        let config = FsConfig {
231            tag: "test".to_string(),
232            source: temp.path().to_string_lossy().to_string(),
233            num_threads: 1,
234            writeback_cache: false,
235            cache_timeout: 1,
236        };
237
238        let mut server = FsServer::new(config);
239        server.start().unwrap();
240
241        // Build INIT request
242        let init_in = FuseInitIn {
243            major: FUSE_KERNEL_VERSION,
244            minor: FUSE_KERNEL_MINOR_VERSION,
245            max_readahead: 128 * 1024,
246            flags: 0,
247        };
248
249        let header = FuseInHeader {
250            len: (FuseInHeader::SIZE + size_of::<FuseInitIn>()) as u32,
251            opcode: FuseOpcode::Init as u32,
252            unique: 1,
253            nodeid: 0,
254            uid: 0,
255            gid: 0,
256            pid: 0,
257            padding: 0,
258        };
259
260        let mut request = Vec::new();
261        request.extend_from_slice(unsafe {
262            std::slice::from_raw_parts(&header as *const _ as *const u8, FuseInHeader::SIZE)
263        });
264        request.extend_from_slice(unsafe {
265            std::slice::from_raw_parts(&init_in as *const _ as *const u8, size_of::<FuseInitIn>())
266        });
267
268        // Handle request
269        let response = server.handle_request(&request).unwrap();
270        assert!(!response.is_empty());
271    }
272
273    #[test]
274    fn test_server_not_started_error() {
275        let config = FsConfig {
276            tag: "test".to_string(),
277            source: "/tmp".to_string(),
278            num_threads: 1,
279            writeback_cache: false,
280            cache_timeout: 1,
281        };
282
283        let server = FsServer::new(config);
284        let result = server.handle_request(&[]);
285        assert!(result.is_err());
286    }
287}