Skip to content

Commit

Permalink
Merge pull request #991 from tgstation/FourTwentyTwo [TGSDeploy][APID…
Browse files Browse the repository at this point in the history
…eploy][NugetDeploy]

v4.2.2
  • Loading branch information
Cyberboss committed May 16, 2020
2 parents ce346e4 + d245213 commit fca44ae
Show file tree
Hide file tree
Showing 22 changed files with 370 additions and 144 deletions.
6 changes: 3 additions & 3 deletions build/Version.props
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
12 changes: 12 additions & 0 deletions src/Tgstation.Server.Api/Models/ErrorCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -471,5 +471,17 @@ public enum ErrorCode : uint
/// </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,
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ ChannelRepresentation GetModelChannelFromDBChannel(Api.Models.ChatChannel channe
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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@ sealed class TopicResponse : DMApiResponse
/// <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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
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;
Expand Down Expand Up @@ -110,6 +112,24 @@ static string SecurityWord(DreamDaemonSecurity securityLevel)
};
}

/// <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>
Expand Down Expand Up @@ -201,7 +221,8 @@ static string SecurityWord(DreamDaemonSecurity securityLevel)
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();

Expand All @@ -214,7 +235,7 @@ static string SecurityWord(DreamDaemonSecurity securityLevel)
// 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,
Expand Down Expand Up @@ -403,10 +424,24 @@ static string SecurityWord(DreamDaemonSecurity securityLevel)
/// <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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,8 @@ public async Task HandleEvent(EventType eventType, IEnumerable<string> parameter
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);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ async Task<bool> DirExistsAndIsNotEmpty()
PrimaryPort = 1337,
SecondaryPort = 1338,
SecurityLevel = DreamDaemonSecurity.Safe,
StartupTimeout = 20,
StartupTimeout = 60,
HeartbeatSeconds = 60
},
DreamMakerSettings = new DreamMakerSettings
Expand Down
5 changes: 3 additions & 2 deletions src/Tgstation.Server.Host/Core/Application.cs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,7 @@ logLevel switch
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>());
Expand All @@ -264,7 +264,7 @@ logLevel switch
services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
services.AddSingleton<IByondInstaller, PosixByondInstaller>();
services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
services.AddSingleton<IProcessSuspender, PosixProcessSuspender>();
services.AddSingleton<IProcessFeatures, PosixProcessFeatures>();
services.AddSingleton<INetworkPromptReaper, PosixNetworkPromptReaper>();
}

Expand Down Expand Up @@ -366,6 +366,7 @@ protected override void ConfigureHostedService(IServiceCollection services)
{
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
Expand Down
10 changes: 9 additions & 1 deletion src/Tgstation.Server.Host/System/IProcess.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading.Tasks;
using System.Threading;
using System.Threading.Tasks;

namespace Tgstation.Server.Host.System
{
Expand Down Expand Up @@ -39,5 +40,12 @@ interface IProcess : IProcessBase
/// 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);
}
}
20 changes: 13 additions & 7 deletions src/Tgstation.Server.Host/System/IProcessExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,23 @@ interface IProcessExecutor
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);
}
}
31 changes: 31 additions & 0 deletions src/Tgstation.Server.Host/System/IProcessFeatures.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
20 changes: 0 additions & 20 deletions src/Tgstation.Server.Host/System/IProcessSuspender.cs

This file was deleted.

97 changes: 97 additions & 0 deletions src/Tgstation.Server.Host/System/PosixProcessFeatures.cs
Original file line number Diff line number Diff line change
@@ -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";
}
}
}

0 comments on commit fca44ae

Please sign in to comment.