11//! Volume mount/unmount watcher for Linux.
22//!
3- //! Watches `/proc/mounts` for changes using `notify` (inotify). When mounts
4- //! change, diffs against the previous state and emits `volume-mounted` /
5- //! `volume-unmounted` Tauri events. Also registers/unregisters volumes with
6- //! the global `VolumeManager`.
3+ //! Two watchers run concurrently:
4+ //! - `/proc/mounts` (inotify) — detects standard mount/unmount operations
5+ //! - `/run/user/<uid>/gvfs/` (inotify) — detects GVFS SMB share mount/unmount
6+ //! (these are subdirectories of a single gvfsd-fuse mount, so they don't
7+ //! appear in `/proc/mounts`)
8+ //!
9+ //! Both diff against known state and emit `volume-mounted` / `volume-unmounted`
10+ //! Tauri events. Also registers/unregisters volumes with the global `VolumeManager`.
711
812use crate :: file_system:: linux_mounts;
9- use log:: { debug, error, info} ;
13+ use log:: { debug, error, info, warn } ;
1014use notify:: { Event , EventKind , RecommendedWatcher , Watcher } ;
11- use std:: collections:: HashMap ;
15+ use std:: collections:: { HashMap , HashSet } ;
1216use std:: path:: Path ;
1317use std:: sync:: { Mutex , OnceLock } ;
1418use tauri:: { AppHandle , Emitter } ;
1519
1620/// Global app handle for emitting events from the watcher.
1721static APP_HANDLE : OnceLock < AppHandle > = OnceLock :: new ( ) ;
1822
19- /// The watcher instance (kept alive for the app's lifetime).
23+ /// The watcher instance for /proc/mounts (kept alive for the app's lifetime).
2024static WATCHER : OnceLock < Mutex < Option < RecommendedWatcher > > > = OnceLock :: new ( ) ;
2125
26+ /// The watcher instance for GVFS directory.
27+ static GVFS_WATCHER : OnceLock < Mutex < Option < RecommendedWatcher > > > = OnceLock :: new ( ) ;
28+
2229/// Known mount points mapped to their filesystem type, for diffing.
2330static KNOWN_MOUNTS : OnceLock < Mutex < HashMap < String , String > > > = OnceLock :: new ( ) ;
2431
32+ /// Known GVFS SMB mount paths, for diffing.
33+ static KNOWN_GVFS_MOUNTS : OnceLock < Mutex < HashSet < String > > > = OnceLock :: new ( ) ;
34+
2535/// Payload for volume mount/unmount events.
2636#[ derive( Clone , serde:: Serialize ) ]
2737#[ serde( rename_all = "camelCase" ) ]
@@ -71,17 +81,24 @@ pub fn start_volume_watcher(app: &AppHandle) {
7181 error ! ( "Failed to create Linux volume watcher: {}" , e) ;
7282 }
7383 }
84+
85+ start_gvfs_watcher ( ) ;
7486}
7587
76- /// Stop the volume watcher .
88+ /// Stop both volume watchers (proc/mounts and GVFS) .
7789#[ allow( dead_code, reason = "Symmetry with macOS, will be used for explicit cleanup" ) ]
7890pub fn stop_volume_watcher ( ) {
7991 if let Some ( storage) = WATCHER . get ( )
8092 && let Ok ( mut guard) = storage. lock ( )
8193 {
8294 * guard = None ;
8395 }
84- debug ! ( "Linux volume watcher stopped" ) ;
96+ if let Some ( storage) = GVFS_WATCHER . get ( )
97+ && let Ok ( mut guard) = storage. lock ( )
98+ {
99+ * guard = None ;
100+ }
101+ debug ! ( "Linux volume watchers stopped" ) ;
85102}
86103
87104/// Handle filesystem events on /proc/mounts.
@@ -163,6 +180,111 @@ fn get_real_mounts() -> HashMap<String, String> {
163180 . collect ( )
164181}
165182
183+ /// Start watching the GVFS directory for SMB share mount/unmount.
184+ /// Skips silently if `/run/user/<uid>/gvfs/` doesn't exist (non-GNOME systems).
185+ fn start_gvfs_watcher ( ) {
186+ let uid = unsafe { libc:: getuid ( ) } ;
187+ let gvfs_dir = format ! ( "/run/user/{}/gvfs" , uid) ;
188+ let gvfs_path = Path :: new ( & gvfs_dir) ;
189+
190+ if !gvfs_path. is_dir ( ) {
191+ debug ! ( "GVFS directory {} not found, skipping GVFS watcher" , gvfs_dir) ;
192+ return ;
193+ }
194+
195+ // Snapshot current GVFS SMB mounts
196+ let initial = get_current_gvfs_smb_paths ( & gvfs_dir) ;
197+ let known = KNOWN_GVFS_MOUNTS . get_or_init ( || Mutex :: new ( HashSet :: new ( ) ) ) ;
198+ if let Ok ( mut guard) = known. lock ( ) {
199+ debug ! ( "Initial GVFS SMB mounts: {} entries" , initial. len( ) ) ;
200+ * guard = initial;
201+ }
202+
203+ let gvfs_dir_owned = gvfs_dir. clone ( ) ;
204+ let watcher_result = notify:: recommended_watcher ( move |result : Result < Event , notify:: Error > | match result {
205+ Ok ( event) => handle_gvfs_event ( event, & gvfs_dir_owned) ,
206+ Err ( e) => error ! ( "GVFS watcher error: {}" , e) ,
207+ } ) ;
208+
209+ match watcher_result {
210+ Ok ( mut watcher) => {
211+ if let Err ( e) = watcher. watch ( gvfs_path, notify:: RecursiveMode :: NonRecursive ) {
212+ warn ! ( "Failed to watch GVFS directory {}: {}" , gvfs_dir, e) ;
213+ return ;
214+ }
215+
216+ let storage = GVFS_WATCHER . get_or_init ( || Mutex :: new ( None ) ) ;
217+ if let Ok ( mut guard) = storage. lock ( ) {
218+ * guard = Some ( watcher) ;
219+ }
220+
221+ info ! ( "GVFS watcher started on {}" , gvfs_dir) ;
222+ }
223+ Err ( e) => {
224+ warn ! ( "Failed to create GVFS watcher: {}" , e) ;
225+ }
226+ }
227+ }
228+
229+ /// Handle inotify events on the GVFS directory.
230+ fn handle_gvfs_event ( event : Event , gvfs_dir : & str ) {
231+ match event. kind {
232+ EventKind :: Create ( _) | EventKind :: Remove ( _) => {
233+ check_for_gvfs_changes ( gvfs_dir) ;
234+ }
235+ _ => { }
236+ }
237+ }
238+
239+ /// Diff current GVFS SMB directories against known state and emit events.
240+ fn check_for_gvfs_changes ( gvfs_dir : & str ) {
241+ let current = get_current_gvfs_smb_paths ( gvfs_dir) ;
242+
243+ let known = match KNOWN_GVFS_MOUNTS . get ( ) {
244+ Some ( k) => k,
245+ None => return ,
246+ } ;
247+
248+ let mut known_guard = match known. lock ( ) {
249+ Ok ( g) => g,
250+ Err ( _) => return ,
251+ } ;
252+
253+ // Newly mounted shares
254+ for path in & current {
255+ if !known_guard. contains ( path) {
256+ debug ! ( "GVFS SMB share mounted: {}" , path) ;
257+ emit_volume_mounted ( path) ;
258+ }
259+ }
260+
261+ // Unmounted shares
262+ for path in known_guard. iter ( ) {
263+ if !current. contains ( path) {
264+ debug ! ( "GVFS SMB share unmounted: {}" , path) ;
265+ emit_volume_unmounted ( path) ;
266+ }
267+ }
268+
269+ * known_guard = current;
270+ }
271+
272+ /// Scan the GVFS directory for current SMB share mount paths.
273+ fn get_current_gvfs_smb_paths ( gvfs_dir : & str ) -> HashSet < String > {
274+ let mut paths = HashSet :: new ( ) ;
275+ let Ok ( entries) = std:: fs:: read_dir ( gvfs_dir) else {
276+ return paths;
277+ } ;
278+ for entry in entries. flatten ( ) {
279+ let name = entry. file_name ( ) ;
280+ let dirname = name. to_string_lossy ( ) ;
281+ if super :: parse_gvfs_smb_dirname ( & dirname) . is_some ( ) {
282+ paths. insert ( entry. path ( ) . to_string_lossy ( ) . to_string ( ) ) ;
283+ }
284+ }
285+ paths
286+ }
287+
166288/// Emit a volume-mounted event and register with VolumeManager.
167289fn emit_volume_mounted ( volume_path : & str ) {
168290 register_volume_with_manager ( volume_path) ;
@@ -201,17 +323,18 @@ fn register_volume_with_manager(volume_path: &str) {
201323 use crate :: file_system:: volume:: LocalPosixVolume ;
202324 use std:: sync:: Arc ;
203325
204- let volume_id: String = volume_path
205- . chars ( )
206- . filter ( |c| c. is_alphanumeric ( ) || * c == '-' )
207- . collect :: < String > ( )
208- . to_lowercase ( ) ;
326+ let volume_id = super :: path_to_id ( volume_path) ;
209327
210- let name = Path :: new ( volume_path)
211- . file_name ( )
212- . and_then ( |n| n. to_str ( ) )
213- . unwrap_or ( "Unknown" )
214- . to_string ( ) ;
328+ // For GVFS SMB shares, extract the share name instead of the raw dirname
329+ let name = if let Some ( dirname) = Path :: new ( volume_path) . file_name ( ) . and_then ( |n| n. to_str ( ) ) {
330+ if let Some ( ( _server, share) ) = super :: parse_gvfs_smb_dirname ( dirname) {
331+ share
332+ } else {
333+ dirname. to_string ( )
334+ }
335+ } else {
336+ "Unknown" . to_string ( )
337+ } ;
215338
216339 let volume = Arc :: new ( LocalPosixVolume :: new ( & name, volume_path) ) ;
217340 get_volume_manager ( ) . register ( & volume_id, volume) ;
@@ -222,12 +345,7 @@ fn register_volume_with_manager(volume_path: &str) {
222345fn unregister_volume_from_manager ( volume_path : & str ) {
223346 use crate :: file_system:: get_volume_manager;
224347
225- let volume_id: String = volume_path
226- . chars ( )
227- . filter ( |c| c. is_alphanumeric ( ) || * c == '-' )
228- . collect :: < String > ( )
229- . to_lowercase ( ) ;
230-
348+ let volume_id = super :: path_to_id ( volume_path) ;
231349 get_volume_manager ( ) . unregister ( & volume_id) ;
232350 debug ! ( "Unregistered volume: {} ({})" , volume_id, volume_path) ;
233351}
0 commit comments