Compare commits

...
Author SHA1 Message Date
Aaron e8e4061564 fix file header
continuous-integration/drone Build is failing
2026-11-07 10:19:01 +00:00
Aaron 10691a9e03 fix file headers 2026-11-07 10:18:33 +00:00
Aaron cebad0b583 Fix USB volume discovery, automounting, and file saves 2026-11-01 20:22:07 +00:00
Aaron 53cc758e60 Add standard license headers and Aaron Hance attribution 2026-10-31 20:56:15 +00:00
17 changed files with 334 additions and 27 deletions

No files matched your search

@@ -1,3 +1,20 @@
<!--
Copyright 2026 Aaron Hance
Author: Aaron Hance <[email protected]>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# USB mass storage
`driver.storage.ctl.usb` implements SCSI transparent commands over USB Mass
@@ -1,3 +1,20 @@
<!--
Copyright 2026 Aaron Hance
Author: Aaron Hance <[email protected]>
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
# driver.storage.mgr
Physical storage device registry and I/O dispatch manager.
+13 -11
View File
@@ -1200,7 +1200,8 @@ begin
vol := findVolumeForPartition(device, part.LBA_start);
if vol <> nil then begin
freeB := uint64(vol^.freeSectors) * vol^.sectorSize;
freeStr := uiConcat(formatSizeStr(freeB), ' free');
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then freeStr := 'Free: unknown'
else freeStr := uiConcat(formatSizeStr(freeB), ' free');
lbl := lv_label_create(row);
lv_label_set_text(lbl, freeStr);
lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0);
@@ -1240,7 +1241,6 @@ var
dw : sint32;
i : uint32;
totalBytes : uint64;
usedSectors : uint64;
freeBytes : uint64;
volCnt : uint32;
devVolCnt : uint32;
@@ -1274,22 +1274,18 @@ begin
totalBytes := uint64(device^.maxSectorCount) * device^.sectorSize;
addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes));
{ Compute free space from in-memory volume data (no I/O) }
usedSectors := 0;
{ Use the same cached partition geometry as Add Partition. Volumes may
still be undergoing filesystem probing after the device is published. }
devVolCnt := 0;
volCnt := driver.storage.vol.mgr.get_volume_count();
if volCnt > 0 then for i := 0 to volCnt - 1 do begin
vol := driver.storage.vol.mgr.get_volume(i);
if (vol <> nil) and (vol^.device = device) then begin
devVolCnt := devVolCnt + 1;
usedSectors := usedSectors + vol^.sectorCount;
end;
end;
if device^.maxSectorCount > usedSectors then
freeBytes := uint64(device^.maxSectorCount - usedSectors) * device^.sectorSize
else
freeBytes := 0;
addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes));
freeBytes := uint64(driver.storage.vol.mgr.get_free_sector_count(device)) * device^.sectorSize;
addInfoRow(detail, dw, 'Unallocated Space', formatSizePrecise(freeBytes));
addInfoRow(detail, dw, 'Volumes', uiInt(devVolCnt));
{ Usage bar }
@@ -1352,7 +1348,8 @@ begin
{ Free space }
freeB := uint64(vol^.freeSectors) * vol^.sectorSize;
freeStr := uiConcat(formatSizeStr(freeB), ' free');
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then freeStr := 'Free: unknown'
else freeStr := uiConcat(formatSizeStr(freeB), ' free');
lbl := lv_label_create(row);
lv_label_set_text(lbl, freeStr);
lv_obj_set_style_text_color(lbl, lv_color_make(100, 180, 130), 0);
@@ -1494,6 +1491,10 @@ begin
usedBytes := totalBytes - freeBytes;
addInfoRow(detail, dw, 'Total Size', formatSizePrecise(totalBytes));
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then begin
addInfoRow(detail, dw, 'Free Space', 'Unknown');
addInfoRow(detail, dw, 'Used Space', 'Unknown');
end else begin
addInfoRow(detail, dw, 'Free Space', formatSizePrecise(freeBytes));
addInfoRow(detail, dw, 'Used Space', formatSizePrecise(usedBytes));
@@ -1502,6 +1503,7 @@ begin
addInfoRow(detail, dw, 'Usage', uiConcat(uiInt(pctUsed), '%'));
addUsageBar(detail, dw, usedBytes, totalBytes);
end;
end;
addInfoRow(detail, dw, 'Boot Drive', boolStr(vol^.isBootDrive));
+4 -2
View File
@@ -355,8 +355,10 @@ begin
proc.mgr.kill(state^.pid);
state^.pid := 0;
end;
freeState(state);
if state^.ta <> nil then lv_group_remove_obj(state^.ta);
state^.ta := nil;
driver.video.windows.destroyWindow(wid);
freeState(state);
end;
{ ============================================================
@@ -440,7 +442,7 @@ begin
written := driver.storage.vfs.WriteFile(fHandle, 0, puint8(buf), len);
driver.storage.vfs.CloseFile(fHandle);
kfree(void(buf));
if written = 0 then begin
if written <> len then begin
io.syslog.logln('NOTEPAD', 'saveFile: WriteFile returned 0');
showErrorMsgbox('Save Failed', 'File write failed. Check filesystem.');
updateStatusBar(state);
+3 -1
View File
@@ -179,7 +179,9 @@ begin
print('Size: '); printsize(vol^.sectorCount * vol^.sectorSize); println('');
print('Free: '); printsize(vol^.freeSectors * vol^.sectorSize); println('');
print('Free: ');
if (vol^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0 then println('Unknown')
else begin printsize(vol^.freeSectors * vol^.sectorSize); println(''); end;
print('Boot Drive: ');
if vol^.isBootDrive then println('yes') else println('no');
@@ -1,6 +1,21 @@
// Copyright 2026 Kieron Morris
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
{
Driver->Bus->driver.bus.usb->USBCore - driver.bus.usb Core Enumeration, Transfer API & HC Management.
@author(Aaron Hance <[email protected]>)
@author(Kieron Morris <[email protected]>)
}
unit driver.bus.usb.core;
@@ -1,3 +1,17 @@
// Copyright 2026 Kieron Morris
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
{
Driver->Bus->driver.bus.usb->driver.bus.usb.ehci - Enhanced Host Controller Interface Driver.
@@ -8,6 +22,7 @@
driver.bus.usb.ehci also handles companion controller (driver.bus.usb.uhci/driver.bus.usb.ohci) routing for
full/low-speed devices via the CONFIGFLAG and PORTSC port-owner bits.
@author(Aaron Hance <[email protected]>)
@author(Kieron Morris <[email protected]>)
}
unit driver.bus.usb.ehci;
@@ -1,3 +1,17 @@
// Copyright 2026 Kieron Morris
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
{
Driver->Bus->driver.bus.usb->driver.bus.usb.ohci - Open Host Controller Interface Driver.
@@ -5,6 +19,7 @@
driver.bus.usb.ohci uses Endpoint Descriptors (ED), Transfer Descriptors (TD), and a
Host Controller Communication Area (HCCA) for scheduling and completion.
@author(Aaron Hance <ah@aaronhance.me>)
@author(Kieron Morris <kjm@kieronmorris.me>)
}
unit driver.bus.usb.ohci;
@@ -1,6 +1,21 @@
// Copyright 2026 Kieron Morris
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
{
Driver->Bus->driver.bus.usb->USBTypes - Shared driver.bus.usb Types, Constants & HC Abstraction.
@author(Aaron Hance <ah@aaronhance.me>)
@author(Kieron Morris <kjm@kieronmorris.me>)
}
unit driver.bus.usb.types;
@@ -1,7 +1,25 @@
// Copyright 2021 Aaron Hance
// Copyright 2026 Aaron Hance
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
{ USB Mass Storage: SCSI transparent commands over Bulk-Only Transport.
LUN 0, one interface, READ/WRITE(10), 512..4096 byte sectors.
A persistent worker owns all storage I/O. No caller buffer is ever DMA-visible.
Protocol reference: https://www.usb.org/sites/default/files/usbmassbulk_10.pdf }
{
@author(Aaron Hance <ah@aaronhance.me>)
}
unit driver.storage.ctl.usb;
interface
@@ -48,6 +66,7 @@ type
Request : PIORequest;
Tag : uint32;
Offline, Ready, InFlight, Attaching, VolumesDetached : boolean;
SyncUnsupported : boolean;
end;
var
@@ -258,6 +277,9 @@ begin
if transport(c, actual) <> 0 then exit;
if actual < 14 then exit;
if (puint8(c^.Buffer)[0] and $7E) <> $70 then exit;
io.syslog.log('USBMSC', 'SCSI sense key='); io.syslog.writeint(puint8(c^.Buffer)[2] and $0F);
io.syslog.writestring(' ASC='); io.syslog.writeint(puint8(c^.Buffer)[12]);
io.syslog.writestring(' ASCQ='); io.syslog.writeintln(puint8(c^.Buffer)[13]);
case puint8(c^.Buffer)[2] and $0F of
2: sense := eDeviceNotReady;
3: sense := eBadSector;
@@ -268,14 +290,36 @@ begin
end;
function execute(c : PMSDevice; required : uint32) : TError;
var actual : uint32; resultCode : uint8;
var actual : uint32; resultCode, opcode : uint8;
begin
opcode := c^.CBW.Command[0];
resultCode := transport(c, actual);
execute := eIOError;
if resultCode = 1 then execute := sense(c)
else if (resultCode = 0) and (actual = required) then execute := eNone;
if c^.Offline then execute := eIOTimeout;
if c^.Original = nil then execute := eDeviceRemoved;
if execute <> eNone then begin
io.syslog.log('USBMSC', 'Command failed: opcode='); io.syslog.writeint(opcode);
io.syslog.writestring(' error='); io.syslog.writeintln(ord(execute));
end;
end;
function sync_cache(c : PMSDevice) : TError;
begin
sync_cache := eNone;
if c^.SyncUnsupported then exit;
command(c, $35, 10, 0, false);
sync_cache := execute(c, 0);
{ Some removable flash devices reject SYNCHRONIZE CACHE with ILLEGAL
REQUEST despite accepting WRITE(10). This optional command must not turn
an acknowledged data write into a zero-byte failure. Other errors remain
failures; do not retry or suppress an unsuccessful WRITE(10). }
if sync_cache = eNotSupported then begin
c^.SyncUnsupported := true;
io.syslog.logln('USBMSC', 'SYNCHRONIZE CACHE unsupported; using device write completion.');
sync_cache := eNone;
end;
end;
function probe(c : PMSDevice; var sectors, sectorSize : uint32) : boolean;
@@ -375,10 +419,10 @@ begin
inc(doneBlocks, blocks); inc(offset, bytes);
end;
end;
{ Complete writes only after the device has committed its cache. }
{ Flush devices which support it; unsupported removable media use the
WRITE command completion contract. }
if (error = eNone) and (r^.RequestType = ioWrite) then begin
command(c, $35, 10, 0, false);
error := execute(c, 0);
error := sync_cache(c);
end;
if error = eReadOnly then c^.Disk^.writable := false;
r^.ByteCount := offset;
@@ -579,6 +623,7 @@ type
Request : TIORequest;
C : TMSDevice;
Stage, Fault, Controls, DataBytes : uint32;
SenseKey : uint8;
GoodWrite, GoodRecovery : boolean;
end;
@@ -611,7 +656,8 @@ begin
case m^.C.CBW.Command[0] of
$12: puint8(t^.Buffer)[0] := 0;
$25: begin put32(puint8(t^.Buffer), 8191); put32(puint8(uint32(t^.Buffer)+4), 512); end;
$03: begin memset(uint32(t^.Buffer),0,t^.BufferLen); puint8(t^.Buffer)[0]:=$70; puint8(t^.Buffer)[2]:=7; end;
$03: begin memset(uint32(t^.Buffer),0,t^.BufferLen); puint8(t^.Buffer)[0]:=$70;
if m^.SenseKey = 0 then puint8(t^.Buffer)[2]:=7 else puint8(t^.Buffer)[2]:=m^.SenseKey; end;
end;
end else begin
m^.GoodWrite := true;
@@ -708,6 +754,16 @@ begin
Assert(transport(@m^.C,actual) = 2, 'short CSW rejected');
m^.Fault := 0; command(@m^.C, $35, 10, 0, false);
Assert(execute(@m^.C,0) = eNone, 'cache synchronization without data stage');
m^.Fault := 3; m^.SenseKey := 3;
Assert(sync_cache(@m^.C) = eBadSector, 'flush media errors propagate');
Assert(not m^.C.SyncUnsupported, 'media error does not disable flush');
m^.Fault := 3; m^.SenseKey := 5;
Assert(sync_cache(@m^.C) = eNone, 'unsupported flush accepts completed write');
sectors := m^.C.Tag;
Assert((sync_cache(@m^.C) = eNone) and (m^.C.Tag = sectors), 'unsupported flush is not repeatedly submitted');
m^.Fault := 3; command(@m^.C, $2A, 10, 512, false);
Assert(execute(@m^.C,512) = eNotSupported, 'unsupported data write remains a failure');
m^.Fault := 0; m^.SenseKey := 0;
m^.Disk.sectorSize := 512; m^.Disk.maxSectorCount := 8192;
m^.Request.Buffer := m^.C.Buffer; m^.Request.LBA := 8191; m^.Request.SectorCount := 1;
Assert(valid_request(@m^.Disk,@m^.Request), 'last sector valid');
+52 -1
View File
@@ -15,6 +15,7 @@ type
TMock = record
Data : puint8;
Writes, FailWrite, Callbacks, Depth, MaxDepth : uint32;
MaxReadSectors, FailReadLBA : uint32;
Error : TError;
OutOfBounds, FailRead : boolean;
end;
@@ -27,7 +28,10 @@ begin
(request^.SectorCount > device^.maxSectorCount-request^.LBA) then begin
m^.OutOfBounds := true; complete_io(request, false, eInvalidArgument); exit;
end;
if (request^.RequestType <> ioWrite) and m^.FailRead then begin
if request^.RequestType <> ioWrite then
if request^.SectorCount > m^.MaxReadSectors then m^.MaxReadSectors := request^.SectorCount;
if (request^.RequestType <> ioWrite) and (m^.FailRead or
((m^.FailReadLBA <> 0) and (request^.LBA = m^.FailReadLBA))) then begin
Inc(m^.Depth); if m^.Depth > m^.MaxDepth then m^.MaxDepth := m^.Depth;
complete_io(request,false,eIOError); Dec(m^.Depth); exit;
end;
@@ -197,6 +201,53 @@ begin
mock^.FailWrite := 0;
Assert(remove_partition(device,0) = eNone,'delete commits');
Assert(get_volume_count() = oldCount,'delete detaches volume after write');
{ More than eight FAT sectors, with a partial final batch and FAT padding. }
kfree(void(mock^.Data));
mock^.Data := puint8(kalloc(4096*512));
if mock^.Data <> nil then begin
memset(uint32(mock^.Data),0,1500*512);
device^.maxSectorCount := 1500;
volume^.sectorStart := 0; volume^.sectorCount := 1500;
Assert(create_volume(volume,1500,0,@params) = eNone,'format multi-batch FAT fixture');
mock^.MaxReadSectors := 0;
Assert(identify_volume(volume),'identify multi-batch FAT');
Assert(volume^.freeSectors = 1454,'batched count excludes padding and allocated clusters');
Assert(mock^.MaxReadSectors = 8,'FAT scan batches eight sectors');
mock^.FailReadLBA := 40;
Assert(not identify_volume(volume),'failure in final FAT batch rejects probe');
mock^.FailReadLBA := 0;
Assert(not mock^.OutOfBounds,'batched scan stays within device');
device^.maxSectorCount := 4096; volume^.sectorCount := 4096;
Assert(create_volume(volume,4096,0,@params) = eNone,'format large FAT fixture');
mock^.FailReadLBA := 32; mock^.MaxReadSectors := 0;
Assert(identify_volume(volume),'large FAT mounts without reading allocation table');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'missing FSInfo reports unknown free space');
Assert(mock^.MaxReadSectors = 1,'large volume discovery uses only metadata sector reads');
PBootRecord(mock^.Data)^.FSInfoCluster := 1;
puint32(uint32(mock^.Data)+512)^ := $41615252;
puint32(uint32(mock^.Data)+512+484)^ := $61417272;
puint32(uint32(mock^.Data)+512+488)^ := 100;
puint32(uint32(mock^.Data)+512+508)^ := $AA550000;
Assert(identify_volume(volume),'valid FSInfo avoids FAT scan');
Assert((volume^.freeSectors = 100) and
((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) = 0),'FSInfo count displayed');
puint32(uint32(mock^.Data)+512+488)^ := $FFFFFFFF;
Assert(identify_volume(volume),'unknown FSInfo count does not prevent mount');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'unknown FSInfo count stays unknown');
puint32(uint32(mock^.Data)+512+488)^ := 4096;
Assert(identify_volume(volume),'out-of-range FSInfo count does not prevent mount');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'out-of-range FSInfo count rejected');
puint32(uint32(mock^.Data)+512+488)^ := 0;
Assert(identify_volume(volume),'full volume FSInfo accepted');
Assert((volume^.freeSectors = 0) and
((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) = 0),'known zero differs from unknown');
puint32(uint32(mock^.Data)+512+508)^ := 0;
Assert(identify_volume(volume),'invalid FSInfo signature does not prevent mount');
Assert((volume^.lifecycleFlags and STORAGE_VOLUME_FLAG_FREE_UNKNOWN) <> 0,'invalid FSInfo signature rejected');
mock^.FailReadLBA := 1;
Assert(identify_volume(volume),'optional FSInfo read failure does not prevent mount');
mock^.FailReadLBA := 0;
end else Assert(false,'allocate multi-batch FAT fixture');
kfree(device^.cachedMBR); CFIFO_Free(device^.requestQueue);
kfree(void(mock^.Data)); kfree(void(mock)); kfree(void(volume)); kfree(void(device)); kfree(void(br));
io.syslog.log('STORMETA','Passed: '); io.syslog.writeint(passed);
@@ -1,4 +1,5 @@
// Copyright 2021 Aaron Hance
// Copyright 2026 Aaron Hance
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -357,6 +357,7 @@ const
STORAGE_VOLUME_FLAG_FORMATTING = 2;
STORAGE_VOLUME_FLAG_INVALIDATING = 4;
STORAGE_VOLUME_FLAG_REMOVED = 8;
STORAGE_VOLUME_FLAG_FREE_UNKNOWN = 16;
implementation
+45 -1
View File
@@ -1,4 +1,5 @@
// Copyright 2021 Kieron Morris
// Copyright 2026 Aaron Hance
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -1500,6 +1501,10 @@ begin
asm popf end;
while puint32(@wait.Done)^ = 0 do asm hlt end;
if wait.Error = eNone then TransferFileSync := wait.BytesDone;
if Writing and (wait.Error <> eNone) then begin
io.syslog.log('VFS', 'Write failed: error='); io.syslog.writeint(ord(wait.Error));
io.syslog.writestring(' bytes='); io.syslog.writeintln(wait.BytesDone);
end;
end;
function WriteFile(FileHandle : TFileHandle; Position : uint32; Buffer : puint8; Length : uint32) : uint32;
@@ -3287,6 +3292,22 @@ begin
debug.tracer.push_trace('driver.storage.vfs.VFS_COMMAND_UMOUNT.exit');
end;
{ Installed at the boot mount phase, after the VFS is ready. Registration
happens in the discovery task (usb.attach for USB), not an IRQ or UI callback.
Mounting only creates VFS objects; filesystem probing has already completed. }
var autoMountWatch : uint32 = 0;
procedure mount_added_volume(event : TStorageLifecycleEvent; device : PStorage_Device;
volume : PStorage_Volume; error : TError; userdata : pointer);
begin
if (event <> sleVolumeAdded) or (error <> eNone) then exit;
if (device = nil) or (volume = nil) then exit;
if device^.removed or (volume^.filesystem = nil) then exit;
if driver.storage.vol.mgr.volume_is_busy(volume) then exit;
if mountDefaultVolume(volume) <> eNone then
io.syslog.logln('VFS','Could not automatically mount newly discovered volume.');
end;
{ Auto-mount all discovered volumes into /disk/ and honour persistent mounts }
procedure auto_mount_volumes();
@@ -3311,6 +3332,11 @@ var
begin
debug.tracer.push_trace('driver.storage.vfs.auto_mount_volumes.enter');
{ Subscribe even when no disks were present at boot. Avoid duplicate watches
if the boot mount pass is invoked again. }
if autoMountWatch = 0 then
autoMountWatch := driver.storage.vol.mgr.watch_lifecycle(@mount_added_volume, nil);
volCount := driver.storage.vol.mgr.get_volume_count();
if volCount = 0 then begin
debug.tracer.push_trace('driver.storage.vfs.auto_mount_volumes.exit');
@@ -3657,7 +3683,25 @@ begin
obj := GetObjectFromPath('/disk/__RENAMED_TEST__');
Assert((obj <> nil) and (obj^.Reference = void(mountVolA)),'renamed mount points to same volume');
InvalidateVolume(mountVolA); InvalidateVolume(mountVolB);
kfree(void(mountVolA)); kfree(void(mountVolB)); kfree(void(mountFS));
{ Exercise real registration notifications after the boot mount pass. }
mountDevice := PStorage_Device(kalloc(sizeof(TStorage_Device)));
memset(uint32(mountDevice),0,sizeof(TStorage_Device));
mountDevice^.writable := true;
mountVolA^.device := mountDevice; mountVolB^.device := mountDevice;
memcpy(uint32(pchar('__HOTPLUG_TEST__')),uint32(@mountVolA^.labelText[0]),17);
mountVolB^.labelText := mountVolA^.labelText;
mountVolA^.filesystem := nil;
driver.storage.vol.mgr.register_volume(mountDevice,mountVolA);
Assert(GetObjectFromPath('/disk/__HOTPLUG_TEST__') = nil,'unrecognized hotplug volume is not mounted');
mountVolB^.filesystem := mountFS;
driver.storage.vol.mgr.register_volume(mountDevice,mountVolB);
obj := GetObjectFromPath('/disk/__HOTPLUG_TEST__');
Assert((obj <> nil) and (obj^.Reference = void(mountVolB)),'registration automatically mounts recognized volume');
Assert((mountVolB^.lifecycleFlags and STORAGE_VOLUME_FLAG_MOUNTED) <> 0,'hotplug mount publishes mounted state');
driver.storage.vol.mgr.delete_volume(mountVolA);
driver.storage.vol.mgr.delete_volume(mountVolB);
Assert(GetObjectFromPath('/disk/__HOTPLUG_TEST__') = nil,'hotplug removal clears mount');
kfree(void(mountDevice)); kfree(void(mountFS));
watchFired := 0;
io.syslog.logln('VFS', 'Unit tests starting...');
@@ -62,24 +62,47 @@ begin
end;
function countFreeFATClusters(volume : PStorage_Volume; br : PBootRecord) : uint32;
var limit, sectorIdx, entryIdx, clusterNum, count : uint32; buffer : puint32;
var limit, sectorIdx, entryIdx, clusterNum, count, sectors, batch : uint32; buffer : puint32;
begin
countFreeFATClusters := $FFFFFFFF;
limit := FAT32ClusterLimit(br);
if limit <= 2 then exit;
buffer := puint32(kalloc(512));
volume^.lifecycleFlags := volume^.lifecycleFlags and (not STORAGE_VOLUME_FLAG_FREE_UNKNOWN);
{ Match the USB storage transfer buffer: eight sectors per command instead
of a CBW/data/CSW round trip for every 512 bytes of a large FAT. }
buffer := puint32(kalloc(4096));
if buffer = nil then exit;
{ FSInfo is optional, and its count is advisory. Validate every signature,
the reserved-sector location, and the count before displaying it. }
if (br^.FSInfoCluster > 0) and (br^.FSInfoCluster < br^.rsvSectors) then begin
if storage_read(volume^.device, volume^.sectorStart + br^.FSInfoCluster, 1, buffer) = eNone then
if (buffer[0] = $41615252) and (buffer[121] = $61417272) and
(buffer[127] = $AA550000) and (buffer[122] <= limit - 2) then begin
countFreeFATClusters := buffer[122]; kfree(buffer); exit;
end;
end;
count := 0; sectorIdx := 0;
while sectorIdx * 128 < limit do begin
if storage_read(volume^.device, volume^.sectorStart + br^.rsvSectors + sectorIdx, 1, buffer) <> eNone then begin
sectors := (limit + 127) div 128;
{ Mounting must not wait for an O(disk-size) allocation scan. Small FATs
remain cheap to count; larger volumes without FSInfo mount with unknown
free space. Allocation always consults the actual FAT, never this hint. }
if sectors > 16 then begin
volume^.lifecycleFlags := volume^.lifecycleFlags or STORAGE_VOLUME_FLAG_FREE_UNKNOWN;
countFreeFATClusters := 0;
kfree(buffer); exit;
end;
for entryIdx := 0 to 127 do begin
while sectorIdx < sectors do begin
batch := sectors - sectorIdx;
if batch > 8 then batch := 8;
if storage_read(volume^.device, volume^.sectorStart + br^.rsvSectors + sectorIdx, batch, buffer) <> eNone then begin
kfree(buffer); exit;
end;
for entryIdx := 0 to batch * 128 - 1 do begin
clusterNum := sectorIdx * 128 + entryIdx;
if clusterNum >= limit then break;
if (clusterNum >= 2) and ((buffer[entryIdx] and $0FFFFFFF) = 0) then Inc(count);
end;
Inc(sectorIdx);
Inc(sectorIdx, batch);
end;
kfree(buffer);
countFreeFATClusters := count;
+14
View File
@@ -1,3 +1,17 @@
// Copyright 2026 Kieron Morris
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
{
ProcessManager - Preemptive process management for the Asuro kernel.
+17
View File
@@ -1,3 +1,20 @@
// Copyright 2026 Aaron Hance
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
{
@author(Aaron Hance <ah@aaronhance.me>)
}
unit test;
interface