wpfsshasync-awaitmonitoringssh.net

SSH.NET real-time command output monitoring


There is a long running script script.sh on a remote Linux machine. I need to start it and monitor it's activity in real time. The script during it's activity may output to stdout and stderr. I am searching for a way to capture both of the streams.

I use Renci SSH.NET to upload script.sh and start it, so it would be great to see a solution bounded to this library. In my mind the perfect solution is the new method:

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();
    }
});

Solution

  • So, here is the solution I came up with. Of course, it can be improved, so it is open to critique.
    I used

    await Dispatcher.Yield(DispatcherPriority.ApplicationIdle);
    

    instead of Task.Yield() because Task.Yield() will make continuation a higher priority than GUI events, but, as a bad consequence, it demands your application to use 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; }
    }