Merge branch 'dev' into 979-Postgres

This commit is contained in:
Jordan Brown
2020-05-16 12:01:55 -04:00
committed by GitHub
22 changed files with 370 additions and 144 deletions
+3 -3
View File
@@ -2,9 +2,9 @@
<PropertyGroup>
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<TgsCoreVersion>4.2.1</TgsCoreVersion>
<TgsApiVersion>6.2.0</TgsApiVersion>
<TgsClientVersion>6.1.0</TgsClientVersion>
<TgsCoreVersion>4.2.2</TgsCoreVersion>
<TgsApiVersion>6.3.0</TgsApiVersion>
<TgsClientVersion>6.2.0</TgsClientVersion>
<TgsDmapiVersion>5.1.1</TgsDmapiVersion>
<TgsControlPanelVersion>0.4.0</TgsControlPanelVersion>
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
@@ -471,5 +471,17 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Description("Cannot set both softShutdown and softReboot at once!")]
DreamDaemonDoubleSoft,
/// <summary>
/// Attempted to launch DreamDaemon on a user account that had the BYOND pager running.
/// </summary>
[Description("Cannot start DreamDaemon headless with the BYOND pager running!")]
DeploymentPagerRunning,
/// <summary>
/// Could not bind to port we wanted to launch DreamDaemon on.
/// </summary>
[Description("Could not bind to requested DreamDaemon port! Is there another service running on that port?")]
DreamDaemonPortInUse,
}
}
@@ -225,7 +225,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return channelModel;
}
Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel.GetType());
Logger.LogWarning("Cound not map channel {0}! Incorrect type: {1}", channelId, discordChannel?.GetType());
return null;
}
@@ -10,11 +10,11 @@ namespace Tgstation.Server.Host.Components.Interop.Topic
/// <summary>
/// The text to reply with as the result of a <see cref="TopicCommandType.ChatCommand"/> request, if any.
/// </summary>
public string CommandResponseMessage { get; private set; }
public string CommandResponseMessage { get; set; }
/// <summary>
/// The <see cref="ChatMessage"/>s to send as the result of a <see cref="TopicCommandType.EventNotification"/> request, if any.
/// </summary>
public ICollection<ChatMessage> ChatResponses { get; private set; }
public ICollection<ChatMessage> ChatResponses { get; set; }
}
}
@@ -3,6 +3,8 @@ using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -110,6 +112,24 @@ namespace Tgstation.Server.Host.Components.Session
};
}
/// <summary>
/// Check if a given <paramref name="port"/> can be bound to.
/// </summary>
/// <param name="port">The port number to test.</param>
static void PortBindTest(ushort port)
{
using var socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
try
{
socket.Bind(new IPEndPoint(IPAddress.Loopback, port));
}
catch (Exception ex)
{
throw new JobException(ErrorCode.DreamDaemonPortInUse, ex);
}
}
/// <summary>
/// Construct a <see cref="SessionControllerFactory"/>
/// </summary>
@@ -201,7 +221,8 @@ namespace Tgstation.Server.Host.Components.Session
if (launchParameters.SecurityLevel == DreamDaemonSecurity.Trusted)
await byondLock.TrustDmbPath(ioManager.ConcatPath(basePath, dmbProvider.DmbName), cancellationToken).ConfigureAwait(false);
CheckPagerIsNotRunning();
PortBindTest(portToUse.Value);
await CheckPagerIsNotRunning(cancellationToken).ConfigureAwait(false);
var accessIdentifier = cryptographySuite.GetSecureString();
@@ -214,7 +235,7 @@ namespace Tgstation.Server.Host.Components.Session
// important to run on all ports to allow port changing
var arguments = String.Format(CultureInfo.InvariantCulture, "{0} -port {1} -ports 1-65535 {2}-close -{3} -{4} -public -params \"{5}\"",
dmbProvider.DmbName,
primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort,
portToUse,
launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty,
SecurityWord(launchParameters.SecurityLevel.Value),
visibility,
@@ -403,10 +424,24 @@ namespace Tgstation.Server.Host.Components.Session
/// <summary>
/// Make sure the BYOND pager is not running.
/// </summary>
void CheckPagerIsNotRunning()
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async Task CheckPagerIsNotRunning(CancellationToken cancellationToken)
{
if (platformIdentifier.IsWindows && processExecutor.IsProcessWithNameRunning("byond"))
throw new JobException("Cannot start DreamDaemon headless with the BYOND pager running!");
if (!platformIdentifier.IsWindows)
return;
using var otherProcess = processExecutor.GetProcessByName("byond");
if (otherProcess == null)
return;
var otherUsernameTask = otherProcess.GetExecutingUsername(cancellationToken);
using var ourProcess = processExecutor.GetCurrentProcess();
var ourUserName = await ourProcess.GetExecutingUsername(cancellationToken).ConfigureAwait(false);
var otherUserName = await otherUsernameTask.ConfigureAwait(false);
if(otherUserName.Equals(ourUserName, StringComparison.Ordinal))
throw new JobException(ErrorCode.DeploymentPagerRunning);
}
}
}
@@ -471,6 +471,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
var scriptOutput = script.GetCombinedOutput();
if (exitCode != 0)
throw new JobException($"Script {I} exited with code {exitCode}:{Environment.NewLine}{scriptOutput}");
else
logger.LogDebug("Script output:{0}{1}", Environment.NewLine, scriptOutput);
}
}
}
@@ -235,7 +235,7 @@ namespace Tgstation.Server.Host.Controllers
PrimaryPort = 1337,
SecondaryPort = 1338,
SecurityLevel = DreamDaemonSecurity.Safe,
StartupTimeout = 20,
StartupTimeout = 60,
HeartbeatSeconds = 60
},
DreamMakerSettings = new DreamMakerSettings
@@ -254,7 +254,7 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
services.AddSingleton<IProcessSuspender, WindowsProcessSuspender>();
services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
services.AddSingleton<WindowsNetworkPromptReaper>();
services.AddSingleton<INetworkPromptReaper>(x => x.GetRequiredService<WindowsNetworkPromptReaper>());
@@ -267,7 +267,7 @@ namespace Tgstation.Server.Host.Core
services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
services.AddSingleton<IByondInstaller, PosixByondInstaller>();
services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
services.AddSingleton<IProcessSuspender, PosixProcessSuspender>();
services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
}
@@ -369,6 +369,7 @@ namespace Tgstation.Server.Host.Core
{
applicationBuilder.UseSwagger();
applicationBuilder.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "TGS API V4"));
logger.LogTrace("Swagger API generation enabled");
}
// Set up CORS based on configuration if necessary
+9 -1
View File
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.System
{
@@ -39,5 +40,12 @@ namespace Tgstation.Server.Host.System
/// Terminates the process
/// </summary>
void Terminate();
/// <summary>
/// Get the name of the account executing the <see cref="IProcess"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the name of the account executing the <see cref="IProcess"/>.</returns>
Task<string> GetExecutingUsername(CancellationToken cancellationToken);
}
}
@@ -18,17 +18,23 @@
IProcess LaunchProcess(string fileName, string workingDirectory, string arguments = null, bool readOutput = false, bool readError = false, bool noShellExecute = false);
/// <summary>
/// Get a <see cref="IProcess"/> by <paramref name="id"/>
/// Get a <see cref="IProcess"/> representing the running executable.
/// </summary>
/// <param name="id">The <see cref="IProcess.Id"/></param>
/// <returns>The <see cref="IProcess"/> represented by <paramref name="id"/> on success, <see langword="null"/> on failure</returns>
/// <returns>The current <see cref="IProcess"/>.</returns>
IProcess GetCurrentProcess();
/// <summary>
/// Get a <see cref="IProcess"/> by <paramref name="id"/>.
/// </summary>
/// <param name="id">The <see cref="IProcess.Id"/>.</param>
/// <returns>The <see cref="IProcess"/> represented by <paramref name="id"/> on success, <see langword="null"/> on failure.</returns>
IProcess GetProcess(int id);
/// <summary>
/// Check if a <see cref="IProcess"/> with a given <paramref name="name"/> is running.
/// Get a <see cref="IProcess"/> with a given <paramref name="name"/>.
/// </summary>
/// <param name="name">The name of the process without the extension.</param>
/// <returns><see langword="true"/> if the process is running, <see langword="false"/> otherwise.</returns>
bool IsProcessWithNameRunning(string name);
/// <param name="name">The name of the process executable without the extension.</param>
/// <returns>The <see cref="IProcess"/> represented by <paramref name="name"/> on success, <see langword="null"/> on failure.</returns>
IProcess GetProcessByName(string name);
}
}
@@ -0,0 +1,31 @@
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.System
{
/// <summary>
/// Abstraction for suspending and resuming processes.
/// </summary>
interface IProcessFeatures
{
/// <summary>
/// Get the name of the user executing a given <paramref name="process"/>.
/// </summary>
/// <param name="process">The <see cref="global::System.Diagnostics.Process"/>.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>The name of the user executing <paramref name="process"/>.</returns>
Task<string> GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken);
/// <summary>
/// Suspend a given <see cref="Process"/>.
/// </summary>
/// <param name="process">The <see cref="Process"/> to suspend.</param>
void SuspendProcess(global::System.Diagnostics.Process process);
/// <summary>
/// Resume a given suspended <see cref="Process"/>.
/// </summary>
/// <param name="process">The <see cref="Process"/> to susperesumend.</param>
void ResumeProcess(global::System.Diagnostics.Process process);
}
}
@@ -1,20 +0,0 @@
namespace Tgstation.Server.Host.System
{
/// <summary>
/// Abstraction for suspending and resuming processes.
/// </summary>
interface IProcessSuspender
{
/// <summary>
/// Suspend a given <see cref="Process"/>.
/// </summary>
/// <param name="process">The <see cref="Process"/> to suspend.</param>
void SuspendProcess(global::System.Diagnostics.Process process);
/// <summary>
/// Resume a given suspended <see cref="Process"/>.
/// </summary>
/// <param name="process">The <see cref="Process"/> to susperesumend.</param>
void ResumeProcess(global::System.Diagnostics.Process process);
}
}
@@ -0,0 +1,97 @@
using Microsoft.Extensions.Logging;
using Mono.Unix;
using Mono.Unix.Native;
using System;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.System
{
/// <inheritdoc />
sealed class PosixProcessFeatures : IProcessFeatures
{
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="PosixProcessFeatures"/>.
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="ILogger{TCategoryName}"/> for the <see cref="PosixProcessFeatures"/>.
/// </summary>
readonly ILogger<PosixProcessFeatures> logger;
/// <summary>
/// Initializes a new instance of the <see cref="PosixProcessFeatures"/> <see langword="class"/>.
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public PosixProcessFeatures(IIOManager ioManager, ILogger<PosixProcessFeatures> logger)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public void ResumeProcess(global::System.Diagnostics.Process process)
{
try
{
var result = Syscall.kill(process.Id, Signum.SIGCONT);
if (result != 0)
throw new UnixIOException(result);
logger.LogTrace("Resumed PID {0}", process.Id);
}
catch (Exception e)
{
logger.LogError(e, "Failed to resume PID {0}!", process.Id);
throw;
}
}
/// <inheritdoc />
public void SuspendProcess(global::System.Diagnostics.Process process)
{
try
{
var result = Syscall.kill(process.Id, Signum.SIGSTOP);
if (result != 0)
throw new UnixIOException(result);
logger.LogTrace("Resumed PID {0}", process.Id);
}
catch (Exception e)
{
logger.LogError(e, "Failed to suspend PID {0}!", process.Id);
throw;
}
}
/// <inheritdoc />
public async Task<string> GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken)
{
if (process == null)
throw new ArgumentNullException(nameof(process));
// Need to read /proc/[pid]/status
// http://man7.org/linux/man-pages/man5/proc.5.html
// https://unix.stackexchange.com/questions/102676/why-is-uid-information-not-in-proc-x-stat
var pid = process.Id;
var statusFile = ioManager.ConcatPath("/proc", pid.ToString(CultureInfo.InvariantCulture), "status");
var statusBytes = await ioManager.ReadAllBytes(statusFile, cancellationToken).ConfigureAwait(false);
var statusText = Encoding.UTF8.GetString(statusBytes);
var splits = statusText.Split('\n', StringSplitOptions.RemoveEmptyEntries);
var entry = splits.FirstOrDefault(x => x.Trim().StartsWith("Uid:", StringComparison.Ordinal));
if (entry == default)
return "UNKNOWN";
return entry
.Substring(4)
.Split(' ', StringSplitOptions.RemoveEmptyEntries)
.FirstOrDefault(x => !String.IsNullOrWhiteSpace(x))
?? "UNPARSABLE";
}
}
}
@@ -1,59 +0,0 @@
using Microsoft.Extensions.Logging;
using Mono.Unix;
using Mono.Unix.Native;
using System;
namespace Tgstation.Server.Host.System
{
/// <inheritdoc />
sealed class PosixProcessSuspender : IProcessSuspender
{
/// <summary>
/// The <see cref="ILogger{TCategoryName}"/> for the <see cref="PosixProcessSuspender"/>.
/// </summary>
readonly ILogger<PosixProcessSuspender> logger;
/// <summary>
/// Initializes a new instance of the <see cref="PosixProcessSuspender"/> <see langword="class"/>.
/// </summary>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public PosixProcessSuspender(ILogger<PosixProcessSuspender> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public void ResumeProcess(global::System.Diagnostics.Process process)
{
try
{
var result = Syscall.kill(process.Id, Signum.SIGCONT);
if (result != 0)
throw new UnixIOException(result);
logger.LogTrace("Resumed PID {0}", process.Id);
}
catch (Exception e)
{
logger.LogError(e, "Failed to resume PID {0}!", process.Id);
throw;
}
}
/// <inheritdoc />
public void SuspendProcess(global::System.Diagnostics.Process process)
{
try
{
var result = Syscall.kill(process.Id, Signum.SIGSTOP);
if (result != 0)
throw new UnixIOException(result);
logger.LogTrace("Resumed PID {0}", process.Id);
}
catch (Exception e)
{
logger.LogError(e, "Failed to suspend PID {0}!", process.Id);
throw;
}
}
}
}
+16 -7
View File
@@ -2,6 +2,7 @@
using System;
using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.System
@@ -19,9 +20,9 @@ namespace Tgstation.Server.Host.System
public Task<int> Lifetime { get; }
/// <summary>
/// The <see cref="IProcessSuspender"/> for the <see cref="Process"/>.
/// The <see cref="IProcessFeatures"/> for the <see cref="Process"/>.
/// </summary>
readonly IProcessSuspender processSuspender;
readonly IProcessFeatures processFeatures;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Process"/>
@@ -37,7 +38,7 @@ namespace Tgstation.Server.Host.System
/// <summary>
/// Construct a <see cref="Process"/>
/// </summary>
/// <param name="processSuspender">The value of <see cref="processSuspender"/></param>
/// <param name="processFeatures">The value of <see cref="processFeatures"/></param>
/// <param name="handle">The value of <see cref="handle"/></param>
/// <param name="lifetime">The value of <see cref="Lifetime"/></param>
/// <param name="outputStringBuilder">The value of <see cref="outputStringBuilder"/></param>
@@ -46,7 +47,7 @@ namespace Tgstation.Server.Host.System
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="preExisting">If <paramref name="handle"/> was NOT just created</param>
public Process(
IProcessSuspender processSuspender,
IProcessFeatures processFeatures,
global::System.Diagnostics.Process handle,
Task<int> lifetime,
StringBuilder outputStringBuilder,
@@ -55,7 +56,7 @@ namespace Tgstation.Server.Host.System
ILogger<Process> logger,
bool preExisting)
{
this.processSuspender = processSuspender ?? throw new ArgumentNullException(nameof(processSuspender));
this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
this.handle = handle ?? throw new ArgumentNullException(nameof(handle));
this.outputStringBuilder = outputStringBuilder;
@@ -152,9 +153,17 @@ namespace Tgstation.Server.Host.System
}
/// <inheritdoc />
public void Suspend() => processSuspender.SuspendProcess(handle);
public void Suspend() => processFeatures.SuspendProcess(handle);
/// <inheritdoc />
public void Resume() => processSuspender.ResumeProcess(handle);
public void Resume() => processFeatures.ResumeProcess(handle);
/// <inheritdoc />
public async Task<string> GetExecutingUsername(CancellationToken cancellationToken)
{
var result = await processFeatures.GetExecutingUsername(handle, cancellationToken).ConfigureAwait(false);
logger.LogTrace("PID {0} Username: {1}", Id, result);
return result;
}
}
}
@@ -1,6 +1,5 @@
using Microsoft.Extensions.Logging;
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
@@ -10,9 +9,9 @@ namespace Tgstation.Server.Host.System
sealed class ProcessExecutor : IProcessExecutor
{
/// <summary>
/// The <see cref="IProcessSuspender"/> for the <see cref="ProcessExecutor"/>.
/// The <see cref="IProcessFeatures"/> for the <see cref="ProcessExecutor"/>.
/// </summary>
readonly IProcessSuspender processSuspender;
readonly IProcessFeatures processFeatures;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="ProcessExecutor"/>
@@ -55,15 +54,15 @@ namespace Tgstation.Server.Host.System
/// <summary>
/// Construct a <see cref="ProcessExecutor"/>
/// </summary>
/// <param name="processSuspender">The value of <see cref="processSuspender"/>.</param>
/// <param name="processFeatures">The value of <see cref="processFeatures"/>.</param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="loggerFactory">The value of <see cref="loggerFactory"/></param>
public ProcessExecutor(
IProcessSuspender processSuspender,
IProcessFeatures processFeatures,
ILogger<ProcessExecutor> logger,
ILoggerFactory loggerFactory)
{
this.processSuspender = processSuspender ?? throw new ArgumentNullException(nameof(processSuspender));
this.processFeatures = processFeatures ?? throw new ArgumentNullException(nameof(processFeatures));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
}
@@ -83,23 +82,15 @@ namespace Tgstation.Server.Host.System
return null;
}
try
{
return new Process(
processSuspender,
handle,
AttachExitHandler(handle),
null,
null,
null,
loggerFactory.CreateLogger<Process>(),
true);
}
catch
{
handle.Dispose();
throw;
}
return CreateFromExistingHandle(handle);
}
/// <inheritdoc />
public IProcess GetCurrentProcess()
{
logger.LogTrace("Getting current process...");
var handle = global::System.Diagnostics.Process.GetCurrentProcess();
return CreateFromExistingHandle(handle);
}
/// <inheritdoc />
@@ -215,7 +206,7 @@ namespace Tgstation.Server.Host.System
catch (InvalidOperationException) { }
return new Process(
processSuspender,
processFeatures,
handle,
lifetimeTask,
outputStringBuilder,
@@ -231,13 +222,50 @@ namespace Tgstation.Server.Host.System
}
/// <inheritdoc />
public bool IsProcessWithNameRunning(string name)
public IProcess GetProcessByName(string name)
{
logger.LogTrace("GetProcessByName: {0}...", name ?? throw new ArgumentNullException(nameof(name)));
var procs = global::System.Diagnostics.Process.GetProcessesByName(name);
global::System.Diagnostics.Process handle = null;
foreach (var proc in procs)
proc.Dispose();
if (handle == null)
handle = proc;
else
{
logger.LogTrace("Disposing extra found PID: {0}", proc.Id);
proc.Dispose();
}
return procs.Any();
if (handle == null)
return null;
return CreateFromExistingHandle(handle);
}
/// <summary>
/// Create a <see cref="IProcess"/> given an existing <paramref name="handle"/>.
/// </summary>
/// <param name="handle">The <see cref="global::System.Diagnostics.Process"/> to create a <see cref="IProcess"/> from.</param>
/// <returns>The <see cref="IProcess"/> based on <paramref name="handle"/>.</returns>
private IProcess CreateFromExistingHandle(global::System.Diagnostics.Process handle)
{
try
{
return new Process(
processFeatures,
handle,
AttachExitHandler(handle),
null,
null,
null,
loggerFactory.CreateLogger<Process>(),
true);
}
catch
{
handle.Dispose();
throw;
}
}
}
}
@@ -2,22 +2,26 @@
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Linq;
using System.Management;
using System.Threading;
using System.Threading.Tasks;
namespace Tgstation.Server.Host.System
{
/// <inheritdoc />
sealed class WindowsProcessSuspender : IProcessSuspender
sealed class WindowsProcessFeatures : IProcessFeatures
{
/// <summary>
/// The <see cref="ILogger{TCategoryName}"/> for the <see cref="WindowsProcessSuspender"/>.
/// The <see cref="ILogger{TCategoryName}"/> for the <see cref="WindowsProcessFeatures"/>.
/// </summary>
readonly ILogger<WindowsProcessSuspender> logger;
readonly ILogger<WindowsProcessFeatures> logger;
/// <summary>
/// Initializes a new instance of the <see cref="WindowsProcessSuspender"/> <see langword="class"/>.
/// Initializes a new instance of the <see cref="WindowsProcessFeatures"/> <see langword="class"/>.
/// </summary>
/// <param name="logger">The value of <see cref="logger"/>.</param>
public WindowsProcessSuspender(ILogger<WindowsProcessSuspender> logger)
public WindowsProcessFeatures(ILogger<WindowsProcessFeatures> logger)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
@@ -25,6 +29,9 @@ namespace Tgstation.Server.Host.System
/// <inheritdoc />
public void ResumeProcess(global::System.Diagnostics.Process process)
{
if (process == null)
throw new ArgumentNullException(nameof(process));
try
{
foreach (ProcessThread thread in process.Threads)
@@ -51,6 +58,9 @@ namespace Tgstation.Server.Host.System
/// <inheritdoc />
public void SuspendProcess(global::System.Diagnostics.Process process)
{
if (process == null)
throw new ArgumentNullException(nameof(process));
try
{
foreach (ProcessThread thread in process.Threads)
@@ -73,5 +83,32 @@ namespace Tgstation.Server.Host.System
throw;
}
}
/// <inheritdoc />
public Task<string> GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken)
{
string query = $"SELECT * FROM Win32_Process WHERE ProcessId = {process?.Id ?? throw new ArgumentNullException(nameof(process))}";
using var searcher = new ManagementObjectSearcher(query);
foreach (ManagementObject obj in searcher.Get())
{
var argList = new string[] { String.Empty, String.Empty };
var returnString = obj.InvokeMethod(
"GetOwner",
argList)
?.ToString();
if (!Int32.TryParse(returnString, out var returnVal))
return Task.FromResult($"BAD RETURN PARSE: {returnString}");
if (returnVal == 0)
{
// return DOMAIN\user
string owner = argList.Last() + "\\" + argList.First();
return Task.FromResult(owner);
}
}
return Task.FromResult("NO OWNER");
}
}
}
@@ -89,6 +89,7 @@
<PackageReference Include="System.Data.SqlClient" Version="4.8.1" />
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.7.0" />
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.5.1" />
<PackageReference Include="System.Management" Version="4.7.0" />
<PackageReference Include="Wangkanai.Detection.Browser" Version="2.0.0" />
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="3.0.50" />
</ItemGroup>
@@ -0,0 +1,37 @@
using Castle.Core.Logging;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using System;
using System.Threading.Tasks;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.System.Tests
{
/// <summary>
/// Tests for <see cref="IProcessFeatures"/>.
/// </summary>
[TestClass]
public sealed class TestProcessFeatures
{
IProcessFeatures features;
[TestInitialize]
public void Init()
{
features = new PlatformIdentifier().IsWindows
? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of<ILogger<WindowsProcessFeatures>>())
: new PosixProcessFeatures(new DefaultIOManager(), Mock.Of<ILogger<PosixProcessFeatures>>());
}
[TestMethod]
public async Task TestGetUsername()
{
if (!String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TRAVIS")))
Assert.Inconclusive("This test doesn't work on TRAVIS CI!");
var username = await features.GetExecutingUsername(global::System.Diagnostics.Process.GetCurrentProcess(), default);
Assert.IsTrue(username.Contains(Environment.UserName), $"Exepcted a string containing \"{Environment.UserName}\", got \"{username}\"");
}
}
}
@@ -12,6 +12,7 @@ using Tgstation.Server.Api.Models;
using Tgstation.Server.Client;
using Tgstation.Server.Client.Components;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Tests.Instance
@@ -101,8 +102,8 @@ namespace Tgstation.Server.Tests.Instance
using var ddProc = ddProcs.Single();
using var ourProcessHandler = new ProcessExecutor(
new PlatformIdentifier().IsWindows
? (IProcessSuspender)new WindowsProcessSuspender(Mock.Of<ILogger<WindowsProcessSuspender>>())
: new PosixProcessSuspender(Mock.Of<ILogger<PosixProcessSuspender>>()),
? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of<ILogger<WindowsProcessFeatures>>())
: new PosixProcessFeatures(Mock.Of<IIOManager>(), Mock.Of<ILogger<PosixProcessFeatures>>()),
Mock.Of<ILogger<ProcessExecutor>>(),
LoggerFactory.Create(x => { }))
.GetProcess(ddProc.Id);
@@ -301,7 +301,7 @@ namespace Tgstation.Server.Tests
{
var platformIdentifier = new PlatformIdentifier();
var processExecutor = new ProcessExecutor(
Mock.Of<IProcessSuspender>(),
Mock.Of<IProcessFeatures>(),
Mock.Of<ILogger<ProcessExecutor>>(),
LoggerFactory.Create(x => { }));
@@ -48,7 +48,7 @@ namespace Tgstation.Server.Tests
}
System.IO.Directory.CreateDirectory(Directory);
const string UrlString = "http://localhost:5001";
const string UrlString = "http://localhost:5010";
Url = new Uri(UrlString);
//so we need a db