0
votes

Let me preface this question by saying I'm absolutely not a pro C# programmer and have pretty much brute forced my way through most of my small programs so far.

I'm working on a small WinForms application to SSH into a few devices, tail -f a log file on each, and display the real-time output in TextBoxes while also saving to log files. Right now, it works, but hogs nearly 30% of my CPU during logging and I'm sure I'm doing something wrong.

After creating the SshClient and connecting, I run the tail command like so (these variables are part of a logger class which exists for each connection):

command = client.CreateCommand("tail -f /tmp/messages")
result = command.BeginExecute();
stream = command.OutputStream;

I then have a log reading/writing function:

public async Task logOutput(IAsyncResult result, Stream stream, TextBox textBox, string logPath)
{
    // Clear textbox ( thread-safe :) )
    textBox.Invoke((MethodInvoker)(() => textBox.Clear()));
    // Create reader for stream and writer for text file
    StreamReader reader = new StreamReader(stream, Encoding.UTF8, true, 1024, true);
    StreamWriter sw = File.AppendText(logPath);
    // Start reading from SSH stream
    while (!result.IsCompleted || !reader.EndOfStream)
    {
        string line = await reader.ReadLineAsync();
        if (line != null)
        {
            // append to textbox
            textBox.Invoke((Action)(() => textBox.AppendText(line + Environment.NewLine)));
            // append to file
            sw.WriteLine(line);
        }
    }
}

Which I call the following way, per device connection:

Task.Run(() => logOutput(logger.result, logger.stream, textBox, fileName), logger.token);

Everything works fine, it's just the CPU usage that's the issue. I'm guessing I'm creating way more than one thread per logging process, but I don't know why or how to fix that.

Does anything stand out as a simple fix to the above code? Or even better - is there a way to set up a callback that only prints the new data when the result object gets new text?

All help is greatly appreciated!

EDIT 3/4/2021

I tried a simple test using CopyToAsync by changing the code inside logOutput() to the following:

public async Task logOutput(IAsyncResult result, Stream stream, string logPath)
{
    using (Stream fileStream = File.Open(logPath, FileMode.OpenOrCreate))
    {
        // While the result is running, copy everything from the command stream to a file
        while (!result.IsCompleted)
        {
            await stream.CopyToAsync(fileStream);
        }
    }
}

However this results in the text files never getting data written to them, and CPU usage is actually slightly worse.

2ND EDIT 3/4/2021

Doing some more debugging, it appears the high CPU usage occurs only when there's no new data coming in. As far as I can tell, this is because the ReadLineAsync() method is constantly firing regardless of whether or not there's actually new data from the SSH command that's running, and it's running as fast as possible hogging all the CPU cycles it can. I'm not entirely sure why that is though, and could really use some help here. I would've assumed that ReadLineAsync() would simply wait until a new line was available from the SSH command to continue.

1
@jdweng see my edit, either I'm using that function wrong or it's not what I need. - Patrick
MSDN says you can use an END method to block an ASYN event like the following code : docs.microsoft.com/en-us/dotnet/standard/… - jdweng

1 Answers

0
votes

The solution ended up being much simpler than I would've thought.

There's a known bug in SSH.NET where the command's OutputStream will continually spit out null data when there's no actual new data recieved. This causes the while loop in my code to be running as fast as possible, consuming a bunch of CPU in the process.

The solution is simply to add a short asynchronous delay in the loop. I included the delay only when the recieved data is null, so that reading isn't interrupted when there's actual valid data coming through.

while (!result.IsCompleted && !token.IsCancellationRequested)
{
    string line = await reader.ReadLineAsync();
    // Append line if it's valid
    if (string.IsNullOrEmpty(line))
    {
        await Task.Delay(10);  // prevents high CPU usage
        continue;
    } 
    // Append line to textbox
    textBox.Invoke((Action)(() => textBox.AppendText(line + Environment.NewLine)));
    // Append line to file
    writer.WriteLine(line);
}

On a Ryzen 5 3600, this brought my CPU usage from ~30-40% while the program was running to less than 1% even when data is flowing. Much better.