如何在编程时停止Console.ReadLine()
?
我有一个控制台应用程序:大部分逻辑在不同的线程上运行,而在主线程中,我使用Console.ReadLine()
接受输入。 我想在分离的线程停止运行时停止从控制台读取。
我该如何实现这个功能?
如何在编程时停止Console.ReadLine()
?
我有一个控制台应用程序:大部分逻辑在不同的线程上运行,而在主线程中,我使用Console.ReadLine()
接受输入。 我想在分离的线程停止运行时停止从控制台读取。
我该如何实现这个功能?
更新:这种技术在Windows 10上已不再可靠,请勿使用。
在Win10中进行了相当大的实现更改,使控制台更像终端。毫无疑问是为了帮助新的Linux子系统。这个方法的一个(非预期?)副作用是,CloseHandle()会死锁直到读取完成,导致此方法无效。我将保留原始帖子,只因为它可能有助于某人找到替代方案。
更新2:请查看wischi的答案以获得一个不错的替代方案。
有可能,你需要通过关闭stdin流来进行操作。这个程序演示了这个思路:
using System;
using System.Threading;
using System.Runtime.InteropServices;
namespace ConsoleApplication2 {
class Program {
static void Main(string[] args) {
ThreadPool.QueueUserWorkItem((o) => {
Thread.Sleep(1000);
IntPtr stdin = GetStdHandle(StdHandle.Stdin);
CloseHandle(stdin);
});
Console.ReadLine();
}
// P/Invoke:
private enum StdHandle { Stdin = -10, Stdout = -11, Stderr = -12 };
[DllImport("kernel32.dll")]
private static extern IntPtr GetStdHandle(StdHandle std);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr hdl);
}
}
AllocConsole
,同时运行Console.ReadLine
会导致IOException
:"句柄无效"。 - GraultCancelIoEX
,请参见https://www.meziantou.net/cancelling-console-read.htm和我下面的答案。 - wischi向当前正在运行的控制台应用程序发送 [enter]:
class Program
{
[DllImport("User32.Dll", EntryPoint = "PostMessageA")]
private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
const int VK_RETURN = 0x0D;
const int WM_KEYDOWN = 0x100;
static void Main(string[] args)
{
Console.Write("Switch focus to another window now.\n");
ThreadPool.QueueUserWorkItem((o) =>
{
Thread.Sleep(4000);
var hWnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
PostMessage(hWnd, WM_KEYDOWN, VK_RETURN, 0);
});
Console.ReadLine();
Console.Write("ReadLine() successfully aborted by background thread.\n");
Console.Write("[any key to exit]");
Console.ReadKey();
}
}
免责声明:这只是一个复制粘贴的答案。
感谢 Gérald Barré 提供如此出色的解决方案:
https://www.meziantou.net/cancelling-console-read.htm
CancelIoEX 的文档:
https://learn.microsoft.com/en-us/windows/win32/fileio/cancelioex-func
我在 Windows 10 上测试了它。它非常好用,而且比其他解决方案(例如重新实现 Console.ReadLine、通过 PostMessage 发送返回或像被接受的答案中那样关闭句柄)更少“hacky”。
如果网站崩溃,我在这里引用代码片段:
class Program
{
const int STD_INPUT_HANDLE = -10;
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern IntPtr GetStdHandle(int nStdHandle);
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool CancelIoEx(IntPtr handle, IntPtr lpOverlapped);
static void Main(string[] args)
{
// Start the timeout
var read = false;
Task.Delay(10000).ContinueWith(_ =>
{
if (!read)
{
// Timeout => cancel the console read
var handle = GetStdHandle(STD_INPUT_HANDLE);
CancelIoEx(handle, IntPtr.Zero);
}
});
try
{
// Start reading from the console
Console.WriteLine("Do you want to continue [Y/n] (10 seconds remaining):");
var key = Console.ReadKey();
read = true;
Console.WriteLine("Key read");
}
// Handle the exception when the operation is canceled
catch (InvalidOperationException)
{
Console.WriteLine("Operation canceled");
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation canceled");
}
}
}
Concole.ReadKey()
,但对于 Concole.ReadLine()
则会出现 ERROR_NOT_FOUND 错误。 - Mx.Wolf我需要一个能够在Mono上使用的解决方案,因此不能使用API调用。我发帖只是为了防止其他人遇到同样的问题,或者想要纯C#方式来完成这个任务。CreateKeyInfoFromInt()函数是棘手的部分(某些键超过一个字节的长度)。在下面的代码中,如果从另一个线程调用ReadKeyReset(),则ReadKey()将抛出异常。下面的代码并不完全完整,但它确实演示了使用现有的Console C#函数创建可中断的GetKey()函数的概念。
static ManualResetEvent resetEvent = new ManualResetEvent(true);
/// <summary>
/// Resets the ReadKey function from another thread.
/// </summary>
public static void ReadKeyReset()
{
resetEvent.Set();
}
/// <summary>
/// Reads a key from stdin
/// </summary>
/// <returns>The ConsoleKeyInfo for the pressed key.</returns>
/// <param name='intercept'>Intercept the key</param>
public static ConsoleKeyInfo ReadKey(bool intercept = false)
{
resetEvent.Reset();
while (!Console.KeyAvailable)
{
if (resetEvent.WaitOne(50))
throw new GetKeyInteruptedException();
}
int x = CursorX, y = CursorY;
ConsoleKeyInfo result = CreateKeyInfoFromInt(Console.In.Read(), false);
if (intercept)
{
// Not really an intercept, but it works with mono at least
if (result.Key != ConsoleKey.Backspace)
{
Write(x, y, " ");
SetCursorPosition(x, y);
}
else
{
if ((x == 0) && (y > 0))
{
y--;
x = WindowWidth - 1;
}
SetCursorPosition(x, y);
}
}
return result;
}
Console.KeyAvailable
与 Console.ReadKey
结合起来使用?另一个问题是:相比让 ReadKey
等待下一个输入事件,这 50 毫秒的自旋会占用更多的资源吗? - ygoe我也在寻找一种在特定条件下停止从控制台读取的方法。我想到的解决方法是使用这两种方法创建一个非阻塞版本的read line。
static IEnumerator<Task<string>> AsyncConsoleInput()
{
var e = loop(); e.MoveNext(); return e;
IEnumerator<Task<string>> loop()
{
while (true) yield return Task.Run(() => Console.ReadLine());
}
}
static Task<string> ReadLine(this IEnumerator<Task<string>> console)
{
if (console.Current.IsCompleted) console.MoveNext();
return console.Current;
}
var console = AsyncConsoleInput();
var task = Task.Run(() =>
{
// your task on separate thread
});
if (Task.WaitAny(console.ReadLine(), task) == 0) // if ReadLine finished first
{
task.Wait();
var x = console.Current.Result; // last user input (await instead of Result in async method)
}
else // task finished first
{
var x = console.ReadLine(); // this wont issue another read line because user did not input anything yet.
}
当前被接受的答案已经不再适用,因此我决定创建一个新的答案。我所能想到的唯一安全的方法是创建自己的ReadLine
方法,我可以想象出许多需要这种功能的场景,这里的代码实现了其中之一:
public static string CancellableReadLine(CancellationToken cancellationToken)
{
StringBuilder stringBuilder = new StringBuilder();
Task.Run(() =>
{
try
{
ConsoleKeyInfo keyInfo;
var startingLeft = Con.CursorLeft;
var startingTop = Con.CursorTop;
var currentIndex = 0;
do
{
var previousLeft = Con.CursorLeft;
var previousTop = Con.CursorTop;
while (!Con.KeyAvailable)
{
cancellationToken.ThrowIfCancellationRequested();
Thread.Sleep(50);
}
keyInfo = Con.ReadKey();
switch (keyInfo.Key)
{
case ConsoleKey.A:
case ConsoleKey.B:
case ConsoleKey.C:
case ConsoleKey.D:
case ConsoleKey.E:
case ConsoleKey.F:
case ConsoleKey.G:
case ConsoleKey.H:
case ConsoleKey.I:
case ConsoleKey.J:
case ConsoleKey.K:
case ConsoleKey.L:
case ConsoleKey.M:
case ConsoleKey.N:
case ConsoleKey.O:
case ConsoleKey.P:
case ConsoleKey.Q:
case ConsoleKey.R:
case ConsoleKey.S:
case ConsoleKey.T:
case ConsoleKey.U:
case ConsoleKey.V:
case ConsoleKey.W:
case ConsoleKey.X:
case ConsoleKey.Y:
case ConsoleKey.Z:
case ConsoleKey.Spacebar:
case ConsoleKey.Decimal:
case ConsoleKey.Add:
case ConsoleKey.Subtract:
case ConsoleKey.Multiply:
case ConsoleKey.Divide:
case ConsoleKey.D0:
case ConsoleKey.D1:
case ConsoleKey.D2:
case ConsoleKey.D3:
case ConsoleKey.D4:
case ConsoleKey.D5:
case ConsoleKey.D6:
case ConsoleKey.D7:
case ConsoleKey.D8:
case ConsoleKey.D9:
case ConsoleKey.NumPad0:
case ConsoleKey.NumPad1:
case ConsoleKey.NumPad2:
case ConsoleKey.NumPad3:
case ConsoleKey.NumPad4:
case ConsoleKey.NumPad5:
case ConsoleKey.NumPad6:
case ConsoleKey.NumPad7:
case ConsoleKey.NumPad8:
case ConsoleKey.NumPad9:
case ConsoleKey.Oem1:
case ConsoleKey.Oem102:
case ConsoleKey.Oem2:
case ConsoleKey.Oem3:
case ConsoleKey.Oem4:
case ConsoleKey.Oem5:
case ConsoleKey.Oem6:
case ConsoleKey.Oem7:
case ConsoleKey.Oem8:
case ConsoleKey.OemComma:
case ConsoleKey.OemMinus:
case ConsoleKey.OemPeriod:
case ConsoleKey.OemPlus:
stringBuilder.Insert(currentIndex, keyInfo.KeyChar);
currentIndex++;
if (currentIndex < stringBuilder.Length)
{
var left = Con.CursorLeft;
var top = Con.CursorTop;
Con.Write(stringBuilder.ToString().Substring(currentIndex));
Con.SetCursorPosition(left, top);
}
break;
case ConsoleKey.Backspace:
if (currentIndex > 0)
{
currentIndex--;
stringBuilder.Remove(currentIndex, 1);
var left = Con.CursorLeft;
var top = Con.CursorTop;
if (left == previousLeft)
{
left = Con.BufferWidth - 1;
top--;
Con.SetCursorPosition(left, top);
}
Con.Write(stringBuilder.ToString().Substring(currentIndex) + " ");
Con.SetCursorPosition(left, top);
}
else
{
Con.SetCursorPosition(startingLeft, startingTop);
}
break;
case ConsoleKey.Delete:
if (stringBuilder.Length > currentIndex)
{
stringBuilder.Remove(currentIndex, 1);
Con.SetCursorPosition(previousLeft, previousTop);
Con.Write(stringBuilder.ToString().Substring(currentIndex) + " ");
Con.SetCursorPosition(previousLeft, previousTop);
}
else
Con.SetCursorPosition(previousLeft, previousTop);
break;
case ConsoleKey.LeftArrow:
if (currentIndex > 0)
{
currentIndex--;
var left = Con.CursorLeft - 2;
var top = Con.CursorTop;
if (left < 0)
{
left = Con.BufferWidth + left;
top--;
}
Con.SetCursorPosition(left, top);
if (currentIndex < stringBuilder.Length - 1)
{
Con.Write(stringBuilder[currentIndex].ToString() + stringBuilder[currentIndex + 1]);
Con.SetCursorPosition(left, top);
}
}
else
{
Con.SetCursorPosition(startingLeft, startingTop);
if (stringBuilder.Length > 0)
Con.Write(stringBuilder[0]);
Con.SetCursorPosition(startingLeft, startingTop);
}
break;
case ConsoleKey.RightArrow:
if (currentIndex < stringBuilder.Length)
{
Con.SetCursorPosition(previousLeft, previousTop);
Con.Write(stringBuilder[currentIndex]);
currentIndex++;
}
else
{
Con.SetCursorPosition(previousLeft, previousTop);
}
break;
case ConsoleKey.Home:
if (stringBuilder.Length > 0 && currentIndex != stringBuilder.Length)
{
Con.SetCursorPosition(previousLeft, previousTop);
Con.Write(stringBuilder[currentIndex]);
}
Con.SetCursorPosition(startingLeft, startingTop);
currentIndex = 0;
break;
case ConsoleKey.End:
if (currentIndex < stringBuilder.Length)
{
Con.SetCursorPosition(previousLeft, previousTop);
Con.Write(stringBuilder[currentIndex]);
var left = previousLeft + stringBuilder.Length - currentIndex;
var top = previousTop;
while (left > Con.BufferWidth)
{
left -= Con.BufferWidth;
top++;
}
currentIndex = stringBuilder.Length;
Con.SetCursorPosition(left, top);
}
else
Con.SetCursorPosition(previousLeft, previousTop);
break;
default:
Con.SetCursorPosition(previousLeft, previousTop);
break;
}
} while (keyInfo.Key != ConsoleKey.Enter);
Con.WriteLine();
}
catch
{
//MARK: Change this based on your need. See description below.
stringBuilder.Clear();
}
}).Wait();
return stringBuilder.ToString();
}
将此函数放置在您的代码中的某个位置,这将为您提供一个可以通过 CancellationToken
取消的函数,为了更好的代码,我已经使用了
using Con = System.Console;
此函数在取消时返回空字符串(对于我的情况来说很好),如果您希望可以在标记的catch
表达式内抛出异常。同时,在相同的catch
表达式中,您可以删除stringBuilder.Clear();
这行代码,这将导致代码返回用户输入的内容。结合成功或已取消的标志,您可以保留用户迄今为止输入的内容并在后续请求中使用它。
您还可以更改的另一件事是,如果您想获得超时功能,可以在循环中设置超时时间以外的取消标记。
我尽可能清晰简洁,但这段代码可能可以更简洁。该方法本身可以变成异步方式,并传递超时和取消标记。
Console.ReadLine()
包装在一个任务中。显然,Console.ReadLine()
调用无法被打断,因此需要使用 Task.WhenAny 来与 Task.Delay 结合使用以使取消令牌生效。Task<string?>? readTask = null;
async Task<string?> ReadLineAsync(CancellationToken cancellationToken = default)
{
readTask ??= Task.Run(() => Console.ReadLine());
await Task.WhenAny(readTask, Task.Delay(-1, cancellationToken));
cancellationToken.ThrowIfCancellationRequested();
string? result = await readTask;
readTask = null;
return result;
}
await ReadLineAsync(token);
或 await Task.Run(() => ReadLineAsync(token))
都不起作用,因为线程在 Console.In.ReadLineAsync()
上停止... - MoonKillCZawait ReadLineAsync(token)
应该可以正常工作。在底层,会创建一个新任务来调用输入流的 ReadLine()
方法。Console.In.ReadLineAsync()
应该立即返回此任务而不阻塞线程。如果这样做不起作用,您可以尝试通过 Task.Run(() => Console.ReadLine())
替换 Console.In.ReadLineAsync()
看看会发生什么。我已经进行了很多测试,所以不知道为什么对您不起作用。您使用的是哪个平台? - RdJNLreadTask ??= Console.In.ReadLineAsync();
这一行。无论如何,我通过使用这个 NuGet 包解决了我的问题:ReadLine.Reboot。 - MoonKillCZConsole.In
上阻塞了。它是什么类型的应用程序?是常规控制台应用程序还是其他类型的应用程序? - RdJNLTask.Run()
中解决了这个问题(完整代码行:readTask ??= Task.Run(() => Console.In.ReadLineAsync())
)。 - glenn223readTask ??= Task.Run(() => Console.ReadLine())
会更有效率一些,但差别可能很小。我会更新我的答案。 - RdJNL以下是一种在Windows 10上工作且不使用任何高级线程或dllimport技巧的解决方案。我希望它对您有所帮助。
我基本上创建了一个坐在标准输入上的流读取器。以“异步”的方式读取它,如果我想取消readline,则只需处理掉流读取器即可。
以下是我的代码:
private System.IO.StreamReader stdinsr = new System.IO.StreamReader(Console.OpenStandardInput());
[DebuggerHidden]
private string ReadLine() {
return stdinsr.ReadLineAsync().Result;
}
protected override void OnExit(ExitEventArgs e) {
base.OnExit(e);
commandLooper.Abort();
stdinsr.Dispose();
}
注意:是的,我已经阅读了async,但我仍在等待任务结果,因此基本上仍在等待用户输入。
using System;
using System.Runtime.InteropServices;
public class Temp
{
//Just need this
//==============================
static IntPtr ConsoleWindowHnd = GetForegroundWindow();
[DllImport("user32.dll")]
static extern IntPtr GetForegroundWindow();
[DllImport("User32.Dll")]
private static extern bool PostMessage(IntPtr hWnd, uint msg, int wParam, int lParam);
const int VK_RETURN = 0x0D;
const int WM_KEYDOWN = 0x100;
//==============================
public static void Main(string[] args)
{
System.Threading.Tasks.Task.Run(() =>
{
System.Threading.Thread.Sleep(2000);
//And use like this
//===================================================
PostMessage(ConsoleWindowHnd, WM_KEYDOWN, VK_RETURN, 0);
//===================================================
});
Console.WriteLine("Waiting");
Console.ReadLine();
Console.WriteLine("Waiting Done");
Console.Write("Press any key to continue . . .");
Console.ReadKey();
}
}
检查前台窗口是否为cmd。如果不是,那么当前进程应启动控制台窗口,因此可以继续使用它。这并不重要,因为前台窗口应该是当前进程窗口,但通过双重检查可以让您感到放心。
int id;
GetWindowThreadProcessId(ConsoleWindowHnd, out id);
if (System.Diagnostics.Process.GetProcessById(id).ProcessName != "cmd")
{
ConsoleWindowHnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
}
我在GitHub上偶然发现了这个小库:https://github.com/tonerdo/readline
ReadLine是一个纯C#编写的GNU Readline类库。它可以作为内置Console.ReadLine()的替代品,并带有一些类似于Unix shell的终端好处,如命令历史记录导航和选项卡自动完成。
它是跨平台的,在任何支持.NET的地方运行,针对netstandard1.3意味着它可以与.NET Core以及完整的.NET Framework一起使用。
尽管该库在编写时不支持中断输入,但更新以实现此功能应该很容易。或者,它可以成为编写自定义解决方案以克服Console.ReadLine
限制的有趣示例。