Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions Library/DiscUtils.Iscsi/DiscUtils.Iscsi.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@
<ProjectReference Include="..\DiscUtils.Core\DiscUtils.Core.csproj" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="LibraryTests" />
</ItemGroup>

</Project>
4 changes: 2 additions & 2 deletions Library/DiscUtils.Iscsi/Disk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ protected override void Dispose(bool disposing)
{
try
{
_stream?.Flush();
_stream?.Flush(flushToDisk: true);
}
catch (Exception e) when (e is IOException or ObjectDisposedException)
{
Expand All @@ -189,4 +189,4 @@ protected override void Dispose(bool disposing)

base.Dispose(disposing);
}
}
}
73 changes: 14 additions & 59 deletions Library/DiscUtils.Iscsi/DiskStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,6 @@ internal class DiskStream : SparseStream
private long _position;
private readonly Session _session;

// Set by a write and cleared by a flush, so a flush with nothing written costs no round trip.
private bool _unflushedWrites;

// The target rejected SYNCHRONIZE CACHE as an unknown command: it has no volatile cache to flush.
private bool _noSynchronizeCache;

public DiskStream(Session session, long lun, FileAccess access)
{
_session = session;
Expand Down Expand Up @@ -78,60 +72,23 @@ public override long Position
}

/// <summary>
/// Makes the target put the writes it acknowledged on stable storage (SCSI SYNCHRONIZE CACHE). A target may
/// acknowledge a write while it is still in a volatile cache; until this returns, a power loss on the target can
/// lose it or keep a later write without an earlier one. Does nothing if nothing was written since the last flush.
/// Requests that the target synchronize its write cache with stable storage (SCSI SYNCHRONIZE CACHE).
/// This covers the entire LUN, including writes made through other streams or before this stream was opened.
/// A failed or unsupported synchronization is reported to the caller.
/// </summary>
public override void Flush()
{
if (!_unflushedWrites)
{
return;
}

if (!_noSynchronizeCache)
{
try
{
_session.SynchronizeCache(_lun);
}
catch (ScsiCommandException e) when (IsUnknownCommand(e))
{
_noSynchronizeCache = true;
}
}
public override void Flush() => Flush(flushToDisk: true);

_unflushedWrites = false;
}
/// <summary>
/// Requests a target-wide cache synchronization. iSCSI has no separate buffered data in this stream to flush,
/// so both overloads preserve the established <see cref="Flush()"/> behavior and issue SYNCHRONIZE CACHE.
/// In particular, <paramref name="flushToDisk"/> set to <c>true</c> is an unconditional durability barrier,
/// even if this stream has not written any data itself.
/// </summary>
public override void Flush(bool flushToDisk) => _session.SynchronizeCache(_lun);

/// <inheritdoc cref="Flush"/>
/// <summary>Asynchronously synchronizes the whole LUN, even if this stream has not written anything.</summary>
public override async Task FlushAsync(CancellationToken cancellationToken)
{
if (!_unflushedWrites)
{
return;
}

if (!_noSynchronizeCache)
{
try
{
await _session.SynchronizeCacheAsync(_lun, cancellationToken).ConfigureAwait(false);
}
catch (ScsiCommandException e) when (IsUnknownCommand(e))
{
_noSynchronizeCache = true;
}
}

_unflushedWrites = false;
}

// ILLEGAL REQUEST / INVALID COMMAND OPERATION CODE: a target without a write cache need not implement the command.
private static bool IsUnknownCommand(ScsiCommandException e)
=> e.TryGetSenseData(out var sense)
&& sense.SenseKey == ScsiSenseKey.IllegalRequest
&& sense.AdditionalSenseCode == 0x20;
=> await _session.SynchronizeCacheAsync(_lun, cancellationToken).ConfigureAwait(false);

public override int Read(byte[] buffer, int offset, int count)
=> Read(buffer.AsSpan(offset, count));
Expand Down Expand Up @@ -232,7 +189,6 @@ public override void Write(ReadOnlySpan<byte> buffer)
? _session.FirstBurstLength
: (_session.ActiveConnection.MaxTargetReceiveDataSegmentLength ?? _session.ActiveConnection.MaxInitiatorTransmitDataSegmentLength);

_unflushedWrites = true;
var numWritten = 0;

while (numWritten < buffer.Length)
Expand Down Expand Up @@ -281,7 +237,6 @@ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, Cancella
? _session.FirstBurstLength
: (_session.ActiveConnection.MaxTargetReceiveDataSegmentLength ?? _session.ActiveConnection.MaxInitiatorTransmitDataSegmentLength);

_unflushedWrites = true;
var numWritten = 0;

while (numWritten < buffer.Length)
Expand All @@ -304,4 +259,4 @@ public override async ValueTask WriteAsync(ReadOnlyMemory<byte> buffer, Cancella
}
}
}
}
}
6 changes: 6 additions & 0 deletions Library/DiscUtils.Iscsi/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ dotnet add package LTRData.DiscUtils.Iscsi

Initiator and Session provide explicit connections and LUN access. The package also supplies an iscsi transport for generic disk opening. This is a client library, not an iSCSI server or an operating-system initiator driver. Registration is AOT-compatible; protocol-key reflection elsewhere in iSCSI is outside that guarantee.

### Write-cache synchronization

`Disk.Content.Flush()` and `Disk.Content.Flush(true)` issue SCSI `SYNCHRONIZE CACHE (10)` for the entire LUN, with `IMMED` clear. The request is sent even when the current stream has not written anything: a stream-local dirty flag cannot establish whether the target has outstanding cached writes from an earlier session or another stream. `FlushAsync()` requests the same synchronization.

An unsupported command (including SCSI `ILLEGAL REQUEST` / ASC `0x20`) or other synchronization failure is reported to the caller. An unsupported command does **not** establish that the target has no volatile write cache. The `Disk` disposal flush remains best-effort and suppresses errors; applications that need a durability barrier must call and check `Content.Flush(true)` explicitly before disposal. Even a successful command depends on the target and backing storage honoring SCSI cache synchronization.

## Registration

For automatic discovery, register the providers implemented by this package:
Expand Down
58 changes: 58 additions & 0 deletions Tests/LibraryTests/Iscsi/ScsiSynchronizeCacheTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
//
// Copyright (c) 2026, DiscUtils contributors
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//

using System;
using DiscUtils.Iscsi;

namespace LibraryTests.Iscsi;

public class ScsiSynchronizeCacheTest
{
[Fact]
public void SynchronizeCache10RequestsWholeLunAndWaitsForCompletion()
{
var command = new ScsiSynchronizeCache10Command(0x123456);
var bytes = new byte[command.Size];
for (var i = 0; i < bytes.Length; i++)
{
bytes[i] = 0xFF;
}

command.WriteTo(bytes);

Assert.Equal(10, command.Size);
Assert.Equal((ulong)0x123456, command.TargetLun);
Assert.Equal(TaskAttributes.Simple, command.TaskAttributes);
// LBA 0, number of blocks 0: all blocks through the end of the LUN.
// IMMED=0: do not acknowledge before synchronization finishes.
Assert.Equal(new byte[] { 0x35, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, bytes);
}

[Fact]
public void DiskStreamOverridesTheStrongSparseFlushOverload()
{
var flush = typeof(DiskStream).GetMethod(nameof(DiskStream.Flush), new[] { typeof(bool) });

Assert.NotNull(flush);
Assert.Equal(typeof(DiskStream), flush.DeclaringType);
}
}
Loading