diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 9b6599a1fd..2fcf5cafc1 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -149,7 +149,32 @@ There is no strict process when it comes to merging pull requests. Pull requests
* Commits MUST be properly titled and commented as we only use merge commits for the pull request process
+## Deployment process
+
+Every issue/pull request in a release should share a common milestone named with the release version i.e. `4.5.3.5`
+
+When every issue and PR in the milestone is closed. Create a version bump PR that changes the version numbers. At the time of this writing they exist in the following files
+
+- `/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj`
+- `/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj`
+- 2 in `/src/Tgstation.Server.Host.Service/Properties/AssemblyInfo.cs`
+- `/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj`
+
+Merge the pull request with `[TGSDeploy]` somewhere in the commit title. The scripts will handle amalgamating release notes, building, closing the milestone, and publishing the release. This will also trigger update notifications on existing TGS deployments.
+
+### API/Client Deployment
+
+The Api/Client project versions must be updated on nuget when changed. The numbers exist in the following files:
+
+- `/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj`
+- `/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj`
+
+These should not be the same numbers as the main suite. When bumping the API version the client should only receive a minor (3rd number) version bump unless major changes to CLIENT code were made.
+
+Merge these alongside regular deployments with `[NugetDeploy]` in the commit title (Won't work without an accompanying `[TGSDeploy]`). This will handle the nuget publishing.
+
## Banned content
+
Do not add any of the following in a Pull Request or risk getting the PR closed:
* National Socialist Party of Germany content, National Socialist Party of Germany related content, or National Socialist Party of Germany references
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
new file mode 100644
index 0000000000..98fe22ab33
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -0,0 +1,13 @@
+[Release Notes]: # (Your PR should contain a detailed list of notable changes, titled appropriately. This includes any observable changes to the server or DMAPI. See example below)
+
+:cl:
+Description of your change
+Each newline corresponds to a release note in the upcoming sprint
+/:cl:
+
+:cl:
+You can also have multiple sets of release notes per pull request
+They will be amalgamated together in the end
+/:cl:
+
+[Why]: # (Please add a short description [two lines down] of why you think these changes would benefit the game. If you can't justify it in words, it might not be worth adding.)
diff --git a/.gitignore b/.gitignore
index 727f85078e..fefd0b5b91 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,4 @@ artifacts/
/src/Tgstation.Server.Host/wwwroot
/src/Tgstation.Server.Host/ClientApp/node_modules
/src/Tgstation.Server.Host/ClientApp/build
+/tools/ReleaseNotes/release_notes.md
diff --git a/appveyor.yml b/appveyor.yml
index c7cc494118..d6362a9ef2 100644
--- a/appveyor.yml
+++ b/appveyor.yml
@@ -6,6 +6,8 @@ environment:
TGS4_TEST_CONNECTION_STRING: Server=(local)\SQL2017;Initial Catalog=TGS_Test;User ID=sa;Password=Password12!
TGS4_TEST_GITHUB_TOKEN:
secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK
+ TGS_RELEASE_NOTES_TOKEN:
+ secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK
branches:
only:
- master
@@ -80,17 +82,16 @@ after_test:
- ps: Move-Item -path artifacts/ServerService/lib/Default/appsettings.json -destination artifacts/ServerService/
- ps: Remove-Item artifacts/ServerHost/appsettings.json
#deploy stuff
- - ps: $env:TGSVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$env:APPVEYOR_BUILD_FOLDER/artifacts/ServerHost/Tgstation.Server.Host.dll").FileVersion
- - ps: if($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[TGSDeploy\]"){ if($env:APPVEYOR_REPO_BRANCH -match "master"){ if($env:CONFIGURATION -match "Release"){ $env:TGSDeploy = "Do it." }}}
- - ps: if($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[NugetDeploy\]"){ if($env:APPVEYOR_REPO_BRANCH -match "master"){ if($env:CONFIGURATION -match "Release"){ $env:NugetDeploy = "Do it." }}}
+ - ps: build/prep_deployment.ps1
deploy:
- provider: GitHub
release: "tgstation-server-v$(TGSVersion)"
- description: 'The /tg/station server suite'
+ description: "$(TGSReleaseNotes)"
auth_token:
secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK
artifact: ServerConsole,ServerService,ServerUpdatePackage,DMAPI
- draft: true
+ draft: $(TGSDraftNotes)
+ prerelease: true
on:
TGSDeploy: "Do it."
- provider: NuGet
diff --git a/build/prep_deployment.ps1 b/build/prep_deployment.ps1
new file mode 100644
index 0000000000..a00431bff8
--- /dev/null
+++ b/build/prep_deployment.ps1
@@ -0,0 +1,25 @@
+$env:TGSVersion = [System.Diagnostics.FileVersionInfo]::GetVersionInfo("$env:APPVEYOR_BUILD_FOLDER/artifacts/ServerHost/Tgstation.Server.Host.dll").FileVersion
+
+Write-Host "TGS Version: $env:TGSVersion"
+
+if (($env:CONFIGURATION -match "Release") -And ($env:APPVEYOR_REPO_BRANCH -match "master") -And ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[TGSDeploy\]")) {
+ Write-Host "Deploying..."
+ $env:TGSDeploy = "Do it."
+
+ Write-Host "Generating release notes..."
+ dotnet run -p "$env:APPVEYOR_BUILD_FOLDER/tools/ReleaseNotes" $env:TGSVersion
+ $env:TGSDraftNotes = !($?)
+ $releaseNotesPath = "$env:APPVEYOR_BUILD_FOLDER/tools/ReleaseNotes/release_notes.md"
+ if (Test-Path $releaseNotesPath -PathType Leaf) {
+ $env:TGSReleaseNotes = [IO.File]::ReadAllText($releaseNotesPath)
+ }
+ else {
+ Write-Host "Release note generation failed, release will be created as a draft!"
+ $env:TGSReleaseNotes = "Automatic generation failed, please fill manually!"
+ }
+
+ if ($env:APPVEYOR_REPO_COMMIT_MESSAGE -match "\[NugetDeploy\]") {
+ $env:NugetDeploy = "Do it."
+ Write-Host "Nuget deployment enabled"
+ }
+}
diff --git a/src/DMAPI/tgs/v3210/api.dm b/src/DMAPI/tgs/v3210/api.dm
index 63bc0beb2b..c536995b04 100644
--- a/src/DMAPI/tgs/v3210/api.dm
+++ b/src/DMAPI/tgs/v3210/api.dm
@@ -171,6 +171,7 @@
var/datum/tgs_revision_information/ri = new
ri.commit = commit
ri.origin_commit = originmastercommit
+ return ri
/datum/tgs_api/v3210/EndProcess()
sleep(world.tick_lag) //flush the buffers
diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm
index 9d74610f9c..d40325cb5a 100644
--- a/src/DMAPI/tgs/v4/api.dm
+++ b/src/DMAPI/tgs/v4/api.dm
@@ -269,21 +269,26 @@
for(var/I in channels)
var/datum/tgs_chat_channel/channel = I
ids += channel.id
- message = list("message" = message, "channels" = ids)
+ message = list("message" = message, "channelIds" = ids)
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
Export(TGS4_COMM_CHAT, message)
/datum/tgs_api/v4/ChatTargetedBroadcast(message, admin_only)
- message = list("message" = message, "channels" = admin_only ? "admin" : "game")
+ var/list/channels = list()
+ for(var/I in ChatChannelInfo())
+ var/datum/tgs_chat_channel/channel = I
+ if (!channel.is_private_channel && ((channel.is_admin_channel && admin_only) || (!channel.is_admin_channel && !admin_only)))
+ channels += channel.id
+ message = list("message" = message, "channelIds" = channels)
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
Export(TGS4_COMM_CHAT, message)
/datum/tgs_api/v4/ChatPrivateMessage(message, datum/tgs_chat_user/user)
- message = list("message" = message, "user" = list("id" = user.id, "channel" = user.channel.id))
+ message = list("message" = message, "channelIds" = list(user.channel.id))
if(intercepted_message_queue)
intercepted_message_queue += list(message)
else
diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
index fb8e8bcfa7..d181d5f605 100644
--- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
+++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj
@@ -4,7 +4,7 @@
Exe
netcoreapp2.1
Full
- 4.0.1.2
+ 4.0.1.4
diff --git a/src/Tgstation.Server.Host.Service/Properties/AssemblyInfo.cs b/src/Tgstation.Server.Host.Service/Properties/AssemblyInfo.cs
index f53415defe..2b77d018db 100644
--- a/src/Tgstation.Server.Host.Service/Properties/AssemblyInfo.cs
+++ b/src/Tgstation.Server.Host.Service/Properties/AssemblyInfo.cs
@@ -7,5 +7,5 @@ using System.Runtime.InteropServices;
[assembly: Guid("29927416-3b78-49a7-a560-5ccaa638b6b4")]
[assembly: InternalsVisibleTo("Tgstation.Server.Host.Service.Tests")]
-[assembly: AssemblyVersion("4.0.1.2")]
-[assembly: AssemblyFileVersion("4.0.1.2")]
+[assembly: AssemblyVersion("4.0.1.4")]
+[assembly: AssemblyFileVersion("4.0.1.4")]
diff --git a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj
index c5a1cb5ab7..ec75cc5b0d 100644
--- a/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj
+++ b/src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj
@@ -4,7 +4,7 @@
netstandard2.0
Full
false
- 4.0.1.2
+ 4.0.1.4
diff --git a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs
index 5bf642cdcb..3c25ac9175 100644
--- a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs
+++ b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs
@@ -82,31 +82,22 @@ namespace Tgstation.Server.Host.Components.Byond
///
public async Task DownloadVersion(Version version, CancellationToken cancellationToken)
{
- var ourVersion = version;
- //lummox is annoying and doesn't like to post linux versions if nothing changed in DreamDaemon/DM
- //if this for's exit condition ever triggers, i get to say I told you so
- Exception lastException = null;
- for (var I = 0; I < 5 && ourVersion.Minor >= 1; ++I, ourVersion = new Version(ourVersion.Major, ourVersion.Minor))
- {
- try
- {
- var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, ourVersion.Major, ourVersion.Minor);
+ if (version == null)
+ throw new ArgumentNullException(nameof(version));
- return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
- }
- catch (WebException e)
- {
- if (!(e.Status == WebExceptionStatus.ProtocolError && e.Response is HttpWebResponse response && response.StatusCode == HttpStatusCode.NotFound))
- throw;
- lastException = e;
- }
- }
- throw lastException;
+ var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
+
+ return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
}
///
public Task InstallByond(string path, Version version, CancellationToken cancellationToken)
{
+ if (path == null)
+ throw new ArgumentNullException(nameof(path));
+ if (version == null)
+ throw new ArgumentNullException(nameof(version));
+
//write the scripts for running the ting
//need to add $ORIGIN to LD_LIBRARY_PATH
const string StandardScript = "#!/bin/sh\nexport LD_LIBRARY_PATH=\"\\$ORIGIN:$LD_LIBRARY_PATH\"\nBASEDIR=$(dirname \"$0\")\nexec \"$BASEDIR/{0}\" \"$@\"\n";
diff --git a/src/Tgstation.Server.Host/Components/Chat/Channel.cs b/src/Tgstation.Server.Host/Components/Chat/Channel.cs
index 131ce67962..fe5d6acb15 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Channel.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Channel.cs
@@ -38,12 +38,12 @@ namespace Tgstation.Server.Host.Components.Chat
///
/// If this is considered a channel for admin commands
///
- public bool IsAdmin { get; set; }
+ public bool IsAdminChannel { get; set; }
///
/// If this is a 1-to-1 chat channel
///
- public bool IsPrivate { get; set; }
+ public bool IsPrivateChannel { get; set; }
///
/// For user use
diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs
index b4d68b1139..fc7321af84 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs
@@ -195,7 +195,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
var providerId = providers.Where(x => x.Value == provider).Select(x => x.Key).First();
var enumerable = mappedChannels.Where(x => x.Value.ProviderId == providerId && x.Value.ProviderChannelId == message.User.Channel.RealId);
- if (message.User.Channel.IsPrivate)
+ if (message.User.Channel.IsPrivateChannel)
lock (mappedChannels)
{
if (!provider.Connected)
@@ -223,7 +223,7 @@ namespace Tgstation.Server.Host.Components.Chat
var mapping = enumerable.First().Value;
message.User.Channel.Id = mapping.Channel.Id;
message.User.Channel.Tag = mapping.Channel.Tag;
- message.User.Channel.IsAdmin = mapping.Channel.IsAdmin;
+ message.User.Channel.IsAdminChannel = mapping.Channel.IsAdminChannel;
}
}
@@ -236,7 +236,7 @@ namespace Tgstation.Server.Host.Components.Chat
var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
- if (!addressed && !message.User.Channel.IsPrivate)
+ if (!addressed && !message.User.Channel.IsPrivateChannel)
//no mention
return;
@@ -302,7 +302,7 @@ namespace Tgstation.Server.Host.Components.Chat
return;
}
- if (commandHandler.AdminOnly && !message.User.Channel.IsAdmin)
+ if (commandHandler.AdminOnly && !message.User.Channel.IsAdminChannel)
{
await SendMessage("Use this command in an admin channel!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
return;
diff --git a/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs
index 8694606731..4ff8799ce7 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Commands/ICommand.cs
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
string HelpText { get; }
///
- /// If the command should only be available to s who's has set
+ /// If the command should only be available to s who's has set
///
bool AdminOnly { get; }
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
index 70808ab9cc..50996ed74a 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs
@@ -49,6 +49,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
///
readonly List mappedChannels;
+ ///
+ /// Normalize a discord mention string
+ ///
+ /// The mention provided by the Discord library
+ /// The normalized mention
static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", "", StringComparison.Ordinal);
///
@@ -92,7 +97,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
Channel = new Channel
{
RealId = e.Channel.Id,
- IsPrivate = pm,
+ IsPrivateChannel = pm,
ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN",
FriendlyName = e.Channel.Name
//isAdmin and Tag populated by manager
@@ -141,6 +146,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return true;
}
+ ///
public override async Task Disconnect(CancellationToken cancellationToken)
{
if (!Connected)
@@ -182,10 +188,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return new Channel
{
RealId = discordChannel.Id,
- IsAdmin = channel.IsAdminChannel == true,
+ IsAdminChannel = channel.IsAdminChannel == true,
ConnectionName = discordChannel.Guild.Name,
FriendlyName = discordChannel.Name,
- IsPrivate = false,
+ IsPrivateChannel = false,
Tag = channel.Tag
};
};
diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
index cd08c1760e..01fa89697e 100644
--- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
+++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs
@@ -204,7 +204,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
ConnectionName = address,
FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
RealId = channelId,
- IsPrivate = isPrivate
+ IsPrivateChannel = isPrivate
//isAdmin and Tag populated by manager
},
FriendlyName = username,
@@ -393,10 +393,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
return new Channel
{
RealId = id.Value,
- IsAdmin = x.IsAdminChannel == true,
+ IsAdminChannel = x.IsAdminChannel == true,
ConnectionName = address,
FriendlyName = channelIdMap[id.Value],
- IsPrivate = false,
+ IsPrivateChannel = false,
Tag = x.Tag
};
}).ToList();
diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs
index 88a2286d47..b9b558cba2 100644
--- a/src/Tgstation.Server.Host/Components/InstanceManager.cs
+++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs
@@ -12,7 +12,7 @@ using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components
{
///
- sealed class InstanceManager : IInstanceManager, IHostedService, IDisposable
+ sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IDisposable
{
///
/// The for the
@@ -49,6 +49,11 @@ namespace Tgstation.Server.Host.Components
///
readonly Dictionary instances;
+ ///
+ /// Used in to determine if database downgrades must be made
+ ///
+ Version downgradeVersion;
+
///
/// If the has been d
///
@@ -62,16 +67,21 @@ namespace Tgstation.Server.Host.Components
/// The value of
/// The value of
/// The value of
+ /// The used to register the as a
/// The value of
- public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger logger)
+ public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger logger)
{
this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory));
this.application = application ?? throw new ArgumentNullException(nameof(application));
this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager));
+ if (serverControl == null)
+ throw new ArgumentNullException(nameof(serverControl));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ serverControl.RegisterForRestart(this);
+
instances = new Dictionary();
}
@@ -225,6 +235,17 @@ namespace Tgstation.Server.Host.Components
await jobManager.StopAsync(cancellationToken).ConfigureAwait(false);
await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false);
await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false);
+
+ //downgrade the db if necessary
+ if (downgradeVersion != null)
+ await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false);
+ }
+
+ ///
+ public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
+ {
+ downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
+ return Task.CompletedTask;
}
}
}
diff --git a/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs b/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs
index 1c49476b35..a8cc05feba 100644
--- a/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/ChatCommand.cs
@@ -5,7 +5,7 @@ namespace Tgstation.Server.Host.Components.Interop
sealed class ChatCommand
{
public string Command { get; set; }
- public string Parameters { get; set; }
+ public string Params { get; set; }
public User User { get; set; }
}
}
diff --git a/src/Tgstation.Server.Host/Components/Interop/CommCommand.cs b/src/Tgstation.Server.Host/Components/Interop/CommCommand.cs
index cc9ab8571a..31b4715897 100644
--- a/src/Tgstation.Server.Host/Components/Interop/CommCommand.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/CommCommand.cs
@@ -8,8 +8,13 @@ namespace Tgstation.Server.Host.Components.Interop
sealed class CommCommand
{
///
- /// The raw JSON decond of the
+ /// The dictionary of the
///
- public IReadOnlyDictionary Parameters { get; set; }
+ public IReadOnlyDictionary Parameters { get; set; }
+
+ ///
+ /// The raw JSON of the
+ ///
+ public string RawJson { get; set; }
}
}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host/Components/Interop/CommContext.cs b/src/Tgstation.Server.Host/Components/Interop/CommContext.cs
index 839086a3bb..e910ad7ffa 100644
--- a/src/Tgstation.Server.Host/Components/Interop/CommContext.cs
+++ b/src/Tgstation.Server.Host/Components/Interop/CommContext.cs
@@ -109,10 +109,11 @@ namespace Tgstation.Server.Host.Components.Interop
{
command = new CommCommand
{
- Parameters = JsonConvert.DeserializeObject>(file)
+ Parameters = JsonConvert.DeserializeObject>(file),
+ RawJson = file
};
}
- catch (JsonSerializationException ex)
+ catch (JsonException ex)
{
//file not fully written yet
logger.LogDebug("Suppressing json convert exception for command file write: {0}", ex);
diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
index bb19579a21..f7a6436e22 100644
--- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
+++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs
@@ -366,6 +366,8 @@ namespace Tgstation.Server.Host.Components.StaticFiles
var success = synchronousIOManager.WriteFileChecked(path, data, ref fileHash, cancellationToken);
if (!success)
return;
+ if (data != null)
+ postWriteHandler.HandleWrite(path);
result = new ConfigurationFile
{
Content = data,
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs
index dd5d2b7ebb..d8fa568f03 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs
@@ -1,6 +1,7 @@
using Byond.TopicSender;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
+using Newtonsoft.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Globalization;
@@ -220,6 +221,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
return result;
};
LaunchResult = GetLaunchResult();
+
+ logger.LogDebug("Created session controller. Primary: {0}, CommsKey: {1}, Port: {2}", IsPrimary, reattachInformation.AccessIdentifier, Port);
}
///
@@ -286,6 +289,25 @@ namespace Tgstation.Server.Host.Components.Watchdog
content = new object();
switch (method)
{
+ case Constants.DMCommandChat:
+ try
+ {
+ var message = JsonConvert.DeserializeObject(command.RawJson, new JsonSerializerSettings
+ {
+ ContractResolver = new CamelCasePropertyNamesContractResolver()
+ });
+ if (message.ChannelIds == null)
+ throw new InvalidOperationException("Missing ChannelIds field!");
+ if (message.Message == null)
+ throw new InvalidOperationException("Missing Message field!");
+ await chat.SendMessage(message.Message, message.ChannelIds, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ logger.LogDebug("Exception while decoding chat message! Exception: {0}", e);
+ goto default;
+ }
+ break;
case Constants.DMCommandServerPrimed:
//currently unused, maybe in the future
break;
@@ -296,7 +318,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
case Constants.DMCommandNewPort:
lock (this)
{
- if (!query.TryGetValue(Constants.DMParameterData, out var stringPort) || !UInt16.TryParse(stringPort, out var currentPort))
+ if (!query.TryGetValue(Constants.DMParameterData, out var stringPortObject) || !UInt16.TryParse(stringPortObject as string, out var currentPort))
{
/////UHHHH
logger.LogWarning("DreamDaemon sent new port command without providing it's own!");
@@ -334,7 +356,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
content = new ErrorMessage { Message = "Invalid API validation request!" };
break;
}
- if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevel) || !Enum.TryParse(stringMinimumSecurityLevel, out var minimumSecurityLevel))
+ if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse(stringMinimumSecurityLevelObject as string, out var minimumSecurityLevel))
apiValidationStatus = ApiValidationStatus.BadValidationRequest;
else
switch (minimumSecurityLevel)
@@ -459,6 +481,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
return false;
}
+ reattachInformation.Port = port;
return true;
}
diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
index 0302b24fce..882a07170e 100644
--- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
+++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs
@@ -453,20 +453,13 @@ namespace Tgstation.Server.Host.Components.Watchdog
//servers now swapped
- //enable this now
- monitorState.ActiveServer.ClosePortOnReboot = true;
+ //enable this now if inactive server is not still valid
+ monitorState.ActiveServer.ClosePortOnReboot = restartOnceSwapped;
if (!restartOnceSwapped)
- {
- //inactive server still valid
-
- //disable this
- monitorState.InactiveServer.ClosePortOnReboot = false;
-
//now try to reopen it on the private port
//failing that, just reboot it
restartOnceSwapped = !await monitorState.InactiveServer.SetPort(ActiveLaunchParameters.SecondaryPort.Value, cancellationToken).ConfigureAwait(false);
- }
//break either way because any issues past this point would be solved by the reboot
if (restartOnceSwapped)
@@ -1059,7 +1052,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
var commandObject = new ChatCommand
{
Command = commandName,
- Parameters = arguments,
+ Params = arguments,
User = sender
};
diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
index 5db9cd5897..e220a74e39 100644
--- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs
+++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs
@@ -9,7 +9,6 @@ using System.Linq;
using System.Linq.Expressions;
using System.Net;
using System.Reflection;
-using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api;
@@ -121,6 +120,17 @@ namespace Tgstation.Server.Host.Controllers
return BadRequest(new ErrorMessage { Message = "path must not be empty!" });
NormalizeModelPath(model, out var rawPath);
+
+ var localPath = ioManager.ResolvePath(".");
+ NormalizeModelPath(new Api.Models.Instance
+ {
+ Path = localPath
+ }, out var normalizedLocalPath);
+
+ if (rawPath.StartsWith(normalizedLocalPath, StringComparison.Ordinal))
+ return Conflict("Instances cannot be created in the installation directory!");
+
+
var dirExistsTask = ioManager.DirectoryExists(model.Path, cancellationToken);
bool attached = false;
if (await ioManager.FileExists(model.Path, cancellationToken).ConfigureAwait(false) || await dirExistsTask.ConfigureAwait(false))
diff --git a/src/Tgstation.Server.Host/Core/JobHandler.cs b/src/Tgstation.Server.Host/Core/JobHandler.cs
index 72fc7faa40..c57c7bdc1f 100644
--- a/src/Tgstation.Server.Host/Core/JobHandler.cs
+++ b/src/Tgstation.Server.Host/Core/JobHandler.cs
@@ -9,24 +9,25 @@ namespace Tgstation.Server.Host.Core
///
sealed class JobHandler : IDisposable
{
- ///
- /// The being run
- ///
- readonly Task task;
///
/// The for
///
readonly CancellationTokenSource cancellationTokenSource;
+ ///
+ /// The being run
+ ///
+ readonly Task task;
///
/// Construct a
///
- /// The value of
- /// The value of
- JobHandler(Task task, CancellationTokenSource cancellationTokenSource)
+ /// A taking a and returning a that the will wrap
+ public JobHandler(Func job)
{
- this.task = task;
- this.cancellationTokenSource = cancellationTokenSource;
+ if (job == null)
+ throw new ArgumentNullException(nameof(job));
+ cancellationTokenSource = new CancellationTokenSource();
+ task = job(cancellationTokenSource.Token);
}
///
@@ -54,16 +55,5 @@ namespace Tgstation.Server.Host.Core
/// Cancels
///
public void Cancel() => cancellationTokenSource.Cancel();
-
- ///
- /// Create a
- ///
- /// A taking a and returning a that the will wrap
- /// A new
- public static JobHandler Create(Func job)
- {
- var cts = new CancellationTokenSource();
- return new JobHandler(job(cts.Token), cts);
- }
}
}
\ No newline at end of file
diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs
index 614661ac0e..b3aeac331d 100644
--- a/src/Tgstation.Server.Host/Core/JobManager.cs
+++ b/src/Tgstation.Server.Host/Core/JobManager.cs
@@ -168,7 +168,7 @@ namespace Tgstation.Server.Host.Core
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description);
- var jobHandler = JobHandler.Create(x => RunJob(job, (jobParam, serviceProvider, ct) =>
+ var jobHandler = new JobHandler(x => RunJob(job, (jobParam, serviceProvider, ct) =>
operation(jobParam, serviceProvider, y =>
{
lock (this)
diff --git a/src/Tgstation.Server.Host/IO/PosixPostWriteHandler.cs b/src/Tgstation.Server.Host/IO/PosixPostWriteHandler.cs
index d656e0fa99..75eaa24bcd 100644
--- a/src/Tgstation.Server.Host/IO/PosixPostWriteHandler.cs
+++ b/src/Tgstation.Server.Host/IO/PosixPostWriteHandler.cs
@@ -1,5 +1,6 @@
using Mono.Unix;
using Mono.Unix.Native;
+using System;
namespace Tgstation.Server.Host.IO
{
@@ -11,6 +12,9 @@ namespace Tgstation.Server.Host.IO
///
public void HandleWrite(string filePath)
{
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+
//set executable bit every time, don't want people calling me when their uploaded "sl" binary doesn't work
if (Syscall.stat(filePath, out var stat) != 0)
throw new UnixIOException(Stdlib.GetLastError());
diff --git a/src/Tgstation.Server.Host/IO/WindowsPostWriteHandler.cs b/src/Tgstation.Server.Host/IO/WindowsPostWriteHandler.cs
index 6d149644fa..86907933fe 100644
--- a/src/Tgstation.Server.Host/IO/WindowsPostWriteHandler.cs
+++ b/src/Tgstation.Server.Host/IO/WindowsPostWriteHandler.cs
@@ -1,4 +1,6 @@
-namespace Tgstation.Server.Host.IO
+using System;
+
+namespace Tgstation.Server.Host.IO
{
///
/// for Windows systems
@@ -6,6 +8,10 @@
sealed class WindowsPostWriteHandler : IPostWriteHandler
{
///
- public void HandleWrite(string filePath) { }
+ public void HandleWrite(string filePath)
+ {
+ if (filePath == null)
+ throw new ArgumentNullException(nameof(filePath));
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs
index 7578c21c8a..e113f1d999 100644
--- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs
@@ -1,7 +1,11 @@
using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
+using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -169,5 +173,51 @@ namespace Tgstation.Server.Host.Models
///
public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
+
+ ///
+ /// If the MY_ class of migrations should be used instead of the MS_ class
+ ///
+ /// if the MY_ class of migrations should be used instead of the MS_ class, otherwise
+ protected abstract bool UseMySQLMigrations();
+
+ ///
+ public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken)
+ {
+ if (version == null)
+ throw new ArgumentNullException(nameof(version));
+ if (version < new Version(4, 0))
+ throw new ArgumentOutOfRangeException(nameof(version), version, "Not a valid V4 version!");
+
+ string targetMigration = null;
+
+ //Update this with new migrations as they are made
+ //Always use the MS class
+
+ //TODO: Uncomment once #816 is merged
+ /*
+ if (version < new Version(4, 0, 2))
+ targetMigration = nameof(MSReattachCompileJobRequired);
+ */
+
+ if (targetMigration == null)
+ return;
+
+ if (UseMySQLMigrations())
+ targetMigration = String.Format(CultureInfo.InvariantCulture, "MY" + targetMigration.Substring(2));
+
+ //even though it clearly implements it in the DatabaseFacade definition this won't work without casting (╯ಠ益ಠ)╯︵ ┻━┻
+ var dbServiceProvider = ((IInfrastructure)Database).Instance;
+ var migrator = dbServiceProvider.GetRequiredService();
+
+ Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration);
+ try
+ {
+ await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false);
+ }
+ catch (Exception e)
+ {
+ Logger.LogCritical("Failed to migrate! Exception: {0}", e);
+ }
+ }
}
}
diff --git a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs
index 8fdb8aad74..e7c8e9dcd2 100644
--- a/src/Tgstation.Server.Host/Models/IDatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Models/IDatabaseContext.cs
@@ -1,4 +1,5 @@
using Microsoft.EntityFrameworkCore;
+using System;
using System.Threading;
using System.Threading.Tasks;
@@ -87,5 +88,13 @@ namespace Tgstation.Server.Host.Models
/// The for the operation
/// A representing the running operation
Task Initialize(CancellationToken cancellationToken);
+
+ ///
+ /// Attempt to downgrade the schema to the migration used for a given server
+ ///
+ /// The tgstation-server that the schema should downgrade for
+ /// The for the operation
+ /// A representing the running operation
+ Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken);
}
}
diff --git a/src/Tgstation.Server.Host/Models/MySqlDatabaseContext.cs b/src/Tgstation.Server.Host/Models/MySqlDatabaseContext.cs
index 8c03b31ee0..718fc04d98 100644
--- a/src/Tgstation.Server.Host/Models/MySqlDatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Models/MySqlDatabaseContext.cs
@@ -38,5 +38,8 @@ namespace Tgstation.Server.Host.Models
else
options.UseMySql(DatabaseConfiguration.ConnectionString);
}
+
+ ///
+ protected override bool UseMySQLMigrations() => true;
}
}
diff --git a/src/Tgstation.Server.Host/Models/SqlServerDatabaseContext.cs b/src/Tgstation.Server.Host/Models/SqlServerDatabaseContext.cs
index d3d5aa8260..94240566d3 100644
--- a/src/Tgstation.Server.Host/Models/SqlServerDatabaseContext.cs
+++ b/src/Tgstation.Server.Host/Models/SqlServerDatabaseContext.cs
@@ -26,5 +26,8 @@ namespace Tgstation.Server.Host.Models
base.OnConfiguring(options);
options.UseSqlServer(DatabaseConfiguration.ConnectionString);
}
+
+ ///
+ protected override bool UseMySQLMigrations() => false;
}
}
diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
index 4d79f98e84..d56e95c6b7 100644
--- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
+++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj
@@ -3,8 +3,7 @@
netcoreapp2.1
Full
- 4.0.1.2
- 3.0
+ 4.0.1.4
@@ -49,21 +48,24 @@
-
-
-
+
+
+
-
+
all
runtime; build; native; contentfiles; analyzers
-
-
-
-
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
@@ -72,7 +74,7 @@
-
+
diff --git a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj
index c395583e37..1ec7f54612 100644
--- a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj
+++ b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj
index 6eaed1daed..4041253f61 100644
--- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj
+++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj
index 1648915ec8..7e17c460e2 100644
--- a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj
+++ b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs b/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs
new file mode 100644
index 0000000000..e2fb2cff0f
--- /dev/null
+++ b/tests/Tgstation.Server.Host.Tests/Components/Byond/TestPosixByondInstaller.cs
@@ -0,0 +1,95 @@
+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.Components.Byond.Tests
+{
+ [TestClass]
+ public sealed class TestPosixByondInstaller
+ {
+ [TestMethod]
+ public void TestConstruction()
+ {
+ Assert.ThrowsException(() => new PosixByondInstaller(null, null, null));
+ var mockIOManager = new Mock();
+ Assert.ThrowsException(() => new PosixByondInstaller(mockIOManager.Object, null, null));
+ var mockPostWriteHandler = new Mock();
+ Assert.ThrowsException(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null));
+
+ var mockLogger = new Mock>();
+ new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+ }
+
+ [TestMethod]
+ public async Task TestCacheClean()
+ {
+ var mockIOManager = new Mock();
+ var mockPostWriteHandler = new Mock();
+ var mockLogger = new Mock>();
+ var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+
+ const string ByondCachePath = "~/.byond/cache";
+
+ mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Returns(Task.CompletedTask).Verifiable();
+
+ await installer.CleanCache(default);
+
+ mockIOManager.Verify();
+
+
+ mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new OperationCanceledException()).Verifiable();
+
+ await Assert.ThrowsExceptionAsync(() => installer.CleanCache(default)).ConfigureAwait(false);
+
+ mockIOManager.Verify();
+
+ mockIOManager.Setup(x => x.DeleteDirectory(ByondCachePath, default)).Throws(new Exception()).Verifiable();
+
+ await installer.CleanCache(default).ConfigureAwait(false);
+
+ mockIOManager.Verify();
+ }
+
+
+ [TestMethod]
+ public async Task TestDownload()
+ {
+ var mockIOManager = new Mock();
+ var mockPostWriteHandler = new Mock();
+ var mockLogger = new Mock>();
+ var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+
+ await Assert.ThrowsExceptionAsync(() => installer.DownloadVersion(null, default)).ConfigureAwait(false);
+
+ var ourArray = Array.Empty();
+ mockIOManager.Setup(x => x.DownloadFile(It.Is(uri => uri == new Uri("https://secure.byond.com/download/build/511/511.1385_byond_linux.zip")), default)).Returns(Task.FromResult(ourArray)).Verifiable();
+
+ var result = await installer.DownloadVersion(new Version(511, 1385), default).ConfigureAwait(false);
+
+ Assert.AreSame(ourArray, result);
+ mockIOManager.Verify();
+ }
+
+ [TestMethod]
+ public async Task TestInstallByond()
+ {
+ var mockIOManager = new Mock();
+ var mockPostWriteHandler = new Mock();
+ var mockLogger = new Mock>();
+ var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
+
+ const string FakePath = "fake";
+ await Assert.ThrowsExceptionAsync(() => installer.InstallByond(null, null, default)).ConfigureAwait(false);
+ await Assert.ThrowsExceptionAsync(() => installer.InstallByond(FakePath, null, default)).ConfigureAwait(false);
+
+ await installer.InstallByond(FakePath, new Version(511, 1385), default).ConfigureAwait(false);
+
+ mockIOManager.Verify(x => x.ConcatPath(It.IsAny(), It.IsNotNull()), Times.Exactly(5));
+ mockIOManager.Verify(x => x.WriteAllBytes(It.IsAny(), It.IsNotNull(), default), Times.Exactly(2));
+ mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny()), Times.Exactly(4));
+ }
+ }
+}
diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs
new file mode 100644
index 0000000000..15fec66f51
--- /dev/null
+++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs
@@ -0,0 +1,84 @@
+using Microsoft.Extensions.Logging;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using Moq;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Components.Chat.Providers.Tests
+{
+ [TestClass]
+ public sealed class TestDiscordProvider
+ {
+ string testToken1;
+
+ [TestInitialize]
+ public void Initialize()
+ {
+ testToken1 = Environment.GetEnvironmentVariable("TGS4_TEST_DISCORD_TOKEN_1");
+ }
+
+ [TestMethod]
+ public void TestConstructionAndDisposal()
+ {
+ Assert.ThrowsException(() => new DiscordProvider(null, null));
+ var mockLogger = new Mock>();
+ Assert.ThrowsException(() => new DiscordProvider(mockLogger.Object, null));
+ var mockToken = "asdf";
+ new DiscordProvider(mockLogger.Object, mockToken).Dispose();
+ }
+
+ [TestMethod]
+ public async Task TestConnectWithFakeTokenFails()
+ {
+ var mockLogger = new Mock>();
+ using (var provider = new DiscordProvider(mockLogger.Object, "asdf"))
+ {
+ Assert.IsFalse(await provider.Connect(default).ConfigureAwait(false));
+ Assert.IsFalse(provider.Connected);
+ }
+ }
+
+ [Ignore("Broken due to dependency issues after first call to .Connect()")]
+ [TestMethod]
+ public async Task TestConnectAndDisconnect()
+ {
+ if (testToken1 == null)
+ Assert.Inconclusive("Required environment variable TGS4_TEST_DISCORD_TOKEN_1 isn't set!");
+
+
+ var mockLogger = new Mock>();
+ using (var provider = new DiscordProvider(mockLogger.Object, testToken1))
+ {
+ Assert.IsFalse(provider.Connected);
+ await provider.Disconnect(default).ConfigureAwait(false);
+ Assert.IsFalse(provider.Connected);
+ Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
+ Assert.IsTrue(provider.Connected);
+ Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
+ Assert.IsTrue(provider.Connected);
+
+ await provider.Disconnect(default).ConfigureAwait(false);
+ Assert.IsFalse(provider.Connected);
+ await provider.Disconnect(default).ConfigureAwait(false);
+ Assert.IsFalse(provider.Connected);
+
+ //now try it with cancellationTokens
+ using (var cts = new CancellationTokenSource())
+ {
+ cts.Cancel();
+ var cancellationToken = cts.Token;
+ await Assert.ThrowsExceptionAsync(() => provider.Connect(cancellationToken)).ConfigureAwait(false);
+ Assert.IsFalse(provider.Connected);
+ Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
+ Assert.IsTrue(provider.Connected);
+ await Assert.ThrowsExceptionAsync(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false);
+ Assert.IsTrue(provider.Connected);
+ await provider.Disconnect(default).ConfigureAwait(false);
+ Assert.IsFalse(provider.Connected);
+ }
+
+ }
+ }
+ }
+}
diff --git a/tests/Tgstation.Server.Host.Tests/Core/TestJobHandler.cs b/tests/Tgstation.Server.Host.Tests/Core/TestJobHandler.cs
new file mode 100644
index 0000000000..4aad145fa5
--- /dev/null
+++ b/tests/Tgstation.Server.Host.Tests/Core/TestJobHandler.cs
@@ -0,0 +1,80 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Tgstation.Server.Host.Core.Tests
+{
+ [TestClass]
+ public sealed class TestJobHandler
+ {
+ Task currentWaitTask;
+ bool cancelled;
+
+ async Task TestJob(CancellationToken cancellationToken)
+ {
+ await currentWaitTask.ConfigureAwait(false);
+ cancelled = cancellationToken.IsCancellationRequested;
+ }
+
+ [TestMethod]
+ public void TestConstructionAndDisposal()
+ {
+ cancelled = false;
+ Assert.ThrowsException(() => new JobHandler(null));
+
+ currentWaitTask = Task.CompletedTask;
+ new JobHandler(TestJob).Dispose();
+ Assert.IsFalse(cancelled);
+ }
+
+ [TestMethod]
+ public async Task TestWait()
+ {
+ cancelled = false;
+ //test with a cancelled cts
+ using (var cts = new CancellationTokenSource())
+ {
+ var tcs = new TaskCompletionSource