/* ========================================================================
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* 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.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Opc.Ua.Client.FileSystem
{
///
/// Wraps a server-allocated temporary write file (returned by
/// ).
/// Owns the close lifecycle: exactly one terminal call —
/// (CloseAndCommit) or
/// (Close, server rollback) — is sent
/// to the server.
///
public sealed class UaTemporaryWriteFile : IAsyncDisposable, IDisposable
{
internal UaTemporaryWriteFile(
TemporaryFileTransferTypeClient transferProxy,
FileTypeClient fileProxy,
NodeId fileNodeId,
uint handle,
int chunkSize)
{
m_transferProxy = transferProxy ?? throw new ArgumentNullException(nameof(transferProxy));
m_fileProxy = fileProxy ?? throw new ArgumentNullException(nameof(fileProxy));
FileNodeId = fileNodeId;
m_handle = handle;
m_inner = new UaFileStream(
fileProxy,
handle,
UaFileMode.Write,
initialLength: 0,
initialPosition: 0,
chunkSize);
Stream = new NonClosingStreamWrapper(m_inner);
}
///
/// The NodeId of the temporary file allocated by the server.
///
public NodeId FileNodeId { get; }
///
/// A write-only wrapper around
/// the underlying server handle. The wrapper's
/// is suppressed —
/// disposal is owned by and
/// must go through or
/// .
///
public Stream Stream { get; }
///
/// Closes the temporary file and commits it on the server via
/// the parent TemporaryFileTransferType.CloseAndCommit
/// method. Returns the NodeId of the completion state machine
/// the server uses to report progress (may be
/// when the server elects not to
/// expose one).
///
/// Cancellation token.
public async ValueTask CommitAsync(CancellationToken ct = default)
{
if (m_terminated)
{
return m_completionStateMachine;
}
await m_lock.WaitAsync(ct).ConfigureAwait(false);
try
{
if (m_terminated)
{
return m_completionStateMachine;
}
m_terminated = true;
m_completionStateMachine = await m_transferProxy
.CloseAndCommitAsync(m_handle, ct)
.ConfigureAwait(false);
m_inner.MarkDisposedWithoutClosing();
return m_completionStateMachine;
}
finally
{
m_lock.Release();
m_lock.Dispose();
}
}
///
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);
Dispose(disposing: false);
GC.SuppressFinalize(this);
}
///
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
///
/// Asynchronous core of the dispose pattern; sends a best-effort
/// Close for the underlying server handle when the
/// wrapper has not already been committed. Idempotent.
///
///
/// Implements the recommended pattern from
/// .
/// The class is sealed, so this helper is private
/// rather than the protected virtual shape recommended
/// for unsealed types.
///
private async ValueTask DisposeAsyncCore()
{
if (m_terminated)
{
return;
}
await m_lock.WaitAsync().ConfigureAwait(false);
try
{
if (m_terminated)
{
return;
}
m_terminated = true;
try
{
await m_fileProxy.CloseAsync(m_handle, CancellationToken.None)
.ConfigureAwait(false);
}
catch (ServiceResultException)
{
// Best-effort: the server may roll back internally
// or the session may already be torn down.
}
m_inner.MarkDisposedWithoutClosing();
}
finally
{
m_lock.Release();
m_lock.Dispose();
}
}
///
/// Synchronous fallback for callers that use using
/// instead of await using. Mirrors the
/// System.IDisposable dispose pattern; prefer the async
/// path to avoid a GetAwaiter().GetResult() hop.
///
private void Dispose(bool disposing)
{
if (m_terminated || !disposing)
{
return;
}
try
{
DisposeAsyncCore().AsTask().GetAwaiter().GetResult();
}
catch
{
// Best-effort: async path surfaces exceptions.
}
}
private readonly TemporaryFileTransferTypeClient m_transferProxy;
private readonly FileTypeClient m_fileProxy;
// CA2213: m_inner is intentionally NOT disposed via the standard
// IDisposable pattern; ownership of the underlying server handle
// belongs to UaTemporaryWriteFile.CommitAsync / DisposeAsync.
// CA1816: see DisposeAsync below.
#pragma warning disable CA2213
private readonly UaFileStream m_inner;
#pragma warning restore CA2213
private readonly uint m_handle;
private readonly SemaphoreSlim m_lock = new(1, 1);
private NodeId m_completionStateMachine = NodeId.Null;
private bool m_terminated;
private sealed class NonClosingStreamWrapper : Stream
{
public NonClosingStreamWrapper(UaFileStream inner)
{
m_inner = inner;
}
public override bool CanRead => m_inner.CanRead;
public override bool CanWrite => m_inner.CanWrite;
public override bool CanSeek => m_inner.CanSeek;
public override long Length => m_inner.Length;
public override long Position
{
get => m_inner.Position;
set => m_inner.Position = value;
}
public override int Read(byte[] buffer, int offset, int count)
{
return m_inner.Read(buffer, offset, count);
}
public override Task ReadAsync(
byte[] buffer, int offset, int count, CancellationToken ct)
{
return m_inner.ReadAsync(buffer, offset, count, ct);
}
#if NETSTANDARD2_1_OR_GREATER || NET
public override ValueTask ReadAsync(
Memory buffer, CancellationToken cancellationToken = default)
{
return m_inner.ReadAsync(buffer, cancellationToken);
}
#endif
public override void Write(byte[] buffer, int offset, int count)
{
m_inner.Write(buffer, offset, count);
}
public override Task WriteAsync(
byte[] buffer, int offset, int count, CancellationToken ct)
{
return m_inner.WriteAsync(buffer, offset, count, ct);
}
#if NETSTANDARD2_1_OR_GREATER || NET
public override ValueTask WriteAsync(
ReadOnlyMemory buffer, CancellationToken cancellationToken = default)
{
return m_inner.WriteAsync(buffer, cancellationToken);
}
#endif
public override long Seek(long offset, SeekOrigin origin)
{
return m_inner.Seek(offset, origin);
}
public override void SetLength(long value)
{
m_inner.SetLength(value);
}
public override void Flush()
{
m_inner.Flush();
}
public override Task FlushAsync(CancellationToken ct)
{
return m_inner.FlushAsync(ct);
}
///
/// CA2215: Disposal of the inner stream (and the server file
/// handle it owns) is intentionally suppressed here —
/// UaTemporaryWriteFile.CommitAsync / DisposeAsync owns the
/// close lifecycle. We still call base.Dispose(disposing) to
/// satisfy the analyzer's contract.
///
///
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
}
private readonly UaFileStream m_inner;
}
}
}