0

I've been struggling with this all week so I'm hoping the experts here can help me out. I have an executable that absolutely must be run from the command line with arguments. What I'm trying to do is instead of launching the command prompt window, I'd like to send the data to the rich text box on my form.

If I setup a batch file and run the batch file with the correct code (running it as a Process), this works no problem. However, I'd like for the user to be able to enter their own arguments into a TextBox instead of creating a batch file and referencing it.

I can only get this application to run correctly by using Call Shell. However, I read that you can't output the data to a RichTextBox if you're using Call Shell and that it needs to be setup as a new Process. I just can't seem to get this running as a Process.

So the question is, is it possible to somehow output the Call Shell data to a RichTextBox control, or is there a way to get this thing to run as a process? The Visual Basic code below will get it to run, but won't output to the RichTextBox. I removed any code that I tried because every try was a failure.

This button will start the Process, or if the Process is running, it will kill it.

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    Dim exeargs As String = txtExeArgs.Text
    Dim p1() As Process
    Dim strCommand As String = "executable.exe " & exeargs & ""
    p1 = Process.GetProcessesByName("executable")
    Dim exepath As String = IO.Path.GetDirectoryName(Me.txtExeLocation.Text)

    If p1.Count <= 0 Then

        RichTextBox1.Clear()

        Call Shell("cmd.exe /c cd /d " & exepath & " & " & strCommand, 0)

    Else
        Dim killprocess = System.Diagnostics.Process.GetProcesses().Where((Function(p) p.ProcessName = "executable"))
        For Each p As Process In killprocess
            p.Kill()
        Next
        RichTextBox1.Clear()
    End If
End Sub
Jimi
  • 29,621
  • 8
  • 43
  • 61

1 Answers1

0

It's quite possible using System.Diagnostics.Process.
The actual result depends on how that program actually works.
It might require some tests to get its output right.

This is a generic procedure you can use to test if your executable behaves in a standard way.
It's using tracert.exe and ouputs its results in a RichTextBox control.

Note that the Process.Start() initialization uses the Process.SynchronizingObject() set to the RichTextBox control Parent Form to avoid InvokeRequired. But if you don't want to use a Synch object, Control.Invoke is handled anyway, using MethodInvoker delegate.

To switch to your executable, subsitute the StartProcess() method parameters as required.

Imports System.Diagnostics
Imports System.IO

Private CurrentProcessID As Integer = -1

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
    StartProcess("C:\Windows\System32\tracert.exe", "stackoverflow.com")
End Sub

Private Sub StartProcess(FileName As String, Arguments As String)

    Dim MyStartInfo As New ProcessStartInfo() With {
        .FileName = FileName,
        .Arguments = Arguments,
        .WorkingDirectory = Path.GetDirectoryName(FileName),
        .RedirectStandardError = True,
        .RedirectStandardOutput = True,
        .UseShellExecute = False,
        .CreateNoWindow = True
    }

    Dim MyProcess As Process = New Process() With {
        .StartInfo = MyStartInfo,
        .EnableRaisingEvents = True,
        ' Setting a SynchronizingObject, we don't need to BeginInvoke. 
        ' I leave it there anyway, in case there's no SynchronizingObject to set
        ' BeginInvoke can be used with or without a synchronization context.
        .SynchronizingObject = Me
    }

    MyProcess.Start()
    MyProcess.BeginErrorReadLine()
    MyProcess.BeginOutputReadLine()

    CurrentProcessID = MyProcess.Id

    AddHandler MyProcess.OutputDataReceived,
        Sub(sender As Object, e As DataReceivedEventArgs)
            If e.Data IsNot Nothing Then
                BeginInvoke(New MethodInvoker(
                Sub()
                    RichTextBox1.AppendText(e.Data + Environment.NewLine)
                    RichTextBox1.ScrollToCaret()
                End Sub))
            End If
        End Sub

    AddHandler MyProcess.ErrorDataReceived,
        Sub(sender As Object, e As DataReceivedEventArgs)
            If e.Data IsNot Nothing Then
                BeginInvoke(New MethodInvoker(
                Sub()
                    RichTextBox1.AppendText(e.Data + Environment.NewLine)
                    RichTextBox1.ScrollToCaret()
                End Sub))
            End If
        End Sub

    AddHandler MyProcess.Exited,
        Sub(source As Object, ev As EventArgs)
            MyProcess.Close()
            If MyProcess IsNot Nothing Then
                MyProcess.Dispose()
            End If
        End Sub
End Sub


Note:
If you need to terminate more that one running processes, you'll have to modify the code a bit more.
You could use a Class object to contain the Process Id, the Process Name (eventually) and a sequential value to maintain a reference to each process run.
Use a List(Of [Class]) for this.
You might also need to modify the StartProcess() method to pass a Control reference (the Control where the different processes output their results).
The code, as it is, needs very few modifications to achieve this.

Jimi
  • 29,621
  • 8
  • 43
  • 61
  • Wow! This worked without a hitch. Absolutely wonderful answer. Thank you so much! – NoobieMcNooberson Jul 12 '18 at 20:41
  • @NoobieMcNooberson Well, I'm glad it was useful. – Jimi Jul 12 '18 at 20:42
  • 1
    You should avoid using the `IsNothing()` function for the reasons mentioned [here](https://stackoverflow.com/a/24494417) and instead check `If MyProcess IsNot Nothing Then`. Apart from that, why did the downvoter think this was eligible for a downvote? – Visual Vincent Jul 12 '18 at 22:59
  • @Visual Vincent Old habit. I write it without even seen it. Thanks for your remark. – Jimi Jul 12 '18 at 23:03
  • 1
    Also, you can move `Dim mi As MethodInvoker = ...` outside the `InvokeRequired` check and then call `mi.Invoke()` in the `Else` statement instead. This allows you to use the `mi` delegate in both situations, which removes the need of having to write the code twice. :) – Visual Vincent Jul 12 '18 at 23:03
  • 2
    @Visual Vincent Yes, as I wrote, that was some kind of out-out. The intent was to let the OP choose one method or the other, deleting the part that was not necessary, depending on what he likes best (if you use a Synch object you don't need an `InvokeRequired` check; if you don't, you don't need the `Else` part). I'll give it a tweak, anyway. (Also, this is more or less what you find in the MSDN examples. It's easier to follow if you're not used to it). – Jimi Jul 12 '18 at 23:11
  • 2
    @Visual Vincent Edited. Should work fine both ways (you don't need to care about if you use a Synch object or not). Any further suggestion on your part is welcome :) – Jimi Jul 12 '18 at 23:30
  • Unrelated but related. :P Is there a way to capture the process ID once you start the process? So if I used two different buttons on my form to launch the executable twice, each with different arguments, then capture the PID so I can end the correct one if needed? – NoobieMcNooberson Jul 13 '18 at 03:40
  • @NoobieMcNooberson See the Edit. – Jimi Jul 13 '18 at 04:24
  • @Jimi I can't thank you enough. All of it is perfect and does exactly what I need it to. Thank you again! – NoobieMcNooberson Jul 14 '18 at 01:43