Skip to content

Commit fd99806

Browse files
authored
Fix incremental history saving when the history queue is full (#1602)
1 parent 6c58aff commit fd99806

File tree

2 files changed

+122
-38
lines changed

2 files changed

+122
-38
lines changed

PSReadLine/History.cs

Lines changed: 67 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -257,12 +257,12 @@ private void IncrementalHistoryWrite()
257257
i -= 1;
258258
}
259259

260-
WriteHistoryRange(i + 1, _history.Count - 1, File.AppendText);
260+
WriteHistoryRange(i + 1, _history.Count - 1, overwritten: false);
261261
}
262262

263263
private void SaveHistoryAtExit()
264264
{
265-
WriteHistoryRange(0, _history.Count - 1, File.CreateText);
265+
WriteHistoryRange(0, _history.Count - 1, overwritten: true);
266266
}
267267

268268
private int historyErrorReportedCount;
@@ -321,70 +321,99 @@ private bool WithHistoryFileMutexDo(int timeout, Action action)
321321
return true;
322322
}
323323

324-
private void WriteHistoryRange(int start, int end, Func<string, StreamWriter> fileOpener)
324+
private void WriteHistoryRange(int start, int end, bool overwritten)
325325
{
326326
WithHistoryFileMutexDo(100, () =>
327327
{
328-
if (!MaybeReadHistoryFile())
329-
return;
330-
331328
bool retry = true;
332-
retry_after_creating_directory:
329+
// Get the new content since the last sync.
330+
List<string> historyLines = overwritten ? null : ReadHistoryFileIncrementally();
331+
333332
try
334333
{
335-
using (var file = fileOpener(Options.HistorySavePath))
334+
retry_after_creating_directory:
335+
try
336336
{
337-
for (var i = start; i <= end; i++)
337+
using (var file = overwritten ? File.CreateText(Options.HistorySavePath) : File.AppendText(Options.HistorySavePath))
338338
{
339-
HistoryItem item = _history[i];
340-
item._saved = true;
339+
for (var i = start; i <= end; i++)
340+
{
341+
HistoryItem item = _history[i];
342+
item._saved = true;
341343

342-
// Actually, skip writing sensitive items to file.
343-
if (item._sensitive) { continue; }
344+
// Actually, skip writing sensitive items to file.
345+
if (item._sensitive) { continue; }
344346

345-
var line = item.CommandLine.Replace("\n", "`\n");
346-
file.WriteLine(line);
347+
var line = item.CommandLine.Replace("\n", "`\n");
348+
file.WriteLine(line);
349+
}
350+
}
351+
var fileInfo = new FileInfo(Options.HistorySavePath);
352+
_historyFileLastSavedSize = fileInfo.Length;
353+
}
354+
catch (DirectoryNotFoundException)
355+
{
356+
// Try making the directory, but just once
357+
if (retry)
358+
{
359+
retry = false;
360+
Directory.CreateDirectory(Path.GetDirectoryName(Options.HistorySavePath));
361+
goto retry_after_creating_directory;
347362
}
348363
}
349-
var fileInfo = new FileInfo(Options.HistorySavePath);
350-
_historyFileLastSavedSize = fileInfo.Length;
351364
}
352-
catch (DirectoryNotFoundException)
365+
finally
353366
{
354-
// Try making the directory, but just once
355-
if (retry)
367+
if (historyLines != null)
356368
{
357-
retry = false;
358-
Directory.CreateDirectory(Path.GetDirectoryName(Options.HistorySavePath));
359-
goto retry_after_creating_directory;
369+
// Populate new history from other sessions to the history queue after we are done
370+
// with writing the specified range to the file.
371+
// We do it at this point to make sure the range of history items from 'start' to
372+
// 'end' do not get changed before the writing to the file.
373+
UpdateHistoryFromFile(historyLines, fromDifferentSession: true, fromInitialRead: false);
360374
}
361375
}
362376
});
363377
}
364378

379+
/// <summary>
380+
/// Helper method to read the incremental part of the history file.
381+
/// Note: the call to this method should be guarded by the mutex that protects the history file.
382+
/// </summary>
383+
private List<string> ReadHistoryFileIncrementally()
384+
{
385+
var fileInfo = new FileInfo(Options.HistorySavePath);
386+
if (fileInfo.Exists && fileInfo.Length != _historyFileLastSavedSize)
387+
{
388+
var historyLines = new List<string>();
389+
using (var fs = new FileStream(Options.HistorySavePath, FileMode.Open))
390+
using (var sr = new StreamReader(fs))
391+
{
392+
fs.Seek(_historyFileLastSavedSize, SeekOrigin.Begin);
393+
394+
while (!sr.EndOfStream)
395+
{
396+
historyLines.Add(sr.ReadLine());
397+
}
398+
}
399+
400+
_historyFileLastSavedSize = fileInfo.Length;
401+
return historyLines.Count > 0 ? historyLines : null;
402+
}
403+
404+
return null;
405+
}
406+
365407
private bool MaybeReadHistoryFile()
366408
{
367409
if (Options.HistorySaveStyle == HistorySaveStyle.SaveIncrementally)
368410
{
369411
return WithHistoryFileMutexDo(1000, () =>
370412
{
371-
var fileInfo = new FileInfo(Options.HistorySavePath);
372-
if (fileInfo.Exists && fileInfo.Length != _historyFileLastSavedSize)
413+
List<string> historyLines = ReadHistoryFileIncrementally();
414+
if (historyLines != null)
373415
{
374-
var historyLines = new List<string>();
375-
using (var fs = new FileStream(Options.HistorySavePath, FileMode.Open))
376-
using (var sr = new StreamReader(fs))
377-
{
378-
fs.Seek(_historyFileLastSavedSize, SeekOrigin.Begin);
379-
380-
while (!sr.EndOfStream)
381-
{
382-
historyLines.Add(sr.ReadLine());
383-
}
384-
}
385416
UpdateHistoryFromFile(historyLines, fromDifferentSession: true, fromInitialRead: false);
386-
387-
_historyFileLastSavedSize = fileInfo.Length;
388417
}
389418
});
390419
}

test/HistoryTest.cs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.IO;
33
using System.Linq;
44
using System.Management.Automation;
5+
using System.Reflection;
56
using Microsoft.PowerShell;
67
using Xunit;
78

@@ -33,6 +34,60 @@ public void History()
3334
Test("dir c*", Keys(_.UpArrow, _.UpArrow, _.DownArrow));
3435
}
3536

37+
[SkippableFact]
38+
public void ParallelHistorySaving()
39+
{
40+
TestSetup(KeyMode.Cmd);
41+
42+
string historySavingFile = Path.GetTempFileName();
43+
var options = new SetPSReadLineOption {
44+
HistorySaveStyle = HistorySaveStyle.SaveIncrementally,
45+
MaximumHistoryCount = 3,
46+
};
47+
48+
typeof(SetPSReadLineOption)
49+
.GetField("_historySavePath", BindingFlags.Instance | BindingFlags.NonPublic)
50+
.SetValue(options, historySavingFile);
51+
52+
PSConsoleReadLine.SetOptions(options);
53+
54+
// Set the initial history items.
55+
string[] initialHistoryItems = new[] { "gcm help", "dir ~" };
56+
SetHistory(initialHistoryItems);
57+
58+
// The initial history items should be saved to file.
59+
string[] text = File.ReadAllLines(historySavingFile);
60+
Assert.Equal(initialHistoryItems.Length, text.Length);
61+
for (int i = 0; i < text.Length; i++)
62+
{
63+
Assert.Equal(initialHistoryItems[i], text[i]);
64+
}
65+
66+
// Add another line to the file to mimic the new history saving from a different session.
67+
using (var file = File.AppendText(historySavingFile))
68+
{
69+
file.WriteLine("cd Downloads");
70+
}
71+
72+
PSConsoleReadLine.AddToHistory("cd Documents");
73+
74+
string[] expectedSavedLines = new[] { "gcm help", "dir ~", "cd Downloads", "cd Documents" };
75+
text = File.ReadAllLines(historySavingFile);
76+
Assert.Equal(expectedSavedLines.Length, text.Length);
77+
for (int i = 0; i < text.Length; i++)
78+
{
79+
Assert.Equal(expectedSavedLines[i], text[i]);
80+
}
81+
82+
string[] expectedHistoryItems = new[] { "dir ~", "cd Documents", "cd Downloads" };
83+
var historyItems = PSConsoleReadLine.GetHistoryItems();
84+
Assert.Equal(expectedHistoryItems.Length, historyItems.Length);
85+
for (int i = 0; i < historyItems.Length; i++)
86+
{
87+
Assert.Equal(expectedHistoryItems[i], historyItems[i].CommandLine);
88+
}
89+
}
90+
3691
[SkippableFact]
3792
public void SensitiveHistoryDefaultBehavior()
3893
{

0 commit comments

Comments
 (0)