/********************************************************************++ Copyright (c) Microsoft Corporation. All rights reserved. --********************************************************************/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; using System.Management.Automation; using System.Management.Automation.Host; using System.Management.Automation.Language; using System.Management.Automation.Runspaces; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using System.Threading; using Microsoft.PowerShell.Commands; using Microsoft.PowerShell.Internal; using Microsoft.PowerShell.PSReadLine; [module: SuppressMessage("Microsoft.Design", "CA1014:MarkAssembliesWithClsCompliant")] [module: SuppressMessage("Microsoft.Design", "CA1026:DefaultParametersShouldNotBeUsed")] namespace Microsoft.PowerShell { [SuppressMessage("Microsoft.Design", "CA1032:ImplementStandardExceptionConstructors")] [SuppressMessage("Microsoft.Usage", "CA2237:MarkISerializableTypesWithSerializable")] class ExitException : Exception { } public partial class PSConsoleReadLine : IPSConsoleReadLineMockableMethods { private const int ConsoleExiting = 1; private const int CancellationRequested = 2; // *must* be initialized in the static ctor // because the static member _clipboard depends upon it // for its own initialization private static readonly PSConsoleReadLine _singleton; private static readonly CancellationToken _defaultCancellationToken = new CancellationTokenSource().Token; // This is used by PowerShellEditorServices (the backend of the PowerShell VSCode extension) // so that it can call PSReadLine from a delegate and not hit nested pipeline issues. #pragma warning disable CS0649 private static Action _handleIdleOverride; #pragma warning restore CS0649 private bool _delayedOneTimeInitCompleted; private IPSConsoleReadLineMockableMethods _mockableMethods; private IConsole _console; private ICharMap _charMap; private Encoding _initialOutputEncoding; private bool _skipOutputEncodingChange; private EngineIntrinsics _engineIntrinsics; private Thread _readKeyThread; private AutoResetEvent _readKeyWaitHandle; private AutoResetEvent _keyReadWaitHandle; private CancellationToken _cancelReadCancellationToken; internal ManualResetEvent _closingWaitHandle; private WaitHandle[] _threadProcWaitHandles; private WaitHandle[] _requestKeyWaitHandles; private readonly StringBuilder _buffer; private readonly StringBuilder _statusBuffer; private bool _statusIsErrorMessage; private string _statusLinePrompt; private string _acceptedCommandLine; private List _edits; private int _editGroupStart; private int _undoEditIndex; private int _mark; private bool _inputAccepted; private readonly Queue _queuedKeys; private Stopwatch _lastRenderTime; private static readonly Stopwatch _readkeyStopwatch = new Stopwatch(); // Save a fixed # of keys so we can reconstruct a repro after a crash private static readonly HistoryQueue _lastNKeys = new HistoryQueue(200); // Tokens etc. private Token[] _tokens; private Ast _ast; private ParseError[] _parseErrors; bool IPSConsoleReadLineMockableMethods.RunspaceIsRemote(Runspace runspace) { return runspace?.ConnectionInfo != null; } private void ReadOneOrMoreKeys() { _readkeyStopwatch.Restart(); while (_console.KeyAvailable) { // _charMap is only guaranteed to accumulate input while KeyAvailable // returns false. Make sure to check KeyAvailable after every ProcessKey call, // and clear it in a loop in case the input was something like ^[[1 which can // be 3, 2, or part of 1 key depending on timing. _charMap.ProcessKey(_console.ReadKey()); while (_charMap.KeyAvailable) { var key = PSKeyInfo.FromConsoleKeyInfo(_charMap.ReadKey()); _lastNKeys.Enqueue(key); _queuedKeys.Enqueue(key); } if (_readkeyStopwatch.ElapsedMilliseconds > 2) { // Don't spend too long in this loop if there are lots of queued keys break; } } if (_queuedKeys.Count == 0) { while (!_charMap.KeyAvailable) { // Don't want to block when there is an escape sequence being read. if (_charMap.InEscapeSequence) { if (_console.KeyAvailable) { _charMap.ProcessKey(_console.ReadKey()); } else { // We don't want to sleep for the whole escape timeout // or the user will have a laggy console, but there's // nothing to block on at this point either, so do a // small sleep to yield the CPU while we're waiting // to decide what the input was. This will only run // if there are no keys waiting to be read. Thread.Sleep(5); } } else { _charMap.ProcessKey(_console.ReadKey()); } } while (_charMap.KeyAvailable) { var key = PSKeyInfo.FromConsoleKeyInfo(_charMap.ReadKey()); _lastNKeys.Enqueue(key); _queuedKeys.Enqueue(key); } } } private void ReadKeyThreadProc() { while (true) { // Wait until ReadKey tells us to read a key (or it's time to exit). int handleId = WaitHandle.WaitAny(_singleton._threadProcWaitHandles); if (handleId == 1) // It was the _closingWaitHandle that was signaled. break; var localCancellationToken = _singleton._cancelReadCancellationToken; ReadOneOrMoreKeys(); if (localCancellationToken.IsCancellationRequested) { continue; } // One or more keys were read - let ReadKey know we're done. _keyReadWaitHandle.Set(); } } internal static PSKeyInfo ReadKey() { // Reading a key is handled on a different thread. During process shutdown, // PowerShell will wait in it's ConsoleCtrlHandler until the pipeline has completed. // If we're running, we're most likely blocked waiting for user input. // This is a problem for two reasons. First, exiting takes a long time (5 seconds // on Win8) because PowerShell is waiting forever, but the OS will forcibly terminate // the console. Also - if there are any event handlers for the engine event // PowerShell.Exiting, those handlers won't get a chance to run. // // By waiting for a key on a different thread, our pipeline execution thread // (the thread ReadLine is called from) avoid being blocked in code that can't // be unblocked and instead blocks on events we control. // First, set an event so the thread to read a key actually attempts to read a key. _singleton._readKeyWaitHandle.Set(); int handleId; System.Management.Automation.PowerShell ps = null; try { while (true) { // Next, wait for one of three things: // - a key is pressed // - the console is exiting // - 300ms timeout - to process events if we're idle // - ReadLine cancellation is requested externally handleId = WaitHandle.WaitAny(_singleton._requestKeyWaitHandles, 300); if (handleId != WaitHandle.WaitTimeout) { break; } if (_handleIdleOverride is not null) { _handleIdleOverride(_singleton._cancelReadCancellationToken); continue; } // If we timed out, check for event subscribers (which is just // a hint that there might be an event waiting to be processed.) var eventSubscribers = _singleton._engineIntrinsics?.Events.Subscribers; if (eventSubscribers?.Count > 0) { bool runPipelineForEventProcessing = false; foreach (var sub in eventSubscribers) { if (sub.SourceIdentifier.Equals(PSEngineEvent.OnIdle, StringComparison.OrdinalIgnoreCase)) { // If the buffer is not empty, let's not consider we are idle because the user is in the middle of typing something. if (_singleton._buffer.Length > 0) { continue; } // There is an OnIdle event subscriber and we are idle because we timed out and the buffer is empty. // Normally PowerShell generates this event, but PowerShell assumes the engine is not idle because // it called PSConsoleHostReadLine which isn't returning. So we generate the event instead. runPipelineForEventProcessing = true; _singleton._engineIntrinsics.Events.GenerateEvent(PSEngineEvent.OnIdle, null, null, null); // Break out so we don't genreate more than one 'OnIdle' event for a timeout. break; } runPipelineForEventProcessing = true; } // If there are any event subscribers, run a tiny useless PowerShell pipeline // so that the events can be processed. if (runPipelineForEventProcessing) { if (ps == null) { ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); ps.AddScript("0", useLocalScope: true); } // To detect output during possible event processing, see if the cursor moved // and rerender if so. var console = _singleton._console; var y = console.CursorTop; ps.Invoke(); if (y != console.CursorTop) { _singleton._initialY = console.CursorTop; _singleton.Render(); } } } } } finally { ps?.Dispose(); } if (handleId == ConsoleExiting) { // The console is exiting - throw an exception to unwind the stack to the point // where we can return from ReadLine. if (_singleton.Options.HistorySaveStyle == HistorySaveStyle.SaveAtExit) { _singleton.SaveHistoryAtExit(); } _singleton._historyFileMutex.Dispose(); throw new OperationCanceledException(); } if (handleId == CancellationRequested) { // ReadLine was cancelled. Save the current line to be restored next time ReadLine // is called, clear the buffer and throw an exception so we can return an empty string. _singleton.SaveCurrentLine(); _singleton._getNextHistoryIndex = _singleton._history.Count; _singleton._current = 0; _singleton._buffer.Clear(); _singleton.Render(); throw new OperationCanceledException(); } var key = _singleton._queuedKeys.Dequeue(); return key; } private void PrependQueuedKeys(PSKeyInfo key) { if (_queuedKeys.Count > 0) { // This should almost never happen so being inefficient is fine. var list = new List(_queuedKeys); _queuedKeys.Clear(); _queuedKeys.Enqueue(key); list.ForEach(k => _queuedKeys.Enqueue(k)); } else { _queuedKeys.Enqueue(key); } } /// /// Entry point - called from the PowerShell function PSConsoleHostReadLine /// after the prompt has been displayed. /// /// The complete command line. public static string ReadLine(Runspace runspace, EngineIntrinsics engineIntrinsics, bool? lastRunStatus) { // Use a default cancellation token instead of CancellationToken.None because the // WaitHandle is shared and could be triggered accidently. return ReadLine(runspace, engineIntrinsics, _defaultCancellationToken, lastRunStatus); } /// /// Entry point - called by custom PSHost implementations that require the /// ability to cancel ReadLine. /// /// The complete command line. public static string ReadLine( Runspace runspace, EngineIntrinsics engineIntrinsics, CancellationToken cancellationToken, bool? lastRunStatus) { var console = _singleton._console; if (Console.IsInputRedirected || Console.IsOutputRedirected) { // System.Console doesn't handle redirected input. It matches the behavior on Windows // by throwing an "InvalidOperationException". // Therefore, if either stdin or stdout is redirected, PSReadLine doesn't really work, // so throw and let PowerShell call Console.ReadLine or do whatever else it decides to do. // // Some CI environments redirect stdin/stdout, but that doesn't affect our test runs // because the console is mocked, so we can skip the exception. if (!IsRunningCI(console)) { throw new NotSupportedException(); } } var oldControlCAsInput = false; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { PlatformWindows.Init(ref _singleton._charMap); } else { try { oldControlCAsInput = Console.TreatControlCAsInput; Console.TreatControlCAsInput = true; } catch {} } if (lastRunStatus.HasValue) { _singleton.ReportExecutionStatus(lastRunStatus.Value); } bool firstTime = true; while (true) { try { if (firstTime) { firstTime = false; _singleton.Initialize(runspace, engineIntrinsics); } _singleton._cancelReadCancellationToken = cancellationToken; _singleton._requestKeyWaitHandles[2] = _singleton._cancelReadCancellationToken.WaitHandle; return _singleton.InputLoop(); } catch (OperationCanceledException) { // Console is either exiting or the cancellation of ReadLine has been requested // by a custom PSHost implementation. return ""; } catch (ExitException) { return "exit"; } catch (CustomHandlerException e) { var oldColor = console.ForegroundColor; console.ForegroundColor = ConsoleColor.Red; console.WriteLine( string.Format(CultureInfo.CurrentUICulture, PSReadLineResources.OopsCustomHandlerException, e.InnerException.Message)); console.ForegroundColor = oldColor; var lineBeforeCrash = _singleton._buffer.ToString(); _singleton.Initialize(runspace, _singleton._engineIntrinsics); InvokePrompt(); Insert(lineBeforeCrash); } catch (Exception e) { // If we're running tests, just throw. if (_singleton._mockableMethods != _singleton) { throw; } while (e.InnerException != null) { e = e.InnerException; } var oldColor = console.ForegroundColor; console.ForegroundColor = ConsoleColor.Red; console.WriteLine(PSReadLineResources.OopsAnErrorMessage1); console.ForegroundColor = oldColor; var sb = new StringBuilder(); for (int i = 0; i < _lastNKeys.Count; i++) { sb.Append(' '); sb.Append(_lastNKeys[i].KeyStr); if (_singleton._dispatchTable.TryGetValue(_lastNKeys[i], out var handler) && "AcceptLine".Equals(handler.BriefDescription, StringComparison.OrdinalIgnoreCase)) { // Make it a little easier to see the keys sb.Append('\n'); } } var psVersion = PSObject.AsPSObject(engineIntrinsics.Host.Version).ToString(); var ourVersion = typeof(PSConsoleReadLine).Assembly.GetCustomAttributes().First().InformationalVersion; var osInfo = RuntimeInformation.OSDescription; var bufferWidth = console.BufferWidth; var bufferHeight = console.BufferHeight; console.WriteLine(string.Format(CultureInfo.CurrentUICulture, PSReadLineResources.OopsAnErrorMessage2, ourVersion, psVersion, osInfo, bufferWidth, bufferHeight, _lastNKeys.Count, sb, e)); var lineBeforeCrash = _singleton._buffer.ToString(); _singleton.Initialize(runspace, _singleton._engineIntrinsics); InvokePrompt(); Insert(lineBeforeCrash); } finally { try { // If we are closing, restoring the old console settings isn't needed, // and some operating systems, it can cause a hang. if (!_singleton._closingWaitHandle.WaitOne(0)) { console.OutputEncoding = _singleton._initialOutputEncoding; bool IsValid(ConsoleColor color) { return color >= ConsoleColor.Black && color <= ConsoleColor.White; } if (IsValid(_singleton._initialForeground)) { console.ForegroundColor = _singleton._initialForeground; } if (IsValid(_singleton._initialBackground)) { console.BackgroundColor = _singleton._initialBackground; } if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Console.TreatControlCAsInput = oldControlCAsInput; } } } catch { } if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { PlatformWindows.Complete(); } } } } private string InputLoop() { while (true) { var killCommandCount = _killCommandCount; var yankCommandCount = _yankCommandCount; var tabCommandCount = _tabCommandCount; var searchHistoryCommandCount = _searchHistoryCommandCount; var recallHistoryCommandCount = _recallHistoryCommandCount; var anyHistoryCommandCount = _anyHistoryCommandCount; var yankLastArgCommandCount = _yankLastArgCommandCount; var visualSelectionCommandCount = _visualSelectionCommandCount; var moveToLineCommandCount = _moveToLineCommandCount; var moveToEndOfLineCommandCount = _moveToEndOfLineCommandCount; var key = ReadKey(); ProcessOneKey(key, _dispatchTable, ignoreIfNoAction: false, arg: null); if (_inputAccepted) { _acceptedCommandLine = _buffer.ToString(); MaybeAddToHistory(_acceptedCommandLine, _edits, _undoEditIndex); _prediction.OnCommandLineAccepted(_acceptedCommandLine); return _acceptedCommandLine; } if (killCommandCount == _killCommandCount) { // Reset kill command count if it didn't change _killCommandCount = 0; } if (yankCommandCount == _yankCommandCount) { // Reset yank command count if it didn't change _yankCommandCount = 0; } if (yankLastArgCommandCount == _yankLastArgCommandCount) { // Reset yank last arg command count if it didn't change _yankLastArgCommandCount = 0; _yankLastArgState = null; } if (tabCommandCount == _tabCommandCount) { // Reset tab command count if it didn't change _tabCommandCount = 0; _tabCompletions = null; } if (searchHistoryCommandCount == _searchHistoryCommandCount) { if (_searchHistoryCommandCount > 0) { _emphasisStart = -1; _emphasisLength = 0; RenderWithPredictionQueryPaused(); } _searchHistoryCommandCount = 0; _searchHistoryPrefix = null; } if (recallHistoryCommandCount == _recallHistoryCommandCount) { _recallHistoryCommandCount = 0; } if (anyHistoryCommandCount == _anyHistoryCommandCount) { if (_anyHistoryCommandCount > 0) { ClearSavedCurrentLine(); _hashedHistory = null; _currentHistoryIndex = _history.Count; } _anyHistoryCommandCount = 0; } if (visualSelectionCommandCount == _visualSelectionCommandCount && _visualSelectionCommandCount > 0) { _visualSelectionCommandCount = 0; RenderWithPredictionQueryPaused(); // Clears the visual selection } if (moveToLineCommandCount == _moveToLineCommandCount) { _moveToLineCommandCount = 0; if (InViCommandMode() && moveToEndOfLineCommandCount == _moveToEndOfLineCommandCount) { // the previous command was neither a "move to end of line" command // nor a "move to line" command. In that case, the desired column // number will be computed from the current position on the logical line. _moveToEndOfLineCommandCount = 0; _moveToLineDesiredColumn = -1; } } } } T CallPossibleExternalApplication(Func func) { try { _console.OutputEncoding = _initialOutputEncoding; return RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? PlatformWindows.CallPossibleExternalApplication(func) : func(); } finally { if (!_skipOutputEncodingChange) { _console.OutputEncoding = Encoding.UTF8; } } } void CallPossibleExternalApplication(Action action) { CallPossibleExternalApplication(() => { action(); return null; }); } void ProcessOneKey(PSKeyInfo key, Dictionary dispatchTable, bool ignoreIfNoAction, object arg) { var consoleKey = key.AsConsoleKeyInfo(); // Our dispatch tables are built as much as possible in a portable way, so for example, // we avoid depending on scan codes like ConsoleKey.Oem6 and instead look at the // PSKeyInfo.Key. We also want to ignore the shift state as that may differ on // different keyboard layouts. // // That said, we first look up exactly what we get from Console.ReadKey - that will fail // most of the time, and when it does, we normalize the key. if (!dispatchTable.TryGetValue(key, out var handler)) { // If we see a control character where Ctrl wasn't used but shift was, treat that like // shift hadn't be pressed. This cleanly allows Shift+Backspace without adding a key binding. if (key.Shift && !key.Control && !key.Alt) { var c = consoleKey.KeyChar; if (c != '\0' && char.IsControl(c)) { key = PSKeyInfo.From(consoleKey.Key); dispatchTable.TryGetValue(key, out handler); } } } if (handler != null) { if (handler.ScriptBlock != null) { CallPossibleExternalApplication(() => handler.Action(consoleKey, arg)); } else { handler.Action(consoleKey, arg); } } else if (!ignoreIfNoAction) { SelfInsert(consoleKey, arg); } } static PSConsoleReadLine() { _singleton = new PSConsoleReadLine(); _viRegister = new ViRegister(_singleton); } private PSConsoleReadLine() { _mockableMethods = this; _console = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? PlatformWindows.OneTimeInit(this) : new VirtualTerminal(); _charMap = new DotNetCharMap(); _buffer = new StringBuilder(8 * 1024); _statusBuffer = new StringBuilder(256); _savedCurrentLine = new HistoryItem(); _queuedKeys = new Queue(); string hostName = null; // This works mostly by luck - we're not doing anything to guarantee the constructor for our // singleton is called on a thread with a runspace, but it is happening by coincidence. using (var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) { try { var results = ps.AddScript("$Host", useLocalScope: true).Invoke(); PSHost host = results.Count == 1 ? results[0] : null; hostName = host?.Name; } catch { } } if (hostName == null) { hostName = PSReadLine; } _options = new PSConsoleReadLineOptions(hostName); _prediction = new Prediction(this); SetDefaultBindings(_options.EditMode); } private void Initialize(Runspace runspace, EngineIntrinsics engineIntrinsics) { _engineIntrinsics = engineIntrinsics; _runspace = runspace; if (!_delayedOneTimeInitCompleted) { DelayedOneTimeInitialize(); _delayedOneTimeInitCompleted = true; } _previousRender = _initialPrevRender; _previousRender.bufferWidth = _console.BufferWidth; _previousRender.bufferHeight = _console.BufferHeight; _previousRender.errorPrompt = false; _buffer.Clear(); _edits = new List(); _undoEditIndex = 0; _editGroupStart = -1; _current = 0; _mark = 0; _emphasisStart = -1; _emphasisLength = 0; _ast = null; _tokens = null; _parseErrors = null; _inputAccepted = false; _initialX = _console.CursorLeft; _initialY = _console.CursorTop; _initialForeground = _console.ForegroundColor; _initialBackground = _console.BackgroundColor; _statusIsErrorMessage = false; _initialOutputEncoding = _console.OutputEncoding; _prediction.Reset(); // Don't change the OutputEncoding if already UTF8, no console, or using raster font on Windows _skipOutputEncodingChange = _initialOutputEncoding == Encoding.UTF8 || (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && PlatformWindows.IsConsoleInput() && PlatformWindows.IsUsingRasterFont()); if (!_skipOutputEncodingChange) { _console.OutputEncoding = Encoding.UTF8; } _lastRenderTime = Stopwatch.StartNew(); _killCommandCount = 0; _yankCommandCount = 0; _yankLastArgCommandCount = 0; _tabCommandCount = 0; _recallHistoryCommandCount = 0; _anyHistoryCommandCount = 0; _visualSelectionCommandCount = 0; _hashedHistory = null; if (_getNextHistoryIndex > 0) { _currentHistoryIndex = _getNextHistoryIndex; UpdateFromHistory(HistoryMoveCursor.ToEnd); _getNextHistoryIndex = 0; if (_searchHistoryCommandCount > 0) { _searchHistoryPrefix = ""; if (Options.HistoryNoDuplicates) { _hashedHistory = new Dictionary(); } } } else { _currentHistoryIndex = _history.Count; _searchHistoryCommandCount = 0; } if (_previousHistoryItem != null) { _previousHistoryItem.ApproximateElapsedTime = DateTime.UtcNow - _previousHistoryItem.StartTime; } } private void DelayedOneTimeInitialize() { // Delayed initialization is needed so that options can be set // after the constuctor but have an affect before the user starts // editing their first command line. For example, if the user // specifies a custom history save file, we don't want to try reading // from the default one. if (_options.MaximumHistoryCount == 0) { // Initialize 'MaximumHistoryCount' if it's not defined in user's profile. var historyCountVar = _engineIntrinsics?.SessionState.PSVariable.Get("MaximumHistoryCount"); _options.MaximumHistoryCount = (historyCountVar?.Value is int historyCountValue) ? historyCountValue : PSConsoleReadLineOptions.DefaultMaximumHistoryCount; } if (_options.PromptText == null && _engineIntrinsics?.InvokeCommand.GetCommand("prompt", CommandTypes.Function) is FunctionInfo promptCommand) { var promptIsPure = null == promptCommand.ScriptBlock.Ast.Find(ast => ast is CommandAst || ast is InvokeMemberExpressionAst, searchNestedScriptBlocks: true); if (promptIsPure) { var res = promptCommand.ScriptBlock.InvokeReturnAsIs(Array.Empty()); string evaluatedPrompt = res as string; if (evaluatedPrompt == null && res is PSObject psobject) { evaluatedPrompt = psobject.BaseObject as string; } if (evaluatedPrompt != null) { int i; for (i = evaluatedPrompt.Length - 1; i >= 0; i--) { if (!char.IsWhiteSpace(evaluatedPrompt[i])) break; } if (i >= 0) { _options.PromptText = new [] { evaluatedPrompt.Substring(i) }; } } } } _historyFileMutex = new Mutex(false, GetHistorySaveFileMutexName()); _history = new HistoryQueue(Options.MaximumHistoryCount); _recentHistory = new HistoryQueue(capacity: 5); _currentHistoryIndex = 0; bool readHistoryFile = true; try { if (_options.HistorySaveStyle == HistorySaveStyle.SaveNothing && Runspace.DefaultRunspace != null) { using (var ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace)) { ps.AddCommand("Microsoft.PowerShell.Core\\Get-History"); foreach (var historyInfo in ps.Invoke()) { AddToHistory(historyInfo.CommandLine); } readHistoryFile = false; } } } catch { } if (readHistoryFile) { ReadHistoryFile(); } _killIndex = -1; // So first add indexes 0. _killRing = new List(Options.MaximumKillRingCount); _singleton._readKeyWaitHandle = new AutoResetEvent(false); _singleton._keyReadWaitHandle = new AutoResetEvent(false); _singleton._closingWaitHandle = new ManualResetEvent(false); _singleton._requestKeyWaitHandles = new WaitHandle[] {_singleton._keyReadWaitHandle, _singleton._closingWaitHandle, _defaultCancellationToken.WaitHandle}; _singleton._threadProcWaitHandles = new WaitHandle[] {_singleton._readKeyWaitHandle, _singleton._closingWaitHandle}; // This is for a "being hosted in an alternate appdomain scenario" (the // DomainUnload event is not raised for the default appdomain). It allows us // to exit cleanly when the appdomain is unloaded but the process is not going // away. if (!AppDomain.CurrentDomain.IsDefaultAppDomain()) { AppDomain.CurrentDomain.DomainUnload += (x, y) => { _singleton._closingWaitHandle.Set(); _singleton._readKeyThread.Join(); // may need to wait for history to be written }; } _singleton._readKeyThread = new Thread(_singleton.ReadKeyThreadProc) {IsBackground = true, Name = "PSReadLine ReadKey Thread"}; _singleton._readKeyThread.Start(); } private static void Chord(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue) { throw new ArgumentNullException(nameof(key)); } if (_singleton._chordDispatchTable.TryGetValue(PSKeyInfo.FromConsoleKeyInfo(key.Value), out var secondKeyDispatchTable)) { var secondKey = ReadKey(); _singleton.ProcessOneKey(secondKey, secondKeyDispatchTable, ignoreIfNoAction: true, arg: arg); } } /// /// Abort current action, e.g. incremental history search. /// public static void Abort(ConsoleKeyInfo? key = null, object arg = null) { } /// /// Start a new digit argument to pass to other functions. /// public static void DigitArgument(ConsoleKeyInfo? key = null, object arg = null) { if (!key.HasValue || char.IsControl(key.Value.KeyChar)) { Ding(); return; } if (_singleton._options.EditMode == EditMode.Vi && key.Value.KeyChar == '0') { BeginningOfLine(); return; } bool sawDigit = false; _singleton._statusLinePrompt = "digit-argument: "; var argBuffer = _singleton._statusBuffer; argBuffer.Append(key.Value.KeyChar); if (key.Value.KeyChar == '-') { argBuffer.Append('1'); } else { sawDigit = true; } _singleton.RenderWithPredictionQueryPaused(); // Render prompt while (true) { var nextKey = ReadKey(); if (_singleton._dispatchTable.TryGetValue(nextKey, out var handler)) { if (handler.Action == DigitArgument) { if (nextKey.KeyChar == '-') { if (argBuffer[0] == '-') { argBuffer.Remove(0, 1); } else { argBuffer.Insert(0, '-'); } _singleton.RenderWithPredictionQueryPaused(); // Render prompt continue; } if (nextKey.KeyChar >= '0' && nextKey.KeyChar <= '9') { if (!sawDigit && argBuffer.Length > 0) { // Buffer is either '-1' or '1' from one or more Alt+- keys // but no digits yet. Remove the '1'. argBuffer.Length -= 1; } sawDigit = true; argBuffer.Append(nextKey.KeyChar); _singleton.RenderWithPredictionQueryPaused(); // Render prompt continue; } } else if (handler.Action == Abort || handler.Action == CancelLine || handler.Action == CopyOrCancelLine) { break; } } if (int.TryParse(argBuffer.ToString(), out var intArg)) { _singleton.ProcessOneKey(nextKey, _singleton._dispatchTable, ignoreIfNoAction: false, arg: intArg); } else { Ding(); } break; } // Remove our status line argBuffer.Clear(); _singleton.ClearStatusMessage(render: true); } /// /// Erases the current prompt and calls the prompt function to redisplay /// the prompt. Useful for custom key handlers that change state, e.g. /// change the current directory. /// public static void InvokePrompt(ConsoleKeyInfo? key = null, object arg = null) { var console = _singleton._console; console.CursorVisible = false; if (arg is int newY) { console.SetCursorPosition(0, newY); } else { newY = _singleton._initialY - _singleton._options.ExtraPromptLineCount; console.SetCursorPosition(0, newY); // We need to rewrite the prompt, so blank out everything from a previous prompt invocation // in case the next one is shorter. var spaces = Spaces(console.BufferWidth); for (int i = 0; i < _singleton._options.ExtraPromptLineCount + 1; i++) { console.Write(spaces); } console.SetCursorPosition(0, newY); } string newPrompt = GetPrompt(); console.Write(newPrompt); _singleton._initialX = console.CursorLeft; _singleton._initialY = console.CursorTop; _singleton._previousRender = _initialPrevRender; _singleton.Render(); console.CursorVisible = true; } private static string GetPrompt() { string newPrompt = null; try { if (_singleton._runspace?.Debugger != null && _singleton._runspace.Debugger.InBreakpoint) { // Run prompt command in debugger API to ensure it is run correctly on the runspace. // This handles remote runspace debugging and nested debugger scenarios. PSDataCollection results = new PSDataCollection(); var command = new PSCommand(); command.AddCommand("prompt"); _singleton._runspace.Debugger.ProcessCommand( command, results); if (results.Count == 1) newPrompt = results[0].BaseObject as string; } else { var runspaceIsRemote = _singleton._mockableMethods.RunspaceIsRemote(_singleton._runspace); System.Management.Automation.PowerShell ps; if (!runspaceIsRemote) { ps = System.Management.Automation.PowerShell.Create(RunspaceMode.CurrentRunspace); } else { ps = System.Management.Automation.PowerShell.Create(); ps.Runspace = _singleton._runspace; } using (ps) { ps.AddCommand("prompt"); var result = ps.Invoke(); if (result.Count == 1) { newPrompt = result[0]; if (runspaceIsRemote) { if (!string.IsNullOrEmpty(_singleton._runspace?.ConnectionInfo?.ComputerName)) { newPrompt = "[" + (_singleton._runspace?.ConnectionInfo).ComputerName + "]: " + newPrompt; } } } } } } catch { // Catching all exceptions makes debugging problems a bit harder, but it avoids some noise if // the remote doesn't define a prompt. } if (string.IsNullOrEmpty(newPrompt)) newPrompt = "PS>"; return newPrompt; } internal static bool IsRunningCI(IConsole console) { Type consoleType = console.GetType(); return consoleType.FullName == "Test.TestConsole" || consoleType.BaseType.FullName == "Test.TestConsole"; } } }