Link to home
Start Free TrialLog in
Avatar of balabaster
balabaster

asked on

Sending keystrokes to background process

Hi, I've got a VB 2005 application function that spawns a background process - in this function I have the following code:


'Global declarations
'------------------------

            Private oProc As System.Diagnostics.Process


'Run process code
'----------------------

            Dim sShellCommand As String = "C:\Program Files\Wireshark\TShark.exe"
            Dim sArgumentString As String = "-i "& iInterfaceID & " -w """& sOutputFile & """ & -a duration:"& lDuration & " -a files:"& lNumFiles & " -a:filesize:"& lFileSize
            Dim oProcInfo As New System.Diagnostics.ProcessStartInfo(sShellCommand)
            With oProcInfo
                .UseShellExecute = False

                'I tried changing this to true and passing a CTRL+C
                'through a Streamwriter - but I couldn't get it to work
                .RedirectStandardInput = False

                .CreateNoWindow = True
                .WindowStyle = ProcessWindowStyle.Hidden
                .Arguments = sArgumentString
            End With

            oProc = System.Diagnostics.Process.Start(oProcInfo)

            'This code basically stops here until the process raises an Exited event which either happens after the specified amount of time has elapsed
            'and TShark has finished doing its thing, or when the process is stopped (interrupted) by the user.
            oProc.WaitForExit()
            oProc.Dispose()

'------------------
'End code block


Now - this process spawns perfectly and does everything it needs to do.  If you know anything about Wireshark - the TShark process will basically run for a specified amount of time [lDuration] and then close the file it's writing.  This all works perfectly.

I have another routine that allows the user to kill the process early that contains the following code:


'Interrupt process code
'-----------------------------

            oProc.Close() 'Need to replace this line with a means of sending CTRL+C to oProc
            oProc.WaitforExit(1000)
            If Not oProc.HasExited Then
                       oProc.Kill()
            End If

'------------------
'End code block



Now - when this process closes, it *should* raise an exited event which would cause the first block of code's "WaitForExit()" line to continue what that block of code is doing.  The problem is that oProc.Close() doesn't appear able to stop the TShark instance.  So I added the "failsafe" mechanism which says if it hasn't closed after a second, then kill it.  oProc.Close() won't (for whatever reason) stop TShark, so the Kill mechanism is always invoked after a second.  The oProc.Kill() terminates TShark immediately - the first block of code receives the "Exited" event and carries on its merry way as it is supposed to do.  However, if you don't stop TShark cleanly (CTRL+C when running manually), the file that TShark was in the middle of writing is just abandoned and not closed properly. Consequently, when you open the file in Wireshark it complains that the file was terminated mid-packet.

When running TShark from the command line, the only way I've found to interrupt the TShark process is using the CTRL+C keystroke.  This initiates TShark's close process which closes the file cleanly.  I can't however find a way to successfully send the CTRL+C sequence to oProc - which I would like to keep running as a background process.

This should only be a minor annoyance, but it's bugging the hell out of me.  I've tried every way I can think of to get the process to close cleanly.  Anyone got any ideas?
ASKER CERTIFIED SOLUTION
Avatar of clockwatcher
clockwatcher

Link to home
membership
This solution is only available to members.
To access this solution, you must be a member of Experts Exchange.
Start Free Trial
Avatar of balabaster
balabaster

ASKER

Hmm....looks fairly straight forward, I'll give this a go.  The only thing that concerns me is that this code is interacted by remote...i.e. the client application resides on one machine and the code that spawns the TShark process is on another.  Consequently the TShark process needs to run in the background so that it doesn't disturb the user that's logged into that machine.  If I set the WindowStyle to hidden and CreateNoWindow to true - can I still generate the console event?  Will it still pick it up?  This is where I've been coming unstuck with this code time and again.  Once I set the process to run in the background, it appears not to receive control events with the single exception of Kill().
The console app will still have a console associated with it regardless of the window state.  

Changing my button1_click to:

   Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Dim sArgumentString As String = "/c dir %systemroot% | more"

        Dim sShellCommand As String = "cmd.exe"
        Dim oProcInfo As New System.Diagnostics.ProcessStartInfo(sShellCommand)
        With oProcInfo
            .CreateNoWindow = True
            .WindowStyle = ProcessWindowStyle.Hidden
            .UseShellExecute = False
            .RedirectStandardInput = False
            .Arguments = sArgumentString
        End With

        oProc = System.Diagnostics.Process.Start(oProcInfo)


    End Sub


The example still works.  You just can't see the window.  But in the TaskList, you'll see cmd.exe exit when you hit button2.  Whether or not it'll work for what you're describing, don't know.
Um...okay, I ran that with the exception of FreeConsole (as my server app currently runs as a console for debugging purposes) and it was killing the server the minute FreeConsole ran.

    Public Sub StopPacketCapture()

        If Not AttachConsole(oProc.ID) Then
            Console.WriteLine("Unable to connect to process.")
        End If

        If Not GenerateConsoleCtrlEvent(0, oProc.ID) Then
            Console.WriteLine("Failed to stop process.")
        End If

        'Console.WriteLine("Request to stop capturing packets received at {0}.", Now())
        'Dim _Proc As System.Diagnostics.Process = System.Diagnostics.Process.GetProcessById(_ProcessID)
        '_Proc.Kill()
        'Console.WriteLine("Packet capture stopped at {0}.", Now())

        'If Not FreeConsole() Then
        '   Console.WriteLine("Failed to free console process.")
        'End If

    End Sub



Here's my console output:

TShark started with process ID 4056.
Packet capture started at 8/5/2007 2:43:28 PM
Unable to connect to process.
Failed to stop process.
If it's any help, TShark is the command line utility that comes with Wireshark that allows you to monitor network traffic on a network adapter.  It's a free application found at http://www.wireshark.org/.
So your app is already a console app?  If that's the case, the the tshark console is already associated with your console.  Try:

Public Sub StopPacketCapture()

       If Not SetConsoleCtrlHandler(AddressOf ctrlhandler, True) Then
            Console.WriteLine("Failed to set up signal handler: " & GetLastError())
        End If

        'generate the sigint
        If Not GenerateConsoleCtrlEvent(0, 0) Then
            Console.WriteLine("Failed to send sigint: " & GetLastError())
        End If

End Sub


The following is an example of a console app that launches tshark and successfully sends tshark a SIGINT and continues running itself.

Imports System.Runtime.InteropServices

Module Module1


    <DllImport("kernel32")> Public Function GenerateConsoleCtrlEvent(ByVal dwCtrlEvent As Integer, ByVal dwProcessId As Integer) As Boolean
    End Function
    <DllImport("kernel32")> Public Function GetLastError() As Integer
    End Function
    <DllImport("kernel32")> Public Function AttachConsole(ByVal dwProcessId As Integer) As Boolean
    End Function
    <DllImport("kernel32")> Public Function FreeConsole() As Boolean
    End Function

    Public Delegate Function CtrlEventHandler(ByVal ctrlType As Integer) As Boolean

    <DllImport("kernel32")> Public Function SetConsoleCtrlHandler(ByVal callback As CtrlEventHandler, ByVal add As Boolean) As Boolean
    End Function

    Dim logfile As System.IO.StreamWriter

    Dim oProc As System.Diagnostics.Process


    Private Sub spawn_app()

        Dim sArgumentString As String = ""

        Dim sShellCommand As String = "c:\program files\wireshark\tshark.exe"
        Dim oProcInfo As New System.Diagnostics.ProcessStartInfo(sShellCommand)
        With oProcInfo
            .UseShellExecute = False
            .RedirectStandardInput = False
            .CreateNoWindow = False
            .WindowStyle = ProcessWindowStyle.Normal
            .Arguments = sArgumentString
        End With

        oProc = System.Diagnostics.Process.Start(oProcInfo)


    End Sub

    Public Function ctrlhandler(ByVal ctrlType As Integer) As Boolean

        logfile.WriteLine("ctrltype: " & ctrlType)
        Return True

    End Function

    Private Sub ctrlc_app()


        'generate the sigint
        If Not GenerateConsoleCtrlEvent(0, 0) Then
            logfile.WriteLine("Failed to send sigint: " & GetLastError())
        End If



    End Sub


    Sub Main()

        System.IO.File.Delete("logfile.txt")

        logfile = New System.IO.StreamWriter("logfile.txt")

        If Not SetConsoleCtrlHandler(AddressOf ctrlhandler, True) Then
            logfile.WriteLine("Failed to set up signal handler: " & GetLastError())
        End If

        Dim i As Integer
        For i = 1 To 3

            spawn_app()
            System.Threading.Thread.Sleep(5000)
            ctrlc_app()
            System.Threading.Thread.Sleep(5000)

        Next


        Console.WriteLine("done")


        logfile.Close()

    End Sub

End Module


Thank you so much; I got it working.  I don't know why such a little thing was stressing me out.  You'd think that such an inconsequential thing as a file not being closed properly is no big deal, but it's just been bugging the crap out of me all weekend.  It works now though, so thank you!
If you don't want the output of tshark mixed in with your console then create the process with the following:

        With oProcInfo
            .UseShellExecute = False
            .RedirectStandardOutput = True
            .RedirectStandardError = True
            .CreateNoWindow = False
            .Arguments = sArgumentString
        End With

Is there a way to do this same procedure with a non-console process?  For instance - if I were to spawn my TShark instance from a Windows Service, it wouldn't be run from the console, it would be run as a background process.  How do I have to modify this approach to get it to work in that scenario?
In that case, it'd be like the windows form example that I posted earlier.  You'll need to specifically attach to the console associated with your launched tshark process id-- other than that it should be the same.
Okay, thanks - I'm just investigating now - I was trying the PostMessage function to try and send the WM_QUIT or WM_CLOSE message - but neither seems to want to work.  For instance:

I changed my ProcessInfo object initialization to:

            With oProcInfo
                .UseShellExecute = True
                .RedirectStandardOutput = False
                .RedirectStandardError = False
                .RedirectStandardInput = False
                '.CreateNoWindow = False
                '.WindowStyle = ProcessWindowStyle.Hidden
                .Arguments = ArgumentString
            End With
            oProc = System.Diagnostics.Process.Start(oProcInfo)


Which would cause TShark to be spawned into a different window - effectively doing the same thing as it would if I initiated it from a Windows Service.  *Now* when the GenerateConsoleCtrlEvent(0, oProc.ID) fires, it doesn't terminate the TShark process anymore.
I don't think you'll ever get GenerateConsoleCtrlEvent(0,oProc.ID) to work.  From the documentation:

   Generates a CTRL+C signal. This signal cannot be generated for process groups. If dwProcessGroupId is nonzero, this function will succeed, but the CTRL+C signal will not be received by processes within the specified process group.

You can't send a CTRL+C to a specific process attached to the console.  It has to go to all of them.  And contrary to the documentation, a non-zero process group results in an invalid handle error.  It may succeed if you use the process id of your process (not the spawned process).  But either way, it's not what you want to do.

You need to use GenerateConsoleCtrlEvent(0,0).  It will send a ctrl-c to all the processes associated with the console.  That's why you need the handler set up so your program isn't affected by it.

And, I really think you want to make sure .CreateNoWindow = false.  It's probably the default, but pretty sure you want your console handles set up-- .CreateNoWindow = true prevents that.
Yeah, that's what I'd feared...I saw that documentation earlier.  It looks like I have one of only a few choices - none of them are ideal:

1). Either suck up the fact that the file isn't closed properly and use the process.kill() method. Which for some reason I find as a personal affront to me.

2). Have my Wireshark Server run as a console process - but this will be run on client machines, and I don't feel this is ideal as users tend to close down console windows.  On top of that, I find it ugly and unprofessional...which is why I was trying to migrate my code away from this towards a Windows Service.

3). Dig into the Wireshark API and actually write the process to record the packet data myself instead of relying on the TShark application to do it for me...therefore bypassing the whole issue.

Sadly, I don't relish any of these ideas...the "it's good enough" option has never really flown with me - but I'm on vacation in 5 days from now, so digging into the Wireshark API and writing it all before I leave isn't really an option.  That can be my project when I come home.

Thanks for your help though - I really appreciate it.
I lied about the .CreateNoWindow.   I forgot I already had it successfully working like that in my windows form app.  This works fine for me.   I don't really understand where you are running into problems.  It should work the same in a service as it does from a windows app.

Imports System.Runtime.InteropServices

Public Class Form1

    <DllImport("kernel32")> Public Shared Function GenerateConsoleCtrlEvent(ByVal dwCtrlEvent As Integer, ByVal dwProcessId As Integer) As Boolean
    End Function
    <DllImport("kernel32")> Public Shared Function GetLastError() As Integer
    End Function
    <DllImport("kernel32")> Public Shared Function AttachConsole(ByVal dwProcessId As Integer) As Boolean
    End Function
    <DllImport("kernel32")> Public Shared Function FreeConsole() As Boolean
    End Function

    Public Delegate Function CtrlEventHandler(ByVal ctrlType As Integer) As Boolean

    <DllImport("kernel32")> Public Shared Function SetConsoleCtrlHandler(ByVal callback As CtrlEventHandler, ByVal add As Boolean) As Boolean
    End Function


    Dim oProc As System.Diagnostics.Process


    Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click

        Dim sArgumentString As String = "-w capfile.cap"

        Dim sShellCommand As String = "c:/program files/wireshark/tshark.exe"
        Dim oProcInfo As New System.Diagnostics.ProcessStartInfo(sShellCommand)
        With oProcInfo
            .CreateNoWindow = True
            .WindowStyle = ProcessWindowStyle.Hidden
            .UseShellExecute = False
            .RedirectStandardInput = False
            .Arguments = sArgumentString
        End With

        oProc = System.Diagnostics.Process.Start(oProcInfo)


    End Sub

    Public Function ctrlhandler(ByVal ctrlType As Integer) As Boolean

        Debug.Print("ctrltype: " & ctrlType)
        Return True

    End Function
    Private Sub Button2_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button2.Click

        If Not AttachConsole(oProc.Id) Then
            Debug.Print("attach: " & GetLastError())
        End If

        'our app needs to ignore the sigint
        If Not SetConsoleCtrlHandler(AddressOf ctrlhandler, True) Then
            Debug.Print("handler: " & GetLastError())
        End If

        'generate the sigint
        If Not GenerateConsoleCtrlEvent(0, 0) Then
            Debug.Print("err: " & GetLastError())
        End If

        'detach our console
        If Not FreeConsole() Then
            Debug.Print("free: " & GetLastError())
        End If

    End Sub

End Class