summaryrefslogtreecommitdiffstats
path: root/rsnix/src/fat.rs
diff options
context:
space:
mode:
authorsalmonik <salmonik@tuta.io>2026-08-01 15:00:47 +0200
committersalmonik <salmonik@tuta.io>2026-08-01 15:00:47 +0200
commitd562e9df2bc5e69bba7298a9439e2adca5c18faf (patch)
tree8d763e60d512e7a4230413f13caaf9153cb2ec54 /rsnix/src/fat.rs
parentklog: [ssss.mmm] millisecond timestamps from the PIT (diff)
downloadrs-dos-d562e9df2bc5e69bba7298a9439e2adca5c18faf.tar.gz
rs-dos-d562e9df2bc5e69bba7298a9439e2adca5c18faf.zip
rsnix: runtime device detection — rescan for hotplugged/removed disks
Phase 1: make the boot discovery pipeline re-runnable and diff it against live hardware state. - ahci: store ABAR/NP/port bitmap; poll_dirty() (cheap PxSSTS.DET check per port) and rescan() which starts newly-present ports and tombstones the slots of removed ones. Slots never shift, so disk indices stay valid. select() now rejects tombstoned slots. - fat: Volume gets a mounted flag; rescan() unmounts volumes whose disk left and mounts every disk without a mounted volume (re-trying failed boot mounts). UID dedup ignores tombstones so a re-plugged disk keeps its /disk/<UID> name. - fs: init's mount loop factored into rescan_disks(), which lazily creates /disk, ensures a mount node per mounted volume and tombstones nodes of unmounted ones. - shell: 'rescan' command + automatic deep rescan before each prompt when ahci::poll_dirty() sees a port-presence change. Rescan events flow through serial_println so they land in klog too. Removal (PxSSTS) is a real-hardware path: QEMU q35 rejects disk hotplug on the ich9-ahci IDE buses, and PCIe device_del is a guest-cooperative protocol this kernel does not implement.
Diffstat (limited to '')
-rw-r--r--rsnix/src/fat.rs70
1 files changed, 68 insertions, 2 deletions
diff --git a/rsnix/src/fat.rs b/rsnix/src/fat.rs
index 335a160..ef75aa9 100644
--- a/rsnix/src/fat.rs
+++ b/rsnix/src/fat.rs
@@ -36,6 +36,7 @@ pub struct Entry {
36struct Volume { 36struct Volume {
37 disk: usize, // AHCI disk index this volume was mounted from 37 disk: usize, // AHCI disk index this volume was mounted from
38 uid: String, // unique mount name: "LABEL-XXXXXXXX" 38 uid: String, // unique mount name: "LABEL-XXXXXXXX"
39 mounted: bool, // still backed by a present disk (false = tombstoned)
39 sectors_per_cluster: u8, 40 sectors_per_cluster: u8,
40 reserved: u16, 41 reserved: u16,
41 fat_size: u32, // sectors per FAT 42 fat_size: u32, // sectors per FAT
@@ -65,6 +66,69 @@ pub fn volume_count() -> usize {
65 vols().len() 66 vols().len()
66} 67}
67 68
69/// Is volume `vol` currently mounted (its disk present and FAT cached)?
70pub fn is_mounted(vol: usize) -> bool {
71 vols().get(vol).map(|v| v.mounted).unwrap_or(false)
72}
73
74/// Re-scan after a disk arrival/removal: unmount volumes whose disk left,
75/// mount every disk that has no mounted volume (which also re-tries
76/// mounts that failed at boot). Volume slots are tombstoned, never
77/// removed, so VFS disk handles keep their indices. Returns
78/// (mounted, unmounted).
79pub fn rescan() -> (usize, usize) {
80 let disks = crate::ahci::disk_count();
81 let mut mounted = 0usize;
82 let mut unmounted = 0usize;
83 // Unmount volumes whose disk is gone.
84 for i in 0..vols().len() {
85 if vols()[i].mounted && !crate::ahci::is_up(vols()[i].disk) {
86 vols()[i].mounted = false;
87 crate::serial_println!("fat: unmounted \"{}\"", vols()[i].uid);
88 unmounted += 1;
89 }
90 }
91 // Mount every disk that has no mounted volume.
92 for d in 0..disks {
93 if !crate::ahci::select(d) {
94 continue;
95 }
96 if vols().iter().any(|v| v.mounted && v.disk == d) {
97 continue;
98 }
99 if let Some(volume) = try_mount(d) {
100 let uid = volume.uid.clone();
101 let vol = match vols().iter().position(|v| !v.mounted && v.disk == d) {
102 // Reuse the disk's own tombstoned slot (indices stay stable).
103 Some(slot) => {
104 vols()[slot] = volume;
105 slot
106 }
107 None => {
108 vols().push(volume);
109 vols().len() - 1
110 }
111 };
112 if select_vol(vol).is_ok() {
113 fsinfo_recount();
114 }
115 let count = list_dir(vol, root_cluster(vol))
116 .map(|es| es.len())
117 .unwrap_or(0);
118 crate::serial_println!(
119 "fat: mounted \"{}\" at /disk/{} ({} clusters, {} B/cluster, root: {} entries)",
120 uid,
121 uid,
122 v().cluster_count,
123 cluster_size(),
124 count
125 );
126 mounted += 1;
127 }
128 }
129 (mounted, unmounted)
130}
131
68/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F"). 132/// Unique mount name of volume `vol` ("RSDOS-5BD1E55F").
69pub fn uid(vol: usize) -> &'static str { 133pub fn uid(vol: usize) -> &'static str {
70 vols().get(vol).map(|v| v.uid.as_str()).unwrap_or("") 134 vols().get(vol).map(|v| v.uid.as_str()).unwrap_or("")
@@ -1099,11 +1163,12 @@ fn try_mount(d: usize) -> Option<Volume> {
1099 if label.is_empty() { 1163 if label.is_empty() {
1100 label = String::from("DISK"); 1164 label = String::from("DISK");
1101 } 1165 }
1102 // Deduplicate against already-mounted volumes (e.g. cloned images). 1166 // Deduplicate against already-MOUNTED volumes (e.g. cloned images).
1167 // Tombstoned slots are skipped so a re-plugged disk keeps its name.
1103 let mut uid = alloc::format!("{}-{:08X}", label, serial); 1168 let mut uid = alloc::format!("{}-{:08X}", label, serial);
1104 let mut n = 2u32; 1169 let mut n = 2u32;
1105 loop { 1170 loop {
1106 let taken = vols().iter().any(|v| v.uid == uid); 1171 let taken = vols().iter().any(|v| v.mounted && v.uid == uid);
1107 if !taken { 1172 if !taken {
1108 break; 1173 break;
1109 } 1174 }
@@ -1113,6 +1178,7 @@ fn try_mount(d: usize) -> Option<Volume> {
1113 Some(Volume { 1178 Some(Volume {
1114 disk: d, 1179 disk: d,
1115 uid, 1180 uid,
1181 mounted: true,
1116 sectors_per_cluster: spc, 1182 sectors_per_cluster: spc,
1117 reserved, 1183 reserved,
1118 fat_size, 1184 fat_size,