Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions docs/FileSystemClient.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,11 @@ share a single instance: concurrent calls are serialised by an internal
corrupt" contract. `DisposeAsync` issues `FileType.Close` exactly once
even when called concurrently.

Server-issued file handles are opaque and bound to the Session that
opened them. They cannot be used by another Session, and the server
automatically closes all remaining handles when their owning Session
closes.

## Error mapping

OPC UA Bad status codes returned by the server are translated into the
Expand Down
20 changes: 19 additions & 1 deletion src/Opc.Ua.Server/FileSystem/DirectoryObjectState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,24 @@ private async ValueTask<CreateFileMethodStateResult> OnCreateFileAsync(
"File name required.")
};
}

NodeId sessionId = NodeId.Null;
if (requestFileOpen)
{
if (context is not ISessionSystemContext sessionContext ||
sessionContext.SessionId is not { IsNull: false } validSessionId)
{
return new CreateFileMethodStateResult
{
ServiceResult = ServiceResult.Create(
StatusCodes.BadSessionIdInvalid,
"A valid Session is required to open a file.")
};
}

sessionId = validSessionId;
}
Comment thread
marcschier marked this conversation as resolved.

string newPath = manager.CombineProviderPath(ProviderPath, fileName);
try
{
Expand All @@ -248,7 +266,7 @@ private async ValueTask<CreateFileMethodStateResult> OnCreateFileAsync(
}
// Open with write + erase to mirror the spec's
// "create + open for write" semantics.
ServiceResult openResult = handle.Open(0x6, out uint fileHandle);
ServiceResult openResult = handle.Open(sessionId, 0x6, out uint fileHandle);
return new CreateFileMethodStateResult
{
ServiceResult = openResult,
Expand Down
113 changes: 92 additions & 21 deletions src/Opc.Ua.Server/FileSystem/FileHandle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,17 +114,20 @@ public string MimeType
}
}

public Stream? GetStream(uint fileHandle)
public Stream? GetStream(NodeId sessionId, uint fileHandle)
{
lock (m_lock)
{
if (m_write != null && fileHandle == 1)
if (m_write != null &&
fileHandle == m_write.Handle &&
m_write.SessionId.Equals(sessionId))
{
return m_write;
return m_write.Stream;
}
if (m_reads.TryGetValue(fileHandle, out Stream? stream))
if (m_reads.TryGetValue(fileHandle, out OpenFile? openFile) &&
openFile.SessionId.Equals(sessionId))
{
return stream;
return openFile.Stream;
}
return null;
}
Expand All @@ -135,9 +138,16 @@ public string MimeType
/// Part 5 §C: 0x1 = Read, 0x2 = Write, 0x4 = EraseExisting,
/// 0x8 = Append.
/// </summary>
public ServiceResult Open(byte mode, out uint fileHandle)
public ServiceResult Open(NodeId sessionId, byte mode, out uint fileHandle)
{
fileHandle = 0u;
if (sessionId.IsNull)
{
return ServiceResult.Create(
StatusCodes.BadSessionIdInvalid,
"A valid Session is required to open a file.");
}
Comment thread
marcschier marked this conversation as resolved.

bool wantsRead = (mode & 0x1) != 0;
bool wantsWrite = (mode & 0x2) != 0;

Expand Down Expand Up @@ -176,8 +186,8 @@ public ServiceResult Open(byte mode, out uint fileHandle)
StatusCodes.BadInvalidState,
"File already open for write.");
}
fileHandle = ++m_nextHandle;
m_reads.Add(fileHandle, stream);
fileHandle = CreateFileHandle();
m_reads.Add(fileHandle, new OpenFile(fileHandle, sessionId, stream));
}
return ServiceResult.Good;
}
Expand Down Expand Up @@ -208,8 +218,8 @@ public ServiceResult Open(byte mode, out uint fileHandle)
StatusCodes.BadInvalidState,
"File already open for read or write.");
}
m_write = writeStream;
fileHandle = 1u;
fileHandle = CreateFileHandle();
m_write = new OpenFile(fileHandle, sessionId, writeStream);
}
return ServiceResult.Good;
}
Expand All @@ -230,44 +240,105 @@ public ServiceResult Open(byte mode, out uint fileHandle)
}
}

public bool Close(uint fileHandle)
public bool Close(NodeId sessionId, uint fileHandle)
{
lock (m_lock)
{
if (m_write != null && fileHandle == 1)
if (m_write != null &&
fileHandle == m_write.Handle &&
m_write.SessionId.Equals(sessionId))
{
m_write.Dispose();
m_write.Stream.Dispose();
m_write = null;
return true;
}
if (m_reads.TryGetValue(fileHandle, out Stream? stream))
if (m_reads.TryGetValue(fileHandle, out OpenFile? openFile) &&
openFile.SessionId.Equals(sessionId))
{
stream.Dispose();
openFile.Stream.Dispose();
m_reads.Remove(fileHandle);
return true;
}
}
return false;
}

public void CloseSession(NodeId sessionId)
{
lock (m_lock)
{
if (m_write != null && m_write.SessionId.Equals(sessionId))
{
m_write.Stream.Dispose();
m_write = null;
}

var handlesToClose = new List<uint>();
foreach (KeyValuePair<uint, OpenFile> entry in m_reads)
{
if (entry.Value.SessionId.Equals(sessionId))
{
entry.Value.Stream.Dispose();
handlesToClose.Add(entry.Key);
}
}

foreach (uint fileHandle in handlesToClose)
{
m_reads.Remove(fileHandle);
}
}
}
Comment thread
marcschier marked this conversation as resolved.

public void Dispose()
{
lock (m_lock)
{
m_write?.Dispose();
m_write?.Stream.Dispose();
m_write = null;
foreach (Stream stream in m_reads.Values)
foreach (OpenFile openFile in m_reads.Values)
{
stream.Dispose();
openFile.Stream.Dispose();
}
m_reads.Clear();
}
}

private uint CreateFileHandle()
{
uint fileHandle;
do
{
fileHandle = BitConverter.ToUInt32(
Nonce.CreateRandomNonceData(sizeof(uint)),
0);
}
while (fileHandle == 0 ||
m_write?.Handle == fileHandle ||
m_reads.ContainsKey(fileHandle));

return fileHandle;
}

private readonly Lock m_lock = new();
private readonly Dictionary<uint, Stream> m_reads = [];
private readonly Dictionary<uint, OpenFile> m_reads = [];
private readonly IFileSystemProvider m_provider;
private uint m_nextHandle = 1;
private Stream? m_write;
private OpenFile? m_write;

private sealed class OpenFile
{
public OpenFile(uint handle, NodeId sessionId, Stream stream)
{
Handle = handle;
SessionId = sessionId;
Stream = stream;
}

public uint Handle { get; }

public NodeId SessionId { get; }

public Stream Stream { get; }
}
}
}
56 changes: 50 additions & 6 deletions src/Opc.Ua.Server/FileSystem/FileObjectState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,7 +250,11 @@ private ServiceResult OnOpen(ISystemContext context, MethodState method,
{
return result;
}
return handle.Open(mode, out fileHandle);
if (!TryGetSessionId(context, out NodeId sessionId, out result))
{
return result;
}
return handle.Open(sessionId, mode, out fileHandle);
}

private ServiceResult OnClose(ISystemContext context, MethodState method,
Expand All @@ -260,7 +264,11 @@ private ServiceResult OnClose(ISystemContext context, MethodState method,
{
return result;
}
return handle.Close(fileHandle)
if (!TryGetSessionId(context, out NodeId sessionId, out result))
{
return result;
}
return handle.Close(sessionId, fileHandle)
? ServiceResult.Good
: ServiceResult.Create(StatusCodes.BadInvalidState,
"File handle could not be closed.");
Comment thread
marcschier marked this conversation as resolved.
Outdated
Expand All @@ -273,7 +281,11 @@ private ServiceResult OnSetPosition(ISystemContext context, MethodState method,
{
return result;
}
Stream? stream = handle.GetStream(fileHandle);
if (!TryGetSessionId(context, out NodeId sessionId, out result))
{
return result;
}
Stream? stream = handle.GetStream(sessionId, fileHandle);
if (stream == null)
{
return ServiceResult.Create(StatusCodes.BadInvalidState,
Expand All @@ -290,7 +302,11 @@ private ServiceResult OnGetPosition(ISystemContext context, MethodState method,
{
return result;
}
Stream? stream = handle.GetStream(fileHandle);
if (!TryGetSessionId(context, out NodeId sessionId, out result))
{
return result;
}
Stream? stream = handle.GetStream(sessionId, fileHandle);
if (stream == null)
{
return ServiceResult.Create(StatusCodes.BadInvalidState,
Expand All @@ -307,7 +323,11 @@ private ServiceResult OnRead(ISystemContext context, MethodState method,
{
return result;
}
Stream? stream = handle.GetStream(fileHandle);
if (!TryGetSessionId(context, out NodeId sessionId, out result))
{
return result;
}
Stream? stream = handle.GetStream(sessionId, fileHandle);
if (stream == null)
{
return ServiceResult.Create(StatusCodes.BadInvalidState,
Expand Down Expand Up @@ -341,7 +361,11 @@ private ServiceResult OnWrite(ISystemContext context, MethodState method,
{
return result;
}
Stream? stream = handle.GetStream(fileHandle);
if (!TryGetSessionId(context, out NodeId sessionId, out result))
{
return result;
}
Stream? stream = handle.GetStream(sessionId, fileHandle);
if (stream == null)
{
return ServiceResult.Create(StatusCodes.BadInvalidState,
Expand Down Expand Up @@ -401,6 +425,26 @@ private bool TryGetHandle(
return true;
}

private static bool TryGetSessionId(
ISystemContext context,
out NodeId sessionId,
out ServiceResult result)
{
if (context is ISessionSystemContext sessionContext &&
sessionContext.SessionId is { IsNull: false } validSessionId)
{
sessionId = validSessionId;
result = ServiceResult.Good;
return true;
}

sessionId = NodeId.Null;
result = ServiceResult.Create(
StatusCodes.BadSessionIdInvalid,
"A valid Session is required to access an open file.");
return false;
}

private static FileSystemNodeManager? ResolveManager(ISystemContext context)
{
return context?.SystemHandle as FileSystemNodeManager;
Expand Down
22 changes: 22 additions & 0 deletions src/Opc.Ua.Server/FileSystem/FileSystemNodeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,28 @@ public override ValueTask DeleteAddressSpaceAsync(CancellationToken cancellation
return base.DeleteAddressSpaceAsync(cancellationToken);
}

/// <inheritdoc/>
public override ValueTask SessionClosingAsync(
OperationContext context,
NodeId sessionId,
bool deleteSubscriptions,
CancellationToken cancellationToken = default)
{
lock (m_lock)
{
foreach (FileHandle handle in m_handles.Values)
{
handle.CloseSession(sessionId);
}
}
Comment thread
marcschier marked this conversation as resolved.

return base.SessionClosingAsync(
context,
sessionId,
deleteSubscriptions,
cancellationToken);
}

/// <inheritdoc/>
protected override void Dispose(bool disposing)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,11 @@ private void UseProvider(IFileSystemProvider provider)
Mock<IServerInternal> mockServer = DeterministicServerMock.Create(out _);
mockServer.Setup(s => s.Telemetry).Returns(m_telemetry);
m_manager = new FileSystemNodeManager(mockServer.Object, new ApplicationConfiguration(), provider);
m_context = m_manager.SystemContext;
var session = new Mock<ISession>();
session.Setup(s => s.Id).Returns(new NodeId("directory-session", 0));
session.Setup(s => s.Identity).Returns(new Mock<IUserIdentity>().Object);
session.Setup(s => s.PreferredLocales).Returns([]);
m_context = m_manager.SystemContext.Copy(session.Object);
}

private DirectoryObjectState CreateRootDirectory()
Expand Down
Loading
Loading