Executing batch file from C# winforms ignores timeout

Viewed 272

All, I am attempting to execute a series of batch files via a C# winforms app. In this early stage, with a test batch file, I am unable to get the process execution to respect the timeout in my batch file unless i set UseShellExecute = true, which is something i am trying to avoid. My goal is to execute the script file and redirect the output to the GUI as shown in the code here:

    Process process;
    public void ExecuteScript(string workingDirectory, string batchFileName)
    {
        if (process != null)
            process.Dispose();
        
        process = new Process();
        process.StartInfo.WorkingDirectory = workingDirectory;
        process.StartInfo.FileName = workingDirectory + batchFileName;
        process.StartInfo.Arguments = "";
     
        process.StartInfo.CreateNoWindow = true;
        process.StartInfo.UseShellExecute = false;

        process.StartInfo.RedirectStandardOutput = true;
        process.StartInfo.RedirectStandardInput = true;
        process.EnableRaisingEvents = true;
        process.OutputDataReceived += proc_OutputDataReceived;

        process.Start();
        process.BeginOutputReadLine();
        process.Exited += OnProcessExit;
    }

    private void OnProcessExit(object sender, EventArgs e)
    {
        Console.WriteLine("the script has ended");
    }
   
    private void proc_OutputDataReceived(object sender, DataReceivedEventArgs e)
    {
        this.Invoke((Action)(() =>
        {
            textBox1.AppendText(Environment.NewLine + e.Data);

        }));

        (sender as Process)?.StandardInput.WriteLine();
    } 

my batch file looks like this:

@echo off
echo This is a running script
timeout /t 10
echo Done sleeping. Will Exit
exit

Is there an appropriate combination of settings i can call to prevent the command window from appearing, while still redirecting the output, and executing the script appropriately?

1 Answers

The problem with your code is that the timeout command is not supported when stdin is redirected. This is a good example of why one should always redirect both stdout and stderr. An error message is actually emitted from the batch file, but because you weren't capturing the stderr stream, you didn't see the error message. All too many questions here on Stack Overflow involving Process scenarios that "don't work" could be easily solved had the person looked at the stderr output.

A work-around to this limitation of the timeout command is to use the waitfor command instead, using a known-nonexistent signal name with a timeout value, e.g. waitfor /t 10 placeholder.

Here is a console program that is entirely self-contained and which demonstrates both the failure of the timeout command when stdin is redirected, as well as the work-around of waitfor:

    const string batchFileText =
@"@echo off
echo Starting batch file
timeout /t 5
waitfor /t 5 placeholder
echo Timeout completed
exit";

    static void Main(string[] args)
    {
        string batchFileName = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".bat");

        try
        {
            File.WriteAllText(batchFileName, batchFileText);

            ProcessStartInfo psi = new ProcessStartInfo
            {
                FileName = batchFileName,
                CreateNoWindow = true,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardInput = true,
                RedirectStandardError = true,
            };
            Process process = new Process
            {
                EnableRaisingEvents = true,
            };

            process.OutputDataReceived += Process_OutputDataReceived;
            process.ErrorDataReceived += Process_ErrorDataReceived;
            process.Exited += Process_Exited;
            process.StartInfo = psi;
            process.Start();
            process.BeginOutputReadLine();
            process.BeginErrorReadLine();
            process.WaitForExit();
        }
        finally
        {
            File.Delete(batchFileName);
        }
    }

    private static void Process_Exited(object sender, EventArgs e)
    {
        WriteLine("Process exited");
    }

    private static void Process_OutputDataReceived(object sender, DataReceivedEventArgs e)
    {
        if (e.Data != null)
        {
            WriteLine($"stdout: {DateTime.Now:HH:mm:ss.sss}: {e.Data}");
        }
    }

    private static void Process_ErrorDataReceived(object sender, DataReceivedEventArgs e)
    {
        if (e.Data != null)
        {
            WriteLine($"stderr: {DateTime.Now:HH:mm:ss.sss}: {e.Data}");
        }
    }

Note that the waitfor command writes a message to stderr if the timeout occurs (which it always will in this case). You may or may not want that to show up in the captured stderr stream. If not, you can redirect the stderr of that command specifically by using 2>nul. E.g. waitfor /t 10 placeholder 2>nul.

Related