SSH.NET实时命令输出监控

6

在远程Linux机器上有一个长时间运行的脚本script.sh。我需要启动它并实时监控其活动情况。该脚本在其活动期间可能会输出到stdoutstderr。我正在寻找一种捕获这两个流的方法。

我使用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();
    }
});
4个回答

20

使用SshClient.CreateCommand方法。它返回SshCommand实例。

SshCommand类有用于标准输出(和结果)的OutputStream,以及用于标准错误输出的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中显示结果.


感谢您对cmd.Execute()OutputStreams的深入洞察。在我完成代码优化后,我会在这里发布一个完美适合我的情况的包装器。 再次感谢您 :) - Egor Okhterov

13

所以,这是我想出来的解决方案。当然,它可以改进,因此它是可以接受批评的。
我使用了

await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);

使用await Task.Delay()代替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; }
}

1
请问您能否提供一个代码片段来说明如何使用这些函数吗?我是 .Net 的新手。 - Sathish
使用await client.CreateCommand("sleep 1; echo 1; sleep 1; echo 2; ps -ax | grep java").ExecuteAsync(progress, new CancellationToken());只返回了一个带有"1"的行,这似乎不正确。 - Simon K.
1
如果你把 sshCommand.EndExecute(asyncResult); 这行代码移到方法的结尾,它会更好地工作。 - Francois
sshCommand.EndExecute(asyncResult); 应该放在最后,即在 CheckOutputAndReportProgress 之后。 - Wojtpl2

1
除了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);
        }

感谢Can I cancel StreamReader.ReadLineAsync with a CancellationToken?提供的帮助。

以下是使用方法:

var stderrLine = await streamReader.ReadToEndAsync().WithCancellation(cancellationToken);

1
以下代码独立等待输出和错误输出,并具有良好的性能。
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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接