diff options
| author | salmonik <salmonik@tuta.io> | 2026-08-01 15:00:47 +0200 |
|---|---|---|
| committer | salmonik <salmonik@tuta.io> | 2026-08-01 15:00:47 +0200 |
| commit | d562e9df2bc5e69bba7298a9439e2adca5c18faf (patch) | |
| tree | 8d763e60d512e7a4230413f13caaf9153cb2ec54 /rsnix/src/fat.rs | |
| parent | klog: [ssss.mmm] millisecond timestamps from the PIT (diff) | |
| download | rs-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 'rsnix/src/fat.rs')
| -rw-r--r-- | rsnix/src/fat.rs | 70 |
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 { | |||
| 36 | struct Volume { | 36 | struct 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)? | ||
| 70 | pub 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). | ||
| 79 | pub 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"). |
| 69 | pub fn uid(vol: usize) -> &'static str { | 133 | pub 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, |
