Pass + as command line arg to client/server to execute commands after init.

This commit is contained in:
Pieter-Jan Briers
2021-11-10 02:01:31 +01:00
parent f9ae3e1fc2
commit 8e3fa3e52d
5 changed files with 387 additions and 340 deletions

View File

@@ -4,177 +4,186 @@ using Robust.Shared;
using Robust.Shared.Utility;
using C = System.Console;
namespace Robust.Client
namespace Robust.Client;
internal sealed class CommandLineArgs
{
internal sealed class CommandLineArgs
public MountOptions MountOptions { get; }
public bool Headless { get; }
public bool SelfContained { get; }
public bool Connect { get; }
public string ConnectAddress { get; }
public string? Ss14Address { get; }
public bool Launcher { get; }
public string? Username { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
public IReadOnlyList<string> ExecCommands { get; set; }
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
{
public MountOptions MountOptions { get; }
public bool Headless { get; }
public bool SelfContained { get; }
public bool Connect { get; }
public string ConnectAddress { get; }
public string? Ss14Address { get; }
public bool Launcher { get; }
public string? Username { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
parsed = null;
var headless = false;
var selfContained = false;
var connect = false;
var connectAddress = "localhost";
string? ss14Address = null;
var launcher = false;
string? username = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
var execCommands = new List<string>();
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
{
parsed = null;
var headless = false;
var selfContained = false;
var connect = false;
var connectAddress = "localhost";
string? ss14Address = null;
var launcher = false;
string? username = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
var arg = enumerator.Current;
if (arg == "--connect")
{
var arg = enumerator.Current;
if (arg == "--connect")
connect = true;
}
else if (arg == "--connect-address")
{
if (!enumerator.MoveNext())
{
connect = true;
}
else if (arg == "--connect-address")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing connection address.");
return false;
}
connectAddress = enumerator.Current;
}
else if (arg == "--ss14-address")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing SS14 address.");
return false;
}
ss14Address = enumerator.Current;
}
else if (arg == "--self-contained")
{
selfContained = true;
}
else if (arg == "--launcher")
{
launcher = true;
}
else if (arg == "--headless")
{
headless = true;
}
else if (arg == "--username")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing username.");
return false;
}
username = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
C.WriteLine("Missing connection address.");
return false;
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
connectAddress = enumerator.Current;
}
else if (arg == "--ss14-address")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing SS14 address.");
return false;
}
parsed = new CommandLineArgs(
headless,
selfContained,
connect,
launcher,
username,
cvars,
logLevels,
connectAddress,
ss14Address,
mountOptions);
ss14Address = enumerator.Current;
}
else if (arg == "--self-contained")
{
selfContained = true;
}
else if (arg == "--launcher")
{
launcher = true;
}
else if (arg == "--headless")
{
headless = true;
}
else if (arg == "--username")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing username.");
return false;
}
return true;
username = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
return false;
}
else if (arg.StartsWith("+"))
{
execCommands.Add(arg[1..]);
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
}
private static void PrintHelp()
{
C.WriteLine(@"
parsed = new CommandLineArgs(
headless,
selfContained,
connect,
launcher,
username,
cvars,
logLevels,
connectAddress,
ss14Address,
mountOptions,
execCommands);
return true;
}
private static void PrintHelp()
{
C.WriteLine(@"
Usage: Robust.Client [options] [+command [+command]]
Options:
--headless Run without graphics/audio/input.
--self-contained Store data relative to executable instead of user-global locations.
@@ -189,30 +198,34 @@ Options:
--mount-dir Resource directory to mount.
--mount-zip Resource zip to mount.
--help Display this help text and exit.
");
}
private CommandLineArgs(
bool headless,
bool selfContained,
bool connect,
bool launcher,
string? username,
IReadOnlyCollection<(string key, string value)> cVars,
IReadOnlyCollection<(string key, string value)> logLevels,
string connectAddress, string? ss14Address,
MountOptions mountOptions)
{
Headless = headless;
SelfContained = selfContained;
Connect = connect;
Launcher = launcher;
Username = username;
CVars = cVars;
LogLevels = logLevels;
ConnectAddress = connectAddress;
Ss14Address = ss14Address;
MountOptions = mountOptions;
}
+command: You can pass a set of commands, prefixed by +,
to be executed in the console in order after the game has finished initializing.
");
}
private CommandLineArgs(
bool headless,
bool selfContained,
bool connect,
bool launcher,
string? username,
IReadOnlyCollection<(string key, string value)> cVars,
IReadOnlyCollection<(string key, string value)> logLevels,
string connectAddress, string? ss14Address,
MountOptions mountOptions,
IReadOnlyList<string> execCommands)
{
Headless = headless;
SelfContained = selfContained;
Connect = connect;
Launcher = launcher;
Username = username;
CVars = cVars;
LogLevels = logLevels;
ConnectAddress = connectAddress;
Ss14Address = ss14Address;
MountOptions = mountOptions;
ExecCommands = execCommands;
}
}

View File

@@ -198,6 +198,8 @@ namespace Robust.Client
_client.ConnectToServer(LaunchState.ConnectEndpoint);
}
ProgramShared.RunExecCommands(_console, _commandLineArgs?.ExecCommands);
return true;
}

View File

@@ -83,6 +83,7 @@ namespace Robust.Server
[Dependency] private readonly IRobustMappedStringSerializer _stringSerializer = default!;
[Dependency] private readonly ILocalizationManagerInternal _loc = default!;
[Dependency] private readonly INetConfigurationManager _netCfgMan = default!;
[Dependency] private readonly IServerConsoleHost _consoleHost = default!;
private readonly Stopwatch _uptimeStopwatch = new();
@@ -337,7 +338,7 @@ namespace Robust.Server
prototypeManager.LoadDirectory(Options.PrototypeDirectory);
prototypeManager.Resync();
IoCManager.Resolve<IServerConsoleHost>().Initialize();
_consoleHost.Initialize();
_entityManager.Startup();
_mapManager.Startup();
IoCManager.Resolve<IEntityLookup>().Startup();
@@ -372,6 +373,8 @@ namespace Robust.Server
GC.Collect();
ProgramShared.RunExecCommands(_consoleHost, _commandLineArgs?.ExecCommands);
return false;
}

View File

@@ -4,130 +4,138 @@ using Robust.Shared;
using Robust.Shared.Utility;
using C = System.Console;
namespace Robust.Server
namespace Robust.Server;
internal sealed class CommandLineArgs
{
internal sealed class CommandLineArgs
public MountOptions MountOptions { get; }
public string? ConfigFile { get; }
public string? DataDir { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
public IReadOnlyList<string> ExecCommands { get; set; }
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
{
public MountOptions MountOptions { get; }
public string? ConfigFile { get; }
public string? DataDir { get; }
public IReadOnlyCollection<(string key, string value)> CVars { get; }
public IReadOnlyCollection<(string key, string value)> LogLevels { get; }
parsed = null;
string? configFile = null;
string? dataDir = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
var execCommands = new List<string>();
// Manual parser because C# has no good command line parsing libraries. Also dependencies bad.
// Also I don't like spending 100ms parsing command line args. Do you?
public static bool TryParse(IReadOnlyList<string> args, [NotNullWhen(true)] out CommandLineArgs? parsed)
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
{
parsed = null;
string? configFile = null;
string? dataDir = null;
var cvars = new List<(string, string)>();
var logLevels = new List<(string, string)>();
var mountOptions = new MountOptions();
using var enumerator = args.GetEnumerator();
while (enumerator.MoveNext())
var arg = enumerator.Current;
if (arg == "--config-file")
{
var arg = enumerator.Current;
if (arg == "--config-file")
if (!enumerator.MoveNext())
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing config file.");
return false;
}
configFile = enumerator.Current;
}
else if (arg == "--data-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing data directory.");
return false;
}
dataDir = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
C.WriteLine("Missing config file.");
return false;
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
configFile = enumerator.Current;
}
else if (arg == "--data-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing data directory.");
return false;
}
parsed = new CommandLineArgs(configFile, dataDir, cvars, logLevels, mountOptions);
return true;
dataDir = enumerator.Current;
}
else if (arg == "--cvar")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing cvar value.");
return false;
}
var cvar = enumerator.Current;
DebugTools.AssertNotNull(cvar);
var pos = cvar.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in cvar.");
return false;
}
cvars.Add((cvar[..pos], cvar[(pos + 1)..]));
}
else if (arg == "--help")
{
PrintHelp();
return false;
}
else if (arg == "--mount-zip")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.ZipMounts.Add(enumerator.Current);
}
else if (arg == "--mount-dir")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing mount path");
return false;
}
mountOptions.DirMounts.Add(enumerator.Current);
}
else if (arg == "--loglevel")
{
if (!enumerator.MoveNext())
{
C.WriteLine("Missing loglevel sawmill.");
return false;
}
var loglevel = enumerator.Current;
DebugTools.AssertNotNull(loglevel);
var pos = loglevel.IndexOf('=');
if (pos == -1)
{
C.WriteLine("Expected = in loglevel.");
return false;
}
logLevels.Add((loglevel[..pos], loglevel[(pos + 1)..]));
}
else if (arg.StartsWith("+"))
{
execCommands.Add(arg[1..]);
}
else
{
C.WriteLine("Unknown argument: {0}", arg);
}
}
private static void PrintHelp()
{
C.WriteLine(@"
parsed = new CommandLineArgs(configFile, dataDir, cvars, logLevels, mountOptions, execCommands);
return true;
}
private static void PrintHelp()
{
C.WriteLine(@"
Usage: Robust.Server [options] [+command [+command]]
Options:
--config-file Path to the config file to read from.
--data-dir Path to the data directory to read/write from/to.
@@ -136,16 +144,25 @@ Options:
--mount-dir Resource directory to mount.
--mount-zip Resource zip to mount.
--help Display this help text and exit.
");
}
private CommandLineArgs(string? configFile, string? dataDir, IReadOnlyCollection<(string, string)> cVars, IReadOnlyCollection<(string, string)> logLevels, MountOptions mountOptions)
{
ConfigFile = configFile;
DataDir = dataDir;
CVars = cVars;
LogLevels = logLevels;
MountOptions = mountOptions;
}
+command: You can pass a set of commands, prefixed by +,
to be executed in the console in order after the game has finished initializing.
");
}
private CommandLineArgs(
string? configFile,
string? dataDir,
IReadOnlyCollection<(string, string)> cVars,
IReadOnlyCollection<(string, string)> logLevels,
MountOptions mountOptions,
IReadOnlyList<string> execCommands)
{
ConfigFile = configFile;
DataDir = dataDir;
CVars = cVars;
LogLevels = logLevels;
MountOptions = mountOptions;
ExecCommands = execCommands;
}
}

View File

@@ -1,59 +1,71 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Robust.Shared.Console;
using Robust.Shared.ContentPack;
using Robust.Shared.Log;
using Robust.Shared.Utility;
namespace Robust.Shared
{
internal static class ProgramShared
{
#if !FULL_RELEASE
private static string FindContentRootDir(bool contentStart)
{
return contentStart ? "../../" : "../../../";
}
namespace Robust.Shared;
private static string FindEngineRootDir(bool contentStart)
internal static class ProgramShared
{
public static void RunExecCommands(IConsoleHost consoleHost, IReadOnlyList<string>? commands)
{
if (commands == null)
return;
foreach (var cmd in commands)
{
return contentStart ? "../../RobustToolbox/" : "../../";
consoleHost.ExecuteCommand(cmd);
}
}
#if !FULL_RELEASE
private static string FindContentRootDir(bool contentStart)
{
return contentStart ? "../../" : "../../../";
}
private static string FindEngineRootDir(bool contentStart)
{
return contentStart ? "../../RobustToolbox/" : "../../";
}
#endif
internal static void PrintRuntimeInfo(ISawmill sawmill)
{
sawmill.Debug($"Runtime: {RuntimeInformation.FrameworkDescription} {RuntimeInformation.RuntimeIdentifier}");
sawmill.Debug($"OS: {RuntimeInformation.OSDescription} {RuntimeInformation.OSArchitecture}");
}
internal static void PrintRuntimeInfo(ISawmill sawmill)
{
sawmill.Debug($"Runtime: {RuntimeInformation.FrameworkDescription} {RuntimeInformation.RuntimeIdentifier}");
sawmill.Debug($"OS: {RuntimeInformation.OSDescription} {RuntimeInformation.OSArchitecture}");
}
internal static void DoMounts(IResourceManagerInternal res, MountOptions? options, string contentBuildDir, ResourcePath assembliesPath, bool loadContentResources = true,
bool loader = false, bool contentStart = false)
{
internal static void DoMounts(IResourceManagerInternal res, MountOptions? options, string contentBuildDir, ResourcePath assembliesPath, bool loadContentResources = true,
bool loader = false, bool contentStart = false)
{
#if FULL_RELEASE
if (!loader)
res.MountContentDirectory(@"Resources/");
#else
res.MountContentDirectory($@"{FindEngineRootDir(contentStart)}Resources/");
res.MountContentDirectory($@"{FindEngineRootDir(contentStart)}Resources/");
if (loadContentResources)
{
var contentRootDir = FindContentRootDir(contentStart);
res.MountContentDirectory($@"{contentRootDir}bin/{contentBuildDir}/", assembliesPath);
res.MountContentDirectory($@"{contentRootDir}Resources/");
}
if (loadContentResources)
{
var contentRootDir = FindContentRootDir(contentStart);
res.MountContentDirectory($@"{contentRootDir}bin/{contentBuildDir}/", assembliesPath);
res.MountContentDirectory($@"{contentRootDir}Resources/");
}
#endif
if (options == null)
return;
if (options == null)
return;
foreach (var diskPath in options.DirMounts)
{
res.MountContentDirectory(diskPath);
}
foreach (var diskPath in options.DirMounts)
{
res.MountContentDirectory(diskPath);
}
foreach (var diskPath in options.ZipMounts)
{
res.MountContentPack(diskPath);
}
foreach (var diskPath in options.ZipMounts)
{
res.MountContentPack(diskPath);
}
}
}