Skip to content

Commit

Permalink
Improved FileIO logging
Browse files Browse the repository at this point in the history
  • Loading branch information
0x7c13 committed Dec 13, 2023
1 parent 7ec989f commit 99c94ed
Showing 1 changed file with 58 additions and 38 deletions.
96 changes: 58 additions & 38 deletions src/Notepads/Utilities/FileSystemUtility.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
namespace Notepads.Utilities
{
using Microsoft.AppCenter.Analytics;
using Notepads.Models;
using Notepads.Services;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AppCenter.Analytics;
using Notepads.Models;
using Notepads.Services;
using UtfUnknown;
using Windows.ApplicationModel.Resources;
using Windows.Storage;
using Windows.Storage.FileProperties;
using UtfUnknown;
using Windows.Storage.Provider;

public enum InvalidFilenameError
{
Expand Down Expand Up @@ -560,40 +561,52 @@ private static Encoding FixUtf8Bom(Encoding encoding, byte[] bom)
/// </summary>
public static async Task WriteTextToFileAsync(StorageFile file, string text, Encoding encoding)
{
await ExecuteFileIOWithRetries(file, async () =>
if (IsFileReadOnly(file) || !await IsFileWritableAsync(file))
{
if (IsFileReadOnly(file) || !await IsFileWritableAsync(file))
{
// For file(s) dragged into Notepads, they are read-only
// StorageFile API won't work on read-only files but can be written by Win32 PathIO API (exploit?)
// In case the file is actually read-only, WriteBytesAsync will throw UnauthorizedAccessException
var content = encoding.GetBytes(text);
var result = encoding.GetPreamble().Concat(content).ToArray();
await PathIO.WriteBytesAsync(file.Path, result);
}
else // Use StorageFile API to save
{
using (var stream = await file.OpenStreamForWriteAsync())
using (var writer = new StreamWriter(stream, encoding))
await ExecuteFileIOOperationWithRetries(file,
"FileSystemUtility_WriteTextToFileAsync_UsingPathIO",
async () =>
{
stream.Position = 0;
await writer.WriteAsync(text);
await writer.FlushAsync();
stream.SetLength(stream.Position); // Truncate
}
}
});
// For file(s) dragged into Notepads, they are read-only
// StorageFile API won't work on read-only files but can be written by Win32 PathIO API (exploit?)
// In case the file is actually read-only, WriteBytesAsync will throw UnauthorizedAccessException
var content = encoding.GetBytes(text);
var result = encoding.GetPreamble().Concat(content).ToArray();
await PathIO.WriteBytesAsync(file.Path, result);
});
}
else // Use StorageFile API to save
{
await ExecuteFileIOOperationWithRetries(file,
"FileSystemUtility_WriteTextToFileAsync_UsingStreamWriter",
async () =>
{
using (var stream = await file.OpenStreamForWriteAsync())
using (var writer = new StreamWriter(stream, encoding))
{
stream.Position = 0;
await writer.WriteAsync(text);
await writer.FlushAsync();
stream.SetLength(stream.Position); // Truncate
}
});
}
}

/// <summary>
/// Save text to a file with UTF-8 encoding using FileIO API with retries for retriable errors
/// </summary>
public static async Task WriteTextToFileAsync(StorageFile storageFile, string text)
{
await ExecuteFileIOWithRetries(storageFile, async () => await FileIO.WriteTextAsync(storageFile, text));
await ExecuteFileIOOperationWithRetries(storageFile,
"FileSystemUtility_WriteTextToFileAsync_UsingFileIO",
async () => await FileIO.WriteTextAsync(storageFile, text));
}

private static async Task ExecuteFileIOWithRetries(StorageFile file, Func<Task> action, int maxRetryAttempts = 5)
private static async Task ExecuteFileIOOperationWithRetries(StorageFile file,
string operationName,
Func<Task> action,
int maxRetryAttempts = 7)
{
bool deferUpdatesUsed = true;
int retryAttempts = 0;
Expand All @@ -610,6 +623,8 @@ private static async Task ExecuteFileIOWithRetries(StorageFile file, Func<Task>
deferUpdatesUsed = false;
}

HashSet<string> errorCodes = new HashSet<string>();

try
{
while (retryAttempts < maxRetryAttempts)
Expand All @@ -625,30 +640,35 @@ private static async Task ExecuteFileIOWithRetries(StorageFile file, Func<Task>
(ex.HResult == ERROR_UNABLE_TO_REMOVE_REPLACED) ||
(ex.HResult == ERROR_FAIL))
{
// Delay 13ms before retrying for all retriable errors
await Task.Delay(13);
errorCodes.Add("0x" + Convert.ToString(ex.HResult, 16));
await Task.Delay(13); // Delay 13ms before retrying for all retriable errors
}
}
}
finally
{
string fileUpdateStatus = string.Empty;

if (deferUpdatesUsed)
{
// Let Windows know that we're finished changing the file so the
// other app can update the remote version of the file.
FileUpdateStatus status = await CachedFileManager.CompleteUpdatesAsync(file);
fileUpdateStatus = status.ToString();
}

if (retryAttempts > 1) // Retry attempts were used
{
// Track retry attempts to better understand the failed scenarios
// File name, path and content are not included to respect/protect user privacy
Analytics.TrackEvent("SaveSerializedSessionMetaDataAsync_RetryAttempts", new Dictionary<string, string>()
Analytics.TrackEvent(operationName, new Dictionary<string, string>()
{
{ "RetryAttempts", (retryAttempts - 1).ToString() },
{ "DeferUpdatesUsed" , deferUpdatesUsed.ToString() }
{ "DeferUpdatesUsed" , deferUpdatesUsed.ToString() },
{ "ErrorCodes", string.Join(", ", errorCodes) },
{ "FileUpdateStatus", fileUpdateStatus }
});
}

if (deferUpdatesUsed)
{
// Let Windows know that we're finished changing the file so the
// other app can update the remote version of the file.
_ = await CachedFileManager.CompleteUpdatesAsync(file);
}
}
}

Expand Down

0 comments on commit 99c94ed

Please sign in to comment.