summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--rsnix/src/ahci.rs131
-rw-r--r--rsnix/src/fat.rs70
-rw-r--r--rsnix/src/fs.rs84
-rw-r--r--shell/src/lib.rs35
4 files changed, 285 insertions, 35 deletions
diff --git a/rsnix/src/ahci.rs b/rsnix/src/ahci.rs
index 9a49431..9ed803d 100644
--- a/rsnix/src/ahci.rs
+++ b/rsnix/src/ahci.rs
@@ -52,17 +52,28 @@ const ATA_WRITE_DMA_EXT: u8 = 0x35;
52/// One attached disk: register map + DMA structures. 52/// One attached disk: register map + DMA structures.
53#[derive(Clone, Copy)] 53#[derive(Clone, Copy)]
54struct Disk { 54struct Disk {
55 port: usize, // port register base = ABAR + 0x100 55 port: usize, // port register base = ABAR + 0x100
56 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned 56 port_idx: u32, // implemented-port index this disk lives on (rescan bookkeeping)
57 ct: usize, // command table, 256 B, 128 B aligned 57 clb: usize, // command list, 1024 B (32 × 32 B), 256 B aligned
58 buf: usize, // 4 KiB DMA data buffer (page aligned) 58 ct: usize, // command table, 256 B, 128 B aligned
59 sectors: u64, // total sectors (LBA48 from IDENTIFY) 59 buf: usize, // 4 KiB DMA data buffer (page aligned)
60 sectors: u64, // total sectors (LBA48 from IDENTIFY)
60} 61}
61 62
62static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS]; 63static mut DISKS: [Option<Disk>; MAX_DISKS] = [None; MAX_DISKS];
63static mut DISK_COUNT: usize = 0; 64static mut DISK_COUNT: usize = 0;
64static mut ACTIVE: usize = 0; 65static mut ACTIVE: usize = 0;
65 66
67// Controller state kept for runtime rescan: ABAR + the implemented-port
68// bitmap, plus the last-known device presence (PxSSTS.DET == 3) per port.
69// `poll_dirty` compares live DET against this, so a hotplug is noticed
70// with a handful of MMIO reads; the full re-scan only runs on a change.
71const MAX_PORTS: usize = 32;
72static mut ABAR: usize = 0;
73static mut NP: u32 = 0;
74static mut IMPL_PORTS: u32 = 0;
75static mut PORT_UP: [bool; MAX_PORTS] = [false; MAX_PORTS];
76
66fn reg(a: usize) -> u32 { 77fn reg(a: usize) -> u32 {
67 unsafe { core::ptr::read_volatile(a as *const u32) } 78 unsafe { core::ptr::read_volatile(a as *const u32) }
68} 79}
@@ -173,6 +184,7 @@ fn start_port(abar: usize, port_idx: u32) -> Option<Disk> {
173 184
174 let mut d = Disk { 185 let mut d = Disk {
175 port, 186 port,
187 port_idx,
176 clb, 188 clb,
177 ct, 189 ct,
178 buf, 190 buf,
@@ -238,16 +250,24 @@ pub fn init() -> bool {
238 250
239 // Scan implemented ports (CAP.NP + 1 of them, filtered by PI). 251 // Scan implemented ports (CAP.NP + 1 of them, filtered by PI).
240 let cap = reg(abar as usize + CAP); 252 let cap = reg(abar as usize + CAP);
241 let np = ((cap & 0x1F) + 1).min(32); 253 let np = ((cap & 0x1F) + 1).min(MAX_PORTS as u32);
242 let pi = reg(abar as usize + PI); 254 let pi = reg(abar as usize + PI);
255 unsafe {
256 ABAR = abar as usize;
257 NP = np;
258 IMPL_PORTS = pi;
259 }
243 for port_idx in 0..np { 260 for port_idx in 0..np {
244 if (pi >> port_idx) & 1 == 0 { 261 if (pi >> port_idx) & 1 == 0 {
245 continue; 262 continue;
246 } 263 }
264 let det = reg(abar as usize + PORT_BASE + port_idx as usize * PORT_STRIDE + P_SSTS) & 0xF;
265 unsafe { PORT_UP[port_idx as usize] = det == 3; }
247 if let Some(d) = start_port(abar as usize, port_idx) { 266 if let Some(d) = start_port(abar as usize, port_idx) {
248 unsafe { 267 // The device answered IDENTIFY — it's definitely present.
249 DISKS[DISK_COUNT] = Some(d); 268 unsafe { PORT_UP[port_idx as usize] = true; }
250 DISK_COUNT += 1; 269 if !place_disk(d) {
270 crate::serial_println!("ahci: port {} up but disk table full", port_idx);
251 } 271 }
252 } 272 }
253 } 273 }
@@ -257,16 +277,103 @@ pub fn init() -> bool {
257 unsafe { DISK_COUNT > 0 } 277 unsafe { DISK_COUNT > 0 }
258} 278}
259 279
260/// Number of disks found at boot. 280/// Record a started disk in the table, reusing a tombstoned (None) slot so
281/// indices stay stable across plug/unplug cycles. Returns false if full.
282fn place_disk(d: Disk) -> bool {
283 unsafe {
284 for i in 0..MAX_DISKS {
285 if DISKS[i].is_none() {
286 DISKS[i] = Some(d);
287 DISK_COUNT = DISK_COUNT.max(i + 1);
288 return true;
289 }
290 }
291 }
292 false
293}
294
295/// Number of disk slots in use (high-water mark; tombstones included).
261pub fn disk_count() -> usize { 296pub fn disk_count() -> usize {
262 unsafe { DISK_COUNT } 297 unsafe { DISK_COUNT }
263} 298}
264 299
300/// Cheap runtime hotplug probe: true if any implemented port's device
301/// presence (PxSSTS.DET == 3) differs from the last known state. A few
302/// MMIO reads — safe to call on every shell prompt.
303pub fn poll_dirty() -> bool {
304 let abar = unsafe { ABAR };
305 if abar == 0 {
306 return false;
307 }
308 let (np, pi) = (unsafe { NP }, unsafe { IMPL_PORTS });
309 for port_idx in 0..np {
310 if (pi >> port_idx) & 1 == 0 {
311 continue;
312 }
313 let det = reg(abar + PORT_BASE + port_idx as usize * PORT_STRIDE + P_SSTS) & 0xF;
314 if (det == 3) != unsafe { PORT_UP[port_idx as usize] } {
315 return true;
316 }
317 }
318 false
319}
320
321/// Re-scan the controller's ports for arrivals/removals. Starts ports
322/// that now have a link, tombstones (None) the slots of ports whose device
323/// vanished, and refreshes the poll state. Slot indices never shift, so
324/// the filesystem's disk handles stay valid. Returns (added, removed).
325pub fn rescan() -> (usize, usize) {
326 let abar = unsafe { ABAR };
327 if abar == 0 {
328 return (0, 0);
329 }
330 let (np, pi) = (unsafe { NP }, unsafe { IMPL_PORTS });
331 let mut added = 0usize;
332 let mut removed = 0usize;
333 for port_idx in 0..np {
334 if (pi >> port_idx) & 1 == 0 {
335 continue;
336 }
337 let port = abar + PORT_BASE + port_idx as usize * PORT_STRIDE;
338 let det = reg(port + P_SSTS) & 0xF;
339 let up = det == 3;
340 let slot = disks()
341 .iter()
342 .position(|d| d.is_some_and(|d| d.port_idx == port_idx));
343 let up_prev = unsafe { PORT_UP[port_idx as usize] };
344 unsafe { PORT_UP[port_idx as usize] = up; }
345 if up == up_prev {
346 continue;
347 }
348 if up {
349 // Arrival: full bring-up on this port.
350 if let Some(d) = start_port(abar, port_idx) {
351 if place_disk(d) {
352 added += 1;
353 } else {
354 crate::serial_println!("ahci: port {} up but disk table full", port_idx);
355 }
356 }
357 } else if let Some(i) = slot {
358 // Removal: tombstone the slot; indices stay stable.
359 unsafe { DISKS[i] = None; }
360 crate::serial_println!("ahci: port {} down — device removed", port_idx);
361 removed += 1;
362 }
363 }
364 (added, removed)
365}
366
367/// Is the disk at index `idx` still attached? (False for tombstones.)
368pub fn is_up(idx: usize) -> bool {
369 disks().get(idx).is_some_and(|d| d.is_some())
370}
371
265/// Make `idx` the disk the sector API reads/writes. Returns false if the 372/// Make `idx` the disk the sector API reads/writes. Returns false if the
266/// index is out of range. 373/// index is out of range or its slot is a tombstone.
267pub fn select(idx: usize) -> bool { 374pub fn select(idx: usize) -> bool {
268 unsafe { 375 unsafe {
269 if idx < DISK_COUNT { 376 if idx < DISK_COUNT && DISKS[idx].is_some() {
270 ACTIVE = idx; 377 ACTIVE = idx;
271 true 378 true
272 } else { 379 } else {
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,
diff --git a/rsnix/src/fs.rs b/rsnix/src/fs.rs
index 063560a..b560bfa 100644
--- a/rsnix/src/fs.rs
+++ b/rsnix/src/fs.rs
@@ -183,33 +183,75 @@ pub fn init() -> (usize, usize) {
183 ); 183 );
184 184
185 // Mount every FAT32 volume under /disk/<UID>/ (UID = label-serial). 185 // Mount every FAT32 volume under /disk/<UID>/ (UID = label-serial).
186 let disks = crate::fat::volume_count(); 186 // `rescan_disks()` also keeps this table current after hotplug events.
187 if disks > 0 { 187 let _ = rescan_disks();
188 let disk_dir = add("disk", Some(root), FileType::Dir, NodeData::Dir(Vec::new()));
189 for vol in 0..disks {
190 let uid = String::from(crate::fat::uid(vol));
191 let root_cl = crate::fat::root_cluster(vol);
192 add(
193 &uid,
194 Some(disk_dir),
195 FileType::Dir,
196 NodeData::Disk(DiskInfo {
197 vol,
198 parent: root_cl,
199 name: String::new(),
200 is_dir: true,
201 size: 0,
202 cluster: root_cl,
203 }),
204 );
205 }
206 }
207 188
208 let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count(); 189 let dirs = nodes().iter().filter(|n| n.kind == FileType::Dir).count();
209 let files = nodes().len() - dirs; 190 let files = nodes().len() - dirs;
210 (dirs, files) 191 (dirs, files)
211} 192}
212 193
194/// (Re)sync the /disk mount table with the FAT volumes: create the /disk
195/// dir lazily, ensure a mount node for every mounted volume (refreshing
196/// its backing info), and tombstone nodes whose volume was unmounted.
197/// Returns (added, removed) node ops.
198pub fn rescan_disks() -> (usize, usize) {
199 let mut added = 0usize;
200 let mut removed = 0usize;
201 let disk_dir = match lookup(0, "disk") {
202 Some(h) => h,
203 None => add("disk", Some(0), FileType::Dir, NodeData::Dir(Vec::new())),
204 };
205 for vol in 0..crate::fat::volume_count() {
206 if !crate::fat::is_mounted(vol) {
207 continue;
208 }
209 let uid = String::from(crate::fat::uid(vol));
210 let root_cl = crate::fat::root_cluster(vol);
211 match lookup(disk_dir, &uid) {
212 Some(h) => {
213 // Refresh the backing info in case the volume's slot moved.
214 if let NodeData::Disk(i) = &mut nodes()[h].data {
215 i.vol = vol;
216 i.parent = root_cl;
217 i.cluster = root_cl;
218 }
219 nodes()[h].removed = false;
220 }
221 None => {
222 add(
223 &uid,
224 Some(disk_dir),
225 FileType::Dir,
226 NodeData::Disk(DiskInfo {
227 vol,
228 parent: root_cl,
229 name: String::new(),
230 is_dir: true,
231 size: 0,
232 cluster: root_cl,
233 }),
234 );
235 added += 1;
236 }
237 }
238 }
239 if let NodeData::Dir(v) = &nodes()[disk_dir].data {
240 for &c in v.clone().iter() {
241 if nodes()[c].removed {
242 continue;
243 }
244 if let NodeData::Disk(i) = &nodes()[c].data
245 && !crate::fat::is_mounted(i.vol)
246 {
247 nodes()[c].removed = true;
248 removed += 1;
249 }
250 }
251 }
252 (added, removed)
253}
254
213fn lookup(dir: usize, name: &str) -> Option<usize> { 255fn lookup(dir: usize, name: &str) -> Option<usize> {
214 let children = match &nodes()[dir].data { 256 let children = match &nodes()[dir].data {
215 NodeData::Dir(v) => v.clone(), 257 NodeData::Dir(v) => v.clone(),
diff --git a/shell/src/lib.rs b/shell/src/lib.rs
index ddd8688..28eb737 100644
--- a/shell/src/lib.rs
+++ b/shell/src/lib.rs
@@ -45,6 +45,12 @@ pub fn run() -> ! {
45 // Push any deferred scrolls so the prompt renders on a current 45 // Push any deferred scrolls so the prompt renders on a current
46 // framebuffer (e.g. after a streaming command's batch of output). 46 // framebuffer (e.g. after a streaming command's batch of output).
47 rsnix::vt::sync_scroll(); 47 rsnix::vt::sync_scroll();
48 // Cheap hotplug probe: if any AHCI port's device presence changed
49 // since the last scan, re-run discovery (mounts new volumes,
50 // unmounts gone ones) before showing the prompt.
51 if rsnix::ahci::poll_dirty() {
52 report_rescan(rescan_all());
53 }
48 // Print prompt 54 // Print prompt
49 term_print("> "); 55 term_print("> ");
50 56
@@ -681,6 +687,7 @@ write `{} <path> TIL <marker>`",
681 "stress" => cmd_stress(&parsed), 687 "stress" => cmd_stress(&parsed),
682 "date" => cmd_date(&parsed), 688 "date" => cmd_date(&parsed),
683 "pci" => cmd_pci(&parsed), 689 "pci" => cmd_pci(&parsed),
690 "rescan" => cmd_rescan(&parsed),
684 "ls" => cmd_ls(&parsed), 691 "ls" => cmd_ls(&parsed),
685 "cat" => cmd_cat(&parsed), 692 "cat" => cmd_cat(&parsed),
686 "klog" => cmd_klog(&parsed), 693 "klog" => cmd_klog(&parsed),
@@ -710,6 +717,7 @@ fn cmd_help(_p: &Parsed) {
710 stress — stress-test heap/page allocators 717 stress — stress-test heap/page allocators
711 date — show current date/time (RTC) 718 date — show current date/time (RTC)
712 pci — list PCI devices 719 pci — list PCI devices
720 rescan — re-scan for hotplugged/removed disks (also runs automatically)
713 ls — list a directory 721 ls — list a directory
714 cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc) 722 cat — print files (cat USE a TO b copies a→b; cat TIL END is a heredoc)
715 klog — kernel boot log (cat /dev/klog; WRITING <dst> persists it) 723 klog — kernel boot log (cat /dev/klog; WRITING <dst> persists it)
@@ -1527,6 +1535,33 @@ fn cmd_date(p: &Parsed) {
1527 pipe.end(); 1535 pipe.end();
1528} 1536}
1529 1537
1538/// Deep device re-scan: AHCI ports → FAT volumes → VFS mount table.
1539/// Returns (disks added, disks removed, vols mounted, vols unmounted,
1540/// nodes added, nodes removed) so callers can report the result.
1541fn rescan_all() -> (usize, usize, usize, usize, usize, usize) {
1542 let (da, dr) = rsnix::ahci::rescan();
1543 let (ma, mu) = rsnix::fat::rescan();
1544 let (na, nr) = rsnix::fs::rescan_disks();
1545 (da, dr, ma, mu, na, nr)
1546}
1547
1548/// Print the one-line change summary; returns true if anything changed.
1549fn report_rescan((da, dr, ma, mu, na, nr): (usize, usize, usize, usize, usize, usize)) -> bool {
1550 term_println(&alloc::format!(
1551 "rescan: {} disk(s) added, {} removed · {} volume(s) mounted, {} unmounted · {} node(s) added, {} removed",
1552 da, dr, ma, mu, na, nr
1553 ));
1554 da + dr + ma + mu + na + nr > 0
1555}
1556
1557fn cmd_rescan(_p: &Parsed) {
1558 if report_rescan(rescan_all()) {
1559 term_println("rescan done — /disk updated");
1560 } else {
1561 term_println("rescan done — nothing changed");
1562 }
1563}
1564
1530fn cmd_pci(p: &Parsed) { 1565fn cmd_pci(p: &Parsed) {
1531 let devices = rsnix::pci::probe(); 1566 let devices = rsnix::pci::probe();
1532 let mut pipe = Pipeline::new(&p.filters, &p.output); 1567 let mut pipe = Pipeline::new(&p.filters, &p.output);