From 1afec670853ccd726ce2ef3f1f8057ef1fe0bb08 Mon Sep 17 00:00:00 2001 From: gmipf Date: Wed, 1 Jul 2026 22:14:16 +0200 Subject: [PATCH 1/5] Enumerate Linux floppy drives for dumping Linux only surfaced optical and fixed/removable block devices, so floppy drives were never offered as dump targets even though Aaru and DiscImageCreator can dump them and Windows already tags floppies via WMI. Add a dedicated floppy enumerator that scans /dev for "fd" followed by digits (fd0, fd1, ...), tags each as InternalDriveType.Floppy, and lists it like an optical drive with media presence left to the dumping program. It is gated behind the fixed-drive toggle to match how Windows surfaces floppies. Co-Authored-By: Claude Opus 4.8 --- CHANGELIST.md | 4 ++ MPF.Frontend.Test/DriveTests.cs | 50 ++++++++++++++++ MPF.Frontend/Drive.Linux.cs | 102 ++++++++++++++++++++++++++++++-- MPF.Frontend/Drive.cs | 3 + 4 files changed, 154 insertions(+), 5 deletions(-) diff --git a/CHANGELIST.md b/CHANGELIST.md index 4819a1c1d..e0de3c128 100644 --- a/CHANGELIST.md +++ b/CHANGELIST.md @@ -1,3 +1,7 @@ +### WIP (xxxx-xx-xx) + +- Enumerate Linux floppy drives (gmipf) + ### 3.8.2 (2026-07-01) - Output used configuration path for commandline programs diff --git a/MPF.Frontend.Test/DriveTests.cs b/MPF.Frontend.Test/DriveTests.cs index 3d673583b..b9dd48ccf 100644 --- a/MPF.Frontend.Test/DriveTests.cs +++ b/MPF.Frontend.Test/DriveTests.cs @@ -107,6 +107,56 @@ public void EnumerateUnixOpticalBlockPaths_OnlyMatchesSrFollowedByDigits() #endregion + #region EnumerateUnixFloppyBlockPaths + + [Fact] + public void EnumerateUnixFloppyBlockPaths_NullOrEmpty_ReturnsEmpty() + { + Assert.Empty(Drive.EnumerateUnixFloppyBlockPaths(null!)); + Assert.Empty(Drive.EnumerateUnixFloppyBlockPaths(string.Empty)); + } + + [Fact] + public void EnumerateUnixFloppyBlockPaths_MissingDirectory_ReturnsEmpty() + { + string missing = Path.Combine(Path.GetTempPath(), "mpf-test-missing-" + Guid.NewGuid().ToString("N")); + Assert.Empty(Drive.EnumerateUnixFloppyBlockPaths(missing)); + } + + [Fact] + public void EnumerateUnixFloppyBlockPaths_OnlyMatchesFdFollowedByDigits() + { + string root = Path.Combine(Path.GetTempPath(), "mpf-test-dev-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(root); + try + { + File.WriteAllText(Path.Combine(root, "fd0"), string.Empty); + File.WriteAllText(Path.Combine(root, "fd1"), string.Empty); + File.WriteAllText(Path.Combine(root, "fd7"), string.Empty); + File.WriteAllText(Path.Combine(root, "fd0u1440"), string.Empty); // format-specific node, skipped + File.WriteAllText(Path.Combine(root, "fda"), string.Empty); // name not all-digits, skipped + File.WriteAllText(Path.Combine(root, "fd"), string.Empty); // no trailing digits, skipped + File.WriteAllText(Path.Combine(root, "sdb"), string.Empty); // unrelated device, skipped + + var actual = Drive.EnumerateUnixFloppyBlockPaths(root); + + var actualNames = new List(); + foreach (var p in actual) + { + actualNames.Add(Path.GetFileName(p)); + } + actualNames.Sort(StringComparer.Ordinal); + + Assert.Equal(new[] { "fd0", "fd1", "fd7" }, actualNames); + } + finally + { + Directory.Delete(root, recursive: true); + } + } + + #endregion + #region EnumerateUnixOpticalGenericPaths [Fact] diff --git a/MPF.Frontend/Drive.Linux.cs b/MPF.Frontend/Drive.Linux.cs index 5eb4b348e..17564cbd2 100644 --- a/MPF.Frontend/Drive.Linux.cs +++ b/MPF.Frontend/Drive.Linux.cs @@ -11,15 +11,15 @@ public partial class Drive #region Fields /// - /// sysfs block-device name prefixes that are not user-facing dump targets: loopback - /// images, ramdisks, device-mapper and software RAID virtual devices, floppy and - /// network block devices, ZFS volumes, and optical drives (surfaced separately via - /// the optical enumerator). + /// sysfs block-device name prefixes that the fixed-drive enumerator does not surface: + /// loopback images, ramdisks, device-mapper and software RAID virtual devices, network + /// block devices, ZFS volumes, and the floppy and optical drives (each surfaced + /// separately by its own enumerator rather than as a fixed or removable disk). /// private static readonly string[] _excludedUnixBlockPrefixes = [ "dm-", // device-mapper (LVM/LUKS, etc.) virtual devices - "fd", // floppy disk + "fd", // floppy drive (surfaced separately by the floppy enumerator) "loop", // loopback-mounted image "md", // software RAID (mdadm) "nbd", // network block device @@ -96,6 +96,65 @@ private static Drive[] AppendUnixOpticalDrives(Drive[] drives) return combined; } + /// + /// Append Linux floppy drives that DriveInfo did not surface. + /// Like optical drives, floppy device nodes (/dev/fd0, /dev/fd1, ...) are never mount + /// points, so DriveInfo does not list them; they are enumerated directly so users can + /// dump disks without mounting them first. This mirrors how Windows surfaces floppy + /// drives (tagged via WMI in MarkWindowsFloppyDrives) and, like the Windows path, is + /// gated behind the fixed-drive toggle rather than always listed as optical drives are. + /// + /// Drives already discovered via DriveInfo + /// The drive array, extended with any floppy drives not already present + private static Drive[] AppendUnixFloppyDrives(Drive[] drives) + { + // Defensive: this reads Linux /dev paths, so never run it off-Unix + if (Environment.OSVersion.Platform != PlatformID.Unix) + return drives; + + var existingNames = new HashSet(); + foreach (var d in drives) + { + if (d?.Name is not null) + existingNames.Add(d.Name); + } + + var extra = new List(); + foreach (var devicePath in EnumerateUnixFloppyBlockPaths("/dev")) + { + // Skip paths already surfaced by DriveInfo or an earlier enumerator + if (!existingNames.Add(devicePath)) + continue; + + try + { + var d = Create(Frontend.InternalDriveType.Floppy, devicePath); + if (d != null) + { + // A Linux floppy /dev node is never a mount point, so DriveInfo always + // reports it as not-ready. Mark it active like optical drives; whether a + // disk is actually inserted is left to the dumping program rather than + // probing the device here, since floppy media state is not reliably + // exposed through sysfs. + d.MarkedActive = true; + extra.Add(d); + } + } + catch + { + // Skip devices that can't be opened + } + } + + if (extra.Count == 0) + return drives; + + var combined = new Drive[drives.Length + extra.Count]; + Array.Copy(drives, combined, drives.Length); + extra.CopyTo(combined, drives.Length); + return combined; + } + /// /// Enumerate Linux optical block devices under a directory by matching /// the kernel convention "sr" followed by one or more digits (sr0, sr1, ...). @@ -127,6 +186,39 @@ internal static List EnumerateUnixOpticalBlockPaths(string devRoot) return result; } + /// + /// Enumerate Linux floppy block devices under a directory by matching the kernel + /// convention "fd" followed by one or more digits (fd0, fd1, ...). This deliberately + /// rejects the "/dev/fd" symlink (no trailing digits) and format-specific nodes such + /// as "fd0u1440" (embedded non-digit characters), which are not whole-device targets. + /// + /// Root directory to scan (typically "/dev") + /// Device paths, or an empty list when the directory is unreadable + internal static List EnumerateUnixFloppyBlockPaths(string devRoot) + { + var result = new List(); + if (string.IsNullOrEmpty(devRoot) || !Directory.Exists(devRoot)) + return result; + + string[] candidates; + try + { + candidates = Directory.GetFiles(devRoot, "fd*"); + } + catch + { + return result; + } + + foreach (var path in candidates) + { + if (HasDeviceIndexSuffix(Path.GetFileName(path), "fd")) + result.Add(path); + } + + return result; + } + /// /// Enumerate Linux optical drives via their generic SCSI nodes (/dev/sg*). /// Unlike the block nodes, "sg" is not optical-specific, so each candidate is diff --git a/MPF.Frontend/Drive.cs b/MPF.Frontend/Drive.cs index 2abc9b93a..351da044e 100644 --- a/MPF.Frontend/Drive.cs +++ b/MPF.Frontend/Drive.cs @@ -324,7 +324,10 @@ private static List GetDriveList(bool ignoreFixedDrives) { drives = AppendUnixOpticalDrives(drives); if (!ignoreFixedDrives) + { drives = AppendUnixFixedDrives(drives); + drives = AppendUnixFloppyDrives(drives); + } } return [.. drives]; From d3e61e7da591b7337cd8ed84673bb0cb129e677b Mon Sep 17 00:00:00 2001 From: gmipf Date: Thu, 2 Jul 2026 15:45:56 +0200 Subject: [PATCH 2/5] Detect USB floppy drives and always list floppies on Linux USB floppy drives enumerate as removable SCSI disks (/dev/sd*), not /dev/fd* nodes, so a removable block device whose media matches a standard floppy geometry (360 KB to 2.88 MB) is now classified as InternalDriveType.Floppy. Floppy drives are listed regardless of the fixed-drive toggle, alongside optical drives, so USB floppies appear without also surfacing fixed disks. Classifying the drive as Floppy makes GetPhysicalMediaType return FloppyDisk, satisfying the removable/floppy check in DumpEnvironment.ParametersValid so DiscImageCreator dumps with fd. This mirrors how Windows tags floppy media via Win32_LogicalDisk.MediaType. Co-Authored-By: Claude Opus 4.8 --- MPF.Frontend.Test/DriveTests.cs | 51 +++++++++++++++++++++ MPF.Frontend/Drive.Linux.cs | 78 ++++++++++++++++++++++++++++++--- MPF.Frontend/Drive.cs | 8 ++-- 3 files changed, 127 insertions(+), 10 deletions(-) diff --git a/MPF.Frontend.Test/DriveTests.cs b/MPF.Frontend.Test/DriveTests.cs index b9dd48ccf..b64e2d32a 100644 --- a/MPF.Frontend.Test/DriveTests.cs +++ b/MPF.Frontend.Test/DriveTests.cs @@ -157,6 +157,57 @@ public void EnumerateUnixFloppyBlockPaths_OnlyMatchesFdFollowedByDigits() #endregion + #region EnumerateUnixUsbFloppyBlockPaths + + [Fact] + public void EnumerateUnixUsbFloppyBlockPaths_NullOrEmpty_ReturnsEmpty() + { + Assert.Empty(Drive.EnumerateUnixUsbFloppyBlockPaths(null!, "/dev")); + Assert.Empty(Drive.EnumerateUnixUsbFloppyBlockPaths(string.Empty, "/dev")); + Assert.Empty(Drive.EnumerateUnixUsbFloppyBlockPaths("/sys/block", null!)); + Assert.Empty(Drive.EnumerateUnixUsbFloppyBlockPaths("/sys/block", string.Empty)); + } + + [Fact] + public void EnumerateUnixUsbFloppyBlockPaths_MissingDirectory_ReturnsEmpty() + { + string missing = Path.Combine(Path.GetTempPath(), "mpf-test-missing-" + Guid.NewGuid().ToString("N")); + Assert.Empty(Drive.EnumerateUnixUsbFloppyBlockPaths(missing, "/dev")); + } + + [Fact] + public void EnumerateUnixUsbFloppyBlockPaths_OnlyMatchesRemovableFloppySizedDisks() + { + string sysfs = Path.Combine(Path.GetTempPath(), "mpf-test-sysblock-" + Guid.NewGuid().ToString("N")); + Directory.CreateDirectory(sysfs); + try + { + WriteSysBlockDevice(sysfs, "sdh", removable: "1", sizeSectors: "2880"); // USB floppy, 1.44 MB, matched + WriteSysBlockDevice(sysfs, "sdi", removable: "1\n", sizeSectors: "1440\n"); // USB floppy, 720 KB (trailing newline), matched + WriteSysBlockDevice(sysfs, "sda", removable: "0", sizeSectors: "2880"); // fixed disk of floppy size, not removable, skipped + WriteSysBlockDevice(sysfs, "sdb", removable: "1", sizeSectors: "30310400"); // USB flash drive, not a floppy size, skipped + WriteSysBlockDevice(sysfs, "sdc", removable: "1", sizeSectors: "0"); // empty floppy drive (no media), skipped + WriteSysBlockDevice(sysfs, "nvme0n1", removable: "1", sizeSectors: "2880"); // not an sd* device, not scanned + + var actual = Drive.EnumerateUnixUsbFloppyBlockPaths(sysfs, "/dev"); + + var actualNames = new List(); + foreach (var p in actual) + { + actualNames.Add(Path.GetFileName(p)); + } + actualNames.Sort(StringComparer.Ordinal); + + Assert.Equal(new[] { "sdh", "sdi" }, actualNames); + } + finally + { + Directory.Delete(sysfs, recursive: true); + } + } + + #endregion + #region EnumerateUnixOpticalGenericPaths [Fact] diff --git a/MPF.Frontend/Drive.Linux.cs b/MPF.Frontend/Drive.Linux.cs index 17564cbd2..abfa98e66 100644 --- a/MPF.Frontend/Drive.Linux.cs +++ b/MPF.Frontend/Drive.Linux.cs @@ -29,6 +29,21 @@ public partial class Drive "zram", // compressed ramdisk ]; + /// + /// Floppy media sizes in bytes for the standard 5.25" and 3.5" PC formats + /// (360 KB, 720 KB, 1.2 MB, 1.44 MB, 2.88 MB). A removable SCSI disk reporting one of + /// these exact capacities is a USB floppy with media inserted; no other removable + /// storage uses these sizes, so this doubles as the floppy identity check. + /// + private static readonly HashSet _unixFloppyMediaSizes = new HashSet + { + 368640, // 360 KB (5.25" DD) + 737280, // 720 KB (3.5" DD) + 1228800, // 1.2 MB (5.25" HD) + 1474560, // 1.44 MB (3.5" HD) + 2949120, // 2.88 MB (3.5" ED) + }; + #endregion #region Linux Helpers @@ -97,12 +112,13 @@ private static Drive[] AppendUnixOpticalDrives(Drive[] drives) } /// - /// Append Linux floppy drives that DriveInfo did not surface. - /// Like optical drives, floppy device nodes (/dev/fd0, /dev/fd1, ...) are never mount - /// points, so DriveInfo does not list them; they are enumerated directly so users can - /// dump disks without mounting them first. This mirrors how Windows surfaces floppy - /// drives (tagged via WMI in MarkWindowsFloppyDrives) and, like the Windows path, is - /// gated behind the fixed-drive toggle rather than always listed as optical drives are. + /// Append Linux floppy drives that DriveInfo did not surface. Two kinds are covered: + /// legacy on-board floppy nodes (/dev/fd0, /dev/fd1, ...) and USB floppy drives, which + /// the kernel exposes as ordinary SCSI disks (/dev/sd*). Neither is ever a mount point, + /// so DriveInfo does not list them; they are enumerated directly so users can dump disks + /// without mounting them first. Like optical drives, floppy drives are surfaced + /// regardless of the fixed-drive toggle, mirroring how Windows tags floppy media + /// (Win32_LogicalDisk.MediaType) in MarkWindowsFloppyDrives. /// /// Drives already discovered via DriveInfo /// The drive array, extended with any floppy drives not already present @@ -119,8 +135,13 @@ private static Drive[] AppendUnixFloppyDrives(Drive[] drives) existingNames.Add(d.Name); } + // Legacy /dev/fd* nodes plus USB floppy drives exposed as SCSI disks (/dev/sd*) + var devicePaths = new List(); + devicePaths.AddRange(EnumerateUnixFloppyBlockPaths("/dev")); + devicePaths.AddRange(EnumerateUnixUsbFloppyBlockPaths("/sys/block", "/dev")); + var extra = new List(); - foreach (var devicePath in EnumerateUnixFloppyBlockPaths("/dev")) + foreach (var devicePath in devicePaths) { // Skip paths already surfaced by DriveInfo or an earlier enumerator if (!existingNames.Add(devicePath)) @@ -219,6 +240,49 @@ internal static List EnumerateUnixFloppyBlockPaths(string devRoot) return result; } + /// + /// Enumerate USB floppy drives, which the kernel exposes as ordinary SCSI disks + /// (/dev/sd*) rather than /dev/fd* nodes. A disk is treated as a floppy when it reports + /// itself removable and its media is one of the standard floppy sizes. This mirrors how + /// Windows identifies a floppy from the inserted medium (Win32_LogicalDisk.MediaType) + /// and, like that path, only recognizes the drive while floppy media is present. + /// + /// sysfs block directory (typically "/sys/block") + /// Root directory device nodes live under (typically "/dev") + /// Device paths, or an empty list when the directory is unreadable + internal static List EnumerateUnixUsbFloppyBlockPaths(string sysBlockRoot, string devRoot) + { + var result = new List(); + if (string.IsNullOrEmpty(sysBlockRoot) || string.IsNullOrEmpty(devRoot)) + return result; + if (!Directory.Exists(sysBlockRoot)) + return result; + + string[] entries; + try + { + entries = Directory.GetFileSystemEntries(sysBlockRoot, "sd*"); + } + catch + { + return result; + } + + foreach (var entry in entries) + { + // A floppy reports itself removable and its media is a standard floppy size; + // both come from world-readable sysfs, so no elevated privileges are needed. + if (!ReadUnixRemovableFlag(entry)) + continue; + if (!_unixFloppyMediaSizes.Contains(ReadUnixBlockDeviceSize(entry))) + continue; + + result.Add(Path.Combine(devRoot, Path.GetFileName(entry))); + } + + return result; + } + /// /// Enumerate Linux optical drives via their generic SCSI nodes (/dev/sg*). /// Unlike the block nodes, "sg" is not optical-specific, so each candidate is diff --git a/MPF.Frontend/Drive.cs b/MPF.Frontend/Drive.cs index 351da044e..db9176c70 100644 --- a/MPF.Frontend/Drive.cs +++ b/MPF.Frontend/Drive.cs @@ -322,12 +322,14 @@ private static List GetDriveList(bool ignoreFixedDrives) } else if (isUnix) { + // Optical and floppy drives are removable dump targets, so surface them + // regardless of the fixed-drive toggle; only fixed and non-floppy removable + // disks are hidden unless the user opts to show them. Floppy runs before the + // fixed enumerator so a USB floppy is tagged Floppy and then skipped there. drives = AppendUnixOpticalDrives(drives); + drives = AppendUnixFloppyDrives(drives); if (!ignoreFixedDrives) - { drives = AppendUnixFixedDrives(drives); - drives = AppendUnixFloppyDrives(drives); - } } return [.. drives]; From 08bec53b06555a5cc98763b77962facb19bb1359 Mon Sep 17 00:00:00 2001 From: gmipf Date: Thu, 2 Jul 2026 15:45:56 +0200 Subject: [PATCH 3/5] Fix media type dropdown not updating dumping parameters The media type ComboBox in both the Avalonia and WPF frontends bound SelectedItem to a CurrentMediaType property that was renamed to CurrentPhysicalMediaType in the RedumpLib 1.11 migration (a4c9ee6d), which updated the code-behind but not the XAML markup. The stale binding failed silently, so changing the media type never wrote back to the view model and the generated DiscImageCreator command was never refreshed (it stayed cd even when Floppy Disk was selected). Point both bindings at CurrentPhysicalMediaType. Co-Authored-By: Claude Opus 4.8 --- MPF.Avalonia/Windows/MainWindow.axaml | 2 +- MPF.UI/Windows/MainWindow.xaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/MPF.Avalonia/Windows/MainWindow.axaml b/MPF.Avalonia/Windows/MainWindow.axaml index 8e0b8f81a..2aeb74dda 100644 --- a/MPF.Avalonia/Windows/MainWindow.axaml +++ b/MPF.Avalonia/Windows/MainWindow.axaml @@ -117,7 +117,7 @@ diff --git a/MPF.UI/Windows/MainWindow.xaml b/MPF.UI/Windows/MainWindow.xaml index decb2a62c..81dd1aee1 100644 --- a/MPF.UI/Windows/MainWindow.xaml +++ b/MPF.UI/Windows/MainWindow.xaml @@ -209,7 +209,7 @@ From f2dcebf18b1367b68d51a5dbb092d205cdd5cf5e Mon Sep 17 00:00:00 2001 From: gmipf Date: Thu, 2 Jul 2026 18:03:15 +0200 Subject: [PATCH 4/5] Reset detected media type when switching drives CacheCurrentMediaType only set _detectedPhysicalMediaType on a successful detection and never cleared it, so a type detected for one drive (e.g. FloppyDisk) carried over when switching to a drive whose media could not be read -- an empty optical drive would keep showing Floppy Disk instead of the system default (CD-ROM). Clear the remembered type at the start of each drive's caching so only a fresh detection sets it. Co-Authored-By: Claude Opus 4.8 --- MPF.Frontend/ViewModels/MainViewModel.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MPF.Frontend/ViewModels/MainViewModel.cs b/MPF.Frontend/ViewModels/MainViewModel.cs index dc8df942b..475c9c881 100644 --- a/MPF.Frontend/ViewModels/MainViewModel.cs +++ b/MPF.Frontend/ViewModels/MainViewModel.cs @@ -1207,6 +1207,12 @@ private void CacheCurrentMediaType() if (CurrentDrive is null) return; + // Forget the type detected for any previously selected drive; only a fresh + // successful detection below sets a new one. Without this reset a type like + // FloppyDisk would stick when switching to a drive whose media cannot be read + // (e.g. an empty optical drive), overriding the correct system default. + _detectedPhysicalMediaType = null; + // Get reasonable default values based on the current system var mediaTypes = CurrentSystem.MediaTypes(); PhysicalMediaType? defaultPhysicalMediaType = mediaTypes.Count > 0 ? mediaTypes[0] : PhysicalMediaType.CDROM; From f6e15eac4f8ea7bd4d2151b14490979b57bc9c95 Mon Sep 17 00:00:00 2001 From: gmipf Date: Thu, 2 Jul 2026 21:06:54 +0200 Subject: [PATCH 5/5] Gate Linux floppy drives behind the fixed-drive toggle Match Windows, where floppy drives enumerate as removable media that is only queried when fixed drives are shown. The default program, Redumper, does not support floppy dumping, so hiding floppies by default avoids listing an unusable target for most users. Requested in review. Co-Authored-By: Claude Opus 4.8 --- MPF.Frontend/Drive.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/MPF.Frontend/Drive.cs b/MPF.Frontend/Drive.cs index db9176c70..840b15c0b 100644 --- a/MPF.Frontend/Drive.cs +++ b/MPF.Frontend/Drive.cs @@ -322,14 +322,18 @@ private static List GetDriveList(bool ignoreFixedDrives) } else if (isUnix) { - // Optical and floppy drives are removable dump targets, so surface them - // regardless of the fixed-drive toggle; only fixed and non-floppy removable - // disks are hidden unless the user opts to show them. Floppy runs before the - // fixed enumerator so a USB floppy is tagged Floppy and then skipped there. + // Optical drives are removable dump targets, so surface them regardless of the + // fixed-drive toggle. Floppy drives are hidden unless the user opts to show + // fixed drives, matching Windows (where floppies enumerate as removable media + // that is only queried alongside fixed drives) and because the default program, + // Redumper, does not support them. Floppy runs before the fixed enumerator so a + // USB floppy is tagged Floppy and then skipped there. drives = AppendUnixOpticalDrives(drives); - drives = AppendUnixFloppyDrives(drives); if (!ignoreFixedDrives) + { + drives = AppendUnixFloppyDrives(drives); drives = AppendUnixFixedDrives(drives); + } } return [.. drives];