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

feat: make session snapshot more full proof #786

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
211 changes: 161 additions & 50 deletions src/Notepads/Core/SessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
Expand All @@ -15,6 +16,7 @@
using Notepads.Models;
using Notepads.Services;
using Notepads.Utilities;
using Windows.ApplicationModel.Resources;
using Windows.Storage;
using Windows.Storage.AccessCache;

Expand Down Expand Up @@ -61,46 +63,27 @@ public async Task<int> LoadLastSessionAsync()
return 0; // Already loaded
}

var data = await SessionUtility.GetSerializedSessionMetaDataAsync(_sessionMetaDataFileName);

if (data == null)
{
return 0; // No session data found
}

NotepadsSessionDataV1 sessionData;

try
{
var json = JsonDocument.Parse(data);
var version = json.RootElement.GetProperty("Version").GetInt32();
IList<ITextEditor> recoveredEditor = new List<ITextEditor>();
var sessionData = await SessionUtility.GetSessionMetaDataAsync(_sessionMetaDataFileName);
var backupFiles = (await SessionUtility.GetAllBackupFilesAsync(_backupFolderName)).ToList();
var orphanedEditorCount = 0;

if (version == 1)
{
sessionData = JsonSerializer.Deserialize<NotepadsSessionDataV1>(data);
}
else
{
throw new Exception($"Invalid version found in session metadata: {version}");
}
}
catch (Exception ex)
if (sessionData == null && backupFiles.Count > 0)
{
LoggingService.LogError($"[{nameof(SessionManager)}] Failed to load last session metadata: {ex.Message}");
Analytics.TrackEvent("SessionManager_FailedToLoadLastSession", new Dictionary<string, string>() { { "Exception", ex.Message } });
recoveredEditor = await RecoverOrphanedTextEditors(backupFiles);
orphanedEditorCount = recoveredEditor.Count;
_notepadsCore.OpenTextEditors(recoveredEditor.ToArray());
await ClearSessionDataAsync();
return 0;
return orphanedEditorCount; // No session meta-data found, only recover from orphaned backup files
}

IList<ITextEditor> recoveredEditor = new List<ITextEditor>();

foreach (var textEditorData in sessionData.TextEditors)
{
ITextEditor textEditor;
Tuple<ITextEditor, IList<StorageFile>> recoveredData;

try
{
textEditor = await RecoverTextEditorAsync(textEditorData);
recoveredData = await RecoverTextEditorAsync(textEditorData, backupFiles);
}
catch (Exception ex)
{
Expand All @@ -109,21 +92,36 @@ public async Task<int> LoadLastSessionAsync()
continue;
}

if (textEditor != null)
if (recoveredData != null)
{
recoveredEditor.Add(textEditor);
_sessionDataCache.TryAdd(textEditor.Id, textEditorData);
if (recoveredData.Item1 != null)
{
recoveredEditor.Add(recoveredData.Item1);
_sessionDataCache.TryAdd(recoveredData.Item1.Id, textEditorData);
}

if (recoveredData.Item2?.Count > 0)
{
recoveredData.Item2.All(file => backupFiles.Remove(file));
}
}
}

if (backupFiles?.Count > 0)
{
var orphanedEditors = await RecoverOrphanedTextEditors(backupFiles);
orphanedEditorCount = orphanedEditors.Count;
recoveredEditor.Concat(orphanedEditors);
}

_notepadsCore.OpenTextEditors(recoveredEditor.ToArray(), sessionData.SelectedTextEditor);
_notepadsCore.SetTabScrollViewerHorizontalOffset(sessionData.TabScrollViewerHorizontalOffset);

_loaded = true;

LoggingService.LogInfo($"[{nameof(SessionManager)}] {_sessionDataCache.Count} tab(s) restored from last session.");

return _sessionDataCache.Count;
return _sessionDataCache.Count + orphanedEditorCount;
}

public async Task SaveSessionAsync(Action actionAfterSaving = null)
Expand Down Expand Up @@ -386,25 +384,28 @@ private void UnbindEditorContentStateChangeEvent(object sender, ITextEditor text
textEditor.FileReloaded -= RemoveTextEditorSessionData;
}

private async Task<ITextEditor> RecoverTextEditorAsync(TextEditorSessionDataV1 editorSessionData)
private async Task<Tuple<ITextEditor, IList<StorageFile>>> RecoverTextEditorAsync(
TextEditorSessionDataV1 editorSessionData,
IList<StorageFile> backupFiles)
{
StorageFile editingFile = null;
IList<StorageFile> recoveredFiles = new List<StorageFile>();

if (editorSessionData.EditingFileFutureAccessToken != null)
{
editingFile = await FutureAccessListUtility.GetFileFromFutureAccessList(editorSessionData.EditingFileFutureAccessToken);
}

string lastSavedFile = editorSessionData.LastSavedBackupFilePath;
string pendingFile = editorSessionData.PendingBackupFilePath;
string lastSavedFilePath = editorSessionData.LastSavedBackupFilePath;
string pendingFilePath = editorSessionData.PendingBackupFilePath;

ITextEditor textEditor;

if (editingFile == null && lastSavedFile == null && pendingFile == null)
if (editingFile == null && lastSavedFilePath == null && pendingFilePath == null)
{
textEditor = null;
}
else if (editingFile != null && lastSavedFile == null && pendingFile == null) // File without pending changes
else if (editingFile != null && lastSavedFilePath == null && pendingFilePath == null) // File without pending changes
{
var encoding = EncodingUtility.GetEncodingByName(editorSessionData.StateMetaData.LastSavedEncoding);
textEditor = await _notepadsCore.CreateTextEditor(editorSessionData.Id, editingFile, encoding: encoding, ignoreFileSizeLimit: true);
Expand All @@ -415,11 +416,20 @@ private async Task<ITextEditor> RecoverTextEditorAsync(TextEditorSessionDataV1 e
string lastSavedText = string.Empty;
string pendingText = null;

if (lastSavedFile != null)
if (lastSavedFilePath != null)
{
TextFile lastSavedTextFile = await FileSystemUtility.ReadFile(lastSavedFile, ignoreFileSizeLimit: true,
EncodingUtility.GetEncodingByName(editorSessionData.StateMetaData.LastSavedEncoding));
lastSavedText = lastSavedTextFile.Content;
var lastSavedFile = backupFiles.First(file => lastSavedFilePath.Equals(file.Path, StringComparison.OrdinalIgnoreCase));
if (lastSavedFile == null)
{
lastSavedText = (await FileSystemUtility.ReadFile(lastSavedFilePath, ignoreFileSizeLimit: true,
EncodingUtility.GetEncodingByName(editorSessionData.StateMetaData.LastSavedEncoding))).Content;
}
else
{
recoveredFiles.Add(lastSavedFile);
lastSavedText = (await FileSystemUtility.ReadFile(lastSavedFile, ignoreFileSizeLimit: true,
EncodingUtility.GetEncodingByName(editorSessionData.StateMetaData.LastSavedEncoding))).Content;
}
}

var textFile = new TextFile(lastSavedText,
Expand All @@ -434,25 +444,121 @@ private async Task<ITextEditor> RecoverTextEditorAsync(TextEditorSessionDataV1 e
editorSessionData.StateMetaData.FileNamePlaceholder,
editorSessionData.StateMetaData.IsModified);

if (pendingFile != null)
if (pendingFilePath != null)
{
TextFile pendingTextFile = await FileSystemUtility.ReadFile(pendingFile,
ignoreFileSizeLimit: true,
EncodingUtility.GetEncodingByName(editorSessionData.StateMetaData.LastSavedEncoding));
pendingText = pendingTextFile.Content;
var pendingFile = backupFiles.First(file => pendingFilePath.Equals(file.Path, StringComparison.OrdinalIgnoreCase));
if (pendingFile == null)
{
pendingText = (await FileSystemUtility.ReadFile(pendingFilePath, ignoreFileSizeLimit: true,
EncodingUtility.GetEncodingByName(editorSessionData.StateMetaData.LastSavedEncoding))).Content;
}
else
{
recoveredFiles.Add(pendingFile);
pendingText = (await FileSystemUtility.ReadFile(pendingFile, ignoreFileSizeLimit: true,
EncodingUtility.GetEncodingByName(editorSessionData.StateMetaData.LastSavedEncoding))).Content;
}
}

textEditor.ResetEditorState(editorSessionData.StateMetaData, pendingText);
}

return textEditor;
return Tuple.Create(textEditor, recoveredFiles);
}

// Recover editos from backup files if they have failed to store metadata
private async Task<IList<ITextEditor>> RecoverOrphanedTextEditors(IList<StorageFile> backupFiles)
{
IList<ITextEditor> recoveredEditors = new List<ITextEditor>();
while (backupFiles.Count > 0)
{
var backupFile = backupFiles[0];
Guid editorId = Guid.NewGuid();
TextFile lastSavedTextFile = null;
TextFile pendingTextFile = null;
if (backupFile.Name.EndsWith("-LastSaved"))
{
editorId = Guid.Parse(backupFile.Name.Replace("-LastSaved", ""));
lastSavedTextFile = await FileSystemUtility.ReadFile(backupFile, ignoreFileSizeLimit: true);
var pendingFile = backupFiles.First(file => file.Name.Equals(ToToken(editorId) + "-Pending"));
pendingTextFile = await FileSystemUtility.ReadFile(pendingFile, ignoreFileSizeLimit: true);

backupFiles.Remove(backupFile);
backupFiles.Remove(pendingFile);
}
else if (backupFile.Name.EndsWith("-Pending"))
{
editorId = Guid.Parse(backupFile.Name.Replace("-Pending", ""));
var lastSavedFile = backupFiles.First(file => file.Name.Equals(ToToken(editorId) + "-LastSaved"));
lastSavedTextFile = await FileSystemUtility.ReadFile(lastSavedFile, ignoreFileSizeLimit: true);
pendingTextFile = await FileSystemUtility.ReadFile(backupFile, ignoreFileSizeLimit: true);

backupFiles.Remove(backupFile);
backupFiles.Remove(lastSavedFile);
}
else
{
backupFiles.Remove(backupFile);
continue;
}

var failedPendingFilePath = Path.Combine(SessionUtility.GetBackupFolderPath(_backupFolderName),
ToToken(editorId) + "-Pending.~TMP");
if (pendingTextFile == null)
{
if (File.Exists(failedPendingFilePath))
{
pendingTextFile = await FileSystemUtility.ReadLocalFile(failedPendingFilePath);
}
else
{
// If there are no pending changes no need to recover
continue;
}
}

ITextEditor textEditor = null;
var defaultName = ResourceLoader.GetForCurrentView().GetString("TextEditor_DefaultNewFileName");
var editingFile = await FutureAccessListUtility.GetFileFromFutureAccessList(ToToken(editorId));
var textFile = await FileSystemUtility.ReadFile(editingFile, ignoreFileSizeLimit: true);
if (lastSavedTextFile == null)
{
textEditor = _notepadsCore.CreateTextEditor(editorId,
textFile ?? pendingTextFile,
editingFile,
editingFile?.Name ?? defaultName,
true);
}
else
{
textEditor = _notepadsCore.CreateTextEditor(editorId,
lastSavedTextFile,
editingFile,
editingFile?.Name ?? defaultName,
true);
}

if (textEditor == null) continue;

textEditor.ResetEditorState(textEditor.GetTextEditorStateMetaData(), pendingTextFile.Content);

if (File.Exists(failedPendingFilePath))
{
var failedPendingText = await File.ReadAllTextAsync(failedPendingFilePath);
textEditor.ResetEditorState(textEditor.GetTextEditorStateMetaData(), failedPendingText);
}

recoveredEditors.Add(textEditor);
}

return recoveredEditors;
}

private static async Task<bool> BackupTextAsync(string text, Encoding encoding, LineEnding lineEnding, StorageFile file)
{
try
{
await FileSystemUtility.WriteToFile(LineEndingUtility.ApplyLineEnding(text, lineEnding), encoding, file);
await FileSystemUtility.SafeWriteToFile(LineEndingUtility.ApplyLineEnding(text, lineEnding), encoding, file);
return true;
}
catch (Exception ex)
Expand Down Expand Up @@ -484,6 +590,11 @@ private async Task DeleteOrphanedBackupFilesAsync(NotepadsSessionDataV1 sessionD
}
}
}

foreach (var filePth in Directory.GetFiles(ApplicationData.Current.LocalFolder.Path, "*.~TMP", SearchOption.AllDirectories))
{
File.Delete(filePth);
}
}

// Cleanup orphaned/dangling entries in FutureAccessList
Expand Down
3 changes: 0 additions & 3 deletions src/Notepads/Notepads.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,6 @@
<PackageReference Include="Microsoft.Xaml.Behaviors.Uwp.Managed">
<Version>2.0.1</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>13.0.1</Version>
</PackageReference>
<PackageReference Include="System.Text.Json">
<Version>5.0.1</Version>
</PackageReference>
Expand Down
Loading