Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: Simplified IFileSystem #3641

Merged
merged 5 commits into from
Oct 1, 2024
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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

### Features

- Added a flag to options `DisableFileWrite` to allow users to opt-out of all file writing operations. Note that toggling this will affect features such as offline caching and auto-session tracking and release health as these rely on some file persistency ([#3614](https://github.com/getsentry/sentry-dotnet/pull/3614))
- Added a flag to options `DisableFileWrite` to allow users to opt-out of all file writing operations. Note that toggling this will affect features such as offline caching and auto-session tracking and release health as these rely on some file persistency ([#3614](https://github.com/getsentry/sentry-dotnet/pull/3614), [#3641](https://github.com/getsentry/sentry-dotnet/pull/3641))

### Dependencies

Expand Down
49 changes: 36 additions & 13 deletions src/Sentry/GlobalSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,29 +51,42 @@ private void PersistSession(SessionUpdate update, DateTimeOffset? pauseTimestamp
return;
}

if (_options.DisableFileWrite)
{
_options.LogInfo("File write has been disabled via the options. Skipping persisting session.");
return;
}

try
{
_options.LogDebug("Creating persistence directory for session file at '{0}'.", _persistenceDirectoryPath);

var result = _options.FileSystem.CreateDirectory(_persistenceDirectoryPath);
if (result is not FileOperationResult.Success)
if (!_options.FileSystem.CreateDirectory(_persistenceDirectoryPath))
{
if (result is FileOperationResult.Disabled)
{
_options.LogInfo("Persistent directory for session file has not been created. File-write has been disabled via the options.");
}
else
{
_options.LogError("Failed to create persistent directory for session file.");
}

_options.LogError("Failed to create persistent directory for session file.");
return;
}

var filePath = Path.Combine(_persistenceDirectoryPath, PersistedSessionFileName);

var persistedSessionUpdate = new PersistedSessionUpdate(update, pauseTimestamp);
persistedSessionUpdate.WriteToFile(_options.FileSystem, filePath, _options.DiagnosticLogger);
if (!_options.FileSystem.CreateFileForWriting(filePath, out var file))
{
_options.LogError("Failed to persist session file.");
return;
}

using var writer = new Utf8JsonWriter(file);

try
{
persistedSessionUpdate.WriteTo(writer, _options.DiagnosticLogger);
writer.Flush();
}
finally
{
file.Dispose();
}

_options.LogDebug("Persisted session to a file '{0}'.", filePath);
}
Expand All @@ -91,6 +104,12 @@ private void DeletePersistedSession()
return;
}

if (_options.DisableFileWrite)
{
_options.LogInfo("File write has been disabled via the options. Skipping deletion of persisted session files.");
return;
}

var filePath = Path.Combine(_persistenceDirectoryPath, PersistedSessionFileName);
try
{
Expand All @@ -108,7 +127,11 @@ private void DeletePersistedSession()
}
}

_options.FileSystem.DeleteFile(filePath);
if (!_options.FileSystem.DeleteFile(filePath))
{
_options.LogError("Failed to delete persisted session file.");
return;
}

_options.LogInfo("Deleted persisted session file '{0}'.", filePath);
}
Expand Down
32 changes: 20 additions & 12 deletions src/Sentry/Http/HttpTransportBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -383,20 +383,24 @@ private void HandleFailure(HttpResponseMessage response, Envelope envelope)
persistLargeEnvelopePathEnvVar,
destinationDirectory);

if (_options.DisableFileWrite)
{
_options.LogInfo("File write has been disabled via the options. Skipping persisting envelope.");
return;
}

var destination = Path.Combine(destinationDirectory, "envelope_too_large",
(eventId ?? SentryId.Create()).ToString());

var createDirectoryResult = _options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!);
if (createDirectoryResult is not FileOperationResult.Success)
if (!_options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!))
{
_options.LogError("Failed to create directory to store the envelope: {0}", createDirectoryResult);
_options.LogError("Failed to create directory to store the envelope.");
return;
}

var result = _options.FileSystem.CreateFileForWriting(destination, out var envelopeFile);
if (result is not FileOperationResult.Success)
if (!_options.FileSystem.CreateFileForWriting(destination, out var envelopeFile))
{
_options.LogError("Failed to create envelope file: {0}", result);
_options.LogError("Failed to create envelope file.");
return;
}

Expand Down Expand Up @@ -449,20 +453,24 @@ private async Task HandleFailureAsync(HttpResponseMessage response, Envelope env
persistLargeEnvelopePathEnvVar,
destinationDirectory);

if (_options.DisableFileWrite)
{
_options.LogInfo("File write has been disabled via the options. Skipping persisting envelope.");
return;
}

var destination = Path.Combine(destinationDirectory, "envelope_too_large",
(eventId ?? SentryId.Create()).ToString());

var createDirectoryResult = _options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!);
if (createDirectoryResult is not FileOperationResult.Success)
if (!_options.FileSystem.CreateDirectory(Path.GetDirectoryName(destination)!))
{
_options.LogError("Failed to create directory to store the envelope: {0}", createDirectoryResult);
_options.LogError("Failed to create directory to store the envelope.");
return;
}

var result = _options.FileSystem.CreateFileForWriting(destination, out var envelopeFile);
if (result is not FileOperationResult.Success)
if (!_options.FileSystem.CreateFileForWriting(destination, out var envelopeFile))
{
_options.LogError("Failed to create envelope file: {0}", result);
_options.LogError("Failed to create envelope file.");
return;
}

Expand Down
18 changes: 0 additions & 18 deletions src/Sentry/ISentryJsonSerializable.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,3 @@ public interface ISentryJsonSerializable
/// </remarks>
void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? logger);
}

internal static class JsonSerializableExtensions
{
public static void WriteToFile(this ISentryJsonSerializable serializable, IFileSystem fileSystem, string filePath, IDiagnosticLogger? logger)
{
var result = fileSystem.CreateFileForWriting(filePath, out var file);
if (result is not FileOperationResult.Success)
{
return;
}

using var writer = new Utf8JsonWriter(file);

serializable.WriteTo(writer, logger);
writer.Flush();
file.Dispose();
}
}
Comment on lines -20 to -37
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only used once in the GlobalSessionsManager. Success/Failure is getting handled there and does not need to be abstracted away into an extension.

12 changes: 6 additions & 6 deletions src/Sentry/Internal/FileSystemBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ public IEnumerable<string> EnumerateFiles(string path, string searchPattern, Sea

public Stream OpenFileForReading(string path) => File.OpenRead(path);

public abstract FileOperationResult CreateDirectory(string path);
public abstract FileOperationResult DeleteDirectory(string path, bool recursive = false);
public abstract FileOperationResult CreateFileForWriting(string path, out Stream fileStream);
public abstract FileOperationResult WriteAllTextToFile(string path, string contents);
public abstract FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false);
public abstract FileOperationResult DeleteFile(string path);
public abstract bool CreateDirectory(string path);
public abstract bool DeleteDirectory(string path, bool recursive = false);
public abstract bool CreateFileForWriting(string path, out Stream fileStream);
public abstract bool WriteAllTextToFile(string path, string contents);
public abstract bool MoveFile(string sourceFileName, string destFileName, bool overwrite = false);
public abstract bool DeleteFile(string path);
}
7 changes: 5 additions & 2 deletions src/Sentry/Internal/Http/CachingTransport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,10 @@ private CachingTransport(ITransport innerTransport, SentryOptions options, bool
options.TryGetProcessSpecificCacheDirectoryPath() ??
throw new InvalidOperationException("Cache directory or DSN is not set.");

// Sanity check: This should never happen in the first place.
// We check for `DisableFileWrite` before creating the CachingTransport.
Debug.Assert(!_options.DisableFileWrite);

_processingDirectoryPath = Path.Combine(_isolatedCacheDirectoryPath, ProcessingFolder);
}

Expand Down Expand Up @@ -451,8 +455,7 @@ private async Task StoreToCacheAsync(

EnsureFreeSpaceInCache();

var result = _options.FileSystem.CreateFileForWriting(envelopeFilePath, out var stream);
if (result is not FileOperationResult.Success)
if (!_options.FileSystem.CreateFileForWriting(envelopeFilePath, out var stream))
{
_options.LogDebug("Failed to store to cache.");
return;
Expand Down
24 changes: 11 additions & 13 deletions src/Sentry/Internal/IFileSystem.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
namespace Sentry.Internal;

internal enum FileOperationResult
{
Success,
Failure,
Disabled
}

internal interface IFileSystem
{
// Note: You are responsible for handling success/failure when attempting to write to disk.
// You are required to check for `Options.FileWriteDisabled` whether you are allowed to call any writing operations.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good call out. Adding a sexton like or (can't recall the options) above methods in the interface with a snippet might make it even more obvious

// The options will automatically pick between `ReadOnly` and `ReadAndWrite` to prevent accidental file writing that
// could cause crashes on restricted platforms like the Nintendo Switch.

// Note: This is not comprehensive. If you need other filesystem methods, add to this interface,
// then implement in both Sentry.Internal.FileSystem and Sentry.Testing.FakeFileSystem.

Expand All @@ -21,10 +19,10 @@ internal interface IFileSystem
string? ReadAllTextFromFile(string file);
Stream OpenFileForReading(string path);

FileOperationResult CreateDirectory(string path);
FileOperationResult DeleteDirectory(string path, bool recursive = false);
FileOperationResult CreateFileForWriting(string path, out Stream fileStream);
FileOperationResult WriteAllTextToFile(string path, string contents);
FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false);
FileOperationResult DeleteFile(string path);
bool CreateDirectory(string path);
bool DeleteDirectory(string path, bool recursive = false);
bool CreateFileForWriting(string path, out Stream fileStream);
bool WriteAllTextToFile(string path, string contents);
bool MoveFile(string sourceFileName, string destFileName, bool overwrite = false);
bool DeleteFile(string path);
}
4 changes: 2 additions & 2 deletions src/Sentry/Internal/InstallationIdHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ internal class InstallationIdHelper(SentryOptions options)
var directoryPath = Path.Combine(rootPath, "Sentry", options.Dsn!.GetHashString());
var fileSystem = options.FileSystem;

if (fileSystem.CreateDirectory(directoryPath) is not FileOperationResult.Success)
if (!fileSystem.CreateDirectory(directoryPath))
{
options.LogDebug("Failed to create a directory for installation ID file ({0}).", directoryPath);
return null;
Expand All @@ -79,7 +79,7 @@ internal class InstallationIdHelper(SentryOptions options)

// Generate new installation ID and store it in a file
var id = Guid.NewGuid().ToString();
if (fileSystem.WriteAllTextToFile(filePath, id) is not FileOperationResult.Success)
if (!fileSystem.WriteAllTextToFile(filePath, id))
{
options.LogDebug("Failed to write Installation ID to file ({0}).", filePath);
return null;
Expand Down
20 changes: 12 additions & 8 deletions src/Sentry/Internal/ReadOnlyFilesystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ namespace Sentry.Internal;

internal class ReadOnlyFileSystem : FileSystemBase
{
public override FileOperationResult CreateDirectory(string path) => FileOperationResult.Disabled;
// Note: You are responsible for handling success/failure when attempting to write to disk.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this won't show up in the IDE at call site. Needs

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These interfaces and classes are all internal, so it looks like the only place to document this is in some remarks or something on SentryOptions.DisableFileWrite.

// You are required to check for `Options.FileWriteDisabled` whether you are allowed to call any writing operations.
// The options will automatically pick between `ReadOnly` and `ReadAndWrite` to prevent accidental file writing that
// could cause crashes on restricted platforms like the Nintendo Switch.

public override FileOperationResult DeleteDirectory(string path, bool recursive = false) => FileOperationResult.Disabled;
public override bool CreateDirectory(string path) => false;

public override FileOperationResult CreateFileForWriting(string path, out Stream fileStream)
public override bool DeleteDirectory(string path, bool recursive = false) => false;

public override bool CreateFileForWriting(string path, out Stream fileStream)
{
fileStream = Stream.Null;
return FileOperationResult.Disabled;
return false;
}

public override FileOperationResult WriteAllTextToFile(string path, string contents) => FileOperationResult.Disabled;
public override bool WriteAllTextToFile(string path, string contents) => false;

public override FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false) =>
FileOperationResult.Disabled;
public override bool MoveFile(string sourceFileName, string destFileName, bool overwrite = false) => false;

public override FileOperationResult DeleteFile(string path) => FileOperationResult.Disabled;
public override bool DeleteFile(string path) => false;
}
31 changes: 18 additions & 13 deletions src/Sentry/Internal/ReadWriteFileSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,36 @@ namespace Sentry.Internal;

internal class ReadWriteFileSystem : FileSystemBase
{
public override FileOperationResult CreateDirectory(string path)
// Note: You are responsible for handling success/failure when attempting to write to disk.
// You are required to check for `Options.FileWriteDisabled` whether you are allowed to call any writing operations.
// The options will automatically pick between `ReadOnly` and `ReadAndWrite` to prevent accidental file writing that
// could cause crashes on restricted platforms like the Nintendo Switch.

public override bool CreateDirectory(string path)
{
Directory.CreateDirectory(path);
return DirectoryExists(path) ? FileOperationResult.Success : FileOperationResult.Failure;
return DirectoryExists(path);
}

public override FileOperationResult DeleteDirectory(string path, bool recursive = false)
public override bool DeleteDirectory(string path, bool recursive = false)
{
Directory.Delete(path, recursive);
return Directory.Exists(path) ? FileOperationResult.Failure : FileOperationResult.Success;
return !Directory.Exists(path);
}

public override FileOperationResult CreateFileForWriting(string path, out Stream fileStream)
public override bool CreateFileForWriting(string path, out Stream fileStream)
{
fileStream = File.Create(path);
return FileOperationResult.Success;
return true;
}

public override FileOperationResult WriteAllTextToFile(string path, string contents)
public override bool WriteAllTextToFile(string path, string contents)
{
File.WriteAllText(path, contents);
return File.Exists(path) ? FileOperationResult.Success : FileOperationResult.Failure;
return File.Exists(path);
}

public override FileOperationResult MoveFile(string sourceFileName, string destFileName, bool overwrite = false)
public override bool MoveFile(string sourceFileName, string destFileName, bool overwrite = false)
{
#if NETCOREAPP3_0_OR_GREATER
File.Move(sourceFileName, destFileName, overwrite);
Expand All @@ -44,15 +49,15 @@ public override FileOperationResult MoveFile(string sourceFileName, string destF

if (File.Exists(sourceFileName) || !File.Exists(destFileName))
{
return FileOperationResult.Failure;
return false;
}

return FileOperationResult.Success;
return true;
}

public override FileOperationResult DeleteFile(string path)
public override bool DeleteFile(string path)
{
File.Delete(path);
return File.Exists(path) ? FileOperationResult.Failure : FileOperationResult.Success;
return !File.Exists(path);
}
}
2 changes: 1 addition & 1 deletion src/Sentry/Internal/SdkComposer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ private ITransport CreateTransport()

if (_options.DisableFileWrite)
{
_options.LogInfo("File writing is disabled, Skipping caching transport creation.");
_options.LogInfo("File write has been disabled via the options. Skipping caching transport creation.");
}
else
{
Expand Down
Loading
Loading