Skip to content

Commit dc36a49

Browse files
committed
Move one-time initialization out of psm1
1 parent e4c1ff0 commit dc36a49

File tree

5 files changed

+118
-113
lines changed

5 files changed

+118
-113
lines changed

PSReadLine/Completion.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ public partial class PSConsoleReadLine
2121
// Tab completion state
2222
private int _tabCommandCount;
2323
private CommandCompletion _tabCompletions;
24-
private Runspace _remoteRunspace;
24+
private Runspace _runspace;
2525

2626
// Stub helper method so completion can be mocked
2727
[ExcludeFromCodeCoverage]
@@ -211,14 +211,14 @@ private CommandCompletion GetCompletions()
211211
// input for coloring) but that overload is a little more complicated in passing in the
212212
// cursor position.
213213
System.Management.Automation.PowerShell ps;
214-
if (_remoteRunspace == null)
214+
if (!_mockableMethods.RunspaceIsRemote(_runspace))
215215
{
216216
ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace);
217217
}
218218
else
219219
{
220220
ps = System.Management.Automation.PowerShell.Create();
221-
ps.Runspace = _remoteRunspace;
221+
ps.Runspace = _runspace;
222222
}
223223
_tabCompletions = _mockableMethods.CompleteInput(_buffer.ToString(), _current, null, ps);
224224

PSReadLine/PSReadLine.psm1

Lines changed: 2 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,5 @@
1-
2-
[bool]$firstTime = $true
3-
4-
#
5-
# .SYNOPSIS
6-
#
7-
# This function is called by the console host when reading input to execute commands.
8-
#
91
function PSConsoleHostReadline
102
{
11-
if ($firstTime)
12-
{
13-
# This initialization is delayed until the first call so that
14-
# a profile with something like:
15-
#
16-
# ipmo PSReadline
17-
# Set-PSReadlineOption -HistorySaveStyle ??????
18-
#
19-
# PSReadline will then handle the history choice in a reasonable way
20-
# regardless of the style.
21-
22-
$script:firstTime = $false
23-
24-
$options = [Microsoft.PowerShell.PSConsoleReadLine]::GetOptions()
25-
26-
# Honor $MaximumHistoryCount, but get it safely in case it was removed in the profile.
27-
$MaximumHistoryCount = Get-Variable -ea Ignore -ValueOnly MaximumHistoryCount
28-
if ($MaximumHistoryCount -gt 0)
29-
{
30-
$options.MaximumHistoryCount = $MaximumHistoryCount
31-
}
32-
33-
if ($options.HistorySaveStyle -eq [Microsoft.PowerShell.HistorySaveStyle]::SaveNothing)
34-
{
35-
# PSReadline isn't saving history, but we might still have history to reuse
36-
Get-History | ForEach-Object { [Microsoft.PowerShell.PSConsoleReadLine]::AddToHistory($_.CommandLine) }
37-
}
38-
}
39-
40-
# PSHost doesn't expose it's runspace. The InternalHost does, but that won't
41-
# work for arbitrary hosts, so we turn off strict mode.
42-
Set-StrictMode -Off
43-
$remoteRunspace = if ($host.IsRunspacePushed) { $host.Runspace } else { $null }
44-
45-
[Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($remoteRunspace, $ExecutionContext)
3+
Microsoft.PowerShell.Core\Set-StrictMode -Off
4+
[Microsoft.PowerShell.PSConsoleReadLine]::ReadLine($host.Runspace, $ExecutionContext)
465
}
47-

PSReadLine/PublicAPI.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Diagnostics.CodeAnalysis;
88
using System.Management.Automation;
99
using System.Management.Automation.Language;
10+
using System.Management.Automation.Runspaces;
1011

1112
namespace Microsoft.PowerShell
1213
{
@@ -24,6 +25,8 @@ public interface IPSConsoleReadLineMockableMethods
2425
void Ding();
2526
/// <summary/>
2627
CommandCompletion CompleteInput(string input, int cursorIndex, Hashtable options, System.Management.Automation.PowerShell powershell);
28+
/// <summary/>
29+
bool RunspaceIsRemote(Runspace runspace);
2730
}
2831
}
2932

PSReadLine/ReadLine.cs

Lines changed: 105 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
using System.Runtime.InteropServices;
1414
using System.Text;
1515
using System.Threading;
16+
using Microsoft.PowerShell.Commands;
1617
using Microsoft.PowerShell.Internal;
1718

1819
[module: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")]
@@ -25,13 +26,14 @@ class ExitException : Exception { }
2526

2627
public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods
2728
{
28-
private static readonly PSConsoleReadLine _singleton;
29+
private static readonly PSConsoleReadLine _singleton = new PSConsoleReadLine();
30+
2931
private bool _delayedOneTimeInitCompleted;
3032

3133
private IPSConsoleReadLineMockableMethods _mockableMethods;
3234

3335
private EngineIntrinsics _engineIntrinsics;
34-
private static readonly GCHandle _breakHandlerGcHandle;
36+
private static GCHandle _breakHandlerGcHandle;
3537
private Thread _readKeyThread;
3638
private AutoResetEvent _readKeyWaitHandle;
3739
private AutoResetEvent _keyReadWaitHandle;
@@ -51,9 +53,10 @@ public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods
5153
private bool _inputAccepted;
5254
private readonly Queue<ConsoleKeyInfo> _queuedKeys;
5355
private Stopwatch _lastRenderTime;
56+
private static Stopwatch _readkeyStopwatch = new Stopwatch();
5457

5558
// Save a fixed # of keys so we can reconstruct a repro after a crash
56-
private readonly static HistoryQueue<ConsoleKeyInfo> _lastNKeys;
59+
private readonly static HistoryQueue<ConsoleKeyInfo> _lastNKeys = new HistoryQueue<ConsoleKeyInfo>(200);
5760

5861
// Tokens etc.
5962
private Token[] _tokens;
@@ -74,32 +77,41 @@ bool IPSConsoleReadLineMockableMethods.KeyAvailable()
7477
return Console.KeyAvailable;
7578
}
7679

80+
bool IPSConsoleReadLineMockableMethods.RunspaceIsRemote(Runspace runspace)
81+
{
82+
return runspace != null && runspace.ConnectionInfo != null;
83+
}
84+
85+
private void ReadOneOrMoreKeys()
86+
{
87+
_readkeyStopwatch.Restart();
88+
while (_mockableMethods.KeyAvailable())
89+
{
90+
_queuedKeys.Enqueue(_mockableMethods.ReadKey());
91+
if (_readkeyStopwatch.ElapsedMilliseconds > 2)
92+
{
93+
// Don't spend too long in this loop if there are lots of queued keys
94+
break;
95+
}
96+
}
97+
98+
if (_queuedKeys.Count == 0)
99+
{
100+
var key = _mockableMethods.ReadKey();
101+
_queuedKeys.Enqueue(key);
102+
}
103+
}
104+
77105
private void ReadKeyThreadProc()
78106
{
79-
var stopwatch = new Stopwatch();
80107
while (true)
81108
{
82109
// Wait until ReadKey tells us to read a key (or it's time to exit).
83110
int handleId = WaitHandle.WaitAny(_singleton._threadProcWaitHandles);
84111
if (handleId == 1) // It was the _closingWaitHandle that was signaled.
85112
break;
86113

87-
stopwatch.Restart();
88-
while (_mockableMethods.KeyAvailable())
89-
{
90-
_queuedKeys.Enqueue(_mockableMethods.ReadKey());
91-
if (stopwatch.ElapsedMilliseconds > 2)
92-
{
93-
// Don't spend too long in this loop if there are lots of queued keys
94-
break;
95-
}
96-
}
97-
98-
if (_queuedKeys.Count == 0)
99-
{
100-
var key = _mockableMethods.ReadKey();
101-
_queuedKeys.Enqueue(key);
102-
}
114+
ReadOneOrMoreKeys();
103115

104116
// One or more keys were read - let ReadKey know we're done.
105117
_keyReadWaitHandle.Set();
@@ -207,6 +219,7 @@ private static ConsoleKeyInfo ReadKey()
207219

208220
throw new OperationCanceledException();
209221
}
222+
210223
var key = _singleton._queuedKeys.Dequeue();
211224
return key;
212225
}
@@ -457,38 +470,6 @@ void ProcessOneKey(ConsoleKeyInfo key, Dictionary<ConsoleKeyInfo, KeyHandler> di
457470
}
458471
}
459472

460-
static PSConsoleReadLine()
461-
{
462-
_singleton = new PSConsoleReadLine();
463-
464-
_breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(_singleton.BreakHandler));
465-
NativeMethods.SetConsoleCtrlHandler((BreakHandler) _breakHandlerGcHandle.Target, true);
466-
_singleton._readKeyWaitHandle = new AutoResetEvent(false);
467-
_singleton._keyReadWaitHandle = new AutoResetEvent(false);
468-
_singleton._closingWaitHandle = new ManualResetEvent(false);
469-
_singleton._requestKeyWaitHandles = new WaitHandle[] { _singleton._keyReadWaitHandle, _singleton._closingWaitHandle };
470-
_singleton._threadProcWaitHandles = new WaitHandle[] { _singleton._readKeyWaitHandle, _singleton._closingWaitHandle };
471-
472-
// This is only used for post-mortem debugging - 200 keys should be enough to reconstruct most command lines.
473-
_lastNKeys = new HistoryQueue<ConsoleKeyInfo>(200);
474-
475-
// This is for a "being hosted in an alternate appdomain scenario" (the
476-
// DomainUnload event is not raised for the default appdomain). It allows us
477-
// to exit cleanly when the appdomain is unloaded but the process is not going
478-
// away.
479-
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
480-
{
481-
AppDomain.CurrentDomain.DomainUnload += (x, y) =>
482-
{
483-
_singleton._closingWaitHandle.Set();
484-
_singleton._readKeyThread.Join(); // may need to wait for history to be written
485-
};
486-
}
487-
488-
_singleton._readKeyThread = new Thread(_singleton.ReadKeyThreadProc) {IsBackground = true};
489-
_singleton._readKeyThread.Start();
490-
}
491-
492473
private PSConsoleReadLine()
493474
{
494475
_mockableMethods = this;
@@ -501,19 +482,23 @@ private PSConsoleReadLine()
501482
_queuedKeys = new Queue<ConsoleKeyInfo>();
502483

503484
string hostName = null;
504-
try
485+
// This works mostly by luck - we're not doing anything to guarantee the constructor for our
486+
// singleton is called on a thread with a runspace, but it is happening by coincidence.
487+
using (var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace))
505488
{
506-
var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)
507-
.AddCommand("Get-Variable").AddParameter("Name", "host").AddParameter("ValueOnly");
508-
var results = ps.Invoke();
509-
dynamic host = results.Count == 1 ? results[0] : null;
510-
if (host != null)
489+
try
490+
{
491+
ps.AddCommand("Get-Variable").AddParameter("Name", "host").AddParameter("ValueOnly");
492+
var results = ps.Invoke();
493+
dynamic host = results.Count == 1 ? results[0] : null;
494+
if (host != null)
495+
{
496+
hostName = host.Name as string;
497+
}
498+
}
499+
catch
511500
{
512-
hostName = host.Name as string;
513501
}
514-
}
515-
catch
516-
{
517502
}
518503
if (hostName == null)
519504
{
@@ -522,15 +507,17 @@ private PSConsoleReadLine()
522507
_options = new PSConsoleReadlineOptions(hostName);
523508
}
524509

525-
private void Initialize(Runspace remoteRunspace, EngineIntrinsics engineIntrinsics)
510+
private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics)
526511
{
512+
_engineIntrinsics = engineIntrinsics;
513+
_runspace = runspace;
514+
527515
if (!_delayedOneTimeInitCompleted)
528516
{
529517
DelayedOneTimeInitialize();
530518
_delayedOneTimeInitCompleted = true;
531519
}
532520

533-
_engineIntrinsics = engineIntrinsics;
534521
_buffer.Clear();
535522
_edits = new List<EditItem>();
536523
_undoEditIndex = 0;
@@ -553,7 +540,6 @@ private void Initialize(Runspace remoteRunspace, EngineIntrinsics engineIntrinsi
553540
_yankLastArgCommandCount = 0;
554541
_tabCommandCount = 0;
555542
_visualSelectionCommandCount = 0;
556-
_remoteRunspace = remoteRunspace;
557543
_statusIsErrorMessage = false;
558544

559545
_consoleBuffer = ReadBufferLines(_initialY, 1 + Options.ExtraPromptLineCount);
@@ -595,15 +581,68 @@ private void DelayedOneTimeInitialize()
595581
// specifies a custom history save file, we don't want to try reading
596582
// from the default one.
597583

584+
var historyCountVar = _engineIntrinsics.SessionState.PSVariable.Get("MaximumHistoryCount");
585+
if (historyCountVar != null && historyCountVar.Value is int)
586+
{
587+
_options.MaximumHistoryCount = (int)historyCountVar.Value;
588+
}
589+
598590
_historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName());
599591

600592
_history = new HistoryQueue<HistoryItem>(Options.MaximumHistoryCount);
601593
_currentHistoryIndex = 0;
602594

603-
ReadHistoryFile();
595+
bool readHistoryFile = true;
596+
try
597+
{
598+
if (_options.HistorySaveStyle == HistorySaveStyle.SaveNothing && Runspace.DefaultRunspace != null)
599+
{
600+
using (var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace))
601+
{
602+
ps.AddCommand("Microsoft.PowerShell.Core\\Get-History");
603+
foreach (var historyInfo in ps.Invoke<HistoryInfo>())
604+
{
605+
AddToHistory(historyInfo.CommandLine);
606+
}
607+
readHistoryFile = false;
608+
}
609+
}
610+
}
611+
catch
612+
{
613+
}
614+
615+
if (readHistoryFile)
616+
{
617+
ReadHistoryFile();
618+
}
604619

605620
_killIndex = -1; // So first add indexes 0.
606621
_killRing = new List<string>(Options.MaximumKillRingCount);
622+
623+
_breakHandlerGcHandle = GCHandle.Alloc(new BreakHandler(_singleton.BreakHandler));
624+
NativeMethods.SetConsoleCtrlHandler((BreakHandler)_breakHandlerGcHandle.Target, true);
625+
_singleton._readKeyWaitHandle = new AutoResetEvent(false);
626+
_singleton._keyReadWaitHandle = new AutoResetEvent(false);
627+
_singleton._closingWaitHandle = new ManualResetEvent(false);
628+
_singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle};
629+
_singleton._threadProcWaitHandles = new WaitHandle[] {_singleton._readKeyWaitHandle, _singleton._closingWaitHandle};
630+
631+
// This is for a "being hosted in an alternate appdomain scenario" (the
632+
// DomainUnload event is not raised for the default appdomain). It allows us
633+
// to exit cleanly when the appdomain is unloaded but the process is not going
634+
// away.
635+
if (!AppDomain.CurrentDomain.IsDefaultAppDomain())
636+
{
637+
AppDomain.CurrentDomain.DomainUnload += (x, y) =>
638+
{
639+
_singleton._closingWaitHandle.Set();
640+
_singleton._readKeyThread.Join(); // may need to wait for history to be written
641+
};
642+
}
643+
644+
_singleton._readKeyThread = new Thread(_singleton.ReadKeyThreadProc) {IsBackground = true};
645+
_singleton._readKeyThread.Start();
607646
}
608647

609648
[SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")]

UnitTestPSReadLine/UnitTestReadLine.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ public CommandCompletion CompleteInput(string input, int cursorIndex, Hashtable
7575
{
7676
return UnitTest.MockedCompleteInput(input, cursorIndex, options, powershell);
7777
}
78+
79+
public bool RunspaceIsRemote(Runspace runspace)
80+
{
81+
return false;
82+
}
7883
}
7984

8085
[TestClass]

0 commit comments

Comments
 (0)