Is there a good way to stream the results from an external process into a Visual Studio output pane?
Asked Answered
A

4

4

I have a custom output pane set up in a VsPackage similar to the following:

    ///--------------------------------------------------------------------------------
    /// <summary>This property gets the custom output pane.</summary>
    ///--------------------------------------------------------------------------------
    private Guid _customPaneGuid = Guid.Empty;
    private IVsOutputWindowPane _customPane = null;
    private IVsOutputWindowPane customPane
    {
        get
        {
            if (_customPane == null)
            {
                IVsOutputWindow outputWindow = GetService(typeof(SVsOutputWindow)) as IVsOutputWindow;
                if (outputWindow != null)
                {
                    // look for existing solution updater pane
                    if (_customPaneGuid == Guid.Empty || ErrorHandler.Failed(outputWindow.GetPane(ref _customPaneGuid, out _customPane)) || _customPane == null)
                    {
                        // create a new solution updater pane
                        outputWindow.CreatePane(ref _customPaneGuid, "My Output", 1, 1);
                        if (ErrorHandler.Failed(outputWindow.GetPane(ref _customPaneGuid, out _customPane)) || _customPane == null)
                        {
                            // pane could not be created and retrieved, throw exception
                            throw new Exception("Custom pane could not be created and/or retrieved");
                        }
                    }
                }
            }
            if (_customPane != null)
            {
                _customPane.Activate();
            }
            return _customPane;
        }
    }

And messages are sent to this pane using a method similar to:

    ///--------------------------------------------------------------------------------
    /// <summary>This method displays a message in the output area.</summary>
    /// 
    /// <param name="outputTitle">The title for the message.</param>
    /// <param name="outputMessage">The message to show.</param>
    /// <param name="appendMessage">Flag indicating whether message should be appended to existing message.</param>
    ///--------------------------------------------------------------------------------
    public void ShowOutput(string outputTitle, string outputMessage, bool appendMessage, bool isException)
    {
        if (appendMessage == false)
        {
            // clear output pane
            CustomPane.Clear();
        }

        if (outputTitle != string.Empty)
        {
            // put output title to output pane
            CustomPane.OutputString("\r\n" + outputTitle);
        }

        // put output message to output pane
        CustomPane.OutputString("\r\n" + outputMessage);

        if (isException == true)
        {
            // show message box
            MessageBox.Show(outputTitle + "\r\n" + outputMessage, outputTitle);
        }
    }

I have an external process that sends diagnostic results of the current solution to the console. It is set up similar to the following:

///-------------------------------------------------------------------------------- 
/// <summary>This method handles clicking on the Run Diagnostics submenu.</summary> 
///  
/// <param term='inputCommandBarControl'>The control that is source of the click.</param> 
/// <param term='handled'>Handled flag.</param> 
/// <param term='cancelDefault'>Cancel default flag.</param> 
///-------------------------------------------------------------------------------- 
protected void RunDiagnostics_Click(object inputCommandBarControl, ref bool handled, ref bool cancelDefault) 
{ 
    try 
    { 
        // set up and execute diagnostics thread 
        RunDiagnosticsDelegate RunDiagnosticsDelegate = RunDiagnostics; 
        RunDiagnosticsDelegate.BeginInvoke(RunDiagnosticsCompleted, RunDiagnosticsDelegate); 
    } 
    catch (Exception ex) 
    { 
        // put exception message in output pane 
        CustomPane.OutputString(ex.Message); 
    } 
} 

protected delegate void RunDiagnosticsDelegate(); 

///-------------------------------------------------------------------------------- 
/// <summary>This method launches the diagnostics to review the solution.</summary> 
///-------------------------------------------------------------------------------- 
protected void RunDiagnostics() 
{ 
    try 
    { 
        // set up diagnostics process 
        string solutionDir = System.IO.Path.GetDirectoryName(_applicationObject.Solution.FullName); 
        System.Diagnostics.ProcessStartInfo procStartInfo = new System.Diagnostics.ProcessStartInfo(@"MyDiagnostics.exe", solutionDir); 
        procStartInfo.RedirectStandardOutput = true; 
        procStartInfo.UseShellExecute = false; 
        procStartInfo.CreateNoWindow = true; 
        System.Diagnostics.Process proc = new System.Diagnostics.Process(); 
        proc.StartInfo = procStartInfo; 

        // execute the diagnostics 
        proc.Start(); 

        // put diagnostics output to output pane 
        CustomPane.OutputString(proc.StandardOutput.ReadToEnd()); 
        CustomPane.OutputString("Diagnostics run complete."); 
    } 
    catch (Exception ex) 
    { 
        // put exception message in output pane 
        CustomPane.OutputString(ex.Message); 
    } 
} 

///-------------------------------------------------------------------------------- 
/// <summary>This method handles completing the run diagnostics thread.</summary> 
///  
/// <param name="ar">IAsyncResult.</param> 
///-------------------------------------------------------------------------------- 
protected void RunDiagnosticsCompleted(IAsyncResult ar) 
{ 
    try 
    { 
        if (ar == null) throw new ArgumentNullException("ar"); 

        RunDiagnosticsDelegate RunDiagnosticsDelegate = ar.AsyncState as RunDiagnosticsDelegate; 
        Trace.Assert(RunDiagnosticsDelegate != null, "Invalid object type"); 

        RunDiagnosticsDelegate.EndInvoke(ar); 
    } 
    catch (Exception ex) 
    { 
        // put exception message in output pane 
        CustomPane.OutputString(ex.Message); 
    } 
} 

When I launch this external process from the VSPackage, I would like to stream these results (indirectly) to the custom output pane, showing messages as the diagnostics tool is reporting them. Is there a good way to do that?

Armington answered 1/12, 2011 at 17:29 Comment(2)
I assume that your example works, but isn't "streaming" the output but writing it all in one big batch? Because according to the docs, CustomPane.OutputString(proc.StandardOutput.ReadToEnd()) will block the thread until the process ends. So you are looking for a way to have the output "pushed" to you instead of "pulling" for it, right?Showker
@J. Tihon, correct, I don't want to stream the standard output directly to the output pane (blocking the process), but want to write messages to the output pane as they occur.Armington
S
3

Apparently you have to use the Process.BeginOutputReadLine() method. An example can be found here: System.Diagnostics.Process.BeginOutputReadLine.

Showker answered 1/12, 2011 at 17:53 Comment(1)
+1 thanks, this approach worked with messages being received asyncronously.Armington
A
3

Updated RunDiagnostics, mostly utilizing J. Tihon's answer and some of the_drow's answer:

///--------------------------------------------------------------------------------  
/// <summary>This method launches the diagnostics to review the solution.</summary>  
///--------------------------------------------------------------------------------  
protected void RunDiagnostics()  
{  
    try  
    {   
        // set up diagnostics process  
        string solutionDir = System.IO.Path.GetDirectoryName(_applicationObject.Solution.FullName);  
        System.Diagnostics.ProcessStartInfo procStartInfo = new System.Diagnostics.ProcessStartInfo(@"MyDiagnostics.exe", solutionDir);  
        procStartInfo.RedirectStandardOutput = true;  
        procStartInfo.UseShellExecute = false;  
        procStartInfo.CreateNoWindow = true;  
        System.Diagnostics.Process proc = new System.Diagnostics.Process();  
        proc.StartInfo = procStartInfo;
        proc.StartInfo.RedirectStandardOutput = true;
        proc.OutputDataReceived += (object sendingProcess, DataReceivedEventArgs outLine)
             => ShowOutput(String.Empty, outLine.Data, true, false);

        // execute the diagnostics  
        proc.Start();
        proc.BeginOutputReadLine();
    }  
    catch (Exception ex)  
    {  
        // put exception message in output pane  
        CustomPane.OutputString(ex.Message);  
    }  
}  
Armington answered 1/12, 2011 at 19:26 Comment(0)
C
1

You can use a listener and attach the process's stdout to it.

ConsoleTraceListener listener = new ConsoleTraceListener(process.StandardOutput);
Debug.Listeners.Add(listener);

Make sure you remove it after the process ends:

proc.Exited += () => Debug.Listeners.Remove(listener);

You'll need to use the process' OutputDataReceived event and than attach a listener:

ConsoleTraceListener listener = new ConsoleTraceListener();
Debug.Listeners.Add(listener);

proc.OutputDataReceived += (object sendingProcess, DataReceivedEventArgs outLine) => Trace.WriteLine(outLine.Data);

Make sure you remove it after the process ends:

proc.Exited += (object sender, EventArgs e) => Debug.Listeners.Remove(listener);
Crabber answered 1/12, 2011 at 17:39 Comment(3)
ConsoleTraceListener doesn't have such a constructor overload. (System.Diagnostics.Process.StandardOutput is of type StreamReader)Showker
+1 for the listener approach, but the messages weren't being received for some reason. proc.Exited += (object sender, EventArgs e) => Debug.Listeners.Remove(listener);Armington
@DaveClemmer: Strange, and thanks for the code fix. You could have just edited it.Crabber
E
1

Although the OutPutDataReceived+BeginOutputReadLine looks like a more elegant and simple solution, I'll give an alternative. I solved the problem with a BackgroundWorker, and a ProcessOutPutHandler inspired from here. This approach also handles messages from stdout and stderr separately, and I can report progress to the BackgroundWorker depending on the output. Here I use the standard VS Output Window for the output, but should work with your OutputPane just as well:

public class ProcessOutputHandler
{
    public Process proc { get; set; }
    public string StdOut { get; set; }
    public string StdErr { get; set; }
    private IVsOutputWindowPane _pane;
    private BackgroundWorker _worker;

    /// <summary>  
    /// The constructor requires a reference to the process that will be read.  
    /// The process should have .RedirectStandardOutput and .RedirectStandardError set to true.  
    /// </summary>  
    /// <param name="process">The process that will have its output read by this class.</param>  
    public ProcessOutputHandler(Process process, BackgroundWorker worker)
    {
        _worker = worker;
        proc = process;
        IVsOutputWindow outputWindow =
        Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow;

        Guid guidGeneral = Microsoft.VisualStudio.VSConstants.OutputWindowPaneGuid.GeneralPane_guid;
        int hr = outputWindow.CreatePane(guidGeneral, "Phone Visualizer", 1, 0);
        hr = outputWindow.GetPane(guidGeneral, out _pane);
        _pane.Activate();
        _pane.OutputString("Starting Ui State workers..");

        StdErr = "";
        StdOut = "";
        Debug.Assert(proc.StartInfo.RedirectStandardError, "RedirectStandardError must be true to use ProcessOutputHandler.");
        Debug.Assert(proc.StartInfo.RedirectStandardOutput, "RedirectStandardOut must be true to use ProcessOutputHandler.");
    }

    /// <summary>  
    /// This method starts reading the standard error stream from Process.  
    /// </summary>  
    public void ReadStdErr()
    {
        string line;
        while ((!proc.HasExited) && ((line = proc.StandardError.ReadLine()) != null))
        {
            StdErr += line;
            _pane.OutputString(line + "\n");
            // Here I could do something special if errors occur
        }
    }
    /// <summary>  
    /// This method starts reading the standard output sream from Process.  
    /// </summary>  
    public void ReadStdOut()
    {
        string line;
        while ((!proc.HasExited) && ((line = proc.StandardOutput.ReadLine()) != null))
        {
            StdOut += line;
            _pane.OutputString(line + "\n");
            if (_worker != null && line.Contains("Something I'm looking for"))
            {                            
               _worker.ReportProgress(20, "Something worth mentioning happened");
            }
        }
    }

}

And usage:

void RunProcess(string fileName, string arguments, BackgroundWorker worker)
{
  // prep process
  ProcessStartInfo psi = new ProcessStartInfo(fileName, arguments);
  psi.UseShellExecute = false;
  psi.RedirectStandardOutput = true;
  psi.RedirectStandardError = true;
  // start process
  using (Process process = new Process())
  {
    // pass process data
    process.StartInfo = psi;
    // prep for multithreaded logging
    ProcessOutputHandler outputHandler = new ProcessOutputHandler(process,worker);
    Thread stdOutReader = new Thread(new ThreadStart(outputHandler.ReadStdOut));
    Thread stdErrReader = new Thread(new ThreadStart(outputHandler.ReadStdErr));
    // start process and stream readers
    process.Start();
    stdOutReader.Start();
    stdErrReader.Start();
    // wait for process to complete
    process.WaitForExit();
   }
}

And this is called from the BackgroundWorker DoWork method, the Worker passed as a reference.

Encumber answered 2/12, 2011 at 8:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.