Borealis Server Manager (BSM) Development Discussion Thread
103 replies, posted
[B]STATUS UPDATE:[/B]
- After over 6 hours of different methods, I came up with the current, yet rough, iteration of Steam Guard code support. There are a lot of servers that cannot be downloaded via SteamCMD unless you log in, and to add insult to injury, Steam Guard kicks in and asks for a dynamically rotating code before it will log you in, and download the gameserver for you. I've done what I can to make the process as streamlined as possible. Steam credentials are only stored in memory while Borealis is running, and while the text is in the textboxes themselves.
[b]POST-VIDEO UPDATE:[/b]
- I have added new code that flushes the contents of the username and password textboxes the moment you reach the Steam Guard code phase, this way, potentially accidental human-leakage is reduced drastically.
- I have also added Steam Guard email authentication to Borealis's internal code, so it will work as expected during the authentication phase.
[b]DOWNLOAD RELEASE 0.4.5.0-Alpha[/b]
[url]https://github.com/cyberstrawberry101/Borealis-Server-Manager/releases/tag/0.4.5.0-Alpha[/url]
[video=youtube;HBDssYgB0nQ]https://www.youtube.com/watch?v=HBDssYgB0nQ&feature=youtu.be[/video]
I've been finishing up some rather crazy stuff recently, and am going back into the Borealis development. I've already made some smaller changes, but plan on trying to tackle some more tonight.
[B]While development is underway, please list any suggestions you would like implemented into Borealis. I will compile it into a to-do list, and depending on the suggestion, it might be implemented very quickly.[/B]
I just wanted to say that the project is NOT dead. Just that I have hit a massive roadblock, that I thought I could take on months ago, but still have not found a decent solution to. I need a way to create processes, possibly dozens, maybe hundreds depending on the scale of the gameserver deployments, that can be stored in memory somehow, whereas Borealis captures the console output (it already can do this, but only for ONE gameserver), and send an input to it (Borealis can already do this as well, but only for ONE gameserver). The user also needs to be able to switch between those processes, and reload the console output with whatever the console says on that specific gameserver. I want to do this for any and all gameservers that SteamCMD supports, but I just can't figure it out.
[B]In summary, I need help from someone knowledgeable in C# to find a way to handle hundreds of processes, piping input and output to multiple gameservers, switching between them using a dropdown list selection system.[/B]
I have been making smaller updates, but nothing major, and definately not a public beta release until this issue gets sorted out.
I think the guys behind this: [url]https://github.com/DioJoestar/SteamCMD-GUI/blob/master/Media/Screenshots/console_tab.png[/url], somehow managed to figure it out, so I may use their code derivitively in order to get this system working if it actually works.
for srcds servers you can run with -condebug to put all console output to console.log, but that's not going to solve for all SteamCMD titles
[QUOTE=WhiteWhiskers;52524596]I just wanted to say that the project is NOT dead. Just that I have hit a massive roadblock, that I thought I could take on months ago, but still have not found a decent solution to. I need a way to create processes, possibly dozens, maybe hundreds depending on the scale of the gameserver deployments, that can be stored in memory somehow, whereas Borealis captures the console output (it already can do this, but only for ONE gameserver), and send an input to it (Borealis can already do this as well, but only for ONE gameserver). The user also needs to be able to switch between those processes, and reload the console output with whatever the console says on that specific gameserver. I want to do this for any and all gameservers that SteamCMD supports, but I just can't figure it out.
[B]In summary, I need help from someone knowledgeable in C# to find a way to handle hundreds of processes, piping input and output to multiple gameservers, switching between them using a dropdown list selection system.[/B]
I have been making smaller updates, but nothing major, and definately not a public beta release until this issue gets sorted out.
I think the guys behind this: [URL]https://github.com/DioJoestar/SteamCMD-GUI/blob/master/Media/Screenshots/console_tab.png[/URL], somehow managed to figure it out, so I may use their code derivitively in order to get this system working if it actually works.[/QUOTE]
I already implemented this partly in the pull request you denied months ago: [URL]https://github.com/cyberstrawberry101/Borealis-Server-Manager/pull/27/commits/62a0975a8ffbbce7673eff5fcf48e1ccd355c5ed#diff-484110177f96ed824205b513b59bdc2d[/URL]
You can attach / deattach the console as long as you know the process' id. I used this to enable ctrl+c events to the process I started when a new game server was fired up. If the process had any output it would call the given delegate with the text, allowing you to do whatever you want with it. I also implemented input handling but never tested it out since you didn't have the input handling necessary for it at the time.
I only implemented it for a single process because that's what I needed, but I'm sure you can extend it to handle more.
[QUOTE=Capsup;52525823]I already implemented this partly in the pull request you denied months ago: [URL]https://github.com/cyberstrawberry101/Borealis-Server-Manager/pull/27/commits/62a0975a8ffbbce7673eff5fcf48e1ccd355c5ed#diff-484110177f96ed824205b513b59bdc2d[/URL]
You can attach / deattach the console as long as you know the process' id. I used this to enable ctrl+c events to the process I started when a new game server was fired up. If the process had any output it would call the given delegate with the text, allowing you to do whatever you want with it. I also implemented input handling but never tested it out since you didn't have the input handling necessary for it at the time.
I only implemented it for a single process because that's what I needed, but I'm sure you can extend it to handle more.[/QUOTE]
I am going to look into this tonight. I must have completely glossed over it. If this becomes a thing, I will have it taken care of in the next 3 days.
[editline]1st August 2017[/editline]
Trying to get Capsup's code functional. It looks like I have it compilable, but I am trying to figure out the rest of it, since the code was created so long ago, its a pain in the butt to try porting over to the newer internal systems.
[editline]1st August 2017[/editline]
[B]For Capsup:[/B]
I've gotten a huge portion of the code moved over, but the background worker code is giving me issues, and my head is just not working today. Since you are the original creator of the code, you would best understand how to port it over in the TAB_Control.cs file.
I am recieving an error (3 times):
[code]The name 'cancelToken' does not exist in the current context[/code]
There is also another section that is ancient, and needs to be incorporated with the Gameserver object code:
[code]
//It's an assumption that these 3 elements MUST exist
var asyncCallback = comboboxGameserverList.BeginInvoke((Func<string[]>)delegate ()
{
return new string[] {
//GameServerXMLData( comboboxGameserverList.SelectedItem as string, "installation_folder" ),
//GameServerXMLData( comboboxGameserverList.SelectedItem as string, "default_launchscript" ),
//GameServerXMLData( comboboxGameserverList.SelectedItem as string, "binaries" )
};
});
[/code]
Here is the full content of [b]TAB_Control.cs[/b] at this time. You can also find the code committed to the GitHub if you want to work with the newest version of the alpha.
[code]
using MetroFramework;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Borealis
{
public partial class TAB_CONTROL : Form
{
#region pInvoke
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool AttachConsole(uint dwProcessId);
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
static extern bool FreeConsole();
[DllImport("kernel32.dll")]
static extern bool SetConsoleCtrlHandler(ConsoleCtrlDelegate HandlerRoutine, bool Add);
// Delegate type to be used as the Handler Routine for SCCH
delegate Boolean ConsoleCtrlDelegate(CtrlTypes CtrlType);
// Enumerated type for the control messages sent to the handler routine
enum CtrlTypes : uint
{
CTRL_C_EVENT = 0,
CTRL_BREAK_EVENT,
CTRL_CLOSE_EVENT,
CTRL_LOGOFF_EVENT = 5,
CTRL_SHUTDOWN_EVENT
}
[DllImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool GenerateConsoleCtrlEvent(CtrlTypes dwCtrlEvent, uint dwProcessGroupId);
public static void StopProgramByAttachingToItsConsoleAndIssuingCtrlCEvent(Process proc)
{
//This does not require the console window to be visible.
if (AttachConsole((uint)proc.Id))
{
//Disable Ctrl-C handling for our program
SetConsoleCtrlHandler(null, true);
GenerateConsoleCtrlEvent(CtrlTypes.CTRL_C_EVENT, 0);
//Must wait here. If we don't and re-enable Ctrl-C handling below too fast, we might terminate ourselves.
var wasAborted = false;
var procTask = Task.Run(() =>
{
//This is necessary because when we kill the process, it obviously exits. At that point, there is no proc object to wait for any longer
if (!wasAborted)
proc.WaitForExit();
});
if (!procTask.Wait(10000))
{
wasAborted = true;
proc.Kill();
}
FreeConsole();
//Re-enable Ctrl-C handling or any subsequently started programs will inherit the disabled state.
SetConsoleCtrlHandler(null, false);
}
}
#endregion
public TAB_CONTROL()
{
InitializeComponent();
}
//===================================================================================//
// STARTUP: //
//===================================================================================//
private void ServerControl_Load(object sender, EventArgs e)
{
//Pull all gameserver data from gameservers.json, split all json strings into a list, iterate through that list for specific data.
if (GameServer_Management.server_collection != null)
{
foreach (GameServer_Object gameserver in GameServer_Management.server_collection)
{
comboboxGameserverList.Items.Add(gameserver.SERVER_name_friendly);
}
}
}
//===================================================================================//
// CONTROL: //
//===================================================================================//
private void Execute(string argProgramName, string argParameters, bool Redirect)
{
try
{
var proc = new Process();
proc.StartInfo.Arguments = argParameters;
proc.StartInfo.FileName = argProgramName;
if (Redirect == true)
{
proc.StartInfo.UseShellExecute = false;
proc.StartInfo.RedirectStandardOutput = true;
proc.StartInfo.RedirectStandardInput = true;
proc.StartInfo.RedirectStandardError = true;
proc.EnableRaisingEvents = true;
proc.StartInfo.CreateNoWindow = true;
proc.ErrorDataReceived += proc_DataReceived;
proc.OutputDataReceived += proc_DataReceived;
proc.Start();
proc.BeginErrorReadLine();
proc.BeginOutputReadLine();
}
else
{
proc.StartInfo.UseShellExecute = true;
proc.Start();
}
}
catch (Exception)
{
MetroMessageBox.Show(BorealisServerManager.ActiveForm, "We cannot find the required executable to launch the server! Either it is missing, or your configuration for this gameserver is corrupted.", "Error Launching GameServer", MessageBoxButtons.OK, MessageBoxIcon.Error);
btnStartServer.Enabled = true;
btnStopServer.Visible = false;
lblAutoRestart.Visible = true;
chkAutoRestart.Visible = true;
lblStandaloneMode.Visible = true;
chkStandaloneMode.Visible = true;
consolePanel.Visible = false;
}
}
//===================================================================================//
// LAUNCH BACKGROUND WORKER TO HANDLE PROCESS //
//===================================================================================//
private void backgroundWorker01_DoWork(object sender, System.ComponentModel.DoWorkEventArgs e)
{
bool stopped = false;
//It's an assumption that these 3 elements MUST exist
var asyncCallback = comboboxGameserverList.BeginInvoke((Func<string[]>)delegate ()
{
return new string[] {
//GameServerXMLData( comboboxGameserverList.SelectedItem as string, "installation_folder" ),
//GameServerXMLData( comboboxGameserverList.SelectedItem as string, "default_launchscript" ),
//GameServerXMLData( comboboxGameserverList.SelectedItem as string, "binaries" )
};
});
asyncCallback.AsyncWaitHandle.WaitOne();
var serverParams = comboboxGameserverList.EndInvoke(asyncCallback) as string[];
Action<string> textAddCallback = (args) =>
{
consoleOutputList.BeginInvoke((Action)delegate ()
{
consoleOutputList.Items.Add(args);
});
};
EventHandler exitedHandler = (sender2, e2) =>
{
if (!cancelToken.IsCancellationRequested)
{
//Wait a little until we restart the server
consoleOutputList.BeginInvoke((Action)delegate ()
{
consoleOutputList.Items.Add(Environment.NewLine);
consoleOutputList.Items.Add(Environment.NewLine);
consoleOutputList.Items.Add(Environment.NewLine);
consoleOutputList.Items.Add("An error occured and the process has crashed. Auto-restarting in 5 seconds...");
//Scroll to the bottom
consoleOutputList.TopIndex = consoleOutputList.Items.Count - 1;
consoleOutputList.Items.Add(Environment.NewLine);
consoleOutputList.Items.Add(Environment.NewLine);
consoleOutputList.Items.Add(Environment.NewLine);
});
Thread.Sleep(5000);
}
else
stopped = true;
};
while (chkAutoRestart.Value && !stopped)
LaunchExternalProgram(serverParams[0] + serverParams[2], serverParams[1], textAddCallback, null, serverParams[0], chkAutoRestart.Value ? exitedHandler : null, chkAutoRestart.Value ? cancelToken.Token : default(System.Threading.CancellationToken));
}
//===================================================================================//
// LAUNCH SERVER WITH GIVEN ARGUMENTS //
//===================================================================================//
public static void LaunchExternalProgram(string argProgramName, string argParameters, Action<string> redirectedOutputCallback = null, TextReader input = null, string argWorkingDirectory = null, EventHandler onExitedCallback = null, CancellationToken cancelToken = default(CancellationToken))
{
ProcessStartInfo startInfo = new ProcessStartInfo();
startInfo.CreateNoWindow = true;
startInfo.Arguments = argParameters;
startInfo.FileName = argProgramName;
if (!string.IsNullOrEmpty(argWorkingDirectory))
startInfo.WorkingDirectory = argWorkingDirectory;
if (redirectedOutputCallback != null) //Redirect Output to somewhere else.
{
startInfo.UseShellExecute = false; //Redirect the programs.
startInfo.WindowStyle = ProcessWindowStyle.Hidden;
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardInput = true;
startInfo.ErrorDialog = false;
try
{
// Start the process with the info we specified.
// Call WaitForExit and then the using statement will close.
using (var process = Process.Start(startInfo))
{
if (onExitedCallback != null)
{
process.EnableRaisingEvents = true;
process.Exited += onExitedCallback;
}
if (cancelToken != null)
{
cancelToken.Register(() =>
{
StopProgramByAttachingToItsConsoleAndIssuingCtrlCEvent(process);
redirectedOutputCallback?.Invoke("Shutting down the server...");
});
}
process.OutputDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args?.Data))
redirectedOutputCallback(args.Data);
};
process.BeginOutputReadLine();
process.ErrorDataReceived += (sender, args) =>
{
if (!string.IsNullOrEmpty(args?.Data))
redirectedOutputCallback(args.Data);
};
process.BeginErrorReadLine();
//For whenever input is needed
string line;
while (input != null && (line = input.ReadLine()) != null)
process.StandardInput.WriteLine(line);
//process.WaitForExit();
}
}
catch
{
StringBuilder errorDialog = new StringBuilder();
errorDialog.Append("There was an error launching the following server:\n")
.Append(startInfo.FileName)
.Append("\n\n")
.Append("[Retry]: Attempt to start the same server again.\n")
.Append("[Cancel]: Cancel attempting to start server.");
}
}
else //No not redirect output somewhere else
{
startInfo.UseShellExecute = true; //Execute the programs.
try
{
// Start the process with the info we specified.
// Call WaitForExit and then the using statement will close.
using (Process exeProcess = Process.Start(startInfo))
{
//exeProcess.WaitForExit();
}
}
catch
{
StringBuilder errorDialog = new StringBuilder();
errorDialog.Append("There was an error launching the following server:\n")
.Append(startInfo.FileName)
.Append("\n\n")
.Append("[Retry]: Attempt to start the same server again.\n")
.Append("[Cancel]: Cancel attempting to start server.");
}
}
}
private void btnStartServer_Click(object sender, EventArgs e)
{
if (GameServer_Management.server_collection != null)
{
foreach (GameServer_Object gameserver in GameServer_Management.server_collection)
{
if (gameserver.SERVER_name_friendly == comboboxGameserverList.Text)
{
//check to see what kind of engine the server is using, and determine the usage of the variables accordingly
//SOURCE ENGINE HANDLER
if (gameserver.ENGINE_type == "SOURCE")
{
//Check to see if the gameserver needs to be run with a visible console, or directly controlled by Borealis.
if (chkStandaloneMode.Value == true) //To be hopefully depreciated soon. Only needed right now as a fallback option to server operators.
{
Execute(gameserver.DIR_install_location + @"\steamapps\common" + gameserver.DIR_root + gameserver.SERVER_executable,
string.Format("{0} +port {1} +map {2} +maxplayers {3}",
gameserver.SERVER_launch_arguments,
gameserver.SERVER_port,
gameserver.GAME_map,
gameserver.GAME_maxplayers), true);
}
else
{
LaunchExternalProgram(gameserver.DIR_install_location + @"\steamapps\common" + gameserver.DIR_root + gameserver.SERVER_executable,
string.Format("{0} +port {1} +map {2} +maxplayers {3}",
gameserver.SERVER_launch_arguments,
gameserver.SERVER_port,
gameserver.GAME_map,
gameserver.GAME_maxplayers));
}
}
//SOURCE ENGINE HANDLER
if (gameserver.ENGINE_type == "UNREAL")
{
//Check to see if the gameserver needs to be run with a visible console, or directly controlled by Borealis.
if (chkStandaloneMode.Value == true)
{
Execute(gameserver.DIR_install_location + @"\steamapps\common" + gameserver.DIR_root + gameserver.SERVER_executable,
string.Format("{0}?{1}?Port={2}?MaxPlayers={3}",
gameserver.GAME_map,
gameserver.SERVER_launch_arguments,
gameserver.SERVER_port,
gameserver.GAME_maxplayers), false);
}
else
{
MetroMessageBox.Show(BorealisServerManager.ActiveForm, "Unfortunately Borealis cannot directly control console output at this time; instead, please launch the server in 'standalone mode'.", "Unable to launch server within Borealis.", MessageBoxButtons.OK, MessageBoxIcon.Error);
/*
chkAutoRestart.Visible = false;
lblAutoRestart.Visible = false;
chkStandaloneMode.Visible = false;
lblStandaloneMode.Visible = false;
btnStartServer.Enabled = false;
btnStopServer.Visible = true;
consolePanel.Visible = true;
txtboxIssueCommand.Visible = true;
txtboxIssueCommand.Text = " > Enter a Command";
txtboxIssueCommand.Enabled = true;
Execute(Environment.CurrentDirectory + gameserver.SERVER_executable, gameserver.SERVER_launch_arguments, true);
*/
}
}
}
}
}
}
private void btnStopServer_Click(object sender, EventArgs e)
{
btnStopServer.Visible = false;
btnStartServer.Enabled = true;
chkAutoRestart.Visible = true;
lblAutoRestart.Visible = true;
txtboxIssueCommand.Visible = false;
consoleOutputList.Items.Clear();
txtboxIssueCommand.Text = " > Server is Not Running";
txtboxIssueCommand.Enabled = false;
cancelToken.Cancel();
backgroundWorker01.RunWorkerCompleted += (sender2, e2) =>
{
btnStopServer.Enabled = false;
btnStartServer.Enabled = true;
txtboxIssueCommand.Enabled = false;
txtboxIssueCommand.Text = "> Server is not running";
consoleOutputList.Items.Add("Server stopped...");
};
}
private void txtboxIssueCommand_MouseClick(object sender, MouseEventArgs e)
{
txtboxIssueCommand.Text = "";
}
private void txtboxIssueCommand_Enter(object sender, EventArgs e)
{
txtboxIssueCommand.Text = "";
}
private void comboboxGameserverList_SelectedValueChanged(object sender, EventArgs e)
{
foreach (GameServer_Object gameserver in GameServer_Management.server_collection)
{
if (gameserver.SERVER_name_friendly == comboboxGameserverList.Text)
{
//Decide what data to pull from the object at this point in time of development.
GameServer_Object Controlled_GameServer = new GameServer_Object();
Controlled_GameServer.DIR_install_location = Controlled_GameServer.DIR_install_location;
Controlled_GameServer.SERVER_executable = Controlled_GameServer.SERVER_executable;
Controlled_GameServer.SERVER_launch_arguments = Controlled_GameServer.SERVER_launch_arguments;
Controlled_GameServer.SERVER_running_status = Controlled_GameServer.SERVER_running_status;
}
}
btnStartServer.Visible = true;
chkAutoRestart.Visible = true;
lblAutoRestart.Visible = true;
chkStandaloneMode.Visible = true;
lblStandaloneMode.Visible = true;
consolePanel.Visible = true;
}
private void proc_DataReceived(object sender, DataReceivedEventArgs e)
{
if (e.Data != null)
{
consoleOutputList.Items.Add(e.Data);
}
}
public void RefreshData()
{
comboboxGameserverList.Items.Clear();
if (GameServer_Management.server_collection != null)
{
foreach (GameServer_Object gameserver in GameServer_Management.server_collection)
{
comboboxGameserverList.Items.Add(gameserver.SERVER_name_friendly);
}
}
}
private void ServerControl_Activated(object sender, EventArgs e)
{
RefreshData();
}
private void chkStandaloneMode_OnValueChange(object sender, EventArgs e)
{
if (chkStandaloneMode.Value == true)
{
consolePanel.Visible = false;
}
else
{
consolePanel.Visible = true;
}
}
}
}
[/code]
I'm still working on merging the code together. It's kind of a clusterfuck, but I'm glad I optimized a lot of the backend code early on. Goshdangit, the code is so old, that it's like I'm rewriting it all over again. I just need a way to manage the processes, and it's such a pain. Im almost tempted to hardcode processes into Borealis, but that would be ugly and a hacked together solution.
[editline]3rd August 2017[/editline]
I have cross-posted the question on a couple different places, hopefully I get a simple answer to this overtly complex problem.
[url]https://facepunch.com/showthread.php?t=1558239&p=52536978#post52536978[/url]
[url]https://stackoverflow.com/questions/45495672/allow-the-user-to-start-multiple-processes-with-redirected-output-and-input-for[/url]
[QUOTE=bord2tears;52542695]So, hopefully this is useful:
[code]
<Query Kind="Program">
<Namespace>System.Runtime.InteropServices</Namespace>
<Namespace>System.Threading.Tasks</Namespace>
<Namespace>System.Runtime.CompilerServices</Namespace>
<Namespace>System.Collections.Concurrent</Namespace>
</Query>
#define TRACE
void Main()
{
//ProcessHelper.traceSwitch.Level = TraceLevel.Warning;
ProcessHelper.traceSwitch.Level = TraceLevel.Verbose;
{
Console.WriteLine("\r\n// silly test: start notepad and kill it via dispose");
var test = new ProcessHelper("notepad", "notepad.exe", string.Empty);
test.EventOccured += (sender, args) => Console.WriteLine(args);
test.Start();
Thread.Sleep(100);
test.Dispose();
Thread.Sleep(1000);
}
{
Console.WriteLine("\r\n// silly test: start notepad and stop, then dispose");
var test = new ProcessHelper("notepad", "notepad.exe", string.Empty);
test.EventOccured += (sender, args) => Console.WriteLine(args);
test.Start();
Thread.Sleep(100);
test.Stop();
Thread.Sleep(100);
test.Dispose();
Thread.Sleep(100);
}
{
Console.WriteLine("\r\n// silly test: start notepad and stop, then start, then dispose");
var test = new ProcessHelper("notepad", "notepad.exe", string.Empty);
test.EventOccured += (sender, args) => Console.WriteLine(args);
test.Start();
Thread.Sleep(100);
test.Stop();
Thread.Sleep(100);
test.Start();
Thread.Sleep(100);
test.Dispose();
Thread.Sleep(100);
}
}
public enum ProcessHelperEventType
{
OutputMessage,
ErrorMessage,
ProcessStarted,
ProcessStopped
}
// basic event type for the process helper
public class ProcessHelperEvent
{
public ProcessHelperEvent(string message, ProcessHelperEventType type, string nickname, int? pid)
{
Message = message;
Type = type;
Occurred = DateTimeOffset.Now;
ProcessNickname = nickname;
Pid = pid;
}
public int? Pid { get; }
public string ProcessNickname { get; }
public string Message { get; }
public ProcessHelperEventType Type { get; }
public DateTimeOffset Occurred { get; }
}
// Goal of this class is to wrap all process start / stop logic
// we avoid blocking methods on the public methods in general here
// keep a buffered log of console redirect content
// ensure methods defined are thread safe in case someone gets click happy in a UI somewhere ;)
public interface IProcess : IDisposable
{
bool IsRunning { get; }
void Start(); // start this process if not already
void Stop(); // ask process to close nicely
void Kill(); // kick chair out from under the process
event EventHandler<ProcessHelperEvent> EventOccured; // an event transpired
IEnumerable<ProcessHelperEvent> ProcessEvents { get; } // events currently stored
}
public class ProcessHelper : IProcess
{
public static readonly TraceSwitch traceSwitch = new TraceSwitch("Process", "Process manager info");
private readonly string nickname;
private readonly string SERVER_executable;
private readonly string SERVER_launch_arguments;
private readonly object processSync = new object();
private readonly ConcurrentQueue<ProcessHelperEvent> processEvents = new ConcurrentQueue<ProcessHelperEvent>();
private bool disposed;
private Process currentProcess;
[DllImport("user32.dll")]
static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
static extern IntPtr SendMessage(IntPtr hWnd, UInt32 Msg, IntPtr wParam, IntPtr lParam);
const int SW_HIDE = 0;
const UInt32 WM_CLOSE = 0x0010; // ask nicely to close window
public ProcessHelper(string nickname, string SERVER_executable, string SERVER_launch_arguments)
{
this.nickname = nickname;
this.SERVER_executable = SERVER_executable;
this.SERVER_launch_arguments = SERVER_launch_arguments;
}
public event EventHandler<ProcessHelperEvent> EventOccured;
public IEnumerable<ProcessHelperEvent> ProcessEvents => processEvents;
// inaccurate, but good enough with the other aspects of this helper
public bool IsRunning => currentProcess != null;
public void Start()
{
lock (processSync)
{
DisposeGuard();
if (currentProcess != null)
{
return;
}
ProcessStartInfo startInfo = new ProcessStartInfo();
// let's be kind here! we want to use WM_CLOSE as a graceful close attempt, and we need a window for that
// insead, lets hide the window all sneaky like
startInfo.CreateNoWindow = false;
startInfo.Arguments = SERVER_launch_arguments;
startInfo.FileName = SERVER_executable;
startInfo.UseShellExecute = false; //Redirect the programs.
startInfo.RedirectStandardError = true;
startInfo.RedirectStandardOutput = true;
startInfo.RedirectStandardInput = true;
startInfo.ErrorDialog = false;
var process = new Process();
process.EnableRaisingEvents = true;
process.OutputDataReceived += (sender, args) => DataRecieved(args,false);
process.ErrorDataReceived += (sender, args) => DataRecieved(args,true);
process.StartInfo = startInfo;
process.Exited += (sender, args) => ProcessClosed(process);
TraceMessage(TraceLevel.Verbose, $"about to start process {nickname} @'{SERVER_executable}'");
if (!process.Start())
{
throw new Exception($"cannot get the process {nickname} @'{SERVER_executable}' to start - returns false");
}
OnEventOccurred(new ProcessHelperEvent(null, ProcessHelperEventType.ProcessStarted, nickname, process?.Id));
TraceMessage(TraceLevel.Info, $"process {nickname} started with pid {process.Id} @'{SERVER_executable}'");
//note: only works as long as your process actually creates a main window.
while (process.MainWindowHandle == IntPtr.Zero)
{
System.Threading.Thread.Sleep(10);
}
// this hides the window without disable message pumping
ShowWindow(process.MainWindowHandle, SW_HIDE);
TraceMessage(TraceLevel.Verbose, $"window hidden for {nickname} pid {process.Id}");
currentProcess = process;
}
}
public void Stop()
{
lock (processSync)
{
DisposeGuard();
if (currentProcess == null)
{
TraceMessage(TraceLevel.Warning, $"attempted to stop process {nickname} when no process is running - likely a race condition!");
return;
}
SendMessage(currentProcess.MainWindowHandle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
TraceMessage(TraceLevel.Verbose, $"sent WM_CLOSE to process {nickname} pid {currentProcess.Id}");
}
}
public void Kill()
{
lock (processSync)
{
DisposeGuard();
if (currentProcess == null)
{
TraceMessage(TraceLevel.Warning, $"attempted to KILL process {nickname} when no process is running - likely a race condition!");
return;
}
currentProcess.Kill();
TraceMessage(TraceLevel.Warning, $"KILLED process {nickname} pid {currentProcess.Id}");
currentProcess.Dispose();
currentProcess = null;
}
}
public void Dispose()
{
lock (processSync)
{
if (!disposed && currentProcess != null)
{
TraceMessage(TraceLevel.Warning, $"KILLING {nickname} beccause you forgot to stop it before calling Dispose");
// welp, time waits for no one!
currentProcess.Kill();
currentProcess = null;
}
else if (!disposed)
{
TraceMessage(TraceLevel.Verbose, $"diposed {nickname} gracefully, good job!");
}
disposed = true;
}
}
private void ProcessClosed(Process process)
{
lock (processSync)
{
OnEventOccurred(new ProcessHelperEvent(null, ProcessHelperEventType.ProcessStopped, nickname, process?.Id));
TraceMessage(TraceLevel.Info, $"process {nickname} pid {process?.Id} has closed, RIP");
process.Dispose();
if (currentProcess == process)
{
currentProcess = null;
}
}
}
private void DataRecieved(DataReceivedEventArgs args, bool wasError)
{
lock (processSync)
{
var type = wasError ? ProcessHelperEventType.ErrorMessage : ProcessHelperEventType.OutputMessage;
OnEventOccurred(new ProcessHelperEvent(args.Data, type, nickname, currentProcess?.Id ));
}
}
private void DisposeGuard([CallerMemberName] string callerName = "")
{
if (disposed)
{
throw new Exception($"attempted to call {callerName} after a dispose!!");
}
}
private void OnEventOccurred(ProcessHelperEvent args)
{
processEvents.Enqueue(args);
EventOccured?.Invoke(this, args);
}
private static void TraceMessage(TraceLevel level, string message)
{
if (traceSwitch.Level >= level)
{
Trace.WriteLine(message, "Process");
}
}
}
[/code]
It is a quick linqpad script, but you can make it a regular console app by making Main() work.
The for a 'test' is as follows:
[img]http://i.imgur.com/mEomfpk.png[/img]
Does this help at all?
Basic idea is to use the same process method you created and wrap it into a pretty, non-blocking, thread safe bow.
I imagine you'd simply enumerate the "ProcessEvents" for the log. Start and stop happens on one process.
Each process/server is a single "ProcessHelper" instance.
Note: you probably want to flush the "ProcessEvents" buffer so it is max 1000 lines or some such, likewise I don't like the event handler since they are easy to leak / mess up while plumbing (I'd prefer making it observable but might not be useful for you).[/QUOTE]
I am looking to include this code into Borealis, and hopefully everything gets up and working as a result.
[B]bord2tears Helped me out a lot tonight, and we revised code a lot to get most things working.[/B]
[b]NEW:[/b]
- Added new internal process manager to handle all of your gameservers
- Added ability to start gameservers
- Added ability to gracefully close gameservers
- Added ability to forcefully kill gameservers
- Added working code that reports back to the "GameServer Control" UI whether servers are running or not, and prevents you from starting the same server instance twice
- Added working code to show what servers are running and which are not via the Dashboard querying the new internal process manager.
- Added the ability to "Subscribe" and "Unsubscribe" to gameserver processes, showing their output and Borealis output for that specific server only on the corresponding gameserver tab when switching between controlling different gameservers
- Added ability to send commands to gameservers directly via Borealis (Experimental)
[b]ISSUES:[/b]
- Currently, I cannot figure out how to pipe output from gameservers into the Borealis console, so all you can see right now is Borealis messages, it doesnt even show the output from the actual windows at this moment, I am looking to fix this probably tomorrow giving an option to fallback to standalone servers when needed.
- There is a bug, where if you type a command to a server, and switch gameserver tabs, and go back to the tab you were on, it only shows the Borealis messages, but not your historical commands you sent
[img]https://i.imgur.com/YXBPklH.png[/img]
[B][I][U]Output finally works![/U][/I][/B]
I am working on implementing a lot of small features, like the ability to run the servers stand-alone for the ones that I cannot pipe the output currently.
[video=youtube;vKLFwbO2f94]https://www.youtube.com/watch?v=vKLFwbO2f94&feature=youtu.be[/video]
A small status update includes that me and a couple other folks refactored about 300+ lines fo code in Borealis, ranging from bad logic systems to bad coding practices, to removing old code and redundancies in the system. The program is a little smaller, more easy to read the source-code on GitHub, and i've added a gentle drop shadow to make it 'pop-out' more from the background of the screen. I've also updated 9 gameservers to be ready to deploy using the new process management system, but they are all SRCDS, so your mileage will vary when it comes to sending commands via Borealis to the consoles, but a simple workaround for the time being is simply typing directly into the visible console to issue commands. Console output in Borealis works just fine. I currently found some frameworks that might allow me to have subprocess control over the dedicated servers, and allow me to send commands directly to them from Borealis. A lot of work needs to be made to get it to work.
Lastly, I am starting up work again, so my availibility will be on-off for a while, then starting September 5th I start at my new full-time job, going through an intensive orientation for 6 weeks.
I appreciate all of the support I have received both on Reddit and Facepunch throughout the process. We are so close to reaching Beta once I am able to create configurations for all of the 95+ gameservers available, and creating a fool-proof solution to redirecting input into SRCDS-based gameservers.
Still making slow progress on Borealis, since I have to actually deploy every server to make it configurable, but it's going along.
Sorry, you need to Log In to post a reply to this thread.