远程 Linux 计算机上有一个长时间运行的脚本
script.sh
。我需要启动它并实时监控它的活动。脚本在其活动期间可能会输出到 stdout
和 stderr
。我正在寻找一种捕获这两个流的方法。
我使用 Renci SSH.NET 上传
script.sh
并启动它,所以很高兴看到一个与这个库绑定的解决方案。在我看来,完美的解决方案是新方法:
var realTimeScreen= ...;
var commandExecutionStatus = sshClient.RunCommandAsync(
command: './script.sh',
stdoutEventHandler: stdoutString => realTimeScreen.UpdateStdout(stdString)
stderrEventHandler: stderrString => realTimeScreen.UpdateStderr(stderrString));
...
commandExecutionStatus.ContinueWith(monitoringTask =>
{
if (monitoringTask.Completed)
{
realTimeScreen.Finish();
}
});
使用
SshClient.CreateCommand
方法。它返回 SshCommand
实例。
SshCommand
类具有用于stdout的OutputStream
(和Result
)和用于stderr的ExtendedOutputStream
。
参见
SshCommandTest.cs
:
public void Test_Execute_ExtendedOutputStream()
{
var host = Resources.HOST;
var username = Resources.USERNAME;
var password = Resources.PASSWORD;
using (var client = new SshClient(host, username, password))
{
#region Example SshCommand CreateCommand Execute ExtendedOutputStream
client.Connect();
var cmd = client.CreateCommand("echo 12345; echo 654321 >&2");
var result = cmd.Execute();
Console.Write(result);
var reader = new StreamReader(cmd.ExtendedOutputStream);
Console.WriteLine("DEBUG:");
Console.Write(reader.ReadToEnd());
client.Disconnect();
#endregion
Assert.Inconclusive();
}
}
另请参阅类似 WinForms 问题的完整代码在 SSH.NET 中执行长时间命令并在 TextBox 中连续显示结果。
所以,这是我想出的解决方案。当然,它还可以改进,所以欢迎批评。
我用过
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
而不是
Task.Yield()
,因为 Task.Yield()
将使连续性比 GUI 事件具有更高的优先级,但是,作为一个坏结果,它要求您的应用程序使用 WindowsBase.dll
。
public static class SshCommandExtensions
{
public static async Task ExecuteAsync(
this SshCommand sshCommand,
IProgress<ScriptOutputLine> progress,
CancellationToken cancellationToken)
{
var asyncResult = sshCommand.BeginExecute();
var stdoutStreamReader = new StreamReader(sshCommand.OutputStream);
var stderrStreamReader = new StreamReader(sshCommand.ExtendedOutputStream);
while (!asyncResult.IsCompleted)
{
await CheckOutputAndReportProgress(
sshCommand,
stdoutStreamReader,
stderrStreamReader,
progress,
cancellationToken);
await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
}
sshCommand.EndExecute(asyncResult);
await CheckOutputAndReportProgress(
sshCommand,
stdoutStreamReader,
stderrStreamReader,
progress,
cancellationToken);
}
private static async Task CheckOutputAndReportProgress(
SshCommand sshCommand,
TextReader stdoutStreamReader,
TextReader stderrStreamReader,
IProgress<ScriptOutputLine> progress,
CancellationToken cancellationToken)
{
if (cancellationToken.IsCancellationRequested)
{
sshCommand.CancelAsync();
}
cancellationToken.ThrowIfCancellationRequested();
await CheckStdoutAndReportProgressAsync(stdoutStreamReader, progress);
await CheckStderrAndReportProgressAsync(stderrStreamReader, progress);
}
private static async Task CheckStdoutAndReportProgressAsync(
TextReader stdoutStreamReader,
IProgress<ScriptOutputLine> stdoutProgress)
{
var stdoutLine = await stdoutStreamReader.ReadToEndAsync();
if (!string.IsNullOrEmpty(stdoutLine))
{
stdoutProgress.Report(new ScriptOutputLine(
line: stdoutLine,
isErrorLine: false));
}
}
private static async Task CheckStderrAndReportProgressAsync(
TextReader stderrStreamReader,
IProgress<ScriptOutputLine> stderrProgress)
{
var stderrLine = await stderrStreamReader.ReadToEndAsync();
if (!string.IsNullOrEmpty(stderrLine))
{
stderrProgress.Report(new ScriptOutputLine(
line: stderrLine,
isErrorLine: true));
}
}
}
public class ScriptOutputLine
{
public ScriptOutputLine(string line, bool isErrorLine)
{
Line = line;
IsErrorLine = isErrorLine;
}
public string Line { get; private set; }
public bool IsErrorLine { get; private set; }
}
除了Wojtpl2的答案。对于像“tail -f”这样的命令,流处理任务之一将锁定 ReadLine 方法:
var stderrLine = await streamReader.ReadLineAsync();
为了克服这个问题,我们需要使用扩展方法将令牌传递给streamReader:
public static Task<T> WithCancellation<T>(this Task<T> task, CancellationToken cancellationToken)
{
return task.IsCompleted // fast-path optimization
? task
: task.ContinueWith(
completedTask => completedTask.GetAwaiter().GetResult(),
cancellationToken,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
谢谢 我可以使用 CancellationToken 取消 StreamReader.ReadLineAsync 吗?
并像这样使用它:
var stderrLine = await streamReader.ReadToEndAsync().WithCancellation(cancellationToken);
以下代码独立等待输出和错误输出,性能良好。
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Renci.SshNet;
namespace DockerTester
{
public static class SshCommandExtensions
{
public static async Task ExecuteAsync(
this SshCommand sshCommand,
IProgress<ScriptOutputLine> progress,
CancellationToken cancellationToken)
{
var asyncResult = sshCommand.BeginExecute();
var stdoutReader = new StreamReader(sshCommand.OutputStream);
var stderrReader = new StreamReader(sshCommand.ExtendedOutputStream);
var stderrTask = CheckOutputAndReportProgressAsync(sshCommand, asyncResult, stderrReader, progress, true, cancellationToken);
var stdoutTask = CheckOutputAndReportProgressAsync(sshCommand, asyncResult, stdoutReader, progress, false, cancellationToken);
await Task.WhenAll(stderrTask, stdoutTask);
sshCommand.EndExecute(asyncResult);
}
private static async Task CheckOutputAndReportProgressAsync(
SshCommand sshCommand,
IAsyncResult asyncResult,
StreamReader streamReader,
IProgress<ScriptOutputLine> progress,
bool isError,
CancellationToken cancellationToken)
{
while (!asyncResult.IsCompleted || !streamReader.EndOfStream)
{
if (cancellationToken.IsCancellationRequested)
{
sshCommand.CancelAsync();
}
cancellationToken.ThrowIfCancellationRequested();
var stderrLine = await streamReader.ReadLineAsync();
if (!string.IsNullOrEmpty(stderrLine))
{
progress.Report(new ScriptOutputLine(
line: stderrLine,
isErrorLine: isError));
}
// wait 10 ms
await Task.Delay(10, cancellationToken);
}
}
}
public class ScriptOutputLine
{
public ScriptOutputLine(string line, bool isErrorLine)
{
Line = line;
IsErrorLine = isErrorLine;
}
public string Line { get; private set; }
public bool IsErrorLine { get; private set; }
}
}
你可以使用它:
var outputs = new Progress<ScriptOutputLine>(ReportProgress);
using (var command =
sshClient.RunCommand(
"LONG_RUNNING_COMMAND"))
{
await command.ExecuteAsync(outputs, CancellationToken.None);
await Console.Out.WriteLineAsync("Status code: " + command.ExitStatus);
}
以及报告进度方法的示例实现:
private static void ReportProgress(ScriptOutputLine obj)
{
var color = Console.ForegroundColor;
if (obj.IsErrorLine)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(obj.Line);
Console.ForegroundColor = color;
}
Console.WriteLine(obj.Line);
}