diff --git a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj index 2b744c1..9c27ef9 100644 --- a/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj +++ b/src/Serilog.Sinks.File/Serilog.Sinks.File.csproj @@ -25,20 +25,20 @@ $(DefineConstants);ATOMIC_APPEND;HRESULTS - + $(DefineConstants);OS_MUTEX - $(DefineConstants);ENUMERABLE_MAXBY + $(DefineConstants);ENUMERABLE_MAXBY;OS_MUTEX - $(DefineConstants);ENUMERABLE_MAXBY + $(DefineConstants);ENUMERABLE_MAXBY;ATOMIC_APPEND - $(DefineConstants);ENUMERABLE_MAXBY + $(DefineConstants);ENUMERABLE_MAXBY;ATOMIC_APPEND @@ -46,4 +46,4 @@ - + \ No newline at end of file diff --git a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs index 485c1e4..f7abd87 100644 --- a/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs +++ b/src/Serilog.Sinks.File/Sinks/File/SharedFileSink.AtomicAppend.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2019 Serilog Contributors +// Copyright 2013-2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,6 +14,9 @@ #if ATOMIC_APPEND +using System.IO; +using System.Runtime.InteropServices; +using System.Runtime.Versioning; using System.Security.AccessControl; using System.Text; using Serilog.Core; @@ -77,13 +80,20 @@ public SharedFileSink(string path, ITextFormatter textFormatter, long? fileSizeL // FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND, but that API is not yet // exposed by .NET Core. - _fileOutput = new FileStream( + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _fileOutput = CreateFile( path, FileMode.Append, FileSystemRights.AppendData, FileShare.ReadWrite, _fileStreamBufferLength, FileOptions.None); + } + else + { + throw new NotSupportedException(); + } _writeBuffer = new MemoryStream(); _output = new StreamWriter(_writeBuffer, @@ -105,8 +115,9 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) if (length > _fileStreamBufferLength) { var oldOutput = _fileOutput; - - _fileOutput = new FileStream( + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + _fileOutput = CreateFile( _path, FileMode.Append, FileSystemRights.AppendData, @@ -114,6 +125,11 @@ bool IFileSink.EmitOrOverflow(LogEvent logEvent) length, FileOptions.None); _fileStreamBufferLength = length; + } + else + { + throw new NotSupportedException(); + } oldOutput.Dispose(); } @@ -188,6 +204,31 @@ void ISetLoggingFailureListener.SetFailureListener(ILoggingFailureListener failu { _failureListener = failureListener ?? throw new ArgumentNullException(nameof(failureListener)); } + + private static FileStream CreateFile(string path, FileMode mode, FileSystemRights rights, FileShare share, int bufferSize, FileOptions options) + { + // FileSystemRights.AppendData sets the Win32 FILE_APPEND_DATA flag. On Linux this is O_APPEND +#if NET48 + _fileOutput = new FileStream(path, mode, rights, share, bufferSize, options); +#else + // In .NET 7 for Windows it's exposed with FileSystemAclExtensions.Create API + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + var _fileOutput = FileSystemAclExtensions.Create(new FileInfo(path), mode, rights, share, bufferSize, options, new FileSecurity()); + + // Inherit ACL from container + var security = new FileSecurity(); + security.SetAccessRuleProtection(false, false); + FileSystemAclExtensions.SetAccessControl(new FileInfo(path), security); + + return _fileOutput; + } + else + { + throw new NotSupportedException(); + } +#endif + } } #endif diff --git a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs index b784d2f..b6e1e39 100644 --- a/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs +++ b/test/Serilog.Sinks.File.Tests/SharedFileSinkTests.cs @@ -1,4 +1,4 @@ -using Serilog.Core; +using Serilog.Core; using Xunit; using Serilog.Formatting.Json; using Serilog.Sinks.File.Tests.Support; @@ -94,4 +94,100 @@ public void WhenLimitIsNotSpecifiedFileSizeIsNotRestricted() var size = new FileInfo(path).Length; Assert.True(size > maxBytes * 2); } + + [Fact] + public void FileIsNotLockedAfterDisposal() + { + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) + { + sink.Emit(evt); + } + + // Ensure the file is not locked after the sink is disposed + var exceptionThrown = false; + try + { + using (var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite)) + { + } + } + catch (IOException) + { + exceptionThrown = true; + } + + Assert.False(exceptionThrown, "File should not be locked after sink disposal."); + } + + [Fact] + public void FileIsLockedByOneUserAndAnotherUserTriesToWrite() + { + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + // Lock the file by one user + using (var stream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None)) + { + using (var writer = new StreamWriter(stream)) + { + writer.WriteLine("Initial content"); + writer.Flush(); + + // Try to write to the locked file by another user + var exceptionThrown = false; + try + { + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) + { + sink.Emit(evt); + } + } + catch (IOException) + { + exceptionThrown = true; + } + + Assert.True(exceptionThrown, "IOException should be thrown when trying to write to a locked file."); + } + } + + // Verify the file content + var lines = System.IO.File.ReadAllLines(path); + Assert.Contains("Initial content", lines); + Assert.DoesNotContain("Hello, world!", lines); + } + + [Fact] + public async Task FileIsNotLockedDuringAsyncOperations() + { + using var tmp = TempFolder.ForCaller(); + var path = tmp.AllocateFilename("txt"); + var evt = Some.LogEvent("Hello, world!"); + + using (var sink = new SharedFileSink(path, new JsonFormatter(), null)) + { + await Task.Run(() => sink.Emit(evt)); + } + + // Ensure the file is not locked after async operations + var exceptionThrown = false; + try + { + using (var stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite)) + { + stream.ReadAllLines(); + } + } + catch (IOException) + { + exceptionThrown = true; + } + + Assert.False(exceptionThrown, "File should not be locked after async operations."); + } }