0

I've been playing around with building a little GUI for automating a command line server (okay, it's Minecraft BDS). While I was doing some testing, I accidentally (more than once) stopped the debugger before the server was properly shut down, which then left the executable's process "orphaned". I couldn't even find it in the Task Manager, so I fell back to adding a check in my code for any already running instances of bedrock_server.exe, then simply killing it. However, I'd like to be able to allow the server software to shut down "normally" instead of just terminating the process.

My question is, because the command window is not visible and is no longer "tied" to an instance of my application, is there any way to send it the stop command to allow it to shut down gracefully?

I've tried using StandardInput, but since the current instance of my application isn't the instance that actually started the process, it can't write to the StandardInput of the orphaned process:

myProc.StandardInput.WriteLine("stop")

results in the exception, System.InvalidOperationException: 'StandardIn has not been redirected.'

I thought about using SendKeys:

AppActivate(myProc.Id)
My.Computer.Keyboard.SendKeys("stop")

but the AppActivate fails when I attempt to AppActivate the process with the exception, System.ArgumentException: 'Process '[Id]' was not found.'

Using myProc.Kill() does terminate the process, I was just wondering if there might be a less "nuclear" option.

Another reason I'd like to figure this out is that I'd like to be able to reuse an existing/running server process rather than killing it. However, if I can't figure out how to hook into that process to issue commands, my only option (at this time) is to kill the process and start over with a fresh instance.


MY CODE

For reference, the code I'm using to start the server is:

'------------------------------
'PUBLIC VARIABLE DECLARATION LOCATED IN ANOTHER FILE
Public BDS_PROC As Process
'------------------------------

        Dim ServerStartInfo As New ProcessStartInfo

        With ServerStartInfo
            .FileName = <PATH TO bedrock_server.exe>
            .UseShellExecute = False
            .WindowStyle = ProcessWindowStyle.Hidden
            .CreateNoWindow = True
            .RedirectStandardError = True
            .RedirectStandardOutput = True
            .RedirectStandardInput = True
        End With

        BDS_PROC = New Process

        With BDS_PROC
            .StartInfo = ServerStartInfo
            .EnableRaisingEvents = True
            AddHandler .OutputDataReceived, AddressOf BDSMessageReceived
            AddHandler .ErrorDataReceived, AddressOf BDSErrorReceived
            .Start()
            .BeginErrorReadLine()
            .BeginOutputReadLine()
        End With

and the full check for currently "orphaned" running processes (with my failed attempts to send the stop command commented out) is:

    Public Function IsInstanceRunning() As Boolean
        Dim IsRunning As Boolean = True
        Dim Proc As Process() = Process.GetProcessesByName("bedrock_server")

        If Proc.Count > 0 Then
            If MessageBox.Show("The server is already running [" & Proc.Count.ToString & " instance(s)]." & vbCrLf & vbCrLf &
                               "Do you want to stop the currently running server?",
                               "SERVER RUNNING",
                               MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) = DialogResult.Yes Then
                Try
                    For p As Integer = 0 To Proc.Count - 1
                        'Proc(p).StandardInput.WriteLine("stop")
                        Proc(p).Kill()
                    Next p

                    IsRunning = False
                Catch ex As Exception
                    'Try
                    '    AppActivate(Proc(0).Id)
                    '    My.Computer.Keyboard.SendKeys("stop")
                    'Catch ex2 As Exception

                    'End Try
                    MessageBox.Show(ex.Message)
                End Try
            End If
        End If

        Return IsRunning
    End Function

EDIT:

I found a suggestion with code from a question on C# Corner and attempted to adopt that implementation for my purposes. I didn't get any exceptions or errors but, unfortunately, neither did it actually stop the server. Here's that code for reference, in case it helps someone else:

Imports System.Runtime.InteropServices
Imports System.Text

Module PUBLIC
    <DllImport("kernel32.dll")>
    Private Function GetConsoleTitle(ByVal lpConsoleTitle As StringBuilder, ByVal nSize As UInteger) As UInteger
    End Function

    <DllImport("user32.dll")>
    Private Function FindWindow(ByVal ZeroOnly As IntPtr, ByVal lpWindowName As String) As IntPtr
    End Function

    <DllImport("kernel32.dll", SetLastError:=True)>
    Private Function AttachConsole(ByVal dwProcessId As UInteger) As Boolean
    End Function

    <DllImport("kernel32")>
    Private Function FreeConsole() As Boolean
    End Function

    <DllImport("user32.dll")>
    Private Function SetForegroundWindow(ByVal hWnd As IntPtr) As Boolean
    End Function

    Public Function IsInstanceRunning() As Boolean
        Dim IsRunning As Boolean = True
        Dim Proc As Process() = Process.GetProcessesByName("bedrock_server")

        If Proc.Count > 0 Then
            If MessageBox.Show("The server is already running [" & Proc.Count.ToString & " instance(s)]." & vbCrLf & vbCrLf &
                               "Do you want to stop the currently running server?",
                               "SERVER RUNNING",
                               MessageBoxButtons.YesNo, MessageBoxIcon.Exclamation) = DialogResult.Yes Then
                Try
                    For p As Integer = 0 To Proc.Count - 1
                        FreeConsole()
                        Dim ok As Boolean = AttachConsole(CUInt(Proc(p).Id))

                        If Not ok Then
                            Dim [error] As Integer = Marshal.GetLastWin32Error()
                            MessageBox.Show([error].ToString)
                        Else
                            Dim sb As StringBuilder = New StringBuilder(256)
                            GetConsoleTitle(sb, 256UI)
                            Dim hWnd As IntPtr = FindWindow(IntPtr.Zero, sb.ToString())
                            SetForegroundWindow(hWnd)
                            SendKeys.SendWait("stop{ENTER}")
                        End If
                    Next p

                    IsRunning = False
                Catch ex As Exception
                    MessageBox.Show(ex.Message)
                End Try
            End If
        End If

        Return IsRunning
    End Function
End Module

The problem I'm seeing is that, because the console application has been started without a window, there's nothing really for me to hook into with this code. I'm still left having to Kill() the process.

G_Hosa_Phat
  • 976
  • 2
  • 18
  • 38
  • `BDS_PROC` represents the Process you run. Use that object (provided that you stored it as a Field or it's otherwise accessible outside the scope where you configured it.) – Jimi Mar 10 '20 at 17:56
  • It would be accessible if I were still running the same instance of my application that spawned the process, but since the debugger has been stopped and restarted, the `BDS_PROC` that started the process is no longer "tied" to that process (unless I'm missing/misunderstanding something here) – G_Hosa_Phat Mar 10 '20 at 17:59
  • Well, you can get it back with `Process.GetProcessesByName([The Name]).FirstOrDefault()` or you could even store the `Process.Id` and verify if it still exists when the app is run again, then get it back with `Process.GetProcessById()`. – Jimi Mar 10 '20 at 18:04
  • As my code above should demonstrate, I have tried to reconnect to the process using `GetProcessesByName` to find the running process, but I still can't hook into the `StandardInput` or get there through `AppActivate` using the process ID to `SendKeys` to it. Again, unless, I'm missing or misunderstanding something, I'm just not able to "get it back". **EDIT**: I didn't see the `FirstOrDefault` bit. I'll give that a try, although I'm already iterating through the found processes (and I only get one) from `GetProcessesByName`. – G_Hosa_Phat Mar 10 '20 at 18:09
  • Unfortunately, that didn't work either. I appreciate the suggestions, but I have a feeling that the "**no window**" element of my question is what's causing most of my trouble. – G_Hosa_Phat Mar 10 '20 at 18:52
  • Whether the Process has a Window or not is irrelevant. `dim proc = Process.GetProcessesByName().FirstOrdDefault()` returns the first Process that has the specified name. It may not be the one you're looking for. Remove `FirstOrDefault()` (and have `dim processes as Process() = Process.GetProcessesByName("name")`) to get all the processes with the same name, see whether you have more than one running. When you get the good one, do whatever you need to do with the Process object. – Jimi Mar 10 '20 at 19:07
  • Thank you again, but, as shown in the code I provided in the question, that's exactly what I tried to do using various methods to send the `stop` command with no success. As stated, I *can* `Kill()` the orphaned process, but that seems to be the only way I can successfully get rid of it. I cannot "hook into" the running process to issue the command. – G_Hosa_Phat Mar 10 '20 at 19:18
  • In the code you posted, you're not redirecting StandardInput of the process you identified. Do this, run Notepad, open up your TaskManager and write down its Process.Id, then write: `dim p as Process = Process.GetProcessById([Notepad.Process.Id]) p.EnableRaisingEvents = true AddHandler p.Exited, Sub() Console.WriteLine("exited")`, put a breakpoint in `Console.WriteLine()`, run this code and close Notepad. What happens? (redirecting StdIn and Stdout of notepad doesn't make much sense, but you should try with your process) – Jimi Mar 10 '20 at 19:33
  • All right. I ran this test and it executed the handler code when closing Notepad. I created a new `ProcessStartInfo` object in my loop using the found process' existing `StartInfo` property (`Dim psi As ProcessStartInfo = Proc(p).StartInfo`), and then `psi.RedirectStandardInput = True`. Unfortunately, `Proc(p).StandardInput.WriteLine("stop")`, still returns `'StandardIn has not been redirected.'` I only have one match from `Process.GetProcessesByName("bedrock_server")`, but I'm certain this is the one I'm looking for b/c when I kill it, everything works normally again. – G_Hosa_Phat Mar 10 '20 at 19:58
  • I even tried accessing the base `IO.Stream` for `Proc(p).StandardInput` and writing directly to that, but got the same exception (`Dim s as IO.Stream = Proc(p).StandardInput.BaseStream` `Dim w As New IO.StreamWriter(s)` `w.WriteLine("stop")` --> `StandardIn has not been redirected.`) I'm really not trying to be obtuse here, and I definitely appreciate your help. Perhaps I'm totally misunderstanding what you're getting at, but I kinda feel like I'm chasing my tail a bit here. – G_Hosa_Phat Mar 10 '20 at 20:10
  • 1
    Nope, I read too quickly your post and actually misunderstood the purpose. Yes, it's quite complicated to hook into a Process that has already been started using the Process class. The Pipe it creates is dead after the Process that first generates it is disposed. I'll see if I can propose something doable, without attaching a debugger to the running process. But I cannot test your executable, so any other *test* tends to be quite volatile. – Jimi Mar 10 '20 at 22:18
  • Okay, thank you very much. If you *wanted* to, it is possible for you to reproduce pretty much the entire issue, but it would require you to download and run the Minecraft Bedrock Dedicated Server, and I certainly don't expect you to go to those lengths. Still, I truly appreciate all of your time and effort. I don't know that I'll be able to achieve what I really want, but this has at least let me know some of the limitations I'll need to take into account. – G_Hosa_Phat Mar 11 '20 at 00:30
  • Also, if there's a better way for me to start the console process in my application while still ***not*** creating the console window (*I'm redirecting the output to a form in my WinForms application, so I'd rather not have yet another window open if I can avoid it*) *and* allowing me to read from and feed into that console, I'm open to any suggestions along that vein as well. – G_Hosa_Phat Mar 11 '20 at 17:48
  • See the `StreamWriter` used to write to `StandardInput` here: [How do I get output from a command to appear in a control on a Form in real-time?](https://stackoverflow.com/a/51682585/7444103) (C#, but it should be clear enough). To be tested on your Process: it may have specific requirements. – Jimi Mar 11 '20 at 17:58
  • Yes, that looks virtually identical to what I've currently got in my code. Starting the console process and reading from/writing to that console currently works pretty much flawlessly as long as my application is running. The problem arises if my application crashes/closes while the console is still running, not allowing it to shut down properly. The example also seems to match with my tests in the above comments where I tried to create new `ProcessStartInfo` and `IO.Stream` objects and "hook" them into the `STDIN` of the orphaned console (retrieved from `Process.GetProcessesByName()`). – G_Hosa_Phat Mar 11 '20 at 18:32

0 Answers0