/// <summary> /// Initializes a new instance of <see cref="AutoSaveTextFile{TUpdate}"/>. /// </summary> /// <param name="remoteState"> /// Object responsible for converting updates to text. /// </param> /// <param name="autoSaveFiles"> /// The <see cref="FileStreamPair"/> containing <see cref="FileStream"/>s to write to. /// Any existing contents in the files will be overwritten. /// <see cref="AutoSaveTextFile{TUpdate}"/> assumes ownership of the <see cref="FileStream"/>s /// so it takes care of disposing it after use. /// To be used as an auto-save <see cref="FileStream"/>, /// it must support seeking, reading and writing, and not be able to time out. /// </param> /// <exception cref="ArgumentNullException"> /// <paramref name="remoteState"/> and/or <paramref name="autoSaveFiles"/> are null. /// </exception> /// <exception cref="ArgumentException"> /// One or both <see cref="FileStream"/>s in <paramref name="autoSaveFiles"/> /// do not have the right capabilities to be used as an auto-save file stream. /// See also: <seealso cref="CanAutoSaveTo"/>. /// </exception> public AutoSaveTextFile(RemoteState remoteState, FileStreamPair autoSaveFiles) { if (remoteState == null) { throw new ArgumentNullException(nameof(remoteState)); } AutoSaveFiles = autoSaveFiles ?? throw new ArgumentNullException(nameof(autoSaveFiles)); // Assert capabilities of the file streams. VerifyFileStream(autoSaveFiles.FileStream1, nameof(autoSaveFiles)); VerifyFileStream(autoSaveFiles.FileStream2, nameof(autoSaveFiles)); // Immediately attempt to load the saved contents from either FileStream. // Choose first auto-save file to load from. FileStream latestAutoSaveFile = autoSaveFiles.FileStream1.Length == 0 ? autoSaveFiles.FileStream2 : autoSaveFiles.FileStream1; string loadedText = null; try { loadedText = Load(latestAutoSaveFile); } catch (Exception firstLoadException) { // Trace and try the other auto-save file as a backup. firstLoadException.Trace(); } // If null is returned from the first Load(), the integrity check failed. if (loadedText == null) { latestAutoSaveFile = autoSaveFiles.Different(latestAutoSaveFile); try { loadedText = Load(latestAutoSaveFile); } catch (Exception secondLoadException) { secondLoadException.Trace(); } } // Initialize remote state with the loaded text. // If both reads failed, loadedText == null. remoteState.Initialize(loadedText); // Initialize encoder and buffers. // Always use UTF8 for auto-saved text files. Encoding encoding = Encoding.UTF8; encoder = encoding.GetEncoder(); buffer = new char[CharBufferSize]; encodedBuffer = new byte[encoding.GetMaxByteCount(CharBufferSize)]; // Set up long running task to keep auto-saving updates. updateQueue = new ConcurrentQueue <TUpdate>(); cts = new CancellationTokenSource(); autoSaveBackgroundTask = Task.Run(() => AutoSaveLoop(latestAutoSaveFile, remoteState, cts.Token)); }
private async Task AutoSaveLoop(FileStream lastWrittenToFile, RemoteState remoteState, CancellationToken ct) { for (; ;) { // If cancellation is requested, stop waiting so the queue can be emptied as quickly as possible. if (!ct.IsCancellationRequested) { try { await Task.Delay(AutoSaveDelay, ct); } catch { // If the task was cancelled, empty the queue before leaving this method. } } // Empty the queue, create a local (thread-safe) list of updates to process. bool hasUpdate = updateQueue.TryDequeue(out TUpdate firstUpdate); if (!hasUpdate) { // Only return if the queue is empty and saved. if (ct.IsCancellationRequested) { break; } } else { List <TUpdate> updates = new List <TUpdate> { firstUpdate }; while (updateQueue.TryDequeue(out TUpdate update)) { updates.Add(update); } try { if (remoteState.ShouldSave(updates, out string textToSave)) { // Alternate between both auto-save files. FileStream targetFile = AutoSaveFiles.Different(lastWrittenToFile); // Only truly necessary in the first iteration if the targetFile was initially a corrupt non-empty file. // Theoretically, two thrown writeExceptions would have the same effect. // In other cases, lastWrittenToFile.SetLength(0) below will already have done this. targetFile.SetLength(0); // Write the contents to the file. await WriteToFileAsync(targetFile, textToSave); // Only truncate the other file when completely successful, to indicate that // the auto-save file which was just saved is in a completely correct format. lastWrittenToFile.SetLength(0); // Switch to writing to the other file in the next iteration. lastWrittenToFile = targetFile; } } catch (Exception writeException) { writeException.Trace(); } } } }