2026-03-01 11:09:22 +01:00
// Copyright (c) 2026 Duplicati Inc. All rights reserved.
using System ;
using System.Buffers ;
2026-03-02 13:50:39 +00:00
using System.Collections.Generic ;
2026-03-01 11:09:22 +01:00
using System.Diagnostics ;
using System.IO ;
2026-03-02 13:50:39 +00:00
using System.Linq ;
2026-03-10 08:59:49 +00:00
using System.Runtime.CompilerServices ;
2026-03-01 11:09:22 +01:00
using System.Runtime.InteropServices ;
using System.Runtime.Versioning ;
using System.Threading ;
using System.Threading.Tasks ;
2026-03-10 08:59:49 +00:00
using System.Text.Json ;
using Duplicati.Proprietary.DiskImage.General ;
2026-03-01 11:09:22 +01:00
namespace Duplicati.Proprietary.DiskImage.Disk
{
/// <summary>
/// Linux implementation of the <see cref="IRawDisk"/> interface for raw disk access.
/// Uses POSIX API calls via P/Invoke to read from and write to block devices.
/// </summary>
[SupportedOSPlatform("linux")]
public partial class Linux : IRawDisk
{
private static readonly string LOGTAG = Duplicati . Library . Logging . Log . LogTagFromType < Linux >();
// Linux block device ioctl constants from <linux/fs.h>
// BLKGETSIZE64: _IOR(0x12, 114, size_t) = 0x80081272 (on x86_64)
// Returns the size of the block device in bytes as uint64.
private const ulong BLKGETSIZE64 = 0x80081272 ;
// BLKSSZGET: _IO(0x12, 104) = 0x1268
// Returns the logical sector size of the block device as int.
private const uint BLKSSZGET = 0x1268 ;
// BLKFLSBUF: _IO(0x12, 97) = 0x1261
// Flushes the buffer cache for the block device.
private const uint BLKFLSBUF = 0x1261 ;
// File open flags from <fcntl.h>
private const int O_RDONLY = 0x0000 ;
private const int O_RDWR = 0x0002 ;
2026-03-02 09:31:35 +00:00
// TODO Should be processor architecture agnostic.
2026-03-01 11:09:22 +01:00
// O_DIRECT: bypass kernel page cache for unbuffered I/O
// Value is architecture-dependent: 0x4000 on x86_64, 0x10000 on aarch64
// Using 0x4000 as the most common value (x86_64)
private const int O_DIRECT = 0x4000 ;
private readonly string m_devicePath ;
private int m_fileDescriptor = - 1 ;
private bool m_disposed = false ;
private bool m_initialized = false ;
private bool m_writeable = false ;
private uint m_sectorSize = 0 ;
private long m_size = 0 ;
private bool m_shouldFlush = false ;
private readonly SemaphoreSlim m_ioLock = new ( 1 , 1 );
2026-03-02 09:31:35 +00:00
// Aligned buffers for O_DIRECT I/O (must be sector-aligned and a multiple of sector size)
private long m_allignedBufferSize = 0 ;
unsafe private byte * m_allignedBufferPtr = null ;
2026-03-10 08:59:49 +00:00
/// <inheritdoc />
public static string Prefix => "/dev/" ;
2026-03-01 11:09:22 +01:00
/// <inheritdoc />
public string DevicePath { get { return m_devicePath ; } }
/// <inheritdoc />
public bool IsWriteable => m_writeable ;
/// <summary>
/// Initializes a new instance of the <see cref="Linux"/> class.
/// </summary>
/// <param name="devicePath">The Linux device path (e.g., "/dev/sda", "/dev/nvme0n1", "/dev/loop0").</param>
/// <exception cref="PlatformNotSupportedException">Thrown when not running on Linux.</exception>
public Linux ( string devicePath )
{
if (! OperatingSystem . IsLinux ())
throw new PlatformNotSupportedException ( "Linux raw disk access is only supported on Linux platforms." );
m_devicePath = devicePath ;
}
/// <inheritdoc />
public long Size
{
get
{
if (! m_initialized )
throw new InvalidOperationException ( "Disk not initialized." );
return m_size ;
}
}
/// <inheritdoc />
public int SectorSize
{
get
{
if (! m_initialized )
throw new InvalidOperationException ( "Disk not initialized." );
return ( int ) m_sectorSize ;
}
}
/// <inheritdoc />
public int Sectors
{
get
{
if (! m_initialized )
throw new InvalidOperationException ( "Disk not initialized." );
return ( int )( m_size / m_sectorSize );
}
}
2026-03-02 13:50:39 +00:00
private async Task < List < string >> GetMountedPartitionsAsync ( CancellationToken cancellationToken )
2026-03-01 11:09:22 +01:00
{
2026-03-02 13:50:39 +00:00
var mountedPartitions = new List < string >();
2026-03-01 11:09:22 +01:00
2026-03-02 13:50:39 +00:00
// Read /proc/mounts to find mounted partitions for this device
using ( var reader = new StreamReader ( "/proc/mounts" ))
2026-03-01 11:09:22 +01:00
{
2026-03-02 13:50:39 +00:00
string? line ;
while (( line = await reader . ReadLineAsync ( cancellationToken ). ConfigureAwait ( false )) != null )
if ( line . StartsWith ( m_devicePath ))
2026-03-01 11:09:22 +01:00
{
2026-03-02 13:50:39 +00:00
var partitionDevice = line . Split ( ' ' ). FirstOrDefault ();
if ( partitionDevice != null )
mountedPartitions . Add ( partitionDevice );
2026-03-01 11:09:22 +01:00
}
2026-03-02 13:50:39 +00:00
}
return mountedPartitions ;
}
/// <inheritdoc />
public async Task < bool > AutoUnmountAsync ( CancellationToken cancellationToken )
{
try
{
var mountPoints = await GetMountedPartitionsAsync ( cancellationToken );
2026-03-01 11:09:22 +01:00
// Unmount each partition
bool allSucceeded = true ;
foreach ( var mountPoint in mountPoints )
{
var psi = new ProcessStartInfo
{
FileName = "umount" ,
Arguments = $"\" { mountPoint } \ "" ,
RedirectStandardOutput = true ,
RedirectStandardError = true ,
UseShellExecute = false ,
CreateNoWindow = true
};
using var process = new Process { StartInfo = psi };
process . Start ();
string output = await process . StandardOutput . ReadToEndAsync ( cancellationToken );
string error = await process . StandardError . ReadToEndAsync ( cancellationToken );
await process . WaitForExitAsync ( cancellationToken );
if ( process . ExitCode != 0 )
{
Duplicati . Library . Logging . Log . WriteWarningMessage ( LOGTAG , "autounmount" , null , $"Failed to unmount {mountPoint}: {error}" );
allSucceeded = false ;
}
else
{
Duplicati . Library . Logging . Log . WriteVerboseMessage ( LOGTAG , "autounmount" , $"Successfully unmounted {mountPoint}" );
}
}
return allSucceeded ;
}
catch ( Exception ex )
{
Duplicati . Library . Logging . Log . WriteErrorMessage ( LOGTAG , "autounmount" , ex , "Failed to auto-unmount disk" );
return false ;
}
}
/// <inheritdoc />
public Task < bool > InitializeAsync ( CancellationToken cancellationToken )
=> InitializeAsync ( false , cancellationToken );
/// <inheritdoc />
2026-03-02 13:50:39 +00:00
public async Task < bool > InitializeAsync ( bool enableWrite , CancellationToken cancellationToken )
2026-03-01 11:09:22 +01:00
{
if ( m_initialized )
2026-03-02 13:50:39 +00:00
return true ;
2026-03-02 15:16:43 +00:00
if ( enableWrite && ( await GetMountedPartitionsAsync ( cancellationToken )). Count > 0 )
2026-03-02 13:50:39 +00:00
{
throw new IOException ( $"Cannot initialize disk {m_devicePath} because it has mounted partitions. Please unmount all partitions before initializing." );
}
2026-03-01 11:09:22 +01:00
// Open the device with O_DIRECT for unbuffered I/O (matching Windows FILE_FLAG_NO_BUFFERING behavior)
// Note: O_DIRECT requires sector-aligned buffers and lengths
int flags = enableWrite ? O_RDWR | O_DIRECT : O_RDONLY | O_DIRECT ;
m_fileDescriptor = open ( m_devicePath , flags );
if ( m_fileDescriptor < 0 )
{
2026-03-02 13:35:51 +00:00
int errorCode = Marshal . GetLastWin32Error ();
2026-03-11 15:06:46 +01:00
string errorMessage = Marshal . GetPInvokeErrorMessage ( errorCode ); ;
2026-03-02 13:35:51 +00:00
Duplicati . Library . Logging . Log . WriteErrorMessage ( LOGTAG , "initialize" , null , $"Failed to open device {m_devicePath}: {errorMessage} (errno: {errorCode})" );
2026-03-02 13:50:39 +00:00
return false ;
2026-03-01 11:09:22 +01:00
}
// Get disk geometry using ioctls
try
{
// Get logical block size (sector size)
uint blockSize = 0 ;
if ( ioctl_uint32 ( m_fileDescriptor , BLKSSZGET , ref blockSize ) < 0 )
{
int errorCode = Marshal . GetLastWin32Error ();
close ( m_fileDescriptor );
m_fileDescriptor = - 1 ;
Duplicati . Library . Logging . Log . WriteErrorMessage ( LOGTAG , "initialize" , null , $"Failed to get block size: errno {errorCode}" );
2026-03-02 13:50:39 +00:00
return false ;
2026-03-01 11:09:22 +01:00
}
m_sectorSize = blockSize ;
// Get disk size in bytes
ulong sizeInBytes = 0 ;
if ( ioctl_uint64 ( m_fileDescriptor , BLKGETSIZE64 , ref sizeInBytes ) < 0 )
{
int errorCode = Marshal . GetLastWin32Error ();
close ( m_fileDescriptor );
m_fileDescriptor = - 1 ;
Duplicati . Library . Logging . Log . WriteErrorMessage ( LOGTAG , "initialize" , null , $"Failed to get disk size: errno {errorCode}" );
2026-03-02 13:50:39 +00:00
return false ;
2026-03-01 11:09:22 +01:00
}
m_size = ( long ) sizeInBytes ;
m_writeable = enableWrite ;
m_initialized = true ;
Duplicati . Library . Logging . Log . WriteInformationMessage ( LOGTAG , "initialize" , $"Successfully initialized disk {m_devicePath}: Size={m_size}, SectorSize={m_sectorSize}" );
2026-03-02 13:50:39 +00:00
return true ;
2026-03-01 11:09:22 +01:00
}
catch ( Exception ex )
{
close ( m_fileDescriptor );
m_fileDescriptor = - 1 ;
Duplicati . Library . Logging . Log . WriteErrorMessage ( LOGTAG , "initialize" , ex , "Failed to initialize disk" );
2026-03-02 13:50:39 +00:00
return false ;
2026-03-01 11:09:22 +01:00
}
}
/// <inheritdoc />
public void Dispose ()
{
if ( m_disposed )
return ;
if ( m_shouldFlush && m_fileDescriptor >= 0 )
{
// Use BLKFLSBUF on Linux to flush the block device buffer cache
if ( ioctl_no_arg ( m_fileDescriptor , BLKFLSBUF ) < 0 )
{
int errorCode = Marshal . GetLastWin32Error ();
2026-03-11 15:06:46 +01:00
string errorMessage = Marshal . GetPInvokeErrorMessage ( errorCode ); ;
2026-03-01 11:09:22 +01:00
Duplicati . Library . Logging . Log . WriteWarningMessage ( LOGTAG , "dispose" , null , $"Failed to flush data: {errorMessage} (errno: {errorCode})" );
}
}
if ( m_fileDescriptor >= 0 )
{
close ( m_fileDescriptor );
m_fileDescriptor = - 1 ;
}
2026-03-02 09:31:35 +00:00
unsafe
{
if ( m_allignedBufferPtr is not null )
{
NativeMemory . AlignedFree ( m_allignedBufferPtr );
m_allignedBufferPtr = null ;
m_allignedBufferSize = 0 ;
}
}
2026-03-01 11:09:22 +01:00
m_ioLock . Dispose ();
m_disposed = true ;
}
/// <inheritdoc />
public Task < bool > FinalizeAsync ( CancellationToken cancellationToken )
{
Dispose ();
return Task . FromResult ( true );
}
/// <inheritdoc />
public Task < Stream > ReadSectorsAsync ( long startSector , int sectorCount , CancellationToken cancellationToken )
{
if (! m_initialized )
throw new InvalidOperationException ( "Disk not initialized." );
long offset = startSector * m_sectorSize ;
int length = sectorCount * ( int ) m_sectorSize ;
return ReadBytesAsync ( offset , length , cancellationToken );
}
/// <inheritdoc />
public async Task < Stream > ReadBytesAsync ( long offset , int length , CancellationToken cancellationToken )
{
// Rent a pooled buffer and read directly into it
var buffer = ArrayPool < byte >. Shared . Rent ( length );
try
{
int bytesRead = await ReadBytesAsync ( offset , buffer . AsMemory ( 0 , length ), cancellationToken );
return new PooledMemoryStream ( buffer , bytesRead );
}
catch
{
ArrayPool < byte >. Shared . Return ( buffer );
throw ;
}
}
/// <inheritdoc />
public async Task < int > ReadBytesAsync ( long offset , Memory < byte > destination , CancellationToken cancellationToken )
{
if (! m_initialized )
throw new InvalidOperationException ( "Disk not initialized." );
if ( m_fileDescriptor < 0 )
throw new InvalidOperationException ( "Device is not open." );
int length = destination . Length ;
if ( offset + length > Size )
throw new InvalidOperationException ( $"The requested read would read beyond disk size: {offset} + {length} > {Size}" );
2026-03-11 09:06:02 +00:00
// Calculate aligned offset and length for O_DIRECT I/O
long alignedOffset = ( offset / SectorSize ) * SectorSize ;
long offsetDelta = offset - alignedOffset ;
long alignedLength = (( offsetDelta + length + SectorSize - 1 ) / SectorSize ) * SectorSize ;
2026-03-02 09:31:35 +00:00
2026-03-01 11:09:22 +01:00
await m_ioLock . WaitAsync ( cancellationToken );
try
{
// Use pread for atomic position + read
int totalBytesRead = 0 ;
unsafe
{
2026-03-11 09:06:02 +00:00
EnsureAllignedBuffer (( int ) alignedLength );
2026-03-02 09:31:35 +00:00
2026-03-11 09:06:02 +00:00
var bytesRead = pread ( m_fileDescriptor , m_allignedBufferPtr , ( nint ) alignedLength , alignedOffset );
2026-03-02 09:31:35 +00:00
if ( bytesRead . ToInt64 () < 0 )
2026-03-01 11:09:22 +01:00
{
2026-03-02 09:31:35 +00:00
int errorCode = Marshal . GetLastWin32Error ();
2026-03-11 15:06:46 +01:00
string errorMessage = Marshal . GetPInvokeErrorMessage ( errorCode ); ;
2026-03-11 09:06:02 +00:00
throw new IOException ( $"Failed to read {alignedLength} bytes from disk at offset {alignedOffset}: {errorMessage} (errno: {errorCode})" );
2026-03-01 11:09:22 +01:00
}
2026-03-02 09:31:35 +00:00
2026-03-11 09:06:02 +00:00
// Copy only the requested portion from the aligned buffer
int bytesToCopy = Math . Min ( length , ( int )( bytesRead . ToInt64 () - offsetDelta ));
if ( bytesToCopy > 0 )
2026-03-02 09:31:35 +00:00
{
2026-03-11 09:06:02 +00:00
var srcSpan = new ReadOnlySpan < byte >( m_allignedBufferPtr + offsetDelta , bytesToCopy );
srcSpan . CopyTo ( destination . Span );
2026-03-02 09:31:35 +00:00
}
2026-03-11 09:06:02 +00:00
totalBytesRead = bytesToCopy ;
2026-03-01 11:09:22 +01:00
}
return totalBytesRead ;
}
finally
{
m_ioLock . Release ();
}
}
/// <inheritdoc />
public async Task < int > WriteSectorsAsync ( long startSector , byte [] data , CancellationToken cancellationToken )
{
if (! m_initialized )
throw new InvalidOperationException ( "Disk not initialized." );
if (! m_writeable )
throw new InvalidOperationException ( "Disk not opened for write access." );
long offset = startSector * m_sectorSize ;
return await WriteBytesAsync ( offset , data , cancellationToken );
}
/// <inheritdoc />
public Task < int > WriteBytesAsync ( long offset , byte [] data , CancellationToken cancellationToken )
=> WriteBytesAsync ( offset , data . AsMemory (), cancellationToken );
/// <inheritdoc />
public async Task < int > WriteBytesAsync ( long offset , ReadOnlyMemory < byte > data , CancellationToken cancellationToken )
{
if (! m_initialized )
throw new InvalidOperationException ( "Disk not initialized." );
if (! m_writeable )
throw new InvalidOperationException ( "Disk not opened for write access." );
if ( m_fileDescriptor < 0 )
throw new InvalidOperationException ( "Device is not open." );
int dataLength = data . Length ;
2026-03-11 09:06:02 +00:00
if ( offset + dataLength > Size )
throw new InvalidOperationException ( $"The requested write would write beyond disk size: {offset} + {dataLength} > {Size}" );
// Calculate aligned offset and length for O_DIRECT I/O
long alignedOffset = ( offset / SectorSize ) * SectorSize ;
long offsetDelta = offset - alignedOffset ;
long alignedLength = (( offsetDelta + dataLength + SectorSize - 1 ) / SectorSize ) * SectorSize ;
2026-03-01 11:09:22 +01:00
await m_ioLock . WaitAsync ( cancellationToken );
try
{
2026-03-11 09:06:02 +00:00
// Ensure we have an aligned buffer for O_DIRECT writes
EnsureAllignedBuffer (( int ) alignedLength );
2026-03-01 11:09:22 +01:00
2026-03-11 09:06:02 +00:00
int totalBytesWritten = 0 ;
unsafe
{
// Check if this is an unaligned write (needs read-modify-write)
bool isUnaligned = offsetDelta != 0 || dataLength != alignedLength ;
2026-03-01 11:09:22 +01:00
2026-03-11 09:06:02 +00:00
if ( isUnaligned )
2026-03-01 11:09:22 +01:00
{
2026-03-11 09:06:02 +00:00
// Read existing data first (read-modify-write)
var bytesRead = pread ( m_fileDescriptor , m_allignedBufferPtr , ( nint ) alignedLength , alignedOffset );
if ( bytesRead . ToInt64 () < 0 )
2026-03-01 11:09:22 +01:00
{
2026-03-11 09:06:02 +00:00
int errorCode = Marshal . GetLastWin32Error ();
2026-03-11 15:06:46 +01:00
string errorMessage = Marshal . GetPInvokeErrorMessage ( errorCode ); ;
2026-03-11 09:06:02 +00:00
throw new IOException ( $"Failed to read existing data for unaligned write at offset {alignedOffset}: {errorMessage} (errno: {errorCode})" );
2026-03-01 11:09:22 +01:00
}
2026-03-02 09:31:35 +00:00
}
2026-03-11 09:06:02 +00:00
// Copy new data into the aligned buffer at the correct offset
var destSpan = new Span < byte >( m_allignedBufferPtr + offsetDelta , dataLength );
data . Span . CopyTo ( destSpan );
2026-03-01 11:09:22 +01:00
2026-03-11 09:06:02 +00:00
// Write the aligned buffer
var bytesWritten = pwrite ( m_fileDescriptor , m_allignedBufferPtr , ( nint ) alignedLength , alignedOffset );
totalBytesWritten = ( int ) bytesWritten . ToInt64 ();
2026-03-01 11:09:22 +01:00
}
2026-03-11 09:06:02 +00:00
if ( totalBytesWritten < 0 )
2026-03-01 11:09:22 +01:00
{
2026-03-11 09:06:02 +00:00
int errorCode = Marshal . GetLastWin32Error ();
2026-03-11 15:06:46 +01:00
string errorMessage = Marshal . GetPInvokeErrorMessage ( errorCode ); ;
2026-03-11 09:06:02 +00:00
throw new IOException ( $"Failed to write {dataLength} bytes to disk at offset {offset}: {errorMessage} (errno: {errorCode})" );
2026-03-01 11:09:22 +01:00
}
2026-03-11 09:06:02 +00:00
m_shouldFlush = true ;
return dataLength ;
2026-03-01 11:09:22 +01:00
}
finally
{
m_ioLock . Release ();
}
}
#region P / Invoke Declarations
// P/Invoke to the native wrapper library for ioctls
[LibraryImport("libc_wrapper.so", SetLastError = true)]
internal static partial int ioctl_uint32 ( int fd , uint request , ref uint value );
[LibraryImport("libc_wrapper.so", SetLastError = true)]
internal static partial int ioctl_uint64 ( int fd , ulong request , ref ulong value );
[LibraryImport("libc_wrapper.so", SetLastError = true)]
internal static partial int ioctl_no_arg ( int fd , uint request );
// Standard libc functions
[LibraryImport("libc", SetLastError = true)]
private static partial int open ([ MarshalAs ( UnmanagedType . LPStr )] string pathname , int flags );
[LibraryImport("libc", SetLastError = true)]
private static partial int close ( int fd );
[LibraryImport("libc", SetLastError = true)]
2026-03-02 09:31:35 +00:00
private static unsafe partial nint pread ( int fd , void * buf , nint count , long offset );
2026-03-01 11:09:22 +01:00
[LibraryImport("libc", SetLastError = true)]
2026-03-02 09:31:35 +00:00
private static unsafe partial nint pwrite ( int fd , void * buf , nint count , long offset );
2026-03-01 11:09:22 +01:00
#endregion
2026-03-02 09:31:35 +00:00
unsafe
private void EnsureAllignedBuffer ( int requiredSize )
{
if ( m_allignedBufferSize >= requiredSize )
return ;
nuint alignedSize = ( nuint )(( requiredSize + SectorSize - 1 ) / SectorSize * SectorSize );
// Free existing buffer if it exists
if ( m_allignedBufferPtr is not null )
{
m_allignedBufferPtr = ( byte *) NativeMemory . AlignedRealloc ( m_allignedBufferPtr , alignedSize , ( nuint ) m_sectorSize );
}
else
{
m_allignedBufferPtr = ( byte *) NativeMemory . AlignedAlloc ( alignedSize , ( nuint ) m_sectorSize );
}
}
2026-03-10 08:59:49 +00:00
/// <inheritdoc />
public static async IAsyncEnumerable < PhysicalDriveInfo > ListPhysicalDrivesAsync ([ EnumeratorCancellation ] CancellationToken cancellationToken )
{
// Use lsblk to get list of block devices in JSON format
// -J: JSON output
// -O: include all available columns
// -b: sizes in bytes
var result = await ProcessRunner . RunProcessAsync ( "lsblk" , "-JO -b" , 30_000 , cancellationToken );
if ( result . ExitCode != 0 )
{
Duplicati . Library . Logging . Log . WriteErrorMessage ( LOGTAG , "listphysicaldrives" , null , $"Failed to list physical drives: {result.Error}" );
yield break ;
}
if ( string . IsNullOrWhiteSpace ( result . Output ))
yield break ;
JsonDocument doc ;
try
{
doc = JsonDocument . Parse ( result . Output );
}
catch ( JsonException ex )
{
Duplicati . Library . Logging . Log . WriteErrorMessage ( LOGTAG , "listphysicaldrives" , ex , "Failed to parse lsblk JSON output" );
yield break ;
}
using ( doc )
{
var rootElement = doc . RootElement ;
if (! rootElement . TryGetProperty ( "blockdevices" , out var blockDevices ))
yield break ;
foreach ( var device in blockDevices . EnumerateArray ())
{
// Only include whole disks (type="disk"), not partitions (type="part")
if (! device . TryGetProperty ( "type" , out var typeElement ) || typeElement . GetString () != "disk" )
continue ;
var path = device . TryGetProperty ( "path" , out var pathElement ) ? pathElement . GetString () : null ;
if ( string . IsNullOrWhiteSpace ( path ))
continue ;
// Get size
var size = device . TryGetProperty ( "size" , out var sizeElement ) ? sizeElement . GetUInt64 () : 0 UL ;
if ( size == 0 )
continue ;
// Get device name/number (e.g., sda, nvme0n1)
var name = device . TryGetProperty ( "name" , out var nameElement ) ? nameElement . GetString () : null ;
// Get mount points from children (partitions)
var mountPoints = new List < string >();
if ( device . TryGetProperty ( "children" , out var children ))
{
foreach ( var child in children . EnumerateArray ())
{
if ( child . TryGetProperty ( "mountpoint" , out var mpElement ) && mpElement . ValueKind == JsonValueKind . String )
{
var mountPoint = mpElement . GetString ();
if (! string . IsNullOrWhiteSpace ( mountPoint ))
mountPoints . Add ( mountPoint );
}
}
}
// Try to get UUID using blkid (this is expensive, so we only do it for valid disks)
string? guid = null ;
try
{
var blkidResult = await ProcessRunner . RunProcessAsync ( "blkid" , $"-o export {path}" , 10_000 , cancellationToken );
if ( blkidResult . ExitCode == 0 && ! string . IsNullOrWhiteSpace ( blkidResult . Output ))
{
// Parse blkid output to find UUID
foreach ( var line in blkidResult . Output . Split ( '\n' ))
{
if ( line . StartsWith ( "UUID=" ))
{
guid = line [ 5. .]. Trim ( '"' );
break ;
}
}
}
}
catch
{
// Ignore blkid failures - not all disks have UUIDs
}
var driveInfo = new PhysicalDriveInfo
{
Number = name ?? path ,
Path = path ,
Size = size ,
DisplayName = name ?? path ,
Guid = guid ,
MountPoints = [.. mountPoints ],
Online = null // Linux doesn't have an equivalent concept to "online" disks
};
yield return driveInfo ;
}
}
}
2026-03-01 11:09:22 +01:00
}
}