mirror of
https://github.com/tgstation/tgstation-server.git
synced 2026-07-20 12:33:00 +01:00
Merge branch 'master' into SPA
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.)
|
||||
@@ -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
|
||||
|
||||
+6
-5
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<DebugType>Full</DebugType>
|
||||
<Version>4.0.1.2</Version>
|
||||
<Version>4.0.1.4</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
<DebugType>Full</DebugType>
|
||||
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
|
||||
<Version>4.0.1.2</Version>
|
||||
<Version>4.0.1.4</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
|
||||
@@ -82,31 +82,22 @@ namespace Tgstation.Server.Host.Components.Byond
|
||||
/// <inheritdoc />
|
||||
public async Task<byte[]> 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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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";
|
||||
|
||||
@@ -38,12 +38,12 @@ namespace Tgstation.Server.Host.Components.Chat
|
||||
/// <summary>
|
||||
/// If this is considered a channel for admin commands
|
||||
/// </summary>
|
||||
public bool IsAdmin { get; set; }
|
||||
public bool IsAdminChannel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If this is a 1-to-1 chat channel
|
||||
/// </summary>
|
||||
public bool IsPrivate { get; set; }
|
||||
public bool IsPrivateChannel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// For user use
|
||||
|
||||
@@ -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<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
|
||||
string HelpText { get; }
|
||||
|
||||
/// <summary>
|
||||
/// If the command should only be available to <see cref="User"/>s who's <see cref="User.Channel"/> has <see cref="Channel.IsAdmin"/> set
|
||||
/// If the command should only be available to <see cref="User"/>s who's <see cref="User.Channel"/> has <see cref="Channel.IsAdminChannel"/> set
|
||||
/// </summary>
|
||||
bool AdminOnly { get; }
|
||||
|
||||
|
||||
@@ -49,6 +49,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
|
||||
/// </summary>
|
||||
readonly List<ulong> mappedChannels;
|
||||
|
||||
/// <summary>
|
||||
/// Normalize a discord mention string
|
||||
/// </summary>
|
||||
/// <param name="fromDiscord">The mention <see cref="string"/> provided by the Discord library</param>
|
||||
/// <returns>The normalized mention <see cref="string"/></returns>
|
||||
static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", "", StringComparison.Ordinal);
|
||||
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -12,7 +12,7 @@ using Tgstation.Server.Host.IO;
|
||||
namespace Tgstation.Server.Host.Components
|
||||
{
|
||||
/// <inheritdoc />
|
||||
sealed class InstanceManager : IInstanceManager, IHostedService, IDisposable
|
||||
sealed class InstanceManager : IInstanceManager, IRestartHandler, IHostedService, IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="IInstanceFactory"/> for the <see cref="InstanceManager"/>
|
||||
@@ -49,6 +49,11 @@ namespace Tgstation.Server.Host.Components
|
||||
/// </summary>
|
||||
readonly Dictionary<long, IInstance> instances;
|
||||
|
||||
/// <summary>
|
||||
/// Used in <see cref="StopAsync(CancellationToken)"/> to determine if database downgrades must be made
|
||||
/// </summary>
|
||||
Version downgradeVersion;
|
||||
|
||||
/// <summary>
|
||||
/// If the <see cref="InstanceManager"/> has been <see cref="Dispose"/>d
|
||||
/// </summary>
|
||||
@@ -62,16 +67,21 @@ namespace Tgstation.Server.Host.Components
|
||||
/// <param name="databaseContextFactory">The value of <paramref name="databaseContextFactory"/></param>
|
||||
/// <param name="application">The value of <see cref="application"/></param>
|
||||
/// <param name="jobManager">The value of <see cref="jobManager"/></param>
|
||||
/// <param name="serverControl">The <see cref="IServerControl"/> used to register the <see cref="InstanceManager"/> as a <see cref="IRestartHandler"/></param>
|
||||
/// <param name="logger">The value of <see cref="logger"/></param>
|
||||
public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger<InstanceManager> logger)
|
||||
public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger<InstanceManager> 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<long, IInstance>();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken)
|
||||
{
|
||||
downgradeVersion = updateVersion != null && updateVersion < application.Version ? updateVersion : null;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,13 @@ namespace Tgstation.Server.Host.Components.Interop
|
||||
sealed class CommCommand
|
||||
{
|
||||
/// <summary>
|
||||
/// The raw JSON decond of the <see cref="CommCommand"/>
|
||||
/// The dictionary of the <see cref="CommCommand"/>
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Parameters { get; set; }
|
||||
public IReadOnlyDictionary<string, object> Parameters { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The raw JSON of the <see cref="CommCommand"/>
|
||||
/// </summary>
|
||||
public string RawJson { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -109,10 +109,11 @@ namespace Tgstation.Server.Host.Components.Interop
|
||||
{
|
||||
command = new CommCommand
|
||||
{
|
||||
Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, string>>(file)
|
||||
Parameters = JsonConvert.DeserializeObject<IReadOnlyDictionary<string, object>>(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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -286,6 +289,25 @@ namespace Tgstation.Server.Host.Components.Watchdog
|
||||
content = new object();
|
||||
switch (method)
|
||||
{
|
||||
case Constants.DMCommandChat:
|
||||
try
|
||||
{
|
||||
var message = JsonConvert.DeserializeObject<Response>(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<DreamDaemonSecurity>(stringMinimumSecurityLevel, out var minimumSecurityLevel))
|
||||
if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevelObject) || !Enum.TryParse<DreamDaemonSecurity>(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -9,24 +9,25 @@ namespace Tgstation.Server.Host.Core
|
||||
/// </summary>
|
||||
sealed class JobHandler : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> being run
|
||||
/// </summary>
|
||||
readonly Task task;
|
||||
/// <summary>
|
||||
/// The <see cref="CancellationTokenSource"/> for <see cref="task"/>
|
||||
/// </summary>
|
||||
readonly CancellationTokenSource cancellationTokenSource;
|
||||
/// <summary>
|
||||
/// The <see cref="Task"/> being run
|
||||
/// </summary>
|
||||
readonly Task task;
|
||||
|
||||
/// <summary>
|
||||
/// Construct a <see cref="JobHandler"/>
|
||||
/// </summary>
|
||||
/// <param name="task">The value of <see cref="Task"/></param>
|
||||
/// <param name="cancellationTokenSource">The value of <see cref="cancellationTokenSource"/></param>
|
||||
JobHandler(Task task, CancellationTokenSource cancellationTokenSource)
|
||||
/// <param name="job">A <see cref="Func{T, TResult}"/> taking a <see cref="CancellationToken"/> and returning a <see cref="Task"/> that the <see cref="JobHandler"/> will wrap</param>
|
||||
public JobHandler(Func<CancellationToken, Task> job)
|
||||
{
|
||||
this.task = task;
|
||||
this.cancellationTokenSource = cancellationTokenSource;
|
||||
if (job == null)
|
||||
throw new ArgumentNullException(nameof(job));
|
||||
cancellationTokenSource = new CancellationTokenSource();
|
||||
task = job(cancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
/// <inehritdoc />
|
||||
@@ -54,16 +55,5 @@ namespace Tgstation.Server.Host.Core
|
||||
/// Cancels <see cref="task"/>
|
||||
/// </summary>
|
||||
public void Cancel() => cancellationTokenSource.Cancel();
|
||||
|
||||
/// <summary>
|
||||
/// Create a <see cref="JobHandler"/>
|
||||
/// </summary>
|
||||
/// <param name="job">A <see cref="Func{T, TResult}"/> taking a <see cref="CancellationToken"/> and returning a <see cref="Task"/> that the <see cref="JobHandler"/> will wrap</param>
|
||||
/// <returns>A new <see cref="JobHandler"/></returns>
|
||||
public static JobHandler Create(Func<CancellationToken, Task> job)
|
||||
{
|
||||
var cts = new CancellationTokenSource();
|
||||
return new JobHandler(job(cts.Token), cts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
/// <inheritdoc />
|
||||
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());
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
namespace Tgstation.Server.Host.IO
|
||||
using System;
|
||||
|
||||
namespace Tgstation.Server.Host.IO
|
||||
{
|
||||
/// <summary>
|
||||
/// <see cref="IPostWriteHandler"/> for Windows systems
|
||||
@@ -6,6 +8,10 @@
|
||||
sealed class WindowsPostWriteHandler : IPostWriteHandler
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void HandleWrite(string filePath) { }
|
||||
public void HandleWrite(string filePath)
|
||||
{
|
||||
if (filePath == null)
|
||||
throw new ArgumentNullException(nameof(filePath));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// If the MY_ class of migrations should be used instead of the MS_ class
|
||||
/// </summary>
|
||||
/// <returns><see langword="true"/> if the MY_ class of migrations should be used instead of the MS_ class, <see langword="false"/> otherwise</returns>
|
||||
protected abstract bool UseMySQLMigrations();
|
||||
|
||||
/// <inheritdoc />
|
||||
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<IServiceProvider>)Database).Instance;
|
||||
var migrator = dbServiceProvider.GetRequiredService<IMigrator>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task Initialize(CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// Attempt to downgrade the schema to the migration used for a given server <paramref name="version"/>
|
||||
/// </summary>
|
||||
/// <param name="version">The tgstation-server <see cref="Version"/> that the schema should downgrade for</param>
|
||||
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
|
||||
/// <returns>A <see cref="Task"/> representing the running operation</returns>
|
||||
Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,5 +38,8 @@ namespace Tgstation.Server.Host.Models
|
||||
else
|
||||
options.UseMySql(DatabaseConfiguration.ConnectionString);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool UseMySQLMigrations() => true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,5 +26,8 @@ namespace Tgstation.Server.Host.Models
|
||||
base.OnConfiguring(options);
|
||||
options.UseSqlServer(DatabaseConfiguration.ConnectionString);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool UseMySQLMigrations() => false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<DebugType>Full</DebugType>
|
||||
<Version>4.0.1.2</Version>
|
||||
<TypeScriptToolsVersion>3.0</TypeScriptToolsVersion>
|
||||
<Version>4.0.1.4</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
|
||||
@@ -49,21 +48,24 @@
|
||||
<PackageReference Include="Byond.TopicSender" Version="1.1.4" />
|
||||
<PackageReference Include="Cyberboss.AspNetCore.AsyncInitializer" Version="1.2.0" />
|
||||
<PackageReference Include="Cyberboss.SmartIrc4net.Standard" Version="0.4.6" />
|
||||
<PackageReference Include="Discord.Net.WebSocket" Version="1.0.2" />
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.26.0-preview-0027" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.3" />
|
||||
<PackageReference Include="Discord.Net.WebSocket" Version="2.0.0-beta" />
|
||||
<PackageReference Include="LibGit2Sharp" Version="0.25.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.StaticFiles" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.6.2">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.3" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.1.3" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.4" PrivateAssets="All" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="2.1.4" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="2.1.4">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Mono.Posix.NETStandard" Version="1.0.0" />
|
||||
<PackageReference Include="Octokit" Version="0.32.0" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="2.1.2" />
|
||||
@@ -72,7 +74,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.3.0" />
|
||||
<PackageReference Include="System.Diagnostics.PerformanceCounter" Version="4.5.0" />
|
||||
<PackageReference Include="System.DirectoryServices.AccountManagement" Version="4.5.0" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.2.4" />
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="5.3.0" />
|
||||
<PackageReference Include="Wangkanai.Detection.Browser" Version="2.0.0-beta9" />
|
||||
<PackageReference Include="Z.EntityFramework.Plus.EFCore" Version="1.8.10" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Moq" Version="4.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Moq" Version="4.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
|
||||
@@ -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<ArgumentNullException>(() => new PosixByondInstaller(null, null, null));
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, null, null));
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, null));
|
||||
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestCacheClean()
|
||||
{
|
||||
var mockIOManager = new Mock<IIOManager>();
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
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<OperationCanceledException>(() => 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<IIOManager>();
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
|
||||
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.DownloadVersion(null, default)).ConfigureAwait(false);
|
||||
|
||||
var ourArray = Array.Empty<byte>();
|
||||
mockIOManager.Setup(x => x.DownloadFile(It.Is<Uri>(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<IIOManager>();
|
||||
var mockPostWriteHandler = new Mock<IPostWriteHandler>();
|
||||
var mockLogger = new Mock<ILogger<PosixByondInstaller>>();
|
||||
var installer = new PosixByondInstaller(mockIOManager.Object, mockPostWriteHandler.Object, mockLogger.Object);
|
||||
|
||||
const string FakePath = "fake";
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => installer.InstallByond(null, null, default)).ConfigureAwait(false);
|
||||
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => 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<string>(), It.IsNotNull<string>()), Times.Exactly(5));
|
||||
mockIOManager.Verify(x => x.WriteAllBytes(It.IsAny<string>(), It.IsNotNull<byte[]>(), default), Times.Exactly(2));
|
||||
mockPostWriteHandler.Verify(x => x.HandleWrite(It.IsAny<string>()), Times.Exactly(4));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ArgumentNullException>(() => new DiscordProvider(null, null));
|
||||
var mockLogger = new Mock<ILogger<DiscordProvider>>();
|
||||
Assert.ThrowsException<ArgumentNullException>(() => new DiscordProvider(mockLogger.Object, null));
|
||||
var mockToken = "asdf";
|
||||
new DiscordProvider(mockLogger.Object, mockToken).Dispose();
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestConnectWithFakeTokenFails()
|
||||
{
|
||||
var mockLogger = new Mock<ILogger<DiscordProvider>>();
|
||||
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<ILogger<DiscordProvider>>();
|
||||
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<OperationCanceledException>(() => provider.Connect(cancellationToken)).ConfigureAwait(false);
|
||||
Assert.IsFalse(provider.Connected);
|
||||
Assert.IsTrue(await provider.Connect(default).ConfigureAwait(false));
|
||||
Assert.IsTrue(provider.Connected);
|
||||
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => provider.Disconnect(cancellationToken)).ConfigureAwait(false);
|
||||
Assert.IsTrue(provider.Connected);
|
||||
await provider.Disconnect(default).ConfigureAwait(false);
|
||||
Assert.IsFalse(provider.Connected);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ArgumentNullException>(() => 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<object>();
|
||||
currentWaitTask = tcs.Task;
|
||||
cts.Cancel();
|
||||
using(var handler = new JobHandler(TestJob))
|
||||
{
|
||||
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => handler.Wait(cts.Token)).ConfigureAwait(false);
|
||||
tcs.SetResult(null);
|
||||
await handler.Wait(default).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
Assert.IsFalse(cancelled);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestProgress()
|
||||
{
|
||||
currentWaitTask = Task.CompletedTask;
|
||||
cancelled = false;
|
||||
using (var handler = new JobHandler(TestJob))
|
||||
{
|
||||
Assert.IsFalse(handler.Progress.HasValue);
|
||||
handler.Progress = 42;
|
||||
Assert.AreEqual(42, handler.Progress);
|
||||
}
|
||||
Assert.IsFalse(cancelled);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestCancellation()
|
||||
{
|
||||
var tcs = new TaskCompletionSource<object>();
|
||||
currentWaitTask = tcs.Task;
|
||||
cancelled = false;
|
||||
using(var handler = new JobHandler(TestJob))
|
||||
{
|
||||
handler.Cancel();
|
||||
tcs.SetResult(null);
|
||||
await handler.Wait(default).ConfigureAwait(false);
|
||||
}
|
||||
Assert.IsTrue(cancelled);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Mono.Unix;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace Tgstation.Server.Host.IO.Tests
|
||||
{
|
||||
[TestClass]
|
||||
public sealed class TestPostWriteHandler
|
||||
{
|
||||
[TestMethod]
|
||||
public void TestThrowsWithNullArg()
|
||||
{
|
||||
IPostWriteHandler postWriteHandler;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
postWriteHandler = new WindowsPostWriteHandler();
|
||||
else
|
||||
postWriteHandler = new PosixPostWriteHandler();
|
||||
|
||||
Assert.ThrowsException<ArgumentNullException>(() => postWriteHandler.HandleWrite(null));
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestPostWrite()
|
||||
{
|
||||
IPostWriteHandler postWriteHandler;
|
||||
var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
|
||||
if (isWindows)
|
||||
postWriteHandler = new WindowsPostWriteHandler();
|
||||
else
|
||||
postWriteHandler = new PosixPostWriteHandler();
|
||||
|
||||
//test on a valid file first
|
||||
var tmpFile = Path.GetTempFileName();
|
||||
try
|
||||
{
|
||||
postWriteHandler.HandleWrite(tmpFile);
|
||||
|
||||
if (isWindows)
|
||||
return; //you do nothing
|
||||
|
||||
//ensure it is now executable
|
||||
File.WriteAllBytes(tmpFile, Encoding.UTF8.GetBytes("#!/bin/sh\n"));
|
||||
|
||||
using (var process = Process.Start(tmpFile))
|
||||
{
|
||||
process.WaitForExit();
|
||||
Assert.AreEqual(0, process.ExitCode);
|
||||
}
|
||||
|
||||
//run it again for the code coverage on that part where no changes are made if it's already executable
|
||||
postWriteHandler.HandleWrite(tmpFile);
|
||||
}
|
||||
finally
|
||||
{
|
||||
File.Delete(tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void TestThrowsOnUnix()
|
||||
{
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return;
|
||||
var postWriteHandler = new PosixPostWriteHandler();
|
||||
var tmpFile = Path.GetTempFileName();
|
||||
File.Delete(tmpFile);
|
||||
Assert.ThrowsException<UnixIOException>(() => postWriteHandler.HandleWrite(tmpFile));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Security.Principal;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Tgstation.Server.Host.IO.Tests
|
||||
@@ -20,9 +21,20 @@ namespace Tgstation.Server.Host.IO.Tests
|
||||
symlinkFactory = new PosixSymlinkFactory();
|
||||
}
|
||||
|
||||
public static bool HasPermissionToMakeSymlinks()
|
||||
{
|
||||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||||
return true;
|
||||
var identity = WindowsIdentity.GetCurrent();
|
||||
var principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public async Task TestFileWorks()
|
||||
{
|
||||
if (!HasPermissionToMakeSymlinks())
|
||||
Assert.Inconclusive("Current user does not have permission to create symlinks!");
|
||||
const string Text = "Hello world";
|
||||
string f2 = null;
|
||||
var f1 = Path.GetTempFileName();
|
||||
@@ -50,6 +62,8 @@ namespace Tgstation.Server.Host.IO.Tests
|
||||
[TestMethod]
|
||||
public async Task TestDirectoryWorks()
|
||||
{
|
||||
if (!HasPermissionToMakeSymlinks())
|
||||
Assert.Inconclusive("Current user does not have permission to create symlinks!");
|
||||
const string FileName = "TestFile.txt";
|
||||
const string Text = "Hello world";
|
||||
string f2 = null;
|
||||
@@ -86,17 +100,14 @@ namespace Tgstation.Server.Host.IO.Tests
|
||||
[TestMethod]
|
||||
public async Task TestFailsProperly()
|
||||
{
|
||||
const string BadPath = "/../../?>O(UF+}P{{??>/////";
|
||||
const string BadPath = "/../../?>!@#$%^&*()O(UF+}P{{??>/////";
|
||||
|
||||
try
|
||||
{
|
||||
await symlinkFactory.CreateSymbolicLink(BadPath, BadPath, default).ConfigureAwait(false);
|
||||
Assert.Fail("No exception thrown!");
|
||||
}
|
||||
catch
|
||||
{
|
||||
return;
|
||||
}
|
||||
Assert.Fail("No exception thrown!");
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Moq" Version="4.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
|
||||
+1
-1
@@ -18,7 +18,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="Moq" Version="4.10.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
|
||||
@@ -3,6 +3,7 @@ using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Tgstation.Server.Api.Models;
|
||||
using Tgstation.Server.Client;
|
||||
|
||||
namespace Tgstation.Server.Tests
|
||||
@@ -23,10 +24,20 @@ namespace Tgstation.Server.Tests
|
||||
|
||||
async Task TestRead(CancellationToken cancellationToken)
|
||||
{
|
||||
var model = await client.Read(cancellationToken).ConfigureAwait(false);
|
||||
Administration model;
|
||||
try
|
||||
{
|
||||
model = await client.Read(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (RateLimitException)
|
||||
{
|
||||
Assert.Inconclusive("GitHub rate limit hit while testing administration endpoint. Set environment variable TGS4_TEST_GITHUB_TOKEN to fix this!");
|
||||
return; //c# needs the equivalent of [noreturn]
|
||||
}
|
||||
Assert.AreEqual(RuntimeInformation.IsOSPlatform(OSPlatform.Windows), model.WindowsHost);
|
||||
|
||||
//uhh not much else to do
|
||||
//we've released a few 4.x versions now, check the release checker is at least somewhat functional
|
||||
Assert.AreEqual(4, model.LatestVersion.Major);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,13 @@ namespace Tgstation.Server.Tests
|
||||
}, cancellationToken)).ConfigureAwait(false);
|
||||
await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(firstTest, cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
//can't create instances in installation directory
|
||||
await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.CreateOrAttach(new Api.Models.Instance
|
||||
{
|
||||
Path = "./A/Local/Path",
|
||||
Name = "NoInstallDirTest"
|
||||
}, cancellationToken)).ConfigureAwait(false);
|
||||
|
||||
//can't move to existent directories
|
||||
await Assert.ThrowsExceptionAsync<ConflictException>(() => instanceManagerClient.Update(new Api.Models.Instance
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
@@ -17,8 +18,9 @@ namespace Tgstation.Server.Tests
|
||||
public sealed class IntegrationTest
|
||||
{
|
||||
readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString()));
|
||||
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("SkipWhenLiveUnitTesting")]
|
||||
public async Task TestUpdate()
|
||||
{
|
||||
var updatePathRoot = Path.GetTempFileName();
|
||||
@@ -93,6 +95,7 @@ namespace Tgstation.Server.Tests
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[TestCategory("SkipWhenLiveUnitTesting")]
|
||||
public async Task TestStandardOperation()
|
||||
{
|
||||
var server = new TestingServer(null);
|
||||
|
||||
@@ -33,10 +33,10 @@ namespace Tgstation.Server.Tests
|
||||
var gitHubAccessToken = Environment.GetEnvironmentVariable("TGS4_TEST_GITHUB_TOKEN");
|
||||
|
||||
if (String.IsNullOrEmpty(databaseType))
|
||||
Assert.Fail("No database type configured in env var TGS4_TEST_DATABASE_TYPE!");
|
||||
Assert.Inconclusive("No database type configured in env var TGS4_TEST_DATABASE_TYPE!");
|
||||
|
||||
if (String.IsNullOrEmpty(connectionString))
|
||||
Assert.Fail("No connection string configured in env var TGS4_TEST_CONNECTION_STRING!");
|
||||
Assert.Inconclusive("No connection string configured in env var TGS4_TEST_CONNECTION_STRING!");
|
||||
|
||||
if (String.IsNullOrEmpty(gitHubAccessToken))
|
||||
Console.WriteLine("WARNING: No GitHub access token configured, test may fail due to rate limits!");
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.8.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.9.0" />
|
||||
<PackageReference Include="MSTest.TestAdapter" Version="1.3.2" />
|
||||
<PackageReference Include="MSTest.TestFramework" Version="1.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -114,6 +114,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{E821
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tgstation.Server.Tests", "tests\Tgstation.Server.Tests\Tgstation.Server.Tests.csproj", "{09056964-1C74-445A-96EC-33F6DFC07916}"
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ReleaseNotes", "tools\ReleaseNotes\ReleaseNotes.csproj", "{5CB51532-55F0-4255-B6E5-69ED5CCD14CD}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -172,6 +174,10 @@ Global
|
||||
{09056964-1C74-445A-96EC-33F6DFC07916}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{09056964-1C74-445A-96EC-33F6DFC07916}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{5CB51532-55F0-4255-B6E5-69ED5CCD14CD}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -183,6 +189,7 @@ Global
|
||||
{3BB10856-AA01-43D1-AAB0-A013085426D6} = {7E4E7CF6-A48D-410E-8191-C49C6B5AB9CC}
|
||||
{82066812-6C73-4360-943B-B23F2F491261} = {7E4E7CF6-A48D-410E-8191-C49C6B5AB9CC}
|
||||
{057FAC33-CC31-4948-91C6-B0977C335890} = {F7765A4B-021C-454E-ABB1-2AB1B85F91B4}
|
||||
{5CB51532-55F0-4255-B6E5-69ED5CCD14CD} = {A55C1117-5808-4AB2-BEA6-4D4A3E66A2F2}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {DFD36C95-3E49-41C7-ACDB-86BAF5B18A79}
|
||||
|
||||
@@ -0,0 +1,309 @@
|
||||
using Octokit;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace ReleaseNotes
|
||||
{
|
||||
/// <summary>
|
||||
/// Contains the application entrypoint
|
||||
/// </summary>
|
||||
static class Program
|
||||
{
|
||||
/// <summary>
|
||||
/// The entrypoint for the <see cref="Program"/>
|
||||
/// </summary>
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
if (args.Length < 1)
|
||||
{
|
||||
Console.WriteLine("Missing version argument!");
|
||||
return 1;
|
||||
}
|
||||
|
||||
var versionString = args[0];
|
||||
if (!Version.TryParse(versionString, out var version))
|
||||
{
|
||||
Console.WriteLine("Invalid version: " + versionString);
|
||||
return 2;
|
||||
}
|
||||
|
||||
var doNotCloseMilestone = args.Length >= 2 && args[1].ToUpperInvariant() == "--NO-CLOSE";
|
||||
|
||||
const string ReleaseNotesEnvVar = "TGS_RELEASE_NOTES_TOKEN";
|
||||
var githubToken = Environment.GetEnvironmentVariable(ReleaseNotesEnvVar);
|
||||
if (String.IsNullOrWhiteSpace(githubToken))
|
||||
{
|
||||
Console.WriteLine("Missing " + ReleaseNotesEnvVar + " environment variable!");
|
||||
return 3;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var client = new GitHubClient(new ProductHeaderValue("tgs_release_notes"));
|
||||
client.Credentials = new Credentials(githubToken);
|
||||
|
||||
const string RepoOwner = "tgstation";
|
||||
const string RepoName = "tgstation-server";
|
||||
|
||||
var releasesTask = client.Repository.Release.GetAll(RepoOwner, RepoName);
|
||||
|
||||
Console.WriteLine("Getting pull requests in milestone " + versionString + "...");
|
||||
var milestonePRs = await client.Search.SearchIssues(new SearchIssuesRequest
|
||||
{
|
||||
Milestone = versionString,
|
||||
Type = IssueTypeQualifier.PullRequest,
|
||||
Repos = { { RepoOwner, RepoName } }
|
||||
}).ConfigureAwait(false);
|
||||
|
||||
if (milestonePRs.IncompleteResults)
|
||||
{
|
||||
Console.WriteLine("Incomplete results for milestone PRs query!");
|
||||
return 5;
|
||||
}
|
||||
Console.WriteLine(milestonePRs.Items.Count + " total pull requests");
|
||||
|
||||
Task<Milestone> milestoneTask = null;
|
||||
var milestoneTaskLock = new object();
|
||||
var releaseDictionary = new Dictionary<int, List<string>>();
|
||||
var authorizedUsers = new Dictionary<long, Task<bool>>();
|
||||
|
||||
async Task GetReleaseNotesFromPR(Issue pullRequest)
|
||||
{
|
||||
//need to check it was merged
|
||||
var fullPR = await client.Repository.PullRequest.Get(RepoOwner, RepoName, pullRequest.Number).ConfigureAwait(false);
|
||||
|
||||
async Task<Milestone> GetMilestone()
|
||||
{
|
||||
if (fullPR.Milestone == null)
|
||||
return null;
|
||||
return await client.Issue.Milestone.Get(RepoOwner, RepoName, fullPR.Milestone.Number);
|
||||
};
|
||||
|
||||
lock (milestoneTaskLock)
|
||||
if (milestoneTask == null)
|
||||
milestoneTask = GetMilestone();
|
||||
|
||||
if (!fullPR.Merged)
|
||||
return;
|
||||
|
||||
async Task BuildNotesFromComment(string comment, User user)
|
||||
{
|
||||
var commentSplits = comment.Split('\n');
|
||||
var notesOpen = false;
|
||||
var notesClosed = false;
|
||||
var notes = new List<string>();
|
||||
foreach (var line in commentSplits)
|
||||
{
|
||||
var trimmedLine = line.Trim();
|
||||
if (!notesOpen)
|
||||
{
|
||||
notesOpen = trimmedLine.StartsWith(":cl:", StringComparison.Ordinal);
|
||||
notesClosed = false;
|
||||
continue;
|
||||
}
|
||||
if (trimmedLine.StartsWith("/:cl:", StringComparison.Ordinal))
|
||||
{
|
||||
notesClosed = true;
|
||||
notesOpen = false;
|
||||
continue;
|
||||
}
|
||||
if (trimmedLine.Length == 0)
|
||||
continue;
|
||||
notes.Add(trimmedLine);
|
||||
}
|
||||
if (!notesClosed || notes.Count == 0)
|
||||
return;
|
||||
|
||||
Task<bool> authTask;
|
||||
TaskCompletionSource<bool> ourTcs = null;
|
||||
lock (authorizedUsers)
|
||||
{
|
||||
if (!authorizedUsers.TryGetValue(user.Id, out authTask))
|
||||
{
|
||||
ourTcs = new TaskCompletionSource<bool>();
|
||||
authTask = ourTcs.Task;
|
||||
authorizedUsers.Add(user.Id, authTask);
|
||||
}
|
||||
}
|
||||
|
||||
if (ourTcs != null)
|
||||
try
|
||||
{
|
||||
//check if the user has access
|
||||
var perm = await client.Repository.Collaborator.ReviewPermission(RepoOwner, RepoName, user.Login).ConfigureAwait(false);
|
||||
ourTcs.SetResult(perm.Permission == PermissionLevel.Write || perm.Permission == PermissionLevel.Admin);
|
||||
}
|
||||
catch
|
||||
{
|
||||
ourTcs.SetResult(false);
|
||||
throw;
|
||||
}
|
||||
|
||||
var authorized = await authTask.ConfigureAwait(false);
|
||||
if (!authorized)
|
||||
return;
|
||||
|
||||
lock (releaseDictionary)
|
||||
{
|
||||
foreach (var I in notes)
|
||||
Console.WriteLine("#" + fullPR.Number + " - " + I + " (@" + user.Login + ")");
|
||||
if (releaseDictionary.TryGetValue(fullPR.Number, out var currentValues))
|
||||
currentValues.AddRange(notes);
|
||||
else
|
||||
releaseDictionary.Add(fullPR.Number, notes);
|
||||
}
|
||||
}
|
||||
|
||||
var comments = await client.Issue.Comment.GetAllForIssue(RepoOwner, RepoName, fullPR.Number).ConfigureAwait(false);
|
||||
await Task.WhenAll(BuildNotesFromComment(fullPR.Body, fullPR.User), Task.WhenAll(comments.Select(x => BuildNotesFromComment(x.Body, x.User)))).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
var tasks = new List<Task>();
|
||||
foreach (var I in milestonePRs.Items)
|
||||
tasks.Add(GetReleaseNotesFromPR(I));
|
||||
|
||||
var releases = await releasesTask.ConfigureAwait(false);
|
||||
|
||||
var releasingSuite = version.Major;
|
||||
|
||||
Version highestReleaseVersion = null;
|
||||
Release highestRelease = null;
|
||||
foreach (var I in releases)
|
||||
{
|
||||
if (!Version.TryParse(I.TagName.Replace("tgstation-server-v", String.Empty), out var currentReleaseVersion))
|
||||
{
|
||||
Console.WriteLine("WARNING: Unable to determine version of release " + I.HtmlUrl);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentReleaseVersion.Major == releasingSuite && (highestReleaseVersion == null || currentReleaseVersion > highestReleaseVersion))
|
||||
{
|
||||
highestReleaseVersion = currentReleaseVersion;
|
||||
highestRelease = I;
|
||||
}
|
||||
}
|
||||
|
||||
if (highestReleaseVersion == null)
|
||||
{
|
||||
Console.WriteLine("Unable to determine highest release version for suite " + releasingSuite + "!");
|
||||
return 6;
|
||||
}
|
||||
|
||||
var oldNotes = highestRelease.Body;
|
||||
|
||||
var splits = new List<string>(oldNotes.Split('\n'));
|
||||
//trim away all the lines that don't start with #
|
||||
|
||||
string keepThisRelease;
|
||||
if (version.Revision == 0)
|
||||
if (version.Build == 0)
|
||||
keepThisRelease = "# ";
|
||||
else
|
||||
keepThisRelease = "## ";
|
||||
else
|
||||
keepThisRelease = "### ";
|
||||
|
||||
for (; !splits[0].StartsWith(keepThisRelease, StringComparison.Ordinal); splits.RemoveAt(0))
|
||||
if (splits.Count == 1)
|
||||
{
|
||||
Console.WriteLine("Error formatting release notes: Can't detemine notes start!");
|
||||
return 7;
|
||||
}
|
||||
|
||||
oldNotes = String.Join('\n', splits);
|
||||
|
||||
string prefix;
|
||||
switch (releasingSuite)
|
||||
{
|
||||
case 3:
|
||||
prefix = "The /tg/station server suite";
|
||||
break;
|
||||
default:
|
||||
prefix = "See https://tgstation.github.io/tgstation-server for installation instructions";
|
||||
break;
|
||||
}
|
||||
|
||||
var newNotes = new StringBuilder(prefix);
|
||||
newNotes.Append(Environment.NewLine);
|
||||
newNotes.Append(Environment.NewLine);
|
||||
if (version.Revision == 0)
|
||||
if (version.Build == 0)
|
||||
{
|
||||
newNotes.Append("# [Version ");
|
||||
newNotes.Append(version.Minor);
|
||||
}
|
||||
else
|
||||
{
|
||||
newNotes.Append("## [Changelog for ");
|
||||
newNotes.Append(version.Build);
|
||||
newNotes.Append(".x");
|
||||
}
|
||||
else
|
||||
{
|
||||
newNotes.Append("### [Patch ");
|
||||
newNotes.Append(version.Revision);
|
||||
}
|
||||
newNotes.Append("](");
|
||||
var milestone = await milestoneTask.ConfigureAwait(false);
|
||||
if (milestone == null)
|
||||
{
|
||||
Console.WriteLine("Unable to detemine milestone!");
|
||||
return 9;
|
||||
}
|
||||
newNotes.Append(milestone.HtmlUrl);
|
||||
newNotes.Append("?closed=1)");
|
||||
newNotes.Append(Environment.NewLine);
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
if (releaseDictionary.Count == 0)
|
||||
{
|
||||
Console.WriteLine("No release notes for this milestone!");
|
||||
return 8;
|
||||
}
|
||||
|
||||
foreach (var I in releaseDictionary)
|
||||
foreach (var note in I.Value)
|
||||
{
|
||||
newNotes.Append(Environment.NewLine);
|
||||
newNotes.Append("- ");
|
||||
newNotes.Append(note);
|
||||
newNotes.Append(" (#");
|
||||
newNotes.Append(I.Key);
|
||||
newNotes.Append(')');
|
||||
}
|
||||
|
||||
newNotes.Append(Environment.NewLine);
|
||||
newNotes.Append(Environment.NewLine);
|
||||
newNotes.Append(oldNotes);
|
||||
|
||||
Console.WriteLine("Writing out new release notes...");
|
||||
var releaseNotes = newNotes.ToString();
|
||||
await File.WriteAllTextAsync("release_notes.md", releaseNotes).ConfigureAwait(false);
|
||||
|
||||
if (doNotCloseMilestone)
|
||||
Console.WriteLine("Not closing milestone due to parameter!");
|
||||
else
|
||||
{
|
||||
Console.WriteLine("Closing milestone...");
|
||||
await client.Issue.Milestone.Update(RepoOwner, RepoName, milestone.Number, new MilestoneUpdate
|
||||
{
|
||||
State = ItemState.Closed
|
||||
}).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
This is a small tool to generate TGS release notes from PR descriptions
|
||||
|
||||
Requires environment variable `TGS_RELEASE_NOTES_TOKEN`
|
||||
|
||||
Run it with `dotnet run <version> [--no-close (optionally doesn't close the milestone, USE WHILE DEBUGGING)]`
|
||||
|
||||
Will close the release milestone and output `release_notes.md` with the updated release notes
|
||||
@@ -0,0 +1,13 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||
<LangVersion>latest</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="octokit" Version="0.32.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user