devlime26
devlime26

Reputation: 59

Task return a StreamReader in c#

I have this task in C# that should return the standard output of DISM, so I can use it where i need:

public async Task<StreamReader> DISM(string Args)
{

   StreamReader DISMstdout = null;

    await Task.Run(() =>
    {
        Process DISMcmd = new Process();

        if (Environment.Is64BitOperatingSystem)
        {
            DISMcmd.StartInfo.FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "SysWOW64", "dism.exe");
        }
        else
        {
            DISMcmd.StartInfo.FileName = System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "System32", "dism.exe");
        }

        DISMcmd.StartInfo.Verb = "runas";

        DISMcmd.StartInfo.Arguments = DISMArguments;

        DISMcmd.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
        DISMcmd.StartInfo.CreateNoWindow = true;
        DISMcmd.StartInfo.UseShellExecute = false;
        DISMcmd.StartInfo.RedirectStandardOutput = true;
        DISMcmd.EnableRaisingEvents = true;
        DISMcmd.Start();

        DISMstdout = DISMcmd.StandardOutput;

        DISMcmd.WaitForExit();
    });
    return DISMstdout;
}

But it doesn't really work. If I want to read the standardoutput from another task I can't (because it is empty) So there must be a problem with my task?.

public async Task Test()
{
    await Task.Run(() =>
    {

    StreamReader DISM = await new DISM("/Get-ImageInfo /ImageFile:" + ImagePath + @" /Index:1");

    string data = string.Empty;
     MessageBox.Show(DISM.ReadToEnd()); // this should display a msgbox with the standardoutput of dism

     while ((data = DISM.ReadLine()) != null)
     {
         if (data.Contains("Version : "))
         {
               // do something
         }
     }
   }); 
}

What is wrong with this piece of code?

Upvotes: 3

Views: 1838

Answers (2)

millimoose
millimoose

Reputation: 39950

After playing around with this using Benchmark.NET, it seems that starting a process (I tried DISM and Atom to have something hefty) - from setup to Start() - takes about 50 milliseconds. This seems pretty negligible to me for this use. After all, 50ms is good enough latency for say playing League of Legends, and you're not going to start these in a tight loop.

I'd like to provide an alternative answer of "don't bother with Task.Run() and just use async I/O in a straightforward way" unless you absolutely need to get rid of that delay and believe spawning off a background thread will help:

static string GetDismPath()
{
    var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
    var systemDir = Environment.Is64BitOperatingSystem ? "SysWOW64" : "System32";
    var dismExePath = Path.Combine(windowsDir, systemDir, "dism.exe");

    return dismExePath;
}

static Process StartDism(string args)
{
    var proc = new Process
    {
        StartInfo =
        {
            FileName = GetDismPath(),
            Verb = "runas",
            Arguments = args,
            WindowStyle = ProcessWindowStyle.Hidden,
            CreateNoWindow = true,
            UseShellExecute = false,
            RedirectStandardOutput = true
        }
    };

    proc.Start();

    return proc;
}
static void Cleanup(Process proc)
{
    Task.Run(async () =>
    {
        proc.StandardInput.Close();
        var buf = new char[0x1000];
        while (await proc.StandardOutput.ReadBlockAsync(buf, 0, buf.Length).ConfigureAwait(false) != 0) { }
        while (await proc.StandardError.ReadBlockAsync(buf, 0, buf.Length).ConfigureAwait(false) != 0) { }

        if (!proc.WaitForExit(5000))
        {
            proc.Kill();
        }
        proc.Dispose();
    });
}
static async Task Main(string[] args)
{
    var dismProc = StartDism("/?");

    // do what you want with the output
    var dismOutput = await dismProc.StandardOutput.ReadToEndAsync().ConfigureAwait(false);

    await Console.Out.WriteAsync(dismOutput).ConfigureAwait(false);
    Cleanup(dismProc);
}

I'm only using Task.Run() to keep the cleanup off the main thread in case you need to do something else while DISM keeps producing output you're not interested in that you do not wish to kill outright.

Upvotes: 2

millimoose
millimoose

Reputation: 39950

The way I'd write your method to exploit async..await as opposed to the legacy asynchronous approaches is like this:

public async Task<TResult> WithDism<TResult>(string args, Func<StreamReader, Task<TResult>> func)
{
    return await Task.Run(async () =>
    {
        var proc = new Process();

        var windowsDir = Environment.GetFolderPath(Environment.SpecialFolder.Windows);
        var systemDir = Environment.Is64BitOperatingSystem ? "SysWOW64" : "System32";
        proc.StartInfo.FileName = Path.Combine(windowsDir, systemDir, "dism.exe");

        proc.StartInfo.Verb = "runas";

        proc.StartInfo.Arguments = args;

        proc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
        proc.StartInfo.CreateNoWindow = true;
        proc.StartInfo.UseShellExecute = false;
        proc.StartInfo.RedirectStandardOutput = true;
        proc.Start();

        Console.Error.WriteLine("dism started");

        var result = await func(proc.StandardOutput);

        Console.Error.WriteLine("func finished");
        // discard rest of stdout
        await proc.StandardOutput.ReadToEndAsync();
        proc.WaitForExit();

        return result;
    });
}

Since realistically, the only part where significant blocking can occur when spawning a process is as you handle the output it produces. Used like this:

var task = WithDism("/?", async sr => await sr.ReadToEndAsync()); // or process line-by-line
Console.WriteLine("dism task running");
Console.WriteLine(await task);

it produces the following output

dism task running
dism started
func finished

Error: 740

Elevated permissions are required to run DISM.
Use an elevated command prompt to complete these tasks.


Do note that when using subprocesses, it's your job to make sure they correctly exit or are shut down to avoid leaving zombie processes around. That's why I've added the possibly redundant ReadToEndAsync() - in case func still leaves some output unconsumed, this should allow the process to reach its natural end.

However, this means the calling function will only proceed once that happens. If you leave behind a lot of unconsumed output you're not interested in, this will cause an unwanted delay. You could work around this by spawning off this cleanup to a different background task and returning the result immediately using something like:

Task.Run(() => {
    // discard rest of stdout and clean up process:
    await proc.StandardOutput.ReadToEndAsync();
    proc.WaitForExit();
});

but I admit I'm going a bit out on a limb there, I'm not entirely sure about the robustness of just letting a task "run wild" like that. What the appropriate way to clean up the process is will, of course, depend on what it's actually doing after you get the output you want to return from func.


I'm using synchronous calls to Console there because they only serve to illustrate the timing of events, I want to know that as execution reaches that point. Normally you would use async in a "viral" way to make sure control passes back to top-level as soon as possible.

Upvotes: 3

Related Questions