Search code examples
cmdio-redirection

How can I redirect the standard output from cmd.exe


This has proven difficult to search as most results are all about redirecting from WITHIN cmd.exe rather than the output of cmd.exe itself.

I have a simple C# example showing a working and non-working test of redirecting process output and just printing the outputted values.

void Main()
{
    // Calling nslookup directly works as expected
    ProcessStartInfo joy = new ProcessStartInfo("nslookup", @"google.com 8.8.8.8");
    // Calling nslookup as a command to cmd.exe does not work as expected
    ProcessStartInfo noJoy = new ProcessStartInfo(Environment.ExpandEnvironmentVariables("%COMSPEC%"), @"/C nslookup google.com 8.8.8.8");

    Console.WriteLine($"*** Running \"{joy.FileName} {joy.Arguments}\"...");
    Console.WriteLine();
    Run(joy);

    Console.WriteLine();
    Console.WriteLine($"*** Running \"{noJoy.FileName} {noJoy.Arguments}\"...");
    Console.WriteLine();
    Run(noJoy);
}

void Run(ProcessStartInfo startInfo)
{
    startInfo.CreateNoWindow = true;
    startInfo.UseShellExecute = false;
    startInfo.RedirectStandardError = true;
    startInfo.RedirectStandardOutput = true;

    Process proc = new Process();
    proc.StartInfo = startInfo;
    
    proc.EnableRaisingEvents = true;
    proc.Exited += ReceiveExitNotification;
    proc.ErrorDataReceived += ReceiveStandardErrorData;
    proc.OutputDataReceived += ReceiveStandardOutputData;
    
    proc.Start();
    
    proc.BeginErrorReadLine();
    proc.BeginOutputReadLine();
    
    proc.WaitForExit();
    
    proc.ExitCode.Dump();
}

void ReceiveStandardOutputData(object sender, DataReceivedEventArgs e)
{
    Console.WriteLine(e.Data);
}

void ReceiveStandardErrorData(object sender, DataReceivedEventArgs e)
{
    Console.WriteLine(e.Data);
}

void ReceiveExitNotification(object sender, EventArgs e)
{
    Console.WriteLine("Exited");
}

And here's the output I'm getting from the above

*** Running "nslookup google.com 8.8.8.8"...

Non-authoritative answer:

Server:  dns.google
Address:  8.8.8.8

Name:    google.com
Addresses:  2607:f8b0:4002:c08::8b
    2607:f8b0:4002:c08::64
    2607:f8b0:4002:c08::65
    2607:f8b0:4002:c08::66
    172.217.10.206

null
null
Exited
0

*** Running "C:\windows\system32\cmd.exe /C nslookup google.com 8.8.8.8"...

null
null
Exited
0

The choice of nslookup in the example is arbitrary, I've tried many others including commands with side effects so I can be sure it's being executed as expected.

I have tried with synchronous reads but no change.

I have no reason to believe it's C# or .NET related. I may try a direct CreateProcess() test to confirm.

For context, it's a batch file from which I'm actually looking to get the output, that's why the intermediate cmd.exe process is needed.

Further context, it's actually an MSBuild Exec task from which I'm trying to get the output, so I have limited control of the actual invocation, but I've watched the task run in the debugger and narrowed it down to this issue.


Solution

  • TLDR; The code example in the question works just fine on any normal machine.

    So as it turns out this is a permissions issue. This is a company computer and I have restricted rights, however they have software installed that gives administrative rights to particular processes. cmd.exe is one of those processes, so by default it launches as admin and so I cannot read the output stream from my non-elevated process.

    Some ideas that almost work around the issue:

    • From a cmd.exe prompt I can run set __COMPAT_LAYER=RUNASINVOKER then run a second cmd.exe which runs unelevated, but this doesn't really help as I still can't get that stream. Setting the __COMPAT_LAYER environment variable seems to only affect processes launched from cmd.exe (from not CreateProcess() which .NET's Process.Start() uses).

    • RunAs.exe has a /trustlevel switch with which I can run an unelevated command, but then my Process object is for runas which does not handle any redirection or even stay open for the life of the child process, so still no good.

    But in my case I think the simplest solution is best. Copy cmd.exe to another directory and add that to the top of the path. This fixes the elevation issue and even works as a final solution to my actual problem by working event with my limited access to the invocation call through the MSBuild task.