1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ServerState {
22 Created,
24 Running,
26 Stopped,
28}
29
30pub struct FsServer {
36 config: FsConfig,
37 state: ServerState,
38 fs: Option<Arc<PassthroughFs>>,
40 dispatcher: Option<FuseDispatcher>,
42}
43
44impl FsServer {
45 #[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 #[must_use]
58 pub const fn state(&self) -> ServerState {
59 self.state
60 }
61
62 #[must_use]
64 pub fn tag(&self) -> &str {
65 &self.config.tag
66 }
67
68 #[must_use]
70 pub fn source(&self) -> &str {
71 &self.config.source
72 }
73
74 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 pub fn start(&mut self) -> Result<()> {
90 if self.state == ServerState::Running {
91 return Ok(());
92 }
93
94 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 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 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 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 #[must_use]
163 pub fn filesystem(&self) -> Option<&Arc<PassthroughFs>> {
164 self.fs.as_ref()
165 }
166
167 #[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 server.start().unwrap();
223 assert_eq!(server.state(), ServerState::Running);
224 assert!(server.filesystem().is_some());
225 assert!(server.dispatcher().is_some());
226
227 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 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 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}