NetEvents.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695
  1. #nullable enable
  2. using System.Diagnostics.CodeAnalysis;
  3. namespace Terminal.Gui;
  4. internal class NetEvents : IDisposable
  5. {
  6. private readonly ManualResetEventSlim _inputReady = new (false);
  7. private CancellationTokenSource? _inputReadyCancellationTokenSource;
  8. private readonly Queue<InputResult> _inputQueue = new ();
  9. private readonly IConsoleDriver _consoleDriver;
  10. private ConsoleKeyInfo []? _cki;
  11. private bool _isEscSeq;
  12. #if PROCESS_REQUEST
  13. bool _neededProcessRequest;
  14. #endif
  15. public NetEvents (IConsoleDriver consoleDriver)
  16. {
  17. _consoleDriver = consoleDriver ?? throw new ArgumentNullException (nameof (consoleDriver));
  18. _inputReadyCancellationTokenSource = new ();
  19. Task.Run (ProcessInputQueue, _inputReadyCancellationTokenSource.Token);
  20. Task.Run (CheckWindowSizeChange, _inputReadyCancellationTokenSource.Token);
  21. }
  22. public InputResult? DequeueInput ()
  23. {
  24. while (_inputReadyCancellationTokenSource is { Token.IsCancellationRequested: false })
  25. {
  26. try
  27. {
  28. if (!_inputReadyCancellationTokenSource.Token.IsCancellationRequested)
  29. {
  30. if (_inputQueue.Count == 0)
  31. {
  32. _inputReady.Wait (_inputReadyCancellationTokenSource.Token);
  33. }
  34. }
  35. if (_inputQueue.Count > 0)
  36. {
  37. return _inputQueue.Dequeue ();
  38. }
  39. }
  40. catch (OperationCanceledException)
  41. {
  42. return null;
  43. }
  44. finally
  45. {
  46. if (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
  47. {
  48. _inputReady.Reset ();
  49. }
  50. }
  51. #if PROCESS_REQUEST
  52. _neededProcessRequest = false;
  53. #endif
  54. }
  55. return null;
  56. }
  57. private ConsoleKeyInfo ReadConsoleKeyInfo (CancellationToken cancellationToken, bool intercept = true)
  58. {
  59. while (!cancellationToken.IsCancellationRequested)
  60. {
  61. // if there is a key available, return it without waiting
  62. // (or dispatching work to the thread queue)
  63. if (Console.KeyAvailable)
  64. {
  65. return Console.ReadKey (intercept);
  66. }
  67. // The delay must be here because it may have a request response after a while
  68. // In WSL it takes longer for keys to be available.
  69. Task.Delay (100, cancellationToken).Wait (cancellationToken);
  70. }
  71. cancellationToken.ThrowIfCancellationRequested ();
  72. return default (ConsoleKeyInfo);
  73. }
  74. private void ProcessInputQueue ()
  75. {
  76. while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
  77. {
  78. try
  79. {
  80. ConsoleKey key = 0;
  81. ConsoleModifiers mod = 0;
  82. ConsoleKeyInfo newConsoleKeyInfo = default;
  83. while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
  84. {
  85. ConsoleKeyInfo consoleKeyInfo;
  86. try
  87. {
  88. consoleKeyInfo = ReadConsoleKeyInfo (_inputReadyCancellationTokenSource.Token);
  89. }
  90. catch (OperationCanceledException)
  91. {
  92. return;
  93. }
  94. var ckiAlreadyResized = false;
  95. if (EscSeqUtils.IncompleteCkInfos is { })
  96. {
  97. ckiAlreadyResized = true;
  98. _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
  99. _cki = EscSeqUtils.InsertArray (EscSeqUtils.IncompleteCkInfos, _cki);
  100. EscSeqUtils.IncompleteCkInfos = null;
  101. if (_cki.Length > 1 && _cki [0].KeyChar == '\u001B')
  102. {
  103. _isEscSeq = true;
  104. }
  105. }
  106. if ((consoleKeyInfo.KeyChar == (char)KeyCode.Esc && !_isEscSeq)
  107. || (consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq))
  108. {
  109. if (_cki is null && consoleKeyInfo.KeyChar != (char)KeyCode.Esc && _isEscSeq)
  110. {
  111. _cki = EscSeqUtils.ResizeArray (
  112. new (
  113. (char)KeyCode.Esc,
  114. 0,
  115. false,
  116. false,
  117. false
  118. ),
  119. _cki
  120. );
  121. }
  122. _isEscSeq = true;
  123. if ((_cki is { } && _cki [^1].KeyChar != Key.Esc && consoleKeyInfo.KeyChar != Key.Esc && consoleKeyInfo.KeyChar <= Key.Space)
  124. || (_cki is { } && _cki [^1].KeyChar != '\u001B' && consoleKeyInfo.KeyChar == 127)
  125. || (_cki is { }
  126. && char.IsLetter (_cki [^1].KeyChar)
  127. && char.IsLower (consoleKeyInfo.KeyChar)
  128. && char.IsLetter (consoleKeyInfo.KeyChar))
  129. || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsLetterOrDigit (consoleKeyInfo.KeyChar))
  130. || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsPunctuation (consoleKeyInfo.KeyChar))
  131. || (_cki is { Length: > 2 } && char.IsLetter (_cki [^1].KeyChar) && char.IsSymbol (consoleKeyInfo.KeyChar)))
  132. {
  133. ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
  134. _cki = null;
  135. _isEscSeq = false;
  136. ProcessMapConsoleKeyInfo (consoleKeyInfo);
  137. }
  138. else
  139. {
  140. newConsoleKeyInfo = consoleKeyInfo;
  141. if (!ckiAlreadyResized)
  142. {
  143. _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
  144. }
  145. if (Console.KeyAvailable)
  146. {
  147. continue;
  148. }
  149. ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki!, ref mod);
  150. _cki = null;
  151. _isEscSeq = false;
  152. }
  153. break;
  154. }
  155. if (consoleKeyInfo.KeyChar == (char)KeyCode.Esc && _isEscSeq && _cki is { })
  156. {
  157. ProcessRequestResponse (ref newConsoleKeyInfo, ref key, _cki, ref mod);
  158. _cki = null;
  159. if (Console.KeyAvailable)
  160. {
  161. _cki = EscSeqUtils.ResizeArray (consoleKeyInfo, _cki);
  162. }
  163. else
  164. {
  165. ProcessMapConsoleKeyInfo (consoleKeyInfo);
  166. }
  167. break;
  168. }
  169. ProcessMapConsoleKeyInfo (consoleKeyInfo);
  170. break;
  171. }
  172. if (_inputQueue.Count > 0)
  173. {
  174. _inputReady.Set ();
  175. }
  176. }
  177. catch (OperationCanceledException)
  178. {
  179. return;
  180. }
  181. }
  182. void ProcessMapConsoleKeyInfo (ConsoleKeyInfo consoleKeyInfo)
  183. {
  184. _inputQueue.Enqueue (
  185. new ()
  186. {
  187. EventType = EventType.Key, ConsoleKeyInfo = EscSeqUtils.MapConsoleKeyInfo (consoleKeyInfo)
  188. }
  189. );
  190. _isEscSeq = false;
  191. }
  192. }
  193. private void CheckWindowSizeChange ()
  194. {
  195. void RequestWindowSize (CancellationToken cancellationToken)
  196. {
  197. while (!cancellationToken.IsCancellationRequested)
  198. {
  199. // Wait for a while then check if screen has changed sizes
  200. Task.Delay (500, cancellationToken).Wait (cancellationToken);
  201. int buffHeight, buffWidth;
  202. if (((NetDriver)_consoleDriver).IsWinPlatform)
  203. {
  204. buffHeight = Math.Max (Console.BufferHeight, 0);
  205. buffWidth = Math.Max (Console.BufferWidth, 0);
  206. }
  207. else
  208. {
  209. buffHeight = _consoleDriver.Rows;
  210. buffWidth = _consoleDriver.Cols;
  211. }
  212. if (EnqueueWindowSizeEvent (
  213. Math.Max (Console.WindowHeight, 0),
  214. Math.Max (Console.WindowWidth, 0),
  215. buffHeight,
  216. buffWidth
  217. ))
  218. {
  219. return;
  220. }
  221. }
  222. cancellationToken.ThrowIfCancellationRequested ();
  223. }
  224. while (_inputReadyCancellationTokenSource is { IsCancellationRequested: false })
  225. {
  226. try
  227. {
  228. RequestWindowSize (_inputReadyCancellationTokenSource.Token);
  229. if (_inputQueue.Count > 0)
  230. {
  231. _inputReady.Set ();
  232. }
  233. }
  234. catch (OperationCanceledException)
  235. {
  236. return;
  237. }
  238. }
  239. }
  240. /// <summary>Enqueue a window size event if the window size has changed.</summary>
  241. /// <param name="winHeight"></param>
  242. /// <param name="winWidth"></param>
  243. /// <param name="buffHeight"></param>
  244. /// <param name="buffWidth"></param>
  245. /// <returns></returns>
  246. private bool EnqueueWindowSizeEvent (int winHeight, int winWidth, int buffHeight, int buffWidth)
  247. {
  248. if (winWidth == _consoleDriver.Cols && winHeight == _consoleDriver.Rows)
  249. {
  250. return false;
  251. }
  252. int w = Math.Max (winWidth, 0);
  253. int h = Math.Max (winHeight, 0);
  254. _inputQueue.Enqueue (
  255. new ()
  256. {
  257. EventType = EventType.WindowSize, WindowSizeEvent = new () { Size = new (w, h) }
  258. }
  259. );
  260. return true;
  261. }
  262. // Process a CSI sequence received by the driver (key pressed, mouse event, or request/response event)
  263. private void ProcessRequestResponse (
  264. ref ConsoleKeyInfo newConsoleKeyInfo,
  265. ref ConsoleKey key,
  266. ConsoleKeyInfo [] cki,
  267. ref ConsoleModifiers mod
  268. )
  269. {
  270. // isMouse is true if it's CSI<, false otherwise
  271. EscSeqUtils.DecodeEscSeq (
  272. ref newConsoleKeyInfo,
  273. ref key,
  274. cki,
  275. ref mod,
  276. out string c1Control,
  277. out string code,
  278. out string [] values,
  279. out string terminating,
  280. out bool isMouse,
  281. out List<MouseFlags> mouseFlags,
  282. out Point pos,
  283. out bool isReq,
  284. (f, p) => HandleMouseEvent (MapMouseFlags (f), p)
  285. );
  286. if (isMouse)
  287. {
  288. foreach (MouseFlags mf in mouseFlags)
  289. {
  290. HandleMouseEvent (MapMouseFlags (mf), pos);
  291. }
  292. return;
  293. }
  294. if (isReq)
  295. {
  296. HandleRequestResponseEvent (c1Control, code, values, terminating);
  297. return;
  298. }
  299. if (newConsoleKeyInfo != default)
  300. {
  301. HandleKeyboardEvent (newConsoleKeyInfo);
  302. }
  303. }
  304. [UnconditionalSuppressMessage ("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "<Pending>")]
  305. private MouseButtonState MapMouseFlags (MouseFlags mouseFlags)
  306. {
  307. MouseButtonState mbs = default;
  308. foreach (object flag in Enum.GetValues (mouseFlags.GetType ()))
  309. {
  310. if (mouseFlags.HasFlag ((MouseFlags)flag))
  311. {
  312. switch (flag)
  313. {
  314. case MouseFlags.Button1Pressed:
  315. mbs |= MouseButtonState.Button1Pressed;
  316. break;
  317. case MouseFlags.Button1Released:
  318. mbs |= MouseButtonState.Button1Released;
  319. break;
  320. case MouseFlags.Button1Clicked:
  321. mbs |= MouseButtonState.Button1Clicked;
  322. break;
  323. case MouseFlags.Button1DoubleClicked:
  324. mbs |= MouseButtonState.Button1DoubleClicked;
  325. break;
  326. case MouseFlags.Button1TripleClicked:
  327. mbs |= MouseButtonState.Button1TripleClicked;
  328. break;
  329. case MouseFlags.Button2Pressed:
  330. mbs |= MouseButtonState.Button2Pressed;
  331. break;
  332. case MouseFlags.Button2Released:
  333. mbs |= MouseButtonState.Button2Released;
  334. break;
  335. case MouseFlags.Button2Clicked:
  336. mbs |= MouseButtonState.Button2Clicked;
  337. break;
  338. case MouseFlags.Button2DoubleClicked:
  339. mbs |= MouseButtonState.Button2DoubleClicked;
  340. break;
  341. case MouseFlags.Button2TripleClicked:
  342. mbs |= MouseButtonState.Button2TripleClicked;
  343. break;
  344. case MouseFlags.Button3Pressed:
  345. mbs |= MouseButtonState.Button3Pressed;
  346. break;
  347. case MouseFlags.Button3Released:
  348. mbs |= MouseButtonState.Button3Released;
  349. break;
  350. case MouseFlags.Button3Clicked:
  351. mbs |= MouseButtonState.Button3Clicked;
  352. break;
  353. case MouseFlags.Button3DoubleClicked:
  354. mbs |= MouseButtonState.Button3DoubleClicked;
  355. break;
  356. case MouseFlags.Button3TripleClicked:
  357. mbs |= MouseButtonState.Button3TripleClicked;
  358. break;
  359. case MouseFlags.WheeledUp:
  360. mbs |= MouseButtonState.ButtonWheeledUp;
  361. break;
  362. case MouseFlags.WheeledDown:
  363. mbs |= MouseButtonState.ButtonWheeledDown;
  364. break;
  365. case MouseFlags.WheeledLeft:
  366. mbs |= MouseButtonState.ButtonWheeledLeft;
  367. break;
  368. case MouseFlags.WheeledRight:
  369. mbs |= MouseButtonState.ButtonWheeledRight;
  370. break;
  371. case MouseFlags.Button4Pressed:
  372. mbs |= MouseButtonState.Button4Pressed;
  373. break;
  374. case MouseFlags.Button4Released:
  375. mbs |= MouseButtonState.Button4Released;
  376. break;
  377. case MouseFlags.Button4Clicked:
  378. mbs |= MouseButtonState.Button4Clicked;
  379. break;
  380. case MouseFlags.Button4DoubleClicked:
  381. mbs |= MouseButtonState.Button4DoubleClicked;
  382. break;
  383. case MouseFlags.Button4TripleClicked:
  384. mbs |= MouseButtonState.Button4TripleClicked;
  385. break;
  386. case MouseFlags.ButtonShift:
  387. mbs |= MouseButtonState.ButtonShift;
  388. break;
  389. case MouseFlags.ButtonCtrl:
  390. mbs |= MouseButtonState.ButtonCtrl;
  391. break;
  392. case MouseFlags.ButtonAlt:
  393. mbs |= MouseButtonState.ButtonAlt;
  394. break;
  395. case MouseFlags.ReportMousePosition:
  396. mbs |= MouseButtonState.ReportMousePosition;
  397. break;
  398. case MouseFlags.AllEvents:
  399. mbs |= MouseButtonState.AllEvents;
  400. break;
  401. }
  402. }
  403. }
  404. return mbs;
  405. }
  406. private Point _lastCursorPosition;
  407. private void HandleRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
  408. {
  409. if (terminating ==
  410. // BUGBUG: I can't find where we send a request for cursor position (ESC[?6n), so I'm not sure if this is needed.
  411. // The observation is correct because the response isn't immediate and this is useless
  412. EscSeqUtils.CSI_RequestCursorPositionReport_Terminator)
  413. {
  414. var point = new Point { X = int.Parse (values [1]) - 1, Y = int.Parse (values [0]) - 1 };
  415. if (_lastCursorPosition.Y != point.Y)
  416. {
  417. _lastCursorPosition = point;
  418. var eventType = EventType.WindowPosition;
  419. var winPositionEv = new WindowPositionEvent { CursorPosition = point };
  420. _inputQueue.Enqueue (
  421. new InputResult { EventType = eventType, WindowPositionEvent = winPositionEv }
  422. );
  423. }
  424. else
  425. {
  426. return;
  427. }
  428. }
  429. else if (terminating == EscSeqUtils.CSI_ReportTerminalSizeInChars_Terminator)
  430. {
  431. if (values [0] == EscSeqUtils.CSI_ReportTerminalSizeInChars_ResponseValue)
  432. {
  433. EnqueueWindowSizeEvent (
  434. Math.Max (int.Parse (values [1]), 0),
  435. Math.Max (int.Parse (values [2]), 0),
  436. Math.Max (int.Parse (values [1]), 0),
  437. Math.Max (int.Parse (values [2]), 0)
  438. );
  439. }
  440. else
  441. {
  442. EnqueueRequestResponseEvent (c1Control, code, values, terminating);
  443. }
  444. }
  445. else
  446. {
  447. EnqueueRequestResponseEvent (c1Control, code, values, terminating);
  448. }
  449. _inputReady.Set ();
  450. }
  451. private void EnqueueRequestResponseEvent (string c1Control, string code, string [] values, string terminating)
  452. {
  453. var eventType = EventType.RequestResponse;
  454. var requestRespEv = new RequestResponseEvent { ResultTuple = (c1Control, code, values, terminating) };
  455. _inputQueue.Enqueue (
  456. new InputResult { EventType = eventType, RequestResponseEvent = requestRespEv }
  457. );
  458. }
  459. private void HandleMouseEvent (MouseButtonState buttonState, Point pos)
  460. {
  461. var mouseEvent = new MouseEvent { Position = pos, ButtonState = buttonState };
  462. _inputQueue.Enqueue (
  463. new () { EventType = EventType.Mouse, MouseEvent = mouseEvent }
  464. );
  465. }
  466. public enum EventType
  467. {
  468. Key = 1,
  469. Mouse = 2,
  470. WindowSize = 3,
  471. WindowPosition = 4,
  472. RequestResponse = 5
  473. }
  474. [Flags]
  475. public enum MouseButtonState
  476. {
  477. Button1Pressed = 0x1,
  478. Button1Released = 0x2,
  479. Button1Clicked = 0x4,
  480. Button1DoubleClicked = 0x8,
  481. Button1TripleClicked = 0x10,
  482. Button2Pressed = 0x20,
  483. Button2Released = 0x40,
  484. Button2Clicked = 0x80,
  485. Button2DoubleClicked = 0x100,
  486. Button2TripleClicked = 0x200,
  487. Button3Pressed = 0x400,
  488. Button3Released = 0x800,
  489. Button3Clicked = 0x1000,
  490. Button3DoubleClicked = 0x2000,
  491. Button3TripleClicked = 0x4000,
  492. ButtonWheeledUp = 0x8000,
  493. ButtonWheeledDown = 0x10000,
  494. ButtonWheeledLeft = 0x20000,
  495. ButtonWheeledRight = 0x40000,
  496. Button4Pressed = 0x80000,
  497. Button4Released = 0x100000,
  498. Button4Clicked = 0x200000,
  499. Button4DoubleClicked = 0x400000,
  500. Button4TripleClicked = 0x800000,
  501. ButtonShift = 0x1000000,
  502. ButtonCtrl = 0x2000000,
  503. ButtonAlt = 0x4000000,
  504. ReportMousePosition = 0x8000000,
  505. AllEvents = -1
  506. }
  507. public struct MouseEvent
  508. {
  509. public Point Position;
  510. public MouseButtonState ButtonState;
  511. }
  512. public struct WindowSizeEvent
  513. {
  514. public Size Size;
  515. }
  516. public struct WindowPositionEvent
  517. {
  518. public int Top;
  519. public int Left;
  520. public Point CursorPosition;
  521. }
  522. public struct RequestResponseEvent
  523. {
  524. public (string c1Control, string code, string [] values, string terminating) ResultTuple;
  525. }
  526. public struct InputResult
  527. {
  528. public EventType EventType;
  529. public ConsoleKeyInfo ConsoleKeyInfo;
  530. public MouseEvent MouseEvent;
  531. public WindowSizeEvent WindowSizeEvent;
  532. public WindowPositionEvent WindowPositionEvent;
  533. public RequestResponseEvent RequestResponseEvent;
  534. public readonly override string ToString ()
  535. {
  536. return (EventType switch
  537. {
  538. EventType.Key => ToString (ConsoleKeyInfo),
  539. EventType.Mouse => MouseEvent.ToString (),
  540. //EventType.WindowSize => WindowSize.ToString (),
  541. //EventType.RequestResponse => RequestResponse.ToString (),
  542. _ => "Unknown event type: " + EventType
  543. })!;
  544. }
  545. /// <summary>Prints a ConsoleKeyInfoEx structure</summary>
  546. /// <param name="cki"></param>
  547. /// <returns></returns>
  548. public readonly string ToString (ConsoleKeyInfo cki)
  549. {
  550. var ke = new Key ((KeyCode)cki.KeyChar);
  551. var sb = new StringBuilder ();
  552. sb.Append ($"Key: {(KeyCode)cki.Key} ({cki.Key})");
  553. sb.Append ((cki.Modifiers & ConsoleModifiers.Shift) != 0 ? " | Shift" : string.Empty);
  554. sb.Append ((cki.Modifiers & ConsoleModifiers.Control) != 0 ? " | Control" : string.Empty);
  555. sb.Append ((cki.Modifiers & ConsoleModifiers.Alt) != 0 ? " | Alt" : string.Empty);
  556. sb.Append ($", KeyChar: {ke.AsRune.MakePrintable ()} ({(uint)cki.KeyChar}) ");
  557. string s = sb.ToString ().TrimEnd (',').TrimEnd (' ');
  558. return $"[ConsoleKeyInfo({s})]";
  559. }
  560. }
  561. private void HandleKeyboardEvent (ConsoleKeyInfo cki)
  562. {
  563. var inputResult = new InputResult { EventType = EventType.Key, ConsoleKeyInfo = cki };
  564. _inputQueue.Enqueue (inputResult);
  565. }
  566. public void Dispose ()
  567. {
  568. _inputReadyCancellationTokenSource?.Cancel ();
  569. _inputReadyCancellationTokenSource?.Dispose ();
  570. _inputReadyCancellationTokenSource = null;
  571. _inputReady.Dispose ();
  572. try
  573. {
  574. // throws away any typeahead that has been typed by
  575. // the user and has not yet been read by the program.
  576. while (Console.KeyAvailable)
  577. {
  578. Console.ReadKey (true);
  579. }
  580. }
  581. catch (InvalidOperationException)
  582. {
  583. // Ignore - Console input has already been closed
  584. }
  585. }
  586. }