0

I have been supplied an executable that communicates with boards under test using USB. The supplied exe is then called in a polling loop from my application using Process to run cmd.exe. My application is a C# WPF app. I am new to Tasks but have gone through many variations to get what I think is the correct way of using Task to run the Process in a separate Task thread. The problem is when Process code is running my UI flashes during Process call, and I can't move the UI for example. Also if trying to use another application, such as Word, anything I'm doing will be interrupted by my application when Process is running.

I've tried a lot of things but where I am right now is I'm using the below code for the Process. The polling loop is started with a button press. All methods that call the Process code are using the public async Task Method(parameters) format, except the button code which is format private async void Execute(object parameter).

Process class code extract:

List<string> TestResults = new List<string>();

private Process process;
private TaskCompletionSource<bool> eventHandled;

public async Task<List<string>> GetData(string cmdString)
{
    process = new Process();
    eventHandled = new TaskCompletionSource<bool>();

    Stopwatch stopwatch = new Stopwatch();
    stopwatch.Start();

    TestResults.Clear();

    process.StartInfo.CreateNoWindow = true;
    process.StartInfo.UseShellExecute = false;
    process.StartInfo.RedirectStandardError = true;
    process.StartInfo.RedirectStandardOutput = true;
    process.StartInfo.WorkingDirectory = WorkingDirectoryPath;

    process.EnableRaisingEvents = true;
    process.Exited += new EventHandler(ExitedProcess);

    process.OutputDataReceived += OutputDataHandler;
    // process.ErrorDataReceived += ErrorDataHandler;

    process.StartInfo.FileName = CommandExe;

    if (cmdString != null)
    {
        process.StartInfo.Arguments = cmdString;
    }
    else
    {
        return null;
    }

    process.Start();

    process.BeginOutputReadLine();

    // process.BeginErrorReadLine();

    // process.WaitForExit(); // comment out if using Task

    await Task.WhenAll(eventHandled.Task, Task.Delay(100));

    process.Dispose();

    stopwatch.Stop();
    FmpViewModel.Notes.Add($"Call Ended: {stopwatch.ElapsedMilliseconds}ms");

    return TestResults;         
}

private void ExitedProcess(object sender, System.EventArgs e)
{
    eventHandled.TrySetResult(true);
}

private  void OutputDataHandler(object sendingProcess,
    DataReceivedEventArgs data)
{
    if (!string.IsNullOrEmpty(data.Data))
    {                
       TestResults.Add(data.Data);
    }
}

The Execute method:

private async void Execute(object parameter)
{
    tokenSource = new CancellationTokenSource();
    token = tokenSource.Token;
    //Turn on Cancel button
    CanExecuteEnable = false;
    //Turn off Initialize button
    CanInitEnable = false;

    await ExecuteTest(token);         
}

EDIT 2: I finally found the problem is caused by the change in focus when Process() runs. The problem is described by nawfal here How to set focus back to form after opening up a process (Notepad)? and he offers a solution that worked for him. In my case adding the Topmost="True" to XAML was the best solution for my situation.

EDIT: I found this post A Tour of Task, Part 9: Delegate Tasks mainly about Task.Run section. So I changed my code to wrap all the calls to the Process() code in a Task.Run to see if it does use separate threads and it does example below. But I still have the same problem, this did not fix it. I did add a Topmost="True" to the xaml to keep the application window on top and that stopped the flicker on the test app. But if I have other apps open they still have the interference like the focus keeps shifting away whenever the Process() calls occur.

UI Thread 1
Thread Task.Run() 9
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 9
Thread during Task.WhenAny() 9
Thread during Task.WhenAny() 9
Thread during Task.WhenAny() 9
Thread during Task.WhenAny() 9
Thread during Task.WhenAny() 9
Thread during Task.WhenAny() 9
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 11
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 13
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
Thread during Task.WhenAny() 7
UI Thread after Task.Run() 1
B A
  • 17
  • 5
  • 1
    we would need to see this `Execute` method – Mong Zhu Mar 14 '23 at 14:35
  • 3
    "*using Task to run the Process in a separate Task thread*" is probably a misconception. A Process does not run in a Thread in your application. It can be started from your application, and you can read from its output, write to its input etc. But it runs in a different process/application. – Clemens Mar 14 '23 at 14:37
  • If you just want to wait until the Process has finished and return its output as List, you may perhaps wrap it in a TaskCompletionSource. – Clemens Mar 14 '23 at 14:39
  • `async void` is a bug because such methods can't be awaited. Your program probably terminates before `ExecuteTest` has a chance to complete. `async void` methods are *only* meant for asynchronous event handlers – Panagiotis Kanavos Mar 14 '23 at 15:02
  • I had read that async void is allowed for things like button event which is what Execute is, for the button press. – B A Mar 14 '23 at 15:04
  • Where is the `GetData` method called? – Theodor Zoulias Mar 14 '23 at 15:18
  • The following may be helpful: https://stackoverflow.com/a/71846819/10024425 – Tu deschizi eu inchid Mar 14 '23 at 15:19
  • user09938 I didn't see your comment before, I will try that and see if it fixes it, thanks. – B A Mar 22 '23 at 19:46

1 Answers1

0

I think is the correct way of using Task to run the Process in a separate Task thread

The actual exe will run in a separate process, and therefore also a separate thread. So what we are really discussing is what thread is used to start, monitor and report the result from that process.

As far as I can tell your current implementation would use the UI thread for everything. I would have expected this to be fine since it should not really need to do much of anything.

But if you are experiencing UI hangs etc there is clearly something wrong. I would recommend a using a profiler to find out if there is any method call that blocks the UI thread for any significant amount of time. I would be a bit suspicious over the process.Start(), since this might block until the process is started, but I'm guessing here.

If that is the actual problem it might be motivated to use a threadpool thread to start the process:

public Task<List<string>> GetData(string cmdString){
    
    var tcs = new TaskCompletionSource<List<string>>();
    Task.Run(StartProcess);
    return tcs;
    void StartProcess(){
        var result = new ConcurrentQueue<string>();
        // setup the process etc...
        process.Exited += (o, e) => tcs.SetResult(result.ToList());
        process.OutputDataReceived  += (o, e) => result.Enqueue(e.Data); 
        process.Start();
    }
}

The basic idea with this is to start the process on a threadpool thread, and return a task representing result of the process. After the process has started this initial thread will return to the pool.

If the process outputs data it will run the OutputDataReceived on a threadpool thread and enqueue the data. Once Exited is run it will take all of this data and set the task as completed. I assume Exited can only be raised after all output has been processed, but I'm not sure about this. This example also skips error handling timeouts etc for simplicity.

JonasH
  • 28,608
  • 2
  • 10
  • 23