Merge pull request #1017 from tgstation/FixMultiMerging [TGSDeploy][APIDeploy][NugetDeploy]

v4.2.5
This commit is contained in:
Jordan Brown
2020-05-24 18:29:51 -04:00
committed by GitHub
60 changed files with 308 additions and 220 deletions
+1
View File
@@ -38,6 +38,7 @@ The following dependencies are required to run tgstation-server on Linux alongsi
- libc6-i386
- libstdc++6:i386
- libssl1.0.0
- gcc-multilib (Only on 64-bit systems)
Note that tgstation-server has only ever been tested on Linux via it's [docker environment](build/Dockerfile#L22). If you are having trouble with something in a native installation, or figure out a required workaround, please contact project maintainers so this documentation may be better updated.
+2 -2
View File
@@ -3,8 +3,8 @@
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<TgsCoreVersion>4.2.5</TgsCoreVersion>
<TgsApiVersion>6.4.0</TgsApiVersion>
<TgsClientVersion>6.3.0</TgsClientVersion>
<TgsApiVersion>6.4.1</TgsApiVersion>
<TgsClientVersion>7.0.0</TgsClientVersion>
<TgsDmapiVersion>5.2.1</TgsDmapiVersion>
<TgsControlPanelVersion>0.4.0</TgsControlPanelVersion>
<TgsHostWatchdogVersion>1.1.0</TgsHostWatchdogVersion>
+7 -7
View File
@@ -69,12 +69,12 @@ namespace Tgstation.Server.Api
/// <summary>
/// The client's user agent as a <see cref="ProductHeaderValue"/> if valid
/// </summary>
public ProductHeaderValue UserAgent => ProductInfoHeaderValue.TryParse(RawUserAgent, out var userAgent) ? userAgent.Product : null;
public ProductHeaderValue? UserAgent => ProductInfoHeaderValue.TryParse(RawUserAgent, out var userAgent) ? userAgent.Product : null;
/// <summary>
/// The client's raw user agent
/// </summary>
public string RawUserAgent { get; }
public string? RawUserAgent { get; }
/// <summary>
/// The client's API version
@@ -84,17 +84,17 @@ namespace Tgstation.Server.Api
/// <summary>
/// The client's JWT
/// </summary>
public string Token { get; }
public string? Token { get; }
/// <summary>
/// The client's username
/// </summary>
public string Username { get; }
public string? Username { get; }
/// <summary>
/// The client's password
/// </summary>
public string Password { get; }
public string? Password { get; }
/// <summary>
/// If the header uses password or JWT authentication
@@ -236,7 +236,7 @@ namespace Tgstation.Server.Api
/// <param name="token">The value of <see cref="Token"/></param>
/// <param name="username">The value of <see cref="Username"/></param>
/// <param name="password">The value of <see cref="Password"/></param>
ApiHeaders(ProductHeaderValue userAgent, string token, string username, string password)
ApiHeaders(ProductHeaderValue userAgent, string? token, string? username, string? password)
{
RawUserAgent = userAgent?.ToString();
Token = token;
@@ -274,7 +274,7 @@ namespace Tgstation.Server.Api
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
instanceId = instanceId ?? InstanceId;
instanceId ??= InstanceId;
if (instanceId.HasValue)
headers.Add(InstanceIdHeader, instanceId.ToString());
}
@@ -15,16 +15,16 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The GitHub repository the server is built to recieve updates from
/// </summary>
public Uri TrackedRepositoryUrl { get; set; }
public Uri? TrackedRepositoryUrl { get; set; }
/// <summary>
/// The latest available version of the Tgstation.Server.Host assembly from the upstream repository. If <see cref="Version.Major"/> is higher than <see cref="NewVersion"/>'s the update cannot be applied due to API changes
/// </summary>
public Version LatestVersion { get; set; }
public Version? LatestVersion { get; set; }
/// <summary>
/// Changes the version of Tgstation.Server.Host to the given version from the upstream repository
/// </summary>
public Version NewVersion { get; set; }
public Version? NewVersion { get; set; }
}
}
+2 -2
View File
@@ -10,11 +10,11 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The <see cref="System.Version"/> of the <see cref="Byond"/> installation used for new compiles. Will be <see langword="null"/> if the user does not have permission to view it or there is no BYOND version installed. Only considers the <see cref="Version.Major"/> and <see cref="Version.Minor"/> numbers.
/// </summary>
public Version Version { get; set; }
public Version? Version { get; set; }
/// <summary>
/// The <see cref="Job"/> being used to install a new <see cref="Version"/>
/// </summary>
public Job InstallJob { get; set; }
public Job? InstallJob { get; set; }
}
}
+6 -9
View File
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// Channels the Discord bot should listen/announce in
/// </summary>
public ICollection<ChatChannel> Channels { get; set; }
public ICollection<ChatChannel>? Channels { get; set; }
/// <summary>
/// Validates <see cref="Channels"/> are correct for the <see cref="Internal.ChatBot.Provider"/>
@@ -20,15 +20,12 @@ namespace Tgstation.Server.Api.Models
{
if (!Provider.HasValue)
return true;
switch (Provider.Value)
return Provider.Value switch
{
case ChatProvider.Discord:
return Channels?.Select(x => x.DiscordChannelId.HasValue && x.IrcChannel == null).All(x => x) ?? true;
case ChatProvider.Irc:
return Channels?.Select(x => !x.DiscordChannelId.HasValue && x.IrcChannel != null).All(x => x) ?? true;
default:
throw new InvalidOperationException("Invalid provider type!");
}
ChatProvider.Discord => Channels?.Select(x => x.DiscordChannelId.HasValue && x.IrcChannel == null).All(x => x) ?? true,
ChatProvider.Irc => Channels?.Select(x => !x.DiscordChannelId.HasValue && x.IrcChannel != null).All(x => x) ?? true,
_ => throw new InvalidOperationException("Invalid provider type!"),
};
}
}
}
@@ -12,7 +12,7 @@ namespace Tgstation.Server.Api.Models
/// If multiple copies of the same channel with different keys are added to the server, the one that will be used is undefined.
/// </summary>
[StringLength(Limits.MaximumIndexableStringLength)]
public string IrcChannel { get; set; }
public string? IrcChannel { get; set; }
/// <summary>
/// The Discord channel ID
@@ -41,6 +41,6 @@ namespace Tgstation.Server.Api.Models
/// A custom tag users can define to group channels together
/// </summary>
[StringLength(Limits.MaximumStringLength)]
public string Tag { get; set; }
public string? Tag { get; set; }
}
}
@@ -8,16 +8,16 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The <see cref="Job"/> relating to this job
/// </summary>
public Job Job { get; set; }
public Job? Job { get; set; }
/// <summary>
/// Git revision the compiler ran on. Not modifiable
/// </summary>
public RevisionInformation RevisionInformation { get; set; }
public RevisionInformation? RevisionInformation { get; set; }
/// <summary>
/// The <see cref="Byond.Version"/> the <see cref="CompileJob"/> was made with
/// </summary>
public Version ByondVersion { get; set; }
public Version? ByondVersion { get; set; }
}
}
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Api.Models
/// The path to the <see cref="ConfigurationFile"/> file
/// </summary>
[StringLength(Limits.MaximumStringLength)]
public string Path { get; set; }
public string? Path { get; set; }
/// <summary>
/// If access to the <see cref="ConfigurationFile"/> file was denied for the operation
@@ -26,13 +26,13 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The MD5 hash of the file when last read by the user. If this doesn't match during update actions, the write will be denied with <see cref="System.Net.HttpStatusCode.Conflict"/>
/// </summary>
public string LastReadHash { get; set; }
public string? LastReadHash { get; set; }
/// <summary>
/// The content of the <see cref="ConfigurationFile"/>. Will be <see langword="null"/> if <see cref="AccessDenied"/> is <see langword="true"/> or during listing and write operations
/// </summary>
#pragma warning disable CA1819 // Properties should not return arrays
public byte[] Content { get; set; }
#pragma warning restore CA1819 // Properties should not return arrays
#pragma warning disable CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space
public byte[]? Content { get; set; }
#pragma warning restore CA1819, SA1011 // Properties should not return arrays, Closing square bracket should be followed by a space
}
}
@@ -15,7 +15,7 @@ namespace Tgstation.Server.Api.Models
/// The Discord bot token
/// </summary>
/// <remarks>See https://discordapp.com/developers/docs/topics/oauth2#bots</remarks>
public string BotToken { get; set; }
public string? BotToken { get; set; }
/// <summary>
/// Construct a <see cref="DiscordConnectionStringBuilder"/>
@@ -32,6 +32,6 @@ namespace Tgstation.Server.Api.Models
}
/// <inheritdoc />
public override string ToString() => BotToken;
public override string ToString() => BotToken ?? "(null)";
}
}
@@ -11,12 +11,12 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The live revision
/// </summary>
public CompileJob ActiveCompileJob { get; set; }
public CompileJob? ActiveCompileJob { get; set; }
/// <summary>
/// The next revision to go live
/// </summary>
public CompileJob StagedCompileJob { get; set; }
public CompileJob? StagedCompileJob { get; set; }
/// <summary>
/// The current status of <see cref="DreamDaemon"/>
@@ -11,7 +11,7 @@ namespace Tgstation.Server.Api.Models
/// The .dme file <see cref="DreamMaker"/> tries to compile with without the extension
/// </summary>
[StringLength(Limits.MaximumStringLength)]
public string ProjectName { get; set; }
public string? ProjectName { get; set; }
/// <summary>
/// The port used during compilation to validate the DMAPI
@@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
namespace Tgstation.Server.Api.Models
@@ -13,9 +14,9 @@ namespace Tgstation.Server.Api.Models
/// </summary>
/// <param name="errorCode">The <see cref="ErrorCode"/> to describe.</param>
/// <returns>A description of the <paramref name="errorCode"/> on success, <see langword="null"/> on failure.</returns>
public static string Describe(this ErrorCode errorCode)
public static string? Describe(this ErrorCode errorCode)
{
var attributes = (DescriptionAttribute[])typeof(ErrorCode)
var attributes = (IEnumerable<DescriptionAttribute>?)typeof(ErrorCode)
.GetField(errorCode.ToString())
?.GetCustomAttributes(typeof(DescriptionAttribute), false);
@@ -12,18 +12,18 @@ namespace Tgstation.Server.Api.Models
/// The version of the API the server is using
/// </summary>
[Required]
public Version ServerApiVersion { get; set; }
public Version? ServerApiVersion { get; set; }
/// <summary>
/// A human readable description of the error
/// </summary>
[Required]
public string Message { get; set; }
public string? Message { get; set; }
/// <summary>
/// Additional data associated with the error message.
/// </summary>
public string AdditionalData { get; set; }
public string? AdditionalData { get; set; }
/// <summary>
/// The <see cref="ErrorCode"/> of the <see cref="ErrorMessage"/>.
@@ -31,6 +31,11 @@ namespace Tgstation.Server.Api.Models
[EnumDataType(typeof(ErrorCode))]
public ErrorCode ErrorCode { get; set; }
/// <summary>
/// Initializes a new instance of the <see cref="ErrorMessage"/> <see langword="class"/>.
/// </summary>
public ErrorMessage() { }
/// <summary>
/// Initializes a new instance of the <see cref="ErrorMessage"/> <see langword="class"/>.
/// </summary>
+3 -3
View File
@@ -18,13 +18,13 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Required]
[StringLength(Limits.MaximumStringLength)]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// The path to where the <see cref="Instance"/> is located. Can only be changed while the <see cref="Instance"/> is offline. Must not exist when the instance is created
/// </summary>
[Required]
public string Path { get; set; }
public string? Path { get; set; }
/// <summary>
/// If the <see cref="Instance"/> is online
@@ -56,7 +56,7 @@ namespace Tgstation.Server.Api.Models
/// </summary>
/// <remarks>Due to how <see cref="Job"/>s are children of <see cref="Instance"/>s but moving one requires the <see cref="Instance"/> to be offline, interactions with this <see cref="Job"/> are performed in a non-standard fashion. The <see cref="Job"/> is read by querying the <see cref="Instance"/> again (either via list or ID lookup) and cancelled by making any sort of update to the <see cref="Instance"/>. Once the <see cref="Instance"/> comes back <see cref="Online"/> it can be queried like a normal job</remarks>
[NotMapped]
public Job MoveJob { get; set; }
public Job? MoveJob { get; set; }
/// <summary>
/// Create a clone of the essential <see cref="Instance"/> metadata
@@ -18,7 +18,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Required]
[StringLength(Limits.MaximumIndexableStringLength)]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// If the connection is enabled
@@ -50,25 +50,22 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Required]
[StringLength(Limits.MaximumStringLength)]
public string ConnectionString { get; set; }
public string? ConnectionString { get; set; }
/// <summary>
/// Get the <see cref="ChatConnectionStringBuilder"/> which maps to the <see cref="ConnectionString"/>.
/// </summary>
/// <returns>A <see cref="ChatConnectionStringBuilder"/> for the <see cref="ChatBot"/>.</returns>
public ChatConnectionStringBuilder CreateConnectionStringBuilder()
public ChatConnectionStringBuilder? CreateConnectionStringBuilder()
{
if (ConnectionString == null)
return null;
switch (Provider)
return Provider switch
{
case ChatProvider.Discord:
return new DiscordConnectionStringBuilder(ConnectionString);
case ChatProvider.Irc:
return new IrcConnectionStringBuilder(ConnectionString);
default:
throw new InvalidOperationException("Invalid Provider!");
}
ChatProvider.Discord => new DiscordConnectionStringBuilder(ConnectionString),
ChatProvider.Irc => new IrcConnectionStringBuilder(ConnectionString),
_ => throw new InvalidOperationException("Invalid Provider!"),
};
}
/// <summary>
@@ -77,7 +74,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <param name="stringBuilder">The optional <see cref="ChatConnectionStringBuilder"/>.</param>
public void SetConnectionStringBuilder(ChatConnectionStringBuilder stringBuilder)
{
ConnectionString = stringBuilder?.ToString();
ConnectionString = stringBuilder?.ToString() ?? throw new ArgumentNullException(nameof(stringBuilder));
}
}
}
@@ -13,13 +13,13 @@ namespace Tgstation.Server.Api.Models.Internal
/// The .dme file used for compilation
/// </summary>
[Required]
public string DmeName { get; set; }
public string? DmeName { get; set; }
/// <summary>
/// Textual output of DM
/// </summary>
[Required]
public string Output { get; set; }
public string? Output { get; set; }
/// <summary>
/// The Game folder the results were compiled into
@@ -37,6 +37,6 @@ namespace Tgstation.Server.Api.Models.Internal
/// The DMAPI <see cref="Version"/>.
/// </summary>
[NotMapped]
public virtual Version DMApiVersion { get; set; }
public virtual Version? DMApiVersion { get; set; }
}
}
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// English description of the <see cref="Job"/>
/// </summary>
[Required]
public string Description { get; set; }
public string? Description { get; set; }
/// <summary>
/// The <see cref="Models.ErrorCode"/> associated with the <see cref="Job"/> if any.
@@ -23,7 +23,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Details of any exceptions caught during the <see cref="Job"/>
/// </summary>
public string ExceptionDetails { get; set; }
public string? ExceptionDetails { get; set; }
/// <summary>
/// When the <see cref="Job"/> was started
@@ -12,7 +12,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Required]
[StringLength(Limits.MaximumStringLength)]
public string CommitterName { get; set; }
public string? CommitterName { get; set; }
/// <summary>
/// The e-mail of the committer
@@ -20,19 +20,19 @@ namespace Tgstation.Server.Api.Models.Internal
[Required]
[StringLength(Limits.MaximumStringLength)]
[EmailAddress]
public string CommitterEmail { get; set; }
public string? CommitterEmail { get; set; }
/// <summary>
/// The username to access the git repository with
/// </summary>
[StringLength(Limits.MaximumStringLength)]
public string AccessUser { get; set; }
public string? AccessUser { get; set; }
/// <summary>
/// The token/password to access the git repository with
/// </summary>
[StringLength(Limits.MaximumStringLength)]
public string AccessToken { get; set; }
public string? AccessToken { get; set; }
/// <summary>
/// If commits created from testmerges are pushed to the remote
@@ -12,13 +12,13 @@ namespace Tgstation.Server.Api.Models.Internal
/// </summary>
[Required]
[StringLength(40)]
public string CommitSha { get; set; }
public string? CommitSha { get; set; }
/// <summary>
/// The sha of the most recent remote commit
/// </summary>
[Required]
[StringLength(40)]
public string OriginCommitSha { get; set; }
public string? OriginCommitSha { get; set; }
}
}
@@ -25,6 +25,6 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Limits the locations instances may be created or attached from.
/// </summary>
public ICollection<string> ValidInstancePaths { get; set; }
public ICollection<string>? ValidInstancePaths { get; set; }
}
}
@@ -12,27 +12,27 @@ namespace Tgstation.Server.Api.Models.Internal
/// The title of the pull request
/// </summary>
[Required]
public string TitleAtMerge { get; set; }
public string? TitleAtMerge { get; set; }
/// <summary>
/// The body of the pull request
/// </summary>
[Required]
public string BodyAtMerge { get; set; }
public string? BodyAtMerge { get; set; }
/// <summary>
/// The URL of the pull request
/// </summary>
[Required]
#pragma warning disable CA1056 // Uri properties should not be strings
public string Url { get; set; }
public string? Url { get; set; }
#pragma warning restore CA1056 // Uri properties should not be strings
/// <summary>
/// The author of the pull request
/// </summary>
[Required]
public string Author { get; set; }
public string? Author { get; set; }
/// <summary>
/// Construct a <see cref="TestMergeBase"/>
@@ -31,14 +31,14 @@ namespace Tgstation.Server.Api.Models.Internal
/// The SID/UID of the <see cref="User"/> on Windows/POSIX respectively
/// </summary>
// No need for StringLength as the server MUST validate it.
public string SystemIdentifier { get; set; }
public string? SystemIdentifier { get; set; }
/// <summary>
/// The name of the <see cref="User"/>
/// </summary>
[Required]
[StringLength(Limits.MaximumStringLength)]
public string Name { get; set; }
public string? Name { get; set; }
/// <summary>
/// The <see cref="Rights.AdministrationRights"/> for the <see cref="User"/>
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The IP address or URL of the IRC server
/// </summary>
public string Address { get; set; }
public string? Address { get; set; }
/// <summary>
/// The port the server runs on
@@ -26,7 +26,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The nickname for the bot to use
/// </summary>
public string Nickname { get; set; }
public string? Nickname { get; set; }
/// <summary>
/// If the connection should be made using SSL
@@ -41,7 +41,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The optional password to use
/// </summary>
public string Password { get; set; }
public string? Password { get; set; }
/// <summary>
/// Construct an <see cref="IrcConnectionStringBuilder"/>
+2 -2
View File
@@ -8,12 +8,12 @@
/// <summary>
/// The <see cref="User"/> that started the job
/// </summary>
public User StartedBy { get; set; }
public User? StartedBy { get; set; }
/// <summary>
/// The <see cref="User"/> that cancelled the job
/// </summary>
public User CancelledBy { get; set; }
public User? CancelledBy { get; set; }
/// <summary>
/// Optional progress between 0 and 100 inclusive
@@ -10,32 +10,32 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The origin URL. If <see langword="null"/>, the <see cref="Repository"/> does not exist
/// </summary>
public string Origin { get; set; }
public string? Origin { get; set; }
/// <summary>
/// The commit HEAD should point to. Not populated in responses, use <see cref="RevisionInformation"/> instead for retrieval
/// </summary>
public string CheckoutSha { get; set; }
public string? CheckoutSha { get; set; }
/// <summary>
/// The current <see cref="Models.RevisionInformation"/> for the <see cref="Repository"/>
/// </summary>
public RevisionInformation RevisionInformation { get; set; }
public RevisionInformation? RevisionInformation { get; set; }
/// <summary>
/// If the repository was cloned from GitHub.com this will be set with the owner of the repository
/// </summary>
public string GitHubOwner { get; set; }
public string? GitHubOwner { get; set; }
/// <summary>
/// If the repository was cloned from GitHub.com this will be set with the name of the repository
/// </summary>
public string GitHubName { get; set; }
public string? GitHubName { get; set; }
/// <summary>
/// The <see cref="Job"/> started by the <see cref="Repository"/> if any
/// </summary>
public Job ActiveJob { get; set; }
public Job? ActiveJob { get; set; }
/// <summary>
/// Do the equivalent of a git pull. Will attempt to merge unless <see cref="Reference"/> is also specified in which case a hard reset will be performed after checking out
@@ -45,11 +45,11 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The branch or tag HEAD points to
/// </summary>
public string Reference { get; set; }
public string? Reference { get; set; }
/// <summary>
/// <see cref="TestMergeParameters"/> for new <see cref="TestMerge"/>s. Note that merges that conflict will not be performed
/// </summary>
public List<TestMergeParameters> NewTestMerges { get; set; }
public ICollection<TestMergeParameters>? NewTestMerges { get; set; }
}
}
@@ -8,16 +8,16 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The <see cref="TestMerge"/> that was created with this <see cref="RevisionInformation"/>
/// </summary>
public TestMerge PrimaryTestMerge { get; set; }
public TestMerge? PrimaryTestMerge { get; set; }
/// <summary>
/// The <see cref="TestMerge"/>s active in the <see cref="RevisionInformation"/>
/// </summary>
public ICollection<TestMerge> ActiveTestMerges { get; set; }
public ICollection<TestMerge>? ActiveTestMerges { get; set; }
/// <summary>
/// The <see cref="CompileJob"/>s made from the <see cref="RevisionInformation"/>
/// </summary>
public ICollection<CompileJob> CompileJobs { get; set; }
public ICollection<CompileJob>? CompileJobs { get; set; }
}
}
@@ -10,16 +10,16 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The version of the host
/// </summary>
public Version Version { get; set; }
public Version? Version { get; set; }
/// <summary>
/// The <see cref="Api"/> version of the host
/// </summary>
public Version ApiVersion { get; set; }
public Version? ApiVersion { get; set; }
/// <summary>
/// The DMAPI version of the host.
/// </summary>
public Version DMApiVersion { get; set; }
public Version? DMApiVersion { get; set; }
}
}
+1 -1
View File
@@ -6,6 +6,6 @@
/// <summary>
/// The <see cref="User"/> who created the <see cref="TestMerge"/>
/// </summary>
public User MergedBy { get; set; }
public User? MergedBy { get; set; }
}
}
@@ -17,12 +17,12 @@ namespace Tgstation.Server.Api.Models
/// </summary>
[Required]
[StringLength(40)]
public string PullRequestRevision { get; set; }
public string? PullRequestRevision { get; set; }
/// <summary>
/// Optional comment about the test
/// </summary>
[StringLength(Limits.MaximumStringLength)]
public string Comment { get; set; }
public string? Comment { get; set; }
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ namespace Tgstation.Server.Api.Models
/// <summary>
/// The value of the JWT
/// </summary>
public string Bearer { get; set; }
public string? Bearer { get; set; }
/// <summary>
/// When the <see cref="Token"/> expires
+3 -3
View File
@@ -6,16 +6,16 @@
/// <summary>
/// The name of the default admin user
/// </summary>
public const string AdminName = "Admin";
public static readonly string AdminName = "Admin";
/// <summary>
/// The default admin password
/// </summary>
public const string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
public static readonly string DefaultAdminPassword = "ISolemlySwearToDeleteTheDataDirectory";
/// <summary>
/// The <see cref="User"/> who created this <see cref="User"/>
/// </summary>
public User CreatedBy { get; set; }
public User? CreatedBy { get; set; }
}
}
@@ -11,6 +11,6 @@ namespace Tgstation.Server.Api.Models
/// Cleartext password of the <see cref="User"/>
/// </summary>
[Required]
public string Password { get; set; }
public string? Password { get; set; }
}
}
@@ -2,15 +2,16 @@
<Import Project="../../build/Version.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<DebugType>Full</DebugType>
<Version>$(TgsApiVersion)</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Cyberboss</Authors>
<Company>/tg/station</Company>
<Company>/tg/station 13</Company>
<Description>API definitions for tgstation-server</Description>
<PackageProjectUrl>https://tgstation.github.io/tgstation-server</PackageProjectUrl>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageIcon>tgs.png</PackageIcon>
<PackageProjectUrl>https://tgstation.github.io/tgstation-server</PackageProjectUrl>
<RepositoryType>Git</RepositoryType>
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
<Copyright>2018</Copyright>
@@ -18,21 +19,16 @@
<PackageReleaseNotes>See https://github.com/tgstation/tgstation-server/releases/tag/api-v$(TgsApiVersion)</PackageReleaseNotes>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Version>$(TgsApiVersion)</Version>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\Tgstation.Server.Api.xml</DocumentationFile>
<NoWarn>CA1028</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netstandard2.1\Tgstation.Server.Api.xml</DocumentationFile>
<NoWarn>1701;1702;CA1028</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702;CA1028</NoWarn>
<DocumentationFile>bin\Debug\netstandard2.1\Tgstation.Server.Api.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
+81 -25
View File
@@ -44,6 +44,16 @@ namespace Tgstation.Server.Client
/// </summary>
readonly List<IRequestLogger> requestLoggers;
/// <summary>
/// Backing field for <see cref="Headers"/>
/// </summary>
readonly ApiHeaders? tokenRefreshHeaders;
/// <summary>
/// The <see cref="SemaphoreSlim"/> for <see cref="Token"/> refreshes.
/// </summary>
readonly SemaphoreSlim semaphoreSlim;
/// <summary>
/// Backing field for <see cref="Headers"/>
/// </summary>
@@ -57,7 +67,7 @@ namespace Tgstation.Server.Client
static void HandleBadResponse(HttpResponseMessage response, string json)
{
ErrorMessage errorMessage = null;
ErrorMessage? errorMessage = null;
try
{
// check if json serializes to an error message
@@ -101,18 +111,25 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="httpClient">The value of <see cref="httpClient"/></param>
/// <param name="url">The value of <see cref="Url"/></param>
/// <param name="apiHeaders">The value of <see cref="ApiHeaders"/></param>
public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders)
/// <param name="apiHeaders">The value of <see cref="Headers"/></param>
/// <param name="tokenRefreshHeaders">The value of <see cref="tokenRefreshHeaders"/></param>
public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders)
{
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Url = url ?? throw new ArgumentNullException(nameof(url));
headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
this.tokenRefreshHeaders = tokenRefreshHeaders;
requestLoggers = new List<IRequestLogger>();
semaphoreSlim = new SemaphoreSlim(1);
}
/// <inheritdoc />
public void Dispose() => httpClient.Dispose();
public void Dispose()
{
httpClient.Dispose();
semaphoreSlim.Dispose();
}
/// <summary>
/// Main request method
@@ -122,9 +139,10 @@ namespace Tgstation.Server.Client
/// <param name="body">The body of the request</param>
/// <param name="method">The method of the request</param>
/// <param name="instanceId">The optional <see cref="Instance.Id"/> for the request</param>
/// <param name="tokenRefresh">If this is a token refresh operation.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response on success</returns>
async Task<TResult> RunRequest<TResult>(string route, object body, HttpMethod method, long? instanceId, CancellationToken cancellationToken)
async Task<TResult> RunRequest<TResult>(string route, object? body, HttpMethod method, long? instanceId, bool tokenRefresh, CancellationToken cancellationToken)
{
if (route == null)
throw new ArgumentNullException(nameof(route));
@@ -141,11 +159,25 @@ namespace Tgstation.Server.Client
if (body != null)
request.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
headers.SetRequestHeaders(request.Headers, instanceId);
var headersToUse = tokenRefresh ? tokenRefreshHeaders! : headers;
headersToUse.SetRequestHeaders(request.Headers, instanceId);
await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false);
// This is meant to be a gate against token refresh operations
await semaphoreSlim.WaitAsync(cancellationToken).ConfigureAwait(false);
if(!tokenRefresh)
semaphoreSlim.Release();
response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
try
{
await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false);
response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
finally
{
if (tokenRefresh)
semaphoreSlim.Release();
}
}
using (response)
@@ -155,14 +187,20 @@ namespace Tgstation.Server.Client
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
{
if (!tokenRefresh
&& response.StatusCode == HttpStatusCode.Unauthorized
&& await RefreshToken(cancellationToken).ConfigureAwait(false))
return await RunRequest<TResult>(route, body, method, instanceId, false, cancellationToken).ConfigureAwait(false);
HandleBadResponse(response, json);
}
if (String.IsNullOrWhiteSpace(json))
json = JsonConvert.SerializeObject(new object());
try
{
return JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
return JsonConvert.DeserializeObject<TResult>(json, serializerSettings) !;
}
catch (JsonException)
{
@@ -171,50 +209,68 @@ namespace Tgstation.Server.Client
}
}
/// <inheritdoc />
public Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, null, cancellationToken);
async Task<bool> RefreshToken(CancellationToken cancellationToken)
{
if (tokenRefreshHeaders == null)
return false;
try
{
var token = await RunRequest<Token>(Routes.Root, null, HttpMethod.Post, null, true, cancellationToken);
headers = new ApiHeaders(headers.UserAgent!, token.Bearer!);
}
catch (ClientException)
{
return false;
}
return true;
}
/// <inheritdoc />
public Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, null, cancellationToken);
public Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, null, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Post, null, cancellationToken);
public Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, null, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, cancellationToken);
public Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Post, null, false, cancellationToken);
/// <inheritdoc />
public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, cancellationToken);
public Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, null, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, null, cancellationToken);
public Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Post, null, false, cancellationToken);
/// <inheritdoc />
public Task Delete(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, null, cancellationToken);
public Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, null, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, instanceId, cancellationToken);
public Task Delete(string route, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, null, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, cancellationToken);
public Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Put, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, instanceId, cancellationToken);
public Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Get, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, instanceId, cancellationToken);
public Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, body, HttpMethod.Post, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Delete, instanceId, cancellationToken);
public Task Delete(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, cancellationToken);
public Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken) => RunRequest<object>(route, body, HttpMethod.Delete, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, instanceId, cancellationToken);
public Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, null, HttpMethod.Delete, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), new HttpMethod("PATCH"), instanceId, cancellationToken);
public Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Put, instanceId, false, cancellationToken);
/// <inheritdoc />
public Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken) => RunRequest<TResult>(route, new object(), HttpMethod.Patch, instanceId, false, cancellationToken);
/// <inheritdoc />
public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger)));
@@ -7,6 +7,6 @@ namespace Tgstation.Server.Client
sealed class ApiClientFactory : IApiClientFactory
{
/// <inheritdoc />
public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders) => new ApiClient(new HttpClient(), url, apiHeaders);
public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders) => new ApiClient(new HttpClient(), url, apiHeaders, tokenRefreshHeaders);
}
}
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ApiException"/></param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> for the <see cref="ClientException"/></param>
public ApiConflictException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
public ApiConflictException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
{ }
/// <summary>
+3 -3
View File
@@ -12,12 +12,12 @@ namespace Tgstation.Server.Client
/// <summary>
/// The <see cref="Version"/> of the server's API.
/// </summary>
public Version ServerApiVersion { get; }
public Version? ServerApiVersion { get; }
/// <summary>
/// Additional error data from the server.
/// </summary>
public string AdditionalServerData { get; }
public string? AdditionalServerData { get; }
/// <summary>
/// The API <see cref="ErrorCode"/> if applicable.
@@ -29,7 +29,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> returned from the API.</param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/>.</param>
protected ApiException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(
protected ApiException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(
responseMessage,
errorMessage?.Message ?? $"HTTP {responseMessage.StatusCode}. Unknown API error, ErrorMessage payload not present!")
{
@@ -12,7 +12,7 @@ namespace Tgstation.Server.Client
/// <summary>
/// The <see cref="HttpStatusCode"/> of the <see cref="ClientException"/>
/// </summary>
public HttpResponseMessage ResponseMessage { get; }
public HttpResponseMessage? ResponseMessage { get; }
/// <summary>
/// Initialize a new instance of the <see cref="ClientException"/> <see langword="class"/>.
@@ -59,7 +59,10 @@ namespace Tgstation.Server.Client.Components
{
if (file == null)
throw new ArgumentNullException(nameof(file));
return apiClient.Read<ConfigurationFile>(Routes.ConfigurationFile + SanitizeGetPath(file.Path), instance.Id, cancellationToken);
return apiClient.Read<ConfigurationFile>(
Routes.ConfigurationFile + SanitizeGetPath(file.Path ?? throw new ArgumentException("file.Path should not be null!", nameof(file))),
instance.Id,
cancellationToken);
}
/// <inheritdoc />
@@ -35,7 +35,12 @@ namespace Tgstation.Server.Client.Components
public Task<InstanceUser> Create(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Create<InstanceUser, InstanceUser>(Routes.InstanceUser, instanceUser ?? throw new ArgumentNullException(nameof(instanceUser)), instance.Id, cancellationToken);
/// <inheritdoc />
public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.InstanceUser, instanceUser.UserId.Value), instance.Id, cancellationToken);
public Task Delete(InstanceUser instanceUser, CancellationToken cancellationToken) => apiClient.Delete(
Routes.SetID(
Routes.InstanceUser,
instanceUser.UserId ?? throw new ArgumentException("Missing instanceUser.UserId!", nameof(instanceUser))),
instance.Id,
cancellationToken);
/// <inheritdoc />
public Task<InstanceUser> Read(CancellationToken cancellationToken) => apiClient.Read<InstanceUser>(Routes.InstanceUser, instance.Id, cancellationToken);
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ApiException"/>.</param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> for the <see cref="ClientException"/>.</param>
public ConflictException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
public ConflictException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
{ }
/// <summary>
@@ -13,7 +13,8 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="url">The base <see cref="Uri"/></param>
/// <param name="apiHeaders">The <see cref="ApiHeaders"/> for the <see cref="IApiClient"/></param>
/// <param name="tokenRefreshHeaders">The <see cref="ApiHeaders"/> to use to generate a new <see cref="Api.Models.Token"/>.</param>
/// <returns>A new <see cref="IApiClient"/></returns>
IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders);
IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders, ApiHeaders? tokenRefreshHeaders);
}
}
@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models;
@@ -16,18 +17,28 @@ namespace Tgstation.Server.Client
/// <param name="host">The URL to access TGS</param>
/// <param name="username">The username to for the <see cref="IServerClient"/></param>
/// <param name="password">The password for the <see cref="IServerClient"/></param>
/// <param name="timeout">The <see cref="TimeSpan"/> representing timeout for the connection</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <param name="requestLoggers">Optional initial <see cref="IRequestLogger"/>s to add to the <see cref="IServerClient"/>.</param>
/// <param name="timeout">Optional <see cref="TimeSpan"/> representing timeout for the connection</param>
/// <param name="attemptLoginRefresh">Attempt to refresh the received <see cref="Token"/> when it expires or becomes invalid. <paramref name="username"/> and <paramref name="password"/> will be stored in memory if this is <see langword="true"/>.</param>
/// <param name="cancellationToken">Optional <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in a new <see cref="IServerClient"/></returns>
Task<IServerClient> CreateServerClient(Uri host, string username, string password, TimeSpan timeout = default, CancellationToken cancellationToken = default);
Task<IServerClient> CreateFromLogin(
Uri host,
string username,
string password,
IEnumerable<IRequestLogger>? requestLoggers = null,
TimeSpan? timeout = null,
bool attemptLoginRefresh = true,
CancellationToken cancellationToken = default);
/// <summary>
/// Create a <see cref="IServerClient"/>
/// </summary>
/// <param name="host">The URL to access TGS</param>
/// <param name="token">The <see cref="Token"/> to access the API with</param>
/// <param name="timeout">The <see cref="TimeSpan"/> representing timeout for the connection</param>
/// <returns>A new <see cref="IServerClient"/></returns>
IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout = default);
IServerClient CreateFromToken(
Uri host,
Token token);
}
}
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ApiException"/>.</param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> for the <see cref="ClientException"/>.</param>
public MethodNotSupportedException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
public MethodNotSupportedException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
{ }
/// <summary>
@@ -21,7 +21,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ApiException"/>.</param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> for the <see cref="ClientException"/>.</param>
public RateLimitException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
public RateLimitException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
{
if (!responseMessage.Headers.TryGetValues(HeaderNames.RetryAfter, out var values))
return;
+1 -1
View File
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Client
set
{
token = value ?? throw new InvalidOperationException("Cannot set a null Token!");
apiClient.Headers = new ApiHeaders(apiClient.Headers.UserAgent, token.Bearer);
apiClient.Headers = new ApiHeaders(apiClient.Headers.UserAgent!, token.Bearer!);
}
}
@@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
@@ -30,7 +32,14 @@ namespace Tgstation.Server.Client
}
/// <inheritdoc />
public async Task<IServerClient> CreateServerClient(Uri host, string username, string password, TimeSpan timeout, CancellationToken cancellationToken)
public async Task<IServerClient> CreateFromLogin(
Uri host,
string username,
string password,
IEnumerable<IRequestLogger>? requestLoggers = null,
TimeSpan? timeout = null,
bool attemptRefreshLogin = true,
CancellationToken cancellationToken = default)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
@@ -39,28 +48,40 @@ namespace Tgstation.Server.Client
if (password == null)
throw new ArgumentNullException(nameof(password));
requestLoggers ??= Enumerable.Empty<IRequestLogger>();
Token token;
using (var api = ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password)))
var loginHeaders = new ApiHeaders(productHeaderValue, username, password);
using (var api = ApiClientFactory.CreateApiClient(host, loginHeaders, null))
{
if (timeout != default)
api.Timeout = timeout;
foreach (var requestLogger in requestLoggers)
api.AddRequestLogger(requestLogger);
if (timeout.HasValue)
api.Timeout = timeout.Value;
token = await api.Update<Token>(Routes.Root, cancellationToken).ConfigureAwait(false);
}
return CreateServerClient(host, token, timeout);
var client = CreateFromToken(host, token);
if (timeout.HasValue)
client.Timeout = timeout.Value;
var apiHeaders = new ApiHeaders(productHeaderValue, token.Bearer!);
return new ServerClient(ApiClientFactory.CreateApiClient(host, apiHeaders, loginHeaders), token);
}
/// <inheritdoc />
public IServerClient CreateServerClient(Uri host, Token token, TimeSpan timeout)
public IServerClient CreateFromToken(Uri host, Token token)
{
if (host == null)
throw new ArgumentNullException(nameof(host));
if (token == null)
throw new ArgumentNullException(nameof(token));
var result = new ServerClient(ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer)), token);
if (timeout != default)
result.Timeout = timeout;
return result;
if (token.Bearer == null)
throw new ArgumentException("token.Bearer should not be null!", nameof(token));
return new ServerClient(ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer), null), token);
}
}
}
@@ -19,7 +19,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ApiException"/></param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> for the <see cref="ClientException"/></param>
public ServerErrorException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
public ServerErrorException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
{
}
@@ -2,41 +2,34 @@
<Import Project="../../build/Version.props" />
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.1</TargetFramework>
<DebugType>Full</DebugType>
<Version>$(TgsClientVersion)</Version>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Authors>Cyberboss</Authors>
<Company>/tg/station 13</Company>
<Product />
<Description>Client library for tgstation-server</Description>
<PackageProjectUrl>https://tgstation.github.io/tgstation-server</PackageProjectUrl>
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
<RepositoryType>Git</RepositoryType>
<NeutralLanguage>en-CA</NeutralLanguage>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
<PackageIcon>tgs.png</PackageIcon>
<RepositoryType>Git</RepositoryType>
<RepositoryUrl>https://github.com/tgstation/tgstation-server</RepositoryUrl>
<Copyright>2018</Copyright>
<PackageTags>json web api tgstation-server tgstation ss13 byond client</PackageTags>
<PackageReleaseNotes>Updated to API version $(TgsApiVersion)</PackageReleaseNotes>
<PackageReleaseNotes>.Net Standard 2.1, allow login requests to be logged.</PackageReleaseNotes>
<IncludeSymbols>true</IncludeSymbols>
<SymbolPackageFormat>snupkg</SymbolPackageFormat>
<Copyright>2018</Copyright>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<DocumentationFile>bin\$(Configuration)\netstandard2.1\Tgstation.Server.Client.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netstandard2.1\Tgstation.Server.Client.xml</DocumentationFile>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>1701;1702</NoWarn>
<DocumentationFile>bin\Debug\netstandard2.1\Tgstation.Server.Client.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.118">
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> returned by the API.</param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/>.</param>
public UnauthorizedException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
public UnauthorizedException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
{ }
/// <summary>
@@ -14,7 +14,7 @@ namespace Tgstation.Server.Client
/// </summary>
/// <param name="errorMessage">The <see cref="ErrorMessage"/> for the <see cref="ApiException"/>.</param>
/// <param name="responseMessage">The <see cref="HttpResponseMessage"/> for the <see cref="ClientException"/>.</param>
public VersionMismatchException(ErrorMessage errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
public VersionMismatchException(ErrorMessage? errorMessage, HttpResponseMessage responseMessage) : base(errorMessage, responseMessage)
{
}
@@ -1,13 +1,16 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Components.Interop;
using Tgstation.Server.Host.Components.Interop.Bridge;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.Database;
using Tgstation.Server.Host.IO;
@@ -78,6 +81,11 @@ namespace Tgstation.Server.Host.Components
/// </summary>
readonly IDictionary<string, IBridgeHandler> bridgeHandlers;
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="InstanceManager"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// The <see cref="TaskCompletionSource{TResult}"/> for <see cref="Ready"/>.
/// </summary>
@@ -104,6 +112,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="serverControl">The value of <see cref="serverControl"/></param>
/// <param name="systemIdentityFactory">The value of <see cref="systemIdentityFactory"/>.</param>
/// <param name="asyncDelayer">The value of <see cref="asyncDelayer"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> containing the value of <see cref="generalConfiguration"/>.</param>
/// <param name="logger">The value of <see cref="logger"/></param>
public InstanceManager(
IInstanceFactory instanceFactory,
@@ -114,6 +123,7 @@ namespace Tgstation.Server.Host.Components
IServerControl serverControl,
ISystemIdentityFactory systemIdentityFactory,
IAsyncDelayer asyncDelayer,
IOptions<GeneralConfiguration> generalConfigurationOptions,
ILogger<InstanceManager> logger)
{
this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory));
@@ -124,6 +134,7 @@ namespace Tgstation.Server.Host.Components
this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl));
this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory));
this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer));
generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
serverControl.RegisterForRestart(this);
@@ -319,6 +330,10 @@ namespace Tgstation.Server.Host.Components
/// </summary>
private void CheckSystemCompatibility()
{
if (generalConfiguration.UseExperimentalWatchdog && !Debugger.IsAttached)
throw new InvalidOperationException(
"The experimental watchdog is currently non-functional! Please disable the '{nameof(generalConfiguration.UseExperimentalWatchdog)}' option in your configuration!");
using var systemIdentity = systemIdentityFactory.GetCurrent();
if (!systemIdentity.CanCreateSymlinks)
throw new InvalidOperationException("The user running tgstation-server cannot create symlinks! Please try running as an administrative user!");
@@ -265,12 +265,12 @@ namespace Tgstation.Server.Host.Components.Repository
if (progressReporter == null)
throw new ArgumentNullException(nameof(progressReporter));
logger.LogDebug("Begin AddTestMerge: #{0} at {1} ({4}) by <{2} ({3})>",
logger.LogDebug("Begin AddTestMerge: #{0} at {1} ({2}) by <{3} ({4})>",
testMergeParameters.Number,
testMergeParameters.PullRequestRevision?.Substring(0, 7),
testMergeParameters.Comment,
committerName,
committerEmail,
testMergeParameters.Comment);
committerEmail);
if (!IsGitHubRepository)
throw new InvalidOperationException("Test merging is only available on GitHub hosted origin repositories!");
@@ -491,6 +491,7 @@ namespace Tgstation.Server.Host.Controllers
async databaseContext =>
{
databaseContext.Instances.Attach(attachedInstance);
var previousRevInfo = lastRevisionInfo;
var needsUpdate = await LoadRevisionInformation(
repo,
databaseContext,
@@ -516,13 +517,14 @@ namespace Tgstation.Server.Host.Controllers
testMergeToAdd.MergedBy = mergedBy;
lastRevisionInfo.ActiveTestMerges.AddRange(previousRevInfo.ActiveTestMerges);
lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
{
TestMerge = testMergeToAdd
});
lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
databaseContext.Users.Attach(testMergeToAdd.MergedBy);
needsUpdate = true;
}
if (needsUpdate)
@@ -532,12 +534,7 @@ namespace Tgstation.Server.Host.Controllers
await CallLoadRevInfo().ConfigureAwait(false);
// apply new rev info, tracking applied test merges
async Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null)
{
var last = lastRevisionInfo;
await CallLoadRevInfo(testMergeToAdd, last.OriginCommitSha).ConfigureAwait(false);
lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges);
}
Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null) => CallLoadRevInfo(testMergeToAdd, lastRevisionInfo.OriginCommitSha);
try
{
@@ -814,7 +811,7 @@ namespace Tgstation.Server.Host.Controllers
doneSteps = 0;
numSteps = 2;
// the stuff didn't make it into the db, forget what we've done and abort
// Forget what we've done and abort
await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false);
if (startReference != null && repo.Head != startSha)
await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false);
@@ -556,10 +556,7 @@ namespace Tgstation.Server.Host.Setup
if (String.IsNullOrWhiteSpace(newGeneralConfiguration.GitHubAccessToken))
newGeneralConfiguration.GitHubAccessToken = null;
newGeneralConfiguration.UseExperimentalWatchdog = await PromptYesNo("Use the experimental watchdog (NOT RECOMMENDED)? (y/n): ", cancellationToken).ConfigureAwait(false);
newGeneralConfiguration.UseBasicWatchdogOnWindows = true;
// newGeneralConfiguration.UseExperimentalWatchdog = await PromptYesNo("Use the experimental watchdog (NOT RECOMMENDED)? (y/n): ", cancellationToken).ConfigureAwait(false);
return newGeneralConfiguration;
}
@@ -9,18 +9,13 @@
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<IsPackable>false</IsPackable>
<IncludeOpenAPIAnalyzers>true</IncludeOpenAPIAnalyzers>
<DocumentationFile>bin\$(Configuration)\netcoreapp3.1\Tgstation.Server.Host.xml</DocumentationFile>
<NoWarn>API1000</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netcoreapp2.1\Tgstation.Server.Host.xml</DocumentationFile>
<NoWarn>API1000</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<NoWarn>API1000</NoWarn>
<DocumentationFile>bin\Debug\netcoreapp2.1\Tgstation.Server.Host.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup>
@@ -39,7 +39,7 @@ namespace Tgstation.Server.Client.Tests
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.SendAsync(It.IsNotNull<HttpRequestMessage>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(response));
var client = new ApiClient(httpClient.Object, new Uri("http://fake.com"), new ApiHeaders(new ProductHeaderValue("fake"), "fake"));
var client = new ApiClient(httpClient.Object, new Uri("http://fake.com"), new ApiHeaders(new ProductHeaderValue("fake"), "fake"), null);
var result = await client.Read<Byond>(Routes.Byond, default).ConfigureAwait(false);
Assert.AreEqual(sample.Version, result.Version);
@@ -64,7 +64,7 @@ namespace Tgstation.Server.Client.Tests
var httpClient = new Mock<IHttpClient>();
httpClient.Setup(x => x.SendAsync(It.IsNotNull<HttpRequestMessage>(), It.IsAny<CancellationToken>())).Returns(Task.FromResult(response));
var client = new ApiClient(httpClient.Object, new Uri("http://fake.com"), new ApiHeaders(new ProductHeaderValue("fake"), "fake"));
var client = new ApiClient(httpClient.Object, new Uri("http://fake.com"), new ApiHeaders(new ProductHeaderValue("fake"), "fake"), null);
await Assert.ThrowsExceptionAsync<UnrecognizedResponseException>(() => client.Read<Byond>(Routes.Byond, default)).ConfigureAwait(false);
}
@@ -86,7 +86,7 @@ namespace Tgstation.Server.Host.Setup.Tests
var mockFailCommand = new Mock<DbCommand>();
mockFailCommand.Setup(x => x.ExecuteNonQueryAsync(It.IsAny<CancellationToken>())).Throws(new Exception()).Verifiable();
void SetDbCommandCreator(Mock<DbConnection> mock, Func<DbCommand> creator) => mock.Protected().Setup<DbCommand>("CreateDbCommand").Returns(creator).Verifiable();
static void SetDbCommandCreator(Mock<DbConnection> mock, Func<DbCommand> creator) => mock.Protected().Setup<DbCommand>("CreateDbCommand").Returns(creator).Verifiable();
var mockGoodDbConnection = new Mock<DbConnection>();
mockGoodDbConnection.Setup(x => x.OpenAsync(It.IsAny<CancellationToken>())).Returns(Task.CompletedTask).Verifiable();
@@ -144,7 +144,6 @@ namespace Tgstation.Server.Host.Setup.Tests
"-27",
"5000",
"fake token",
"y",
//logging config
"no",
//cp config
@@ -167,7 +166,6 @@ namespace Tgstation.Server.Host.Setup.Tests
String.Empty,
String.Empty,
"n",
"y",
//logging config
"y",
"not actually verified because lol mocks /../!@#$%^&*()/..///.",
@@ -191,7 +189,6 @@ namespace Tgstation.Server.Host.Setup.Tests
String.Empty,
String.Empty,
"y",
"y",
"will faile",
String.Empty,
String.Empty,
@@ -43,7 +43,7 @@ namespace Tgstation.Server.Tests
{
try
{
adminClient = await clientFactory.CreateServerClient(server.Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false);
adminClient = await clientFactory.CreateFromLogin(server.Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false);
break;
}
catch (HttpRequestException)
@@ -144,7 +144,7 @@ namespace Tgstation.Server.Tests
{
try
{
return await clientFactory.CreateServerClient(server.Url, User.AdminName, User.DefaultAdminPassword).ConfigureAwait(false);
return await clientFactory.CreateFromLogin(server.Url, User.AdminName, User.DefaultAdminPassword, attemptLoginRefresh: false).ConfigureAwait(false);
}
catch (HttpRequestException)
{
@@ -193,7 +193,7 @@ namespace Tgstation.Server.Tests
Bearer = adminClient.Token.Bearer + '0'
};
var badClient = clientFactory.CreateServerClient(server.Url, newToken);
var badClient = clientFactory.CreateFromToken(server.Url, newToken);
await Assert.ThrowsExceptionAsync<UnauthorizedException>(() => badClient.Version(cancellationToken)).ConfigureAwait(false);
var adminTest = new AdministrationTest(adminClient.Administration).Run(cancellationToken);