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.