Merge branch 'master' of https://github.com/tgstation/tgstation-server into thingymajig

This commit is contained in:
steamport
2018-12-02 11:33:43 -05:00
199 changed files with 17889 additions and 1024 deletions
+22 -1
View File
@@ -1,9 +1,30 @@
.dockerignore
.git
.github
.gitignore
.vs
.vscode
packages
.travis.yml
.codecov.yml
appveyor.yml
LICENSE
README
build/**
!build/analyzers.ruleset
!build/tgs.docker.sh
!build/tgs.ico
!build/stylecop.json
*/bin
*/obj
tests
docs
tools
src/DMAPI
src/Tgstation.Server.Host/ClientApp/build
src/Tgstation.Server.Host/ClientApp/node_modules
src/Tgstation.Server.Host/wwwroot
src/Tgstation.Server.Host/appsettings.Development.json
src/Tgstation.Server.Host/tgs.bat
src/Tgstation.Server.Host/tgs.sh
src/Tgstation.Server.Host.Client
src/Tgstation.Server.Host.Service
+27 -7
View File
@@ -34,7 +34,7 @@ You can of course, as always, ask for help at [#coderbus](irc://irc.rizon.net/co
### Development Environment
You need the Dotnet 2.1 SDK to compile the main server and command line programs. In order to build the service version you also need a .NET 4.7.1 build chain
You need the Dotnet 2.1 SDK and npm>=v5.7 (in your PATH) to compile the server. In order to build the service version you also need a .NET 4.7.1 build chain
The recommended IDE is Visual Studio 2017 which has installation options for both of these.
@@ -47,11 +47,6 @@ As mentioned before, you are expected to follow these specifications in order to
### Object Oriented Code
As C# is an object-oriented language, code must be object-oriented when possible in order to be more flexible when adding content to it. If you don't know what "object-oriented" means, we highly recommend you do some light research to grasp the basics.
### Tabs, not spaces
You must use tabs to indent your code, NOT SPACES.
(You may use spaces to align something, but you should tab to the block level first, then add the remaining spaces)
### No hacky code
Hacky code, such as adding specific checks, is highly discouraged and only allowed when there is ***no*** other option. (Protip: 'I couldn't immediately think of a proper way so thus there must be no other option' is not gonna cut it here! If you can't think of anything else, say that outright and admit that you need help with it. Maintainers exist for exactly that reason.)
@@ -97,7 +92,7 @@ The version format we use is 4.\<major\>.\<minor\>.\<patch\>. The first number n
### Formatting
The formatting style is the same one Visual studio uses by default. Quick formatting of code blocks can be achieved by deleting and retyping the trailing `}`
Stylecop will throw warnings if your code does not match style guidelines. Do NOT suppress these
### Use early return
Do not enclose a function in an if-block when returning on a condition is more feasible
@@ -149,7 +144,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
+2
View File
@@ -1,3 +1,5 @@
If your issue is for the web control panel, please report it here instead: https://github.com/tgstation/tgstation-server-control-panel
Please include:
- A description of the issue
+13
View File
@@ -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.)
+4
View File
@@ -15,3 +15,7 @@ artifacts/
/tests/DMAPI/travistester.int
/tests/DMAPI/travistester.dmb
/src/Tgstation.Server.Host/appsettings.Development.json
/src/Tgstation.Server.Host/wwwroot
/src/Tgstation.Server.Host/ClientApp/node_modules
/src/Tgstation.Server.Host/ClientApp/build
/tools/ReleaseNotes/release_notes.md
+6
View File
@@ -92,6 +92,12 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi
- `Database:ConnectionString`: Connection string for your database. Click [here](https://www.developerfusion.com/tools/sql-connection-string/) for an SQL Server generator or see [here](https://www.connectionstrings.com/mysql/) for a MySQL guide ([You should probably use '127.0.0.1' instead of 'localhost'](https://stackoverflow.com/questions/19712307/mysql-localhost-127-0-0-1)).
- `ControlPanel:Enable`: Enable the javascript based control panel to be served from the server via /index.html
- `ControlPanel:AllowAnyOrigin`: Set the Access-Control-Allow-Origin header to * for all responses (also enables all headers and methods)
- `ControlPanel:AllowedOrigins`: Set the Access-Control-Allow-Origin headers to this list of origins for all responses (also enables all headers and methods). This is overridden by `ControlPanel:AllowAnyOrigin`
### Database Configuration
If using a MariaDB/MySQL server, our client library [recommends you set 'utf8mb4' as your default charset](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql#1-recommended-server-charset) disregard at your own risk.
+9 -5
View File
@@ -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
@@ -24,6 +26,8 @@ artifacts:
name: ServerUpdatePackage
- path: src/DMAPI
name: DMAPI
- path: tgsdox
name: DocumentationHtml
cache:
- ~\.nuget\packages -> **\*.csproj
- C:\ProgramData\chocolatey\bin -> appveyor.yml
@@ -33,6 +37,7 @@ services:
install:
- choco install doxygen.portable codecov graphviz.portable opencover.portable
- nuget restore tgstation-server.sln
- ps: Install-Product node 10
build:
project: tgstation-server.sln
parallel: false
@@ -77,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
+2
View File
@@ -32,4 +32,6 @@ if($publish_dox){
git commit -m "Deploy code docs to GitHub Pages for Appveyor build $Env:APPVEYOR_BUILD_NUMBER" -m "Commit: $Env:APPVEYOR_REPO_COMMIT"
git push -f "https://$Env:TGS4_TEST_GITHUB_TOKEN@$github_url" 2>&1 | out-null
cd "$bf"
rm -r "$doxdir/.git"
}
mv C:/tgsdox "$bf/tgsdox"
+21
View File
@@ -1,5 +1,23 @@
FROM microsoft/dotnet:2.1-sdk AS build
# install node and npm
# replace shell with bash so we can source files
RUN curl --silent -o- https://raw.githubusercontent.com/creationix/nvm/v0.31.2/install.sh | sh
ENV NODE_VERSION=10.13.0
ENV NVM_DIR /root/.nvm
RUN . $NVM_DIR/nvm.sh \
&& nvm install $NODE_VERSION \
&& nvm use $NODE_VERSION \
&& apt-get update \
&& apt-get install -y \
dos2unix \
&& rm -rf /var/lib/apt/lists/*
ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
WORKDIR /src
COPY tgstation-server.sln ./
@@ -13,6 +31,9 @@ RUN dotnet restore -nowarn:MSB3202,nu1503 -p:RestoreUseSkipNonexistentTargets=fa
COPY . .
#run dos2unix on tgs.docker.sh so we can build without issue on windows
RUN dos2unix build/tgs.docker.sh
WORKDIR /src/src/Tgstation.Server.Host.Console
RUN dotnet publish -c Release -o /app
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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"
}
}
+8
View File
@@ -0,0 +1,8 @@
{
"settings": {
"indentation": {
"tabSize": 4,
"useTabs": true
}
}
}
+2
View File
@@ -5,6 +5,8 @@ dotnet tool install --global coverlet.console
mkdir TestResults
source ~/.nvm/nvm.sh && nvm install 10
cd tests/Tgstation.Server.Api.Tests
dotnet build -c $CONFIG /p:CopyLocalLockFileAssemblies=true
+6 -3
View File
@@ -1,6 +1,9 @@
#!/bin/sh
touch /config_data/appsettings.Production.json
ln -s /config_data/appsettings.Production.json /app/appsettings.Production.json
PROD_CONFIG=/config_data/appsettings.Production.json
if [ ! -f $PROD_CONFIG ]; then
echo "{}" > $PROD_CONFIG
fi
ln -s $PROD_CONFIG /app/appsettings.Production.json
exec dotnet Tgstation.Server.Host.Console.dll "$@"
exec dotnet Tgstation.Server.Host.Console.dll
+1 -1
View File
@@ -864,7 +864,7 @@ RECURSIVE = YES
# Note that relative paths are relative to the directory from which doxygen is
# run.
EXCLUDE = packages
EXCLUDE = packages src/Tgstation.Server.Host/webroot src/Tgstation.Server.Host/ClientApp/node_modules src/Tgstation.Server.Host/ClientApp/build
# The EXCLUDE_SYMLINKS tag can be used to select whether or not files or
# directories that are symbolic links (a Unix file system feature) are excluded
+21 -5
View File
@@ -107,6 +107,22 @@
var/commit //full sha of compiled commit
var/origin_commit //full sha of last known remote commit. This may be null if the TGS repository is not currently tracking a remote branch
//represents a version of tgstation-server
/datum/tgs_version
var/suite //The suite version, can be >=3
//this group of variables can be null to represent a wild card
var/major //The major version
var/minor //The minor version
var/patch //The patch version
var/raw_parameter //The unparsed parameter
var/deprefixed_parameter //The version only bit of raw_parameter
//if the tgs_version is a wildcard version
/datum/tgs_version/proc/Wildcard()
return
//represents a merge of a GitHub pull request
/datum/tgs_revision_information/test_merge
var/number //pull request number
@@ -155,22 +171,22 @@
//FUNCTIONS
//Returns the respective string version of the API
//Returns the respective supported /datum/tgs_version of the API
/world/proc/TgsMaximumAPIVersion()
return
/world/proc/TgsMinimumAPIVersion()
return
//Gets the current version of the server tools running the server
/world/proc/TgsVersion()
return
//Returns TRUE if the world was launched under the server tools and the API matches, FALSE otherwise
//No function below this succeeds if it returns FALSE
/world/proc/TgsAvailable()
return
//Gets the current /datum/tgs_version of the server tools running the server
/world/proc/TgsVersion()
return
/world/proc/TgsInstanceName()
return
+36 -41
View File
@@ -1,24 +1,46 @@
/world/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE)
var/current_api = TGS_READ_GLOBAL(tgs)
if(current_api)
TGS_ERROR_LOG("TgsNew(): TGS API datum already set ([current_api])!")
TGS_ERROR_LOG("TgsNew(): TGS API datum already set ([current_api])! Was TgsNew() called more than once?")
return
#ifdef TGS_V3_API
minimum_required_security_level = TGS_SECURITY_TRUSTED
#endif
var/tgs_version = world.params[TGS_VERSION_PARAMETER]
if(!tgs_version)
var/raw_parameter = world.params[TGS_VERSION_PARAMETER]
if(!raw_parameter)
return
var/path = SelectTgsApi(tgs_version)
if(!path)
TGS_ERROR_LOG("Found unsupported API version: [tgs_version]. If this is a valid version please report this, backporting is done on demand.")
var/datum/tgs_version/version = new(raw_parameter)
if(!version.Valid(FALSE))
TGS_ERROR_LOG("Failed to validate TGS version parameter: [raw_parameter]!")
return
TGS_INFO_LOG("Activating API for version [tgs_version]")
var/datum/tgs_api/new_api = new path
var/api_datum
switch(version.suite)
if(3)
#ifndef TGS_V3_API
TGS_ERROR_LOG("Detected V3 API but TGS_V3_API isn't defined!")
#else
switch(version.major)
if(2)
api_datum = /datum/tgs_api/v3210
#endif
if(4)
switch(version.major)
if(0)
api_datum = /datum/tgs_api/v4
if(version.suite != null && version.major != null && version.minor != null && version.patch != null && version.deprefixed_parameter > TgsMaximumAPIVersion())
TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
api_datum = /datum/tgs_api/latest
if(!api_datum)
TGS_ERROR_LOG("Found unsupported API version: [raw_parameter]. If this is a valid version please report this, backporting is done on demand.")
return
TGS_INFO_LOG("Activating API for version [version.deprefixed_parameter]")
var/datum/tgs_api/new_api = new api_datum(version)
TGS_WRITE_GLOBAL(tgs, new_api)
@@ -27,40 +49,11 @@
TGS_WRITE_GLOBAL(tgs, null)
TGS_ERROR_LOG("Failed to activate API!")
/world/proc/SelectTgsApi(tgs_version)
//remove the old 3.0 header
tgs_version = replacetext(tgs_version, "/tg/station 13 Server v", "")
var/list/version_bits = splittext(tgs_version, ".")
var/super = text2num(version_bits[1])
var/major = text2num(version_bits[2])
var/minor = text2num(version_bits[3])
var/patch = text2num(version_bits[4])
switch(super)
if(3)
#ifndef TGS_V3_API
TGS_ERROR_LOG("Detected V3 API but TGS_V3_API isn't defined!")
#else
switch(major)
if(2)
return /datum/tgs_api/v3210
#endif
if(4)
switch(major)
if(0)
return /datum/tgs_api/v4
if(super != null && major != null && minor != null && patch != null && tgs_version > TgsMaximumAPIVersion())
TGS_ERROR_LOG("Detected unknown API version! Defaulting to latest. Update the DMAPI to fix this problem.")
return /datum/tgs_api/latest
/world/TgsMaximumAPIVersion()
return "4.0.0.0"
return new /datum/tgs_version("4.0.x.x")
/world/TgsMinimumAPIVersion()
return "3.2.0.0"
return new /datum/tgs_version("3.2.0.0")
/world/TgsInitializationComplete()
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
@@ -90,7 +83,9 @@
return TGS_READ_GLOBAL(tgs) != null
/world/TgsVersion()
return world.params[TGS_VERSION_PARAMETER]
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
if(api)
return api.version
/world/TgsInstanceName()
var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs)
+5
View File
@@ -1,6 +1,11 @@
TGS_DEFINE_AND_SET_GLOBAL(tgs, null)
/datum/tgs_api
var/datum/tgs_version/version
/datum/tgs_api/New(datum/tgs_version/version)
. = ..()
src.version = version
/datum/tgs_api/latest
parent_type = /datum/tgs_api/v4
+22
View File
@@ -0,0 +1,22 @@
/datum/tgs_version/New(raw_parameter)
src.raw_parameter = raw_parameter
deprefixed_parameter = replacetext(raw_parameter, "/tg/station 13 Server v", "")
var/list/version_bits = splittext(deprefixed_parameter, ".")
suite = text2num(version_bits[1])
if(version_bits.len > 1)
major = text2num(version_bits[2])
if(version_bits.len > 2)
minor = text2num(version_bits[3])
if(version_bits.len == 4)
patch = text2num(version_bits[4])
/datum/tgs_version/proc/Valid(allow_wildcards = FALSE)
if(suite == null)
return FALSE
if(allow_wildcards)
return TRUE
return !Wildcard()
/datum/tgs_version/Wildcard()
return major == null || minor == null || patch == null
+1
View File
@@ -1,6 +1,7 @@
#include "core\_definitions.dm"
#include "core\core.dm"
#include "core\datum.dm"
#include "core\tgs_version.dm"
#ifdef TGS_V3_API
#include "v3210\api.dm"
#include "v3210\commands.dm"
+25 -23
View File
@@ -1,12 +1,12 @@
using System;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net.Http.Headers;
using System.Reflection;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.Net.Http.Headers;
using Microsoft.Extensions.Primitives;
namespace Tgstation.Server.Api
{
@@ -28,32 +28,32 @@ namespace Tgstation.Server.Api
/// <summary>
/// The <see cref="Username"/> header key
/// </summary>
const string usernameHeader = "Username";
const string UsernameHeader = "Username";
/// <summary>
/// The <see cref="InstanceId"/> header key
/// </summary>
const string instanceIdHeader = "Instance";
const string InstanceIdHeader = "Instance";
/// <summary>
/// The JWT authentication header scheme
/// </summary>
const string jwtAuthenticationScheme = "Bearer";
const string JwtAuthenticationScheme = "Bearer";
/// <summary>
/// The password authentication header scheme
/// </summary>
const string passwordAuthenticationScheme = "Password";
const string PasswordAuthenticationScheme = "Password";
/// <summary>
/// The current <see cref="AssemblyName"/>
/// The current <see cref="System.Reflection.AssemblyName"/>
/// </summary>
static readonly AssemblyName assemblyName = Assembly.GetExecutingAssembly().GetName();
static readonly AssemblyName AssemblyName = Assembly.GetExecutingAssembly().GetName();
/// <summary>
/// Get the version of the <see cref="Api"/> the caller is using
/// </summary>
public static Version Version => assemblyName.Version;
public static Version Version => AssemblyName.Version;
/// <summary>
/// The <see cref="Models.Instance.Id"/> being accessed
@@ -139,12 +139,12 @@ namespace Tgstation.Server.Api
if (!requestHeaders.Headers.TryGetValue(HeaderNames.UserAgent, out var userAgentValues) || !ProductInfoHeaderValue.TryParse(userAgentValues.FirstOrDefault(), out var clientUserAgent))
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Missing {0} headers!", HeaderNames.UserAgent));
//assure the client user agent has a name and version
// assure the client user agent has a name and version
if (String.IsNullOrWhiteSpace(clientUserAgent.Product.Name) || !Version.TryParse(clientUserAgent.Product.Version, out var clientVersion))
throw new InvalidOperationException("Malformed client user agent!");
//make sure the api header matches ours
if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != assemblyName.Name)
// make sure the api header matches ours
if (!requestHeaders.Headers.TryGetValue(ApiVersionHeader, out var apiUserAgentHeaderValues) || !ProductInfoHeaderValue.TryParse(apiUserAgentHeaderValues.FirstOrDefault(), out var apiUserAgent) || apiUserAgent.Product.Name != AssemblyName.Name)
throw new InvalidOperationException("Missing API version!");
if (!Version.TryParse(apiUserAgent.Product.Version, out var apiVersion))
@@ -166,7 +166,7 @@ namespace Tgstation.Server.Api
if (String.IsNullOrEmpty(parameter))
throw new InvalidOperationException("Missing authentication parameter!");
if (requestHeaders.Headers.TryGetValue(instanceIdHeader, out var instanceIdValues))
if (requestHeaders.Headers.TryGetValue(InstanceIdHeader, out var instanceIdValues))
{
var instanceIdString = instanceIdValues.FirstOrDefault();
if (instanceIdString != default && Int64.TryParse(instanceIdString, out var instanceId))
@@ -175,17 +175,18 @@ namespace Tgstation.Server.Api
switch (scheme)
{
case jwtAuthenticationScheme:
case JwtAuthenticationScheme:
Token = parameter;
break;
case passwordAuthenticationScheme:
case PasswordAuthenticationScheme:
Password = parameter;
var fail = !requestHeaders.Headers.TryGetValue(usernameHeader, out var values);
var fail = !requestHeaders.Headers.TryGetValue(UsernameHeader, out var values);
if (!fail)
{
Username = values.FirstOrDefault();
fail = String.IsNullOrWhiteSpace(Username);
}
if (fail)
throw new InvalidOperationException("Missing Username header!");
break;
@@ -231,17 +232,18 @@ namespace Tgstation.Server.Api
headers.Clear();
headers.Accept.Add(new MediaTypeWithQualityHeaderValue(ApplicationJson));
if (IsTokenAuthentication)
headers.Authorization = new AuthenticationHeaderValue(jwtAuthenticationScheme, Token);
headers.Authorization = new AuthenticationHeaderValue(JwtAuthenticationScheme, Token);
else
{
headers.Authorization = new AuthenticationHeaderValue(passwordAuthenticationScheme, Password);
headers.Add(usernameHeader, Username);
headers.Authorization = new AuthenticationHeaderValue(PasswordAuthenticationScheme, Password);
headers.Add(UsernameHeader, Username);
}
headers.UserAgent.Add(new ProductInfoHeaderValue(UserAgent));
headers.Add(ApiVersionHeader, new ProductHeaderValue(assemblyName.Name, ApiVersion.ToString()).ToString());
headers.Add(ApiVersionHeader, new ProductHeaderValue(AssemblyName.Name, ApiVersion.ToString()).ToString());
instanceId = instanceId ?? InstanceId;
if (instanceId.HasValue)
headers.Add(instanceIdHeader, instanceId.ToString());
headers.Add(InstanceIdHeader, instanceId.ToString());
}
}
}
@@ -2,7 +2,9 @@
namespace Tgstation.Server.Api.Models
{
/// <inheritdoc />
/// <summary>
/// Represents administrative server information
/// </summary>
public sealed class Administration
{
/// <summary>
@@ -9,6 +9,7 @@
/// Internet relay chat
/// </summary>
Irc,
/// <summary>
/// Superior chat service
/// </summary>
@@ -28,6 +28,8 @@
/// <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
}
}
@@ -9,10 +9,12 @@
/// Configuration editing is not allowed
/// </summary>
Disallowed,
/// <summary>
/// Configuration editing is allowed by all users on all files
/// </summary>
HostWrite,
/// <summary>
/// Co
/// </summary>
@@ -9,10 +9,12 @@
/// Server is unrestricted in terms of file access and shell commands
/// </summary>
Trusted,
/// <summary>
/// Server will not be able to run shell commands or access files outside it's working directory
/// </summary>
Safe,
/// <summary>
/// Server will not be able to run shell commands or access anything but temporary files. Currently unsupported!
/// </summary>
@@ -11,10 +11,12 @@
/// Server is not running
/// </summary>
Offline,
/// <summary>
/// Server is being rebooted
/// </summary>
HardRebooting,
/// <summary>
/// Server is running
/// </summary>
+4 -1
View File
@@ -50,7 +50,10 @@ namespace Tgstation.Server.Api.Models
[NotMapped]
public Job MoveJob { get; set; }
/// <inheritdoc />
/// <summary>
/// Create a clone of the essential <see cref="Instance"/> metadata
/// </summary>
/// <returns>A clone of the essential <see cref="Instance"/> metadata</returns>
public Instance CloneMetadata() => new Instance
{
Id = Id,
@@ -13,7 +13,7 @@
/// <summary>
/// Gets the <see cref="ChatBot.ConnectionString"/> associated with the <see cref="ChatConnectionStringBuilder"/>
/// </summary>
/// <returns></returns>
/// <returns>The <see cref="ChatBot.ConnectionString"/> associated with the <see cref="ChatConnectionStringBuilder"/></returns>
public abstract override string ToString();
}
}
@@ -10,13 +10,15 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// The revision sha
/// </summary>
[Required, StringLength(40)]
[Required]
[StringLength(40)]
public string CommitSha { get; set; }
/// <summary>
/// The sha of the most recent remote commit
/// </summary>
[Required, StringLength(40)]
[Required]
[StringLength(40)]
public string OriginCommitSha { get; set; }
}
}
@@ -42,7 +42,7 @@ namespace Tgstation.Server.Api.Models.Internal
/// <summary>
/// Construct a <see cref="TestMergeBase"/> from a <paramref name="copy"/>
/// </summary>
/// <param name="copy"></param>
/// <param name="copy">The <see cref="TestMergeBase"/> to copy data from</param>
protected TestMergeBase(TestMergeBase copy)
{
if (copy == null)
@@ -87,6 +87,8 @@ namespace Tgstation.Server.Api.Models
case IrcPasswordType.Server:
PasswordType = passwordType;
break;
default:
break;
}
if (splits.Length < 6)
@@ -116,6 +118,7 @@ namespace Tgstation.Server.Api.Models
sb.Append(';');
sb.Append(Password);
}
return sb.ToString();
}
}
@@ -9,10 +9,12 @@
/// Use server authentication
/// </summary>
Server,
/// <summary>
/// Use PLAIN sasl authentication
/// </summary>
Sasl,
/// <summary>
/// Use NickServ authentication
/// </summary>
@@ -12,22 +12,27 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User can edit themself and other <see cref="Models.User"/>s and also create others
/// </summary>
WriteUsers = 1,
/// <summary>
/// User can gracefully restart the host
/// </summary>
RestartHost = 2,
/// <summary>
/// User can change <see cref="Models.Administration.NewVersion"/>
/// </summary>
ChangeVersion = 4,
/// <summary>
/// User can change their password
/// </summary>
EditOwnPassword = 8,
/// <summary>
/// User can read info and rights of other users
/// </summary>
@@ -12,18 +12,22 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User may check the active installed BYOND version
/// </summary>
ReadActive = 1,
/// <summary>
/// User may list all installed BYOND versions
/// </summary>
ListInstalled = 2,
/// <summary>
/// User may change the active BYOND version
/// </summary>
ChangeVersion = 4,
/// <summary>
/// User may cancel version installations
/// </summary>
@@ -12,38 +12,47 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User can change <see cref="Models.Internal.ChatBot.Enabled"/>
/// </summary>
WriteEnabled = 1,
/// <summary>
/// User can change <see cref="Models.Internal.ChatBot.Provider"/>
/// </summary>
WriteProvider = 2,
/// <summary>
/// User can change <see cref="Models.ChatBot.Channels"/>
/// </summary>
WriteChannels = 4,
/// <summary>
/// User can change <see cref="Models.Internal.ChatBot.ConnectionString"/>
/// </summary>
WriteConnectionString = 8,
/// <summary>
/// User can read <see cref="Models.Internal.ChatBot.ConnectionString"/> requires <see cref="Read"/>
/// </summary>
ReadConnectionString = 16,
/// <summary>
/// User can read all chat settings except <see cref="Models.Internal.ChatBot.ConnectionString"/>
/// </summary>
Read = 32,
/// <summary>
/// User can create new <see cref="Models.ChatBot"/>
/// </summary>
Create = 64,
/// <summary>
/// User can delete <see cref="Models.ChatBot"/>
/// </summary>
Delete = 128,
/// <summary>
/// User can change <see cref="Models.Internal.ChatBot.Name"/>
/// </summary>
@@ -12,18 +12,22 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User may read files
/// </summary>
Read = 1,
/// <summary>
/// User may write files
/// </summary>
Write = 2,
/// <summary>
/// User may list files
/// </summary>
List = 4,
/// <summary>
/// User may delete empty folders
/// </summary>
@@ -12,50 +12,62 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User can read <see cref="Models.DreamDaemon.ActiveCompileJob"/> and <see cref="Models.DreamDaemon.StagedCompileJob"/>
/// </summary>
ReadRevision = 1,
/// <summary>
/// User can change both primary and secondary ports
/// </summary>
SetPorts = 2,
/// <summary>
/// User can change <see cref="Models.Internal.DreamDaemonSettings.AutoStart"/>
/// </summary>
SetAutoStart = 4,
/// <summary>
/// User set <see cref="Models.Internal.DreamDaemonLaunchParameters.SecurityLevel"/>
/// </summary>
SetSecurity = 8,
/// <summary>
/// User can read all ports, <see cref="Models.Internal.DreamDaemonSettings.SoftRestart"/>, <see cref="Models.Internal.DreamDaemonSettings.SoftShutdown"/>, <see cref="Models.DreamDaemon.Running"/>, <see cref="Models.Internal.DreamDaemonLaunchParameters.AllowWebClient"/>, and <see cref="Models.Internal.DreamDaemonSettings.AutoStart"/>
/// </summary>
ReadMetadata = 16,
/// <summary>
/// User can change <see cref="Models.Internal.DreamDaemonLaunchParameters.AllowWebClient"/>
/// </summary>
SetWebClient = 32,
/// <summary>
/// User can enable <see cref="Models.Internal.DreamDaemonSettings.SoftRestart"/>
/// </summary>
SoftRestart = 64,
/// <summary>
/// User can enable <see cref="Models.Internal.DreamDaemonSettings.SoftShutdown"/>
/// </summary>
SoftShutdown = 128,
/// <summary>
/// User can immediately restart <see cref="Models.DreamDaemon"/>
/// </summary>
Restart = 256,
/// <summary>
/// User can immediately shutdown <see cref="Models.DreamDaemon"/>
/// </summary>
Shutdown = 512,
/// <summary>
/// User can start <see cref="Models.DreamDaemon"/> and disable <see cref="Models.Internal.DreamDaemonSettings.SoftRestart"/> and <see cref="Models.Internal.DreamDaemonSettings.SoftShutdown"/>
/// </summary>
Start = 1024,
/// <summary>
/// User can change <see cref="Models.Internal.DreamDaemonLaunchParameters.StartupTimeout"/>
/// </summary>
@@ -12,30 +12,37 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User may read <see cref="Models.DreamMaker"/> status
/// </summary>
Read = 1,
/// <summary>
/// User may trigger compiles
/// </summary>
Compile = 2,
/// <summary>
/// User may cancel compiles
/// </summary>
CancelCompile = 4,
/// <summary>
/// User may modify <see cref="Models.DreamMaker.ProjectName"/>
/// </summary>
SetDme = 8,
/// <summary>
/// User may modify <see cref="Models.DreamMaker.ApiValidationPort"/>
/// </summary>
SetApiValidationPort = 16,
/// <summary>
/// User may list and read all <see cref="Models.CompileJob"/>s
/// </summary>
CompileJobs = 32,
/// <summary>
/// User may modify <see cref="Models.DreamMaker.ApiValidationSecurityLevel"/>
/// </summary>
@@ -12,38 +12,47 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User can view <see cref="Models.Instance"/>s which they have any rights for
/// </summary>
Read = 1,
/// <summary>
/// User can create <see cref="Models.Instance"/>s
/// </summary>
Create = 2,
/// <summary>
/// User can rename <see cref="Models.Instance"/>s they can view
/// </summary>
Rename = 4,
/// <summary>
/// User can relocate <see cref="Models.Instance"/>s they can view
/// </summary>
Relocate = 8,
/// <summary>
/// User can online <see cref="Models.Instance"/>s they can view
/// </summary>
SetOnline = 16,
/// <summary>
/// User can delete <see cref="Models.Instance"/>s they can view
/// </summary>
Delete = 32,
/// <summary>
/// User can view all <see cref="Models.Instance"/>s
/// </summary>
List = 64,
/// <summary>
/// User can change <see cref="Models.Instance.ConfigurationType"/>
/// </summary>
SetConfiguration = 128,
/// <summary>
/// User can change <see cref="Models.Instance.AutoUpdateInterval"/>
/// </summary>
@@ -12,14 +12,17 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// Allow read access to <see cref="Models.InstanceUser"/> for the <see cref="Models.Instance"/>
/// </summary>
ReadUsers = 1,
/// <summary>
/// Allow write and delete access to <see cref="Models.InstanceUser"/> for the <see cref="Models.Instance"/>
/// </summary>
WriteUsers = 2,
/// <summary>
/// Allow adding additional <see cref="Models.InstanceUser"/> to the <see cref="Models.Instance"/>
/// </summary>
@@ -12,54 +12,67 @@ namespace Tgstation.Server.Api.Rights
/// User has no rights
/// </summary>
None = 0,
/// <summary>
/// User may cancel synchronize operations
/// </summary>
CancelPendingChanges = 1,
/// <summary>
/// User may create the <see cref="Models.Repository"/> if it does not exist
/// </summary>
SetOrigin = 2,
/// <summary>
/// User may directly set the sha the <see cref="Models.Repository"/>'s HEAD points to
/// </summary>
SetSha = 4,
/// <summary>
/// User may fetch and merge GitHub pull requests
/// </summary>
MergePullRequest = 8,
/// <summary>
/// User may use the update feature
/// </summary>
UpdateBranch = 16,
/// <summary>
/// User may change <see cref="Models.Internal.RepositorySettings.CommitterName"/> and <see cref="Models.Internal.RepositorySettings.CommitterEmail"/>
/// </summary>
ChangeCommitter = 32,
/// <summary>
/// User may change <see cref="Models.Internal.RepositorySettings.PushTestMergeCommits"/> and <see cref="Models.Internal.RepositorySettings.PostTestMergeComment"/>
/// </summary>
ChangeTestMergeCommits = 64,
/// <summary>
/// User may read and change <see cref="Models.Internal.RepositorySettings.AccessUser"/> and <see cref="Models.Internal.RepositorySettings.AccessToken"/>
/// </summary>
ChangeCredentials = 128,
/// <summary>
/// User may set <see cref="Models.Repository.Reference"/> to another git reference (not a SHA)
/// </summary>
SetReference = 256,
/// <summary>
/// User may read all fields in the <see cref="Models.Repository"/> with the exception of <see cref="Models.Internal.RepositorySettings.AccessToken"/>
/// </summary>
Read = 512,
/// <summary>
/// User may change <see cref="Models.Internal.RepositorySettings.AutoUpdatesKeepTestMerges"/> and <see cref="Models.Internal.RepositorySettings.AutoUpdatesSynchronize"/>
/// </summary>
ChangeAutoUpdateSettings = 1024,
/// <summary>
/// User may delete the <see cref="Models.Repository"/>
/// </summary>
Delete = 2048,
/// <summary>
/// User may cancel clone operations
/// </summary>
@@ -12,11 +12,10 @@ namespace Tgstation.Server.Api.Rights
/// <summary>
/// Map of <see cref="RightsType"/>s to their respective flag <see cref="Enum"/>s
/// </summary>
static readonly IReadOnlyDictionary<RightsType, Type> typeMap = new Dictionary<RightsType, Type>
static readonly IReadOnlyDictionary<RightsType, Type> TypeMap = new Dictionary<RightsType, Type>
{
{ RightsType.Administration, typeof(AdministrationRights) },
{ RightsType.InstanceManager, typeof(InstanceManagerRights) },
{ RightsType.Repository, typeof(RepositoryRights) },
{ RightsType.Byond, typeof(ByondRights) },
{ RightsType.DreamMaker, typeof(DreamMakerRights) },
@@ -31,7 +30,7 @@ namespace Tgstation.Server.Api.Rights
/// </summary>
/// <param name="rightsType">The <see cref="RightsType"/> to lookup</param>
/// <returns>The <see cref="Enum"/> <see cref="Type"/> of the given <paramref name="rightsType"/></returns>
public static Type RightToType(RightsType rightsType) => typeMap[rightsType];
public static Type RightToType(RightsType rightsType) => TypeMap[rightsType];
/// <summary>
/// Gets the role claim name used for a given <paramref name="right"/>
@@ -47,7 +46,8 @@ namespace Tgstation.Server.Api.Rights
foreach (Enum J in Enum.GetValues(right.GetType()))
if (Convert.ToInt32(J, CultureInfo.InvariantCulture) != 0 && right.HasFlag(J))
yield return String.Concat(typeof(TRight).Name, '.', J.ToString());
};
}
var names = GetRoleNames();
return String.Join(",", names);
}
@@ -60,7 +60,7 @@ namespace Tgstation.Server.Api.Rights
/// <returns>A <see cref="string"/> representing the claim role name</returns>
public static string RoleName(RightsType rightsType, Enum right)
{
var enumType = typeMap[rightsType];
var enumType = TypeMap[rightsType];
return String.Concat(enumType.Name, '.', Enum.GetName(enumType, right));
}
@@ -9,34 +9,42 @@
/// <see cref="AdministrationRights"/>
/// </summary>
Administration,
/// <summary>
/// <see cref="InstanceManagerRights"/>
/// </summary>
InstanceManager,
/// <summary>
/// <see cref="RepositoryRights"/>
/// </summary>
Repository,
/// <summary>
/// <see cref="ByondRights"/>
/// </summary>
Byond,
/// <summary>
/// <see cref="DreamMakerRights"/>
/// </summary>
DreamMaker,
/// <summary>
/// <see cref="DreamDaemonRights"/>
/// </summary>
DreamDaemon,
/// <summary>
/// <see cref="ChatBotRights"/>
/// </summary>
ChatBots,
/// <summary>
/// <see cref="ConfigurationRights"/>
/// </summary>
Configuration,
/// <summary>
/// <see cref="InstanceUserRights"/>
/// </summary>
@@ -16,19 +16,20 @@
<PackageTags>json web api tgstation-server tgstation ss13 byond</PackageTags>
<PackageReleaseNotes>Initial release</PackageReleaseNotes>
<Version>4.0.1.0</Version>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netstandard2.0\Tgstation.Server.Api.xml</DocumentationFile>
<NoWarn>1701;1702;1705;CA2227;CA1819;CA1028</NoWarn>
<NoWarn>1701;1702;CA1028</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
<NoWarn>1701;1702;1705;CA2227;CA1819;CA1028</NoWarn>
<NoWarn>1701;1702;CA1028</NoWarn>
<DocumentationFile>bin\Debug\netstandard2.0\Tgstation.Server.Api.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -37,7 +38,14 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>compile; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="System.ComponentModel.Annotations" Version="4.5.0" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="../../build/stylecop.json" />
</ItemGroup>
</Project>
+99 -85
View File
@@ -49,6 +49,79 @@ namespace Tgstation.Server.Client
/// </summary>
ApiHeaders headers;
static JsonSerializerSettings GetSerializerSettings() => new JsonSerializerSettings
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new[] { new VersionConverter() }
};
static void HandleBadResponse(HttpResponseMessage response, string json)
{
ErrorMessage errorMessage = null;
try
{
// check if json serializes to an error message
errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json, GetSerializerSettings());
}
catch (JsonException) { }
const string BadSpecExtension = " This is not part of TGS4 communication specification and should be reported if it was returned from a TGS4 server!";
#pragma warning disable IDE0010 // Add missing cases
switch (response.StatusCode)
#pragma warning restore IDE0010 // Add missing cases
{
case HttpStatusCode.UpgradeRequired:
throw new ApiMismatchException(errorMessage ?? new ErrorMessage
{
Message = "API Mismatch but no current API version provided!" + BadSpecExtension,
SeverApiVersion = null
});
case HttpStatusCode.Unauthorized:
throw new UnauthorizedException();
case HttpStatusCode.RequestTimeout:
throw new RequestTimeoutException();
case HttpStatusCode.Forbidden:
throw new InsufficientPermissionsException();
case HttpStatusCode.ServiceUnavailable:
throw new ServiceUnavailableException();
case HttpStatusCode.Gone:
errorMessage = errorMessage ?? new ErrorMessage
{
Message = "The requested resource could not be found!",
SeverApiVersion = null
};
goto case HttpStatusCode.Conflict;
case HttpStatusCode.NotFound:
// our fault somehow
errorMessage = errorMessage ?? new ErrorMessage
{
Message = "This is not a valid route!" + BadSpecExtension,
SeverApiVersion = null
};
goto case HttpStatusCode.Conflict;
case HttpStatusCode.Conflict:
throw new ConflictException(errorMessage ?? new ErrorMessage
{
Message = "An undescribed conflict occurred!" + BadSpecExtension,
SeverApiVersion = null
}, response.StatusCode);
case HttpStatusCode.NotImplemented:
// unprocessable entity
case (HttpStatusCode)422:
throw new MethodNotSupportedException();
case HttpStatusCode.InternalServerError:
// response json is html
throw new ServerErrorException(json);
case (HttpStatusCode)429:
// rate limited
response.Headers.TryGetValues("Retry-After", out var values);
throw new RateLimitException(values?.FirstOrDefault());
default:
throw new ApiConflictException(errorMessage, response.StatusCode);
}
}
/// <summary>
/// Construct an <see cref="ApiClient"/>
/// </summary>
@@ -60,7 +133,7 @@ namespace Tgstation.Server.Client
this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
Url = url ?? throw new ArgumentNullException(nameof(url));
headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders));
requestLoggers = new List<IRequestLogger>();
}
@@ -70,10 +143,11 @@ namespace Tgstation.Server.Client
/// <summary>
/// Main request method
/// </summary>
/// <typeparam name="TResult">The resulting POCO type</typeparam>
/// <param name="route">The route to run</param>
/// <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="Api.Models.Instance.Id"/> for the request</param>
/// <param name="instanceId">The optional <see cref="Instance.Id"/> for the request</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)
@@ -85,102 +159,42 @@ namespace Tgstation.Server.Client
if (body == null && (method == HttpMethod.Post || method == HttpMethod.Put))
throw new InvalidOperationException("Body cannot be null for POST or PUT!");
HttpResponseMessage response;
var fullUri = new Uri(Url, route);
var message = new HttpRequestMessage(method, fullUri);
var serializerSettings = new JsonSerializerSettings
var serializerSettings = GetSerializerSettings();
using (var request = new HttpRequestMessage(method, fullUri))
{
ContractResolver = new CamelCasePropertyNamesContractResolver(),
Converters = new[] { new VersionConverter() }
};
if (body != null)
request.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
if (body != null)
message.Content = new StringContent(JsonConvert.SerializeObject(body, serializerSettings), Encoding.UTF8, ApiHeaders.ApplicationJson);
headers.SetRequestHeaders(request.Headers, instanceId);
headers.SetRequestHeaders(message.Headers, instanceId);
await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(request, cancellationToken))).ConfigureAwait(false);
await Task.WhenAll(requestLoggers.Select(x => x.LogRequest(message, cancellationToken))).ConfigureAwait(false);
response = await httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
var response = await httpClient.SendAsync(message, cancellationToken).ConfigureAwait(false);
await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false);
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
using (response)
{
ErrorMessage errorMessage = null;
await Task.WhenAll(requestLoggers.Select(x => x.LogResponse(response, cancellationToken))).ConfigureAwait(false);
var json = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
if (!response.IsSuccessStatusCode)
HandleBadResponse(response, json);
if (String.IsNullOrWhiteSpace(json))
json = JsonConvert.SerializeObject(new object());
try
{
//check if json serializes to an error message
errorMessage = JsonConvert.DeserializeObject<ErrorMessage>(json, serializerSettings);
return JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
}
catch (JsonException) { }
const string BadSpecExtension = " This is not part of TGS4 communication specification and should be reported if it was returned from a TGS4 server!";
switch (response.StatusCode)
catch (JsonException)
{
case HttpStatusCode.UpgradeRequired:
throw new ApiMismatchException(errorMessage ?? new ErrorMessage
{
Message = "API Mismatch but no current API version provided!" + BadSpecExtension,
SeverApiVersion = null
});
case HttpStatusCode.Unauthorized:
throw new UnauthorizedException();
case HttpStatusCode.RequestTimeout:
throw new RequestTimeoutException();
case HttpStatusCode.Forbidden:
throw new InsufficientPermissionsException();
case HttpStatusCode.ServiceUnavailable:
throw new ServiceUnavailableException();
case HttpStatusCode.Gone:
errorMessage = errorMessage ?? new ErrorMessage
{
Message = "The requested resource could not be found!",
SeverApiVersion = null
};
goto case HttpStatusCode.Conflict;
case HttpStatusCode.NotFound: //our fault somehow
errorMessage = errorMessage ?? new ErrorMessage
{
Message = "This is not a valid route!" + BadSpecExtension,
SeverApiVersion = null
};
goto case HttpStatusCode.Conflict;
case HttpStatusCode.Conflict:
throw new ConflictException(errorMessage ?? new ErrorMessage
{
Message = "An undescribed conflict occurred!" + BadSpecExtension,
SeverApiVersion = null
}, response.StatusCode);
case HttpStatusCode.NotImplemented:
case (HttpStatusCode)422: //unprocessable entity
throw new MethodNotSupportedException();
case HttpStatusCode.InternalServerError:
//response
throw new ServerErrorException(json); //json is html
case (HttpStatusCode)429: //rate limited
response.Headers.TryGetValues("Retry-After", out var values);
throw new RateLimitException(values?.FirstOrDefault());
default:
throw new ApiConflictException(errorMessage, response.StatusCode);
throw new UnrecognizedResponseException(json, response.StatusCode);
}
}
if (String.IsNullOrWhiteSpace(json))
json = JsonConvert.SerializeObject(new object());
try
{
return JsonConvert.DeserializeObject<TResult>(json, serializerSettings);
}
catch (JsonException)
{
throw new UnrecognizedResponseException(json, response.StatusCode);
}
}
/// <inheritdoc />
@@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components
/// The <see cref="IApiClient"/> for the <see cref="ByondClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ByondClient"/>
/// </summary>
@@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components
/// The <see cref="IApiClient"/> for the <see cref="ChatBotsClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ChatBotsClient"/>
/// </summary>
@@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components
/// The <see cref="IApiClient"/> for the <see cref="ConfigurationClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="ConfigurationClient"/>
/// </summary>
@@ -13,6 +13,7 @@ namespace Tgstation.Server.Client.Components
/// The <see cref="IApiClient"/> for the <see cref="DreamDaemonClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="DreamDaemonClient"/>
/// </summary>
@@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components
/// The <see cref="IApiClient"/> for the <see cref="InstanceUserClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="InstanceUserClient"/>
/// </summary>
@@ -33,6 +34,7 @@ namespace Tgstation.Server.Client.Components
/// <inheritdoc />
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);
/// <inheritdoc />
@@ -14,6 +14,7 @@ namespace Tgstation.Server.Client.Components
/// The <see cref="IApiClient"/> for the <see cref="JobsClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="JobsClient"/>
/// </summary>
@@ -13,6 +13,7 @@ namespace Tgstation.Server.Client.Components
/// The <see cref="IApiClient"/> for the <see cref="RepositoryClient"/>
/// </summary>
readonly IApiClient apiClient;
/// <summary>
/// The <see cref="Instance"/> for the <see cref="RepositoryClient"/>
/// </summary>
@@ -22,7 +23,7 @@ namespace Tgstation.Server.Client.Components
/// Construct a <see cref="RepositoryClient"/>
/// </summary>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
/// <param name="instance"></param>
/// <param name="instance">The value of <see cref="instance"/></param>
public RepositoryClient(IApiClient apiClient, Instance instance)
{
this.apiClient = apiClient;
+143
View File
@@ -10,10 +10,19 @@ namespace Tgstation.Server.Client
/// </summary>
interface IApiClient : IDisposable
{
/// <summary>
/// The <see cref="ApiHeaders"/> the <see cref="IApiClient"/> uses
/// </summary>
ApiHeaders Headers { get; set; }
/// <summary>
/// The <see cref="Uri"/> pointing the tgstation-server
/// </summary>
Uri Url { get; }
/// <summary>
/// The request timeout
/// </summary>
TimeSpan Timeout { get; set; }
/// <summary>
@@ -22,21 +31,155 @@ namespace Tgstation.Server.Client
/// <param name="requestLogger">The <see cref="IRequestLogger"/> to add</param>
void AddRequestLogger(IRequestLogger requestLogger);
/// <summary>
/// Run an HTTP PUT request
/// </summary>
/// <typeparam name="TBody">The type to of the request body</typeparam>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="body">The request body</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Create<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP PUT request
/// </summary>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Create<TResult>(string route, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP GET request
/// </summary>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Read<TResult>(string route, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP POST request
/// </summary>
/// <typeparam name="TBody">The type to of the request body</typeparam>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="body">The request body</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Update<TBody, TResult>(string route, TBody body, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP POST request
/// </summary>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Update<TResult>(string route, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP POST request
/// </summary>
/// <typeparam name="TBody">The type to of the request body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="body">The request body</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Update<TBody>(string route, TBody body, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP DELETE request
/// </summary>
/// <param name="route">The server route to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete(string route, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP PUT request
/// </summary>
/// <typeparam name="TBody">The type to of the request body</typeparam>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="body">The request body</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Create<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP PUT request
/// </summary>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Create<TResult>(string route, long instanceId, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP PATCH request
/// </summary>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Patch<TResult>(string route, long instanceId, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP GET request
/// </summary>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Read<TResult>(string route, long instanceId, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP POST request
/// </summary>
/// <typeparam name="TBody">The type to of the request body</typeparam>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="body">The request body</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Update<TBody, TResult>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP DELETE request
/// </summary>
/// <param name="route">The server route to make the request to</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete(string route, long instanceId, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP DELETE request
/// </summary>
/// <typeparam name="TBody">The type to of the request body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="body">The request body</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task Delete<TBody>(string route, TBody body, long instanceId, CancellationToken cancellationToken);
/// <summary>
/// Run an HTTP DELETE request
/// </summary>
/// <typeparam name="TResult">The type of the response body</typeparam>
/// <param name="route">The server route to make the request to</param>
/// <param name="instanceId">The <see cref="Api.Models.Instance.Id"/> to make the request to</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the response body as a <typeparamref name="TResult"/></returns>
Task<TResult> Delete<TResult>(string route, long instanceId, CancellationToken cancellationToken);
}
}
@@ -43,6 +43,8 @@ namespace Tgstation.Server.Client
/// <summary>
/// The <see cref="ServerInformation"/> of the <see cref="IServerClient"/>
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the <see cref="ServerInformation"/> of the target server</returns>
Task<ServerInformation> Version(CancellationToken cancellationToken);
/// <summary>
@@ -16,8 +16,8 @@ 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="cancellationToken">The <see cref="CancellationToken"/> for the operation</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>
/// <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);
@@ -24,7 +24,7 @@ namespace Tgstation.Server.Client
/// <summary>
/// Construct an <see cref="InstanceManagerClient"/>
/// </summary>
/// <param name="apiClient"></param>
/// <param name="apiClient">The value of <see cref="apiClient"/></param>
public InstanceManagerClient(IApiClient apiClient)
{
this.apiClient = apiClient ?? throw new ArgumentNullException(nameof(apiClient));
@@ -55,6 +55,7 @@ namespace Tgstation.Server.Client
client = new InstanceClient(apiClient, instance);
cachedClients.Add(instance.Id, client);
}
return client;
}
}
@@ -13,7 +13,7 @@ namespace Tgstation.Server.Client
/// <summary>
/// The <see cref="IApiClientFactory"/> for the <see cref="ServerClientFactory"/>
/// </summary>
static readonly IApiClientFactory apiClientFactory = new ApiClientFactory();
static readonly IApiClientFactory ApiClientFactory = new ApiClientFactory();
/// <summary>
/// The <see cref="ProductHeaderValue"/> for the <see cref="ServerClientFactory"/>
@@ -40,12 +40,13 @@ namespace Tgstation.Server.Client
throw new ArgumentNullException(nameof(password));
Token token;
using (var api = apiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password)))
using (var api = ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, username, password)))
{
if (timeout != default)
api.Timeout = timeout;
token = await api.Update<Token>(Routes.Root, cancellationToken).ConfigureAwait(false);
}
return CreateServerClient(host, token, timeout);
}
@@ -56,7 +57,7 @@ namespace Tgstation.Server.Client
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);
var result = new ServerClient(ApiClientFactory.CreateApiClient(host, new ApiHeaders(productHeaderValue, token.Bearer)), token);
if (timeout != default)
result.Timeout = timeout;
return result;
@@ -18,18 +18,20 @@
<PackageTags>json web api tgstation-server tgstation ss13 byond client</PackageTags>
<PackageReleaseNotes>Added the ability to change the Token of IServerClient</PackageReleaseNotes>
<Copyright>2018</Copyright>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netstandard2.0\Tgstation.Server.Client.xml</DocumentationFile>
<WarningLevel>0</WarningLevel>
<NoWarn>1701;1702</NoWarn>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
<NoWarn>1701;1702</NoWarn>
<DocumentationFile>bin\Debug\netstandard2.0\Tgstation.Server.Client.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -38,10 +40,17 @@
<IncludeAssets>compile; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Newtonsoft.Json" Version="11.0.2" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Tgstation.Server.Api\Tgstation.Server.Api.csproj" />
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="../../build/stylecop.json" />
</ItemGroup>
</Project>
@@ -37,6 +37,7 @@ namespace Tgstation.Server.Host.Console
loggerFactory.CreateLogger(nameof(Program)).LogCritical("Please specify only 1 of --trace-host-watchdog or --debug-host-watchdog!");
return;
}
using (var cts = new CancellationTokenSource())
{
void AppDomainHandler(object a, EventArgs b) => cts.Cancel();
@@ -4,18 +4,20 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<DebugType>Full</DebugType>
<Version>4.0.1.3</Version>
<Version>4.0.1.4</Version>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netcoreapp2.1\Tgstation.Server.Host.Console.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
<NoWarn>1701;1702;SA1652</NoWarn>
<DocumentationFile>bin\Debug\netcoreapp2.1\Tgstation.Server.Host.Console.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
@@ -24,6 +26,10 @@
<IncludeAssets>compile; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.1.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -39,4 +45,7 @@
</None>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="../../build/stylecop.json" />
</ItemGroup>
</Project>
+19 -14
View File
@@ -20,6 +20,13 @@ namespace Tgstation.Server.Host.Service
/// </summary>
sealed class Program
{
#pragma warning disable SA1401 // Fields should be private
/// <summary>
/// The <see cref="IWatchdogFactory"/> for the <see cref="Program"/>
/// </summary>
internal static IWatchdogFactory WatchdogFactory = new WatchdogFactory();
#pragma warning restore SA1401 // Fields should be private
/// <summary>
/// The --uninstall or -u option
/// </summary>
@@ -50,8 +57,6 @@ namespace Tgstation.Server.Host.Service
[Option(ShortName = "d")]
public bool Debug { get; set; }
static readonly IWatchdogFactory watchdogFactory = new WatchdogFactory();
/// <summary>
/// Check if the running user is a system administrator
/// </summary>
@@ -63,9 +68,15 @@ namespace Tgstation.Server.Host.Service
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
/// <summary>
/// Entrypoint for the application
/// </summary>
static Task<int> Main(string[] args) => CommandLineApplication.ExecuteAsync<Program>(args);
/// <summary>
/// Command line handler, always runs
/// </summary>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
public async Task OnExecuteAsync()
{
if (Environment.UserInteractive)
@@ -81,8 +92,8 @@ namespace Tgstation.Server.Host.Service
if (!IsAdministrator())
{
//try to restart as admin
//its windows, first arg is .exe name guaranteed
// try to restart as admin
// its windows, first arg is .exe name guaranteed
var exe = Environment.GetCommandLineArgs().First();
var startInfo = new ProcessStartInfo
{
@@ -100,13 +111,12 @@ namespace Tgstation.Server.Host.Service
using (var loggerFactory = new LoggerFactory())
{
if (Configure)
await watchdogFactory.CreateWatchdog(loggerFactory).RunAsync(true, Array.Empty<string>(), default).ConfigureAwait(false);
await WatchdogFactory.CreateWatchdog(loggerFactory).RunAsync(true, Array.Empty<string>(), default).ConfigureAwait(false);
if (Install)
{
if (Uninstall)
//oh no, it's retarded...
return;
return; // oh no, it's retarded...
using (var processInstaller = new ServiceProcessInstaller())
using (var installer = new ServiceInstaller())
{
@@ -132,14 +142,9 @@ namespace Tgstation.Server.Host.Service
installer.ServiceName = ServerService.Name;
installer.Uninstall(null);
}
else if(!Configure)
ServiceBase.Run(new ServerService(watchdogFactory, loggerFactory, Trace ? LogLevel.Trace : Debug ? LogLevel.Debug : LogLevel.Information));
else if (!Configure)
ServiceBase.Run(new ServerService(WatchdogFactory, loggerFactory, Trace ? LogLevel.Trace : Debug ? LogLevel.Debug : LogLevel.Information));
}
}
/// <summary>
/// Entrypoint for the application
/// </summary>
static Task<int> Main(string[] args) => CommandLineApplication.ExecuteAsync<Program>(args);
}
}
@@ -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.3")]
[assembly: AssemblyFileVersion("4.0.1.3")]
[assembly: AssemblyVersion("4.0.1.4")]
[assembly: AssemblyFileVersion("4.0.1.4")]
@@ -74,7 +74,7 @@ namespace Tgstation.Server.Host.Service
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task RunWatchdog(string[] args, CancellationToken cancellationToken)
{
await watchdog.RunAsync(false, args, cancellationTokenSource.Token).ConfigureAwait(false);
await watchdog.RunAsync(false, args, cancellationToken).ConfigureAwait(false);
void StopServiceAsync()
{
@@ -88,11 +88,12 @@ namespace Tgstation.Server.Host.Service
EventLog.WriteEntry(String.Format(CultureInfo.InvariantCulture, "Error stopping service! Exception: {0}", e));
}
}
StopServiceAsync();
}
/// <inheritdoc />
[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "cancellationTokenSource")]
[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "cancellationTokenSource", Justification = "IT'S DISPOSED RIGHT THERE YOU FUCCBOI!")]
protected override void Dispose(bool disposing)
{
cancellationTokenSource?.Dispose();
@@ -26,33 +26,33 @@
<IsWebBootstrapper>false</IsWebBootstrapper>
<UseApplicationTrust>false</UseApplicationTrust>
<BootstrapperEnabled>true</BootstrapperEnabled>
<LangVersion>latest</LangVersion>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<RunCodeAnalysis>true</RunCodeAnalysis>
<PlatformTarget>AnyCPU</PlatformTarget>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>latest</LangVersion>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<DocumentationFile>bin\Debug\Tgstation.Server.Host.Service.xml</DocumentationFile>
<CodeAnalysisRuleSet />
<NoWarn>SA1101;SA1121;SA1200;SA1202;SA1208;SA1400;SA1413;SA1501;SA1503;SA1519;SA1520;SA1623;SA1629;SA1633;SA1642</NoWarn>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<DocumentationFile>bin\Release\Tgstation.Server.Host.Service.xml</DocumentationFile>
<RunCodeAnalysis>true</RunCodeAnalysis>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
<CodeAnalysisRuleSet />
<NoWarn>SA1101;SA1121;SA1200;SA1202;SA1208;SA1400;SA1413;SA1501;SA1503;SA1519;SA1520;SA1623;SA1629;SA1633;SA1642</NoWarn>
</PropertyGroup>
<PropertyGroup>
<StartupObject>Tgstation.Server.Host.Service.Program</StartupObject>
@@ -104,6 +104,15 @@
<PackageReference Include="Microsoft.Extensions.Logging.EventLog">
<Version>2.1.1</Version>
</PackageReference>
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="..\..\build\stylecop.json">
<Link>stylecop.json</Link>
</AdditionalFiles>
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
</Project>
@@ -4,18 +4,20 @@
<TargetFramework>netstandard2.0</TargetFramework>
<DebugType>Full</DebugType>
<AddSyntheticProjectReferencesForSolutionDependencies>false</AddSyntheticProjectReferencesForSolutionDependencies>
<Version>4.0.1.3</Version>
<Version>4.0.1.4</Version>
<CodeAnalysisRuleSet>../../build/analyzers.ruleset</CodeAnalysisRuleSet>
<LangVersion>latest</LangVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<LangVersion>latest</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<WarningsAsErrors />
<DocumentationFile>bin\Release\netstandard2.0\Tgstation.Server.Host.Watchdog.xml</DocumentationFile>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<LangVersion>latest</LangVersion>
<DocumentationFile>bin\Debug\netstandard2.0\Tgstation.Server.Host.Watchdog.xml</DocumentationFile>
<NoWarn>1701;1702;SA1652</NoWarn>
</PropertyGroup>
<ItemGroup>
@@ -24,6 +26,10 @@
<IncludeAssets>compile; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.1.1" />
<PackageReference Include="StyleCop.Analyzers" Version="1.1.1-beta.61">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
@@ -31,4 +37,8 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
<ItemGroup>
<AdditionalFiles Include="../../build/stylecop.json" />
</ItemGroup>
</Project>
+21 -13
View File
@@ -15,7 +15,6 @@ namespace Tgstation.Server.Host.Watchdog
/// <inheritdoc />
sealed class Watchdog : IWatchdog
{
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="Watchdog"/>
/// </summary>
@@ -31,6 +30,7 @@ namespace Tgstation.Server.Host.Watchdog
}
/// <inheritdoc />
#pragma warning disable CA1502 // TODO: Decomplexify
public async Task RunAsync(bool runConfigure, string[] args, CancellationToken cancellationToken)
{
logger.LogInformation("Host watchdog starting...");
@@ -67,17 +67,18 @@ namespace Tgstation.Server.Host.Watchdog
logger.LogCritical("Unable to locate dotnet executable in PATH! Please ensure the .NET Core runtime is installed and is in your PATH!");
return;
}
logger.LogInformation("Detected dotnet executable at {0}", dotnetPath);
var rootLocation = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
var assemblyStoragePath = Path.Combine(rootLocation, "lib"); //always always next to watchdog
var assemblyStoragePath = Path.Combine(rootLocation, "lib"); // always always next to watchdog
#if DEBUG
Directory.CreateDirectory(assemblyStoragePath);
#endif
var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, "Default"));
#if DEBUG
//just copy the shit where it belongs
// just copy the shit where it belongs
Directory.Delete(assemblyStoragePath, true);
Directory.CreateDirectory(defaultAssemblyPath);
@@ -117,7 +118,7 @@ namespace Tgstation.Server.Host.Watchdog
using (var process = new Process())
{
process.StartInfo.FileName = dotnetPath;
process.StartInfo.WorkingDirectory = rootLocation; //for appsettings
process.StartInfo.WorkingDirectory = rootLocation; // for appsettings
var arguments = new List<string>
{
@@ -138,7 +139,7 @@ namespace Tgstation.Server.Host.Watchdog
process.StartInfo.Arguments = String.Join(" ", arguments);
process.StartInfo.UseShellExecute = false; //runs in the same console
process.StartInfo.UseShellExecute = false; // runs in the same console
var tcs = new TaskCompletionSource<object>();
process.Exited += (a, b) =>
@@ -172,7 +173,7 @@ namespace Tgstation.Server.Host.Watchdog
{
processCts.CancelAfter(TimeSpan.FromSeconds(10));
}
catch (ObjectDisposedException) { } //race conditions
catch (ObjectDisposedException) { } // race conditions
}))
await tcs.Task.ConfigureAwait(false);
}
@@ -204,13 +205,12 @@ namespace Tgstation.Server.Host.Watchdog
return;
case 1:
if (!cancellationToken.IsCancellationRequested)
//just a restart
logger.LogInformation("Watchdog will restart host...");
logger.LogInformation("Watchdog will restart host..."); // just a restart
else
logger.LogWarning("Host requested restart but watchdog shutdown is in progress!");
break;
case 2:
//update path is now an exception document
// update path is now an exception document
logger.LogCritical("Host crashed, propagating exception dump...");
var data = File.ReadAllText(updateDirectory);
try
@@ -221,29 +221,36 @@ namespace Tgstation.Server.Host.Watchdog
{
logger.LogWarning("Unable to delete exception dump file at {0}! Exception: {1}", updateDirectory, e);
}
#pragma warning disable CA2201 // Do not raise reserved exception types
throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host propagated exception: {0}", data));
#pragma warning restore CA2201 // Do not raise reserved exception types
default:
if (killedHostProcess)
{
logger.LogWarning("Watchdog forced to kill host process!");
cancellationToken.ThrowIfCancellationRequested();
}
#pragma warning disable CA2201 // Do not raise reserved exception types
throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host crashed with exit code {0}!", process.ExitCode));
#pragma warning restore CA2201 // Do not raise reserved exception types
}
}
//HEY YOU
//BE WARNED THAT IF YOU DEBUGGED THE HOST PROCESS THAT JUST LAUNCHED THE DEBUGGER WILL HOLD A LOCK ON THE DIRECTORY
//THIS MEANS THE FIRST DIRECTORY.MOVE WILL THROW
// HEY YOU
// BE WARNED THAT IF YOU DEBUGGED THE HOST PROCESS THAT JUST LAUNCHED THE DEBUGGER WILL HOLD A LOCK ON THE DIRECTORY
// THIS MEANS THE FIRST DIRECTORY.MOVE WILL THROW
if (Directory.Exists(updateDirectory))
{
logger.LogInformation("Applying server update...");
if (isWindows)
{
//windows dick sucking resource unlocking
// windows dick sucking resource unlocking
GC.Collect(Int32.MaxValue, GCCollectionMode.Default, true);
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken).ConfigureAwait(false);
}
var tempPath = Path.Combine(assemblyStoragePath, Guid.NewGuid().ToString());
try
{
@@ -293,5 +300,6 @@ namespace Tgstation.Server.Host.Watchdog
logger.LogInformation("Host watchdog exiting...");
}
}
#pragma warning restore CA1502
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,20 @@
{
"private": true,
"dependencies": {
"react-dom": "^16.5.2",
"tgstation-server-control-panel": "^0.1.5"
},
"scripts": {
"build": "rimraf build && react-scripts-ts build && cp-cli node_modules/tgstation-server-control-panel/build/public/ build/ && cp-cli ../../../build/tgs.ico build/favicon.ico && minify-json build && rimraf ../wwwroot && move-cli build ../wwwroot"
},
"devDependencies": {
"@types/node": "^10.12.0",
"@types/react-dom": "^16.0.9",
"cp-cli": "^1.1.2",
"minify-json": "^1.0.0",
"move-cli": "^1.2.0",
"react-scripts-ts": "^3.1.0",
"rimraf": "^2.6.2",
"typescript": "^3.1.3"
}
}
@@ -0,0 +1,40 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Tgstation Server Control Panel</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
@@ -0,0 +1,15 @@
{
"short_name": "TGS",
"name": "Tgstation Server",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
@@ -0,0 +1,5 @@
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
@@ -0,0 +1,17 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import ControlPanel from 'tgstation-server-control-panel';
import registerServiceWorker from './registerServiceWorker';
import './index.css';
const contentElement = document.getElementById('root') as HTMLElement;
const legacyUserLanguageKey = 'userLanguage';
const userLang = navigator.language || navigator[legacyUserLanguageKey];
const serverAddress = window.location.href;
const controlPanel = <ControlPanel serverAddress={serverAddress} locale={userLang} />;
registerServiceWorker();
ReactDOM.render(controlPanel, contentElement);
@@ -0,0 +1,124 @@
// tslint:disable:no-console
// tslint:disable:curly
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the 'N+1' visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(
process.env.PUBLIC_URL!,
window.location.toString()
);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
navigator.serviceWorker.ready.then(() => {
console.log(
'This web app is being served cache-first by a service ' +
'worker. To learn more, visit https://goo.gl/SC7cgQ'
);
});
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl: string) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (installingWorker) {
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a 'New content is
// available; please refresh.' message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// 'Content is cached for offline use.' message.
console.log('Content is cached for offline use.');
}
}
};
}
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl: string) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type')!.indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
@@ -0,0 +1,31 @@
{
"compilerOptions": {
"baseUrl": ".",
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"rootDir": "src",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"importHelpers": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true
},
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts"
]
}
@@ -0,0 +1,3 @@
{
"extends": "./tsconfig.json"
}
@@ -0,0 +1,14 @@
{
"extends": [ "tslint:recommended", "tslint-react", "tslint-config-prettier" ],
"linterOptions": {
"exclude": [
"config/**/*.js",
"node_modules/**/*.ts",
"coverage/lcov-report/*.js"
]
},
"rules": {
"curly": [ true, "as-needed" ],
"semicolon": [ true, "always" ]
}
}
@@ -27,8 +27,8 @@ namespace Tgstation.Server.Host.Components.Byond
DreamMakerPath = dreamMakerPath ?? throw new ArgumentNullException(nameof(dreamMakerPath));
}
//at one point in design, byond versions were to delete themselves if they werent the active version
//That changed at some point so these functions are intentioanlly left blank
// at one point in design, byond versions were to delete themselves if they weren't the active version
// That changed at some point so these functions are intentioanlly left blank
/// <inheritdoc />
public void Dispose() { }
@@ -24,6 +24,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// The file in which we store the <see cref="VersionKey(Version)"/> for installations
/// </summary>
const string VersionFileName = "Version.txt";
/// <summary>
/// The file in which we store the <see cref="VersionKey(Version)"/> for the active installation
/// </summary>
@@ -119,6 +120,7 @@ namespace Tgstation.Server.Host.Components.Byond
if (!installed)
installedVersions.Add(versionKey, ourTcs.Task);
}
if (installed)
using (cancellationToken.Register(() => ourTcs.SetCanceled()))
{
@@ -126,7 +128,8 @@ namespace Tgstation.Server.Host.Components.Byond
cancellationToken.ThrowIfCancellationRequested();
return;
}
//okay up to us to install it then
// okay up to us to install it then
try
{
await eventConsumer.HandleEvent(EventType.ByondInstallStart, new List<string> { versionKey }, cancellationToken).ConfigureAwait(false);
@@ -141,12 +144,12 @@ namespace Tgstation.Server.Host.Components.Byond
await ioManager.ZipToDirectory(versionKey, download, cancellationToken).ConfigureAwait(false);
await byondInstaller.InstallByond(ioManager.ResolvePath(versionKey), version, cancellationToken).ConfigureAwait(false);
//make sure to do this last because this is what tells us we have a valid version in the future
// make sure to do this last because this is what tells us we have a valid version in the future
await ioManager.WriteAllBytes(ioManager.ConcatPath(versionKey, VersionFileName), Encoding.UTF8.GetBytes(version.ToString()), cancellationToken).ConfigureAwait(false);
}
catch (WebException e)
{
//since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
// since the user can easily provide non-exitent version numbers, we'll turn this into a JobException
throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message));
}
catch (OperationCanceledException)
@@ -205,9 +208,8 @@ namespace Tgstation.Server.Host.Components.Byond
{
async Task<byte[]> GetActiveVersion()
{
if (!await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false))
return null;
return await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
var activeVersionFileExists = await ioManager.FileExists(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
return !activeVersionFileExists ? null : await ioManager.ReadAllBytes(ActiveVersionFileName, cancellationToken).ConfigureAwait(false);
}
var activeVersionBytesTask = GetActiveVersion();
@@ -221,9 +223,10 @@ namespace Tgstation.Server.Host.Components.Byond
if (!await ioManager.FileExists(versionFile, cancellationToken).ConfigureAwait(false))
{
logger.LogInformation("Cleaning unparsable version path: {0}", ioManager.ResolvePath(path));
await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); //cleanup
await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false); // cleanup
return;
}
var bytes = await ioManager.ReadAllBytes(versionFile, cancellationToken).ConfigureAwait(false);
var text = Encoding.UTF8.GetString(bytes);
if (Version.TryParse(text, out var version))
@@ -236,8 +239,9 @@ namespace Tgstation.Server.Host.Components.Byond
return;
}
}
await ioManager.DeleteDirectory(path, cancellationToken).ConfigureAwait(false);
};
}
await Task.WhenAll(directories.Select(x => ReadVersion(x))).ConfigureAwait(false);
@@ -33,7 +33,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// <param name="path">The path to the BYOND installation</param>
/// <param name="version">The <see cref="Version"/> of BYOND being installed</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns></returns>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task InstallByond(string path, Version version, CancellationToken cancellationToken);
/// <summary>
@@ -26,6 +26,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// </summary>
/// <param name="version">The new <see cref="Version"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task ChangeVersion(Version version, CancellationToken cancellationToken);
/// <summary>
@@ -1,11 +1,9 @@
using Microsoft.Extensions.Logging;
using System;
using System.Globalization;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Byond
@@ -18,7 +16,8 @@ namespace Tgstation.Server.Host.Components.Byond
/// <summary>
/// The URL format string for getting BYOND linux version {0}.{1} zipfile
/// </summary>
const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond_linux.zip";
/// <summary>
/// Path to the BYOND cache
/// </summary>
@@ -85,7 +84,7 @@ namespace Tgstation.Server.Host.Components.Byond
if (version == null)
throw new ArgumentNullException(nameof(version));
var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
return await ioManager.DownloadFile(new Uri(url), cancellationToken).ConfigureAwait(false);
}
@@ -98,8 +97,8 @@ namespace Tgstation.Server.Host.Components.Byond
if (version == null)
throw new ArgumentNullException(nameof(version));
//write the scripts for running the ting
//need to add $ORIGIN to LD_LIBRARY_PATH
// 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";
var dreamDaemonScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamDaemonExecutableName);
@@ -17,19 +17,23 @@ namespace Tgstation.Server.Host.Components.Byond
/// <summary>
/// The URL format string for getting BYOND windows version {0}.{1} zipfile
/// </summary>
const string ByondRevisionsURL = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
const string ByondRevisionsURLTemplate = "https://secure.byond.com/download/build/{0}/{0}.{1}_byond.zip";
/// <summary>
/// Directory to byond installation configuration
/// </summary>
const string ByondConfigDir = "byond/cfg";
/// <summary>
/// BYOND's DreamDaemon config file
/// </summary>
const string ByondDDConfig = "daemon.txt";
/// <summary>
/// Setting to add to <see cref="ByondDDConfig"/> to suppress an invisible user prompt for running a trusted mode .dmb
/// </summary>
const string ByondNoPromptTrustedMode = "trusted-check 0";
/// <summary>
/// The directory that contains the BYOND directx redistributable
/// </summary>
@@ -105,7 +109,7 @@ namespace Tgstation.Server.Host.Components.Byond
/// <inheritdoc />
public Task<byte[]> DownloadVersion(Version version, CancellationToken cancellationToken)
{
var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURL, version.Major, version.Minor);
var url = String.Format(CultureInfo.InvariantCulture, ByondRevisionsURLTemplate, version.Major, version.Minor);
return ioManager.DownloadFile(new Uri(url), cancellationToken);
}
@@ -118,20 +122,21 @@ namespace Tgstation.Server.Host.Components.Byond
var configPath = ioManager.ConcatPath(path, ByondConfigDir);
await ioManager.CreateDirectory(configPath, cancellationToken).ConfigureAwait(false);
await ioManager.WriteAllBytes(ioManager.ConcatPath(configPath, ByondDDConfig), Encoding.UTF8.GetBytes(ByondNoPromptTrustedMode), cancellationToken).ConfigureAwait(false);
};
}
var setNoPromptTrustedModeTask = SetNoPromptTrusted();
//after this version lummox made DD depend of directx lol
//but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
// after this version lummox made DD depend of directx lol
// but then he became amazing and not only fixed it but also gave us 30s compiles \[T]/
if (version.Major == 512 && version.Minor >= 1427 && version.Minor < 1452 && !installedDirectX)
using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
//check again because race conditions
if (!installedDirectX)
{
//always install it, it's pretty fast and will do better redundancy checking than us
// ^check again because race conditions
// always install it, it's pretty fast and will do better redundancy checking than us
var rbdx = ioManager.ConcatPath(path, ByondDXDir);
//noShellExecute because we aren't doing runas shennanigans
// noShellExecute because we aren't doing runas shennanigans
IProcess directXInstaller;
try
{
@@ -141,6 +146,7 @@ namespace Tgstation.Server.Host.Components.Byond
{
throw new JobException("Unable to start DirectX installer process! Is the server running with admin privileges?", e);
}
using (directXInstaller)
{
int exitCode;
@@ -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
@@ -15,6 +15,8 @@ using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Chat
{
/// <inheritdoc />
// TODO: Decomplexify
#pragma warning disable CA1506
sealed class Chat : IChat, IRestartHandler
{
const string CommonMention = "!tgs";
@@ -177,6 +179,7 @@ namespace Tgstation.Server.Host.Components.Chat
else
task = Task.CompletedTask;
}
await task.ConfigureAwait(false);
return provider;
}
@@ -190,12 +193,12 @@ namespace Tgstation.Server.Host.Components.Chat
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task ProcessMessage(IProvider provider, Message message, CancellationToken cancellationToken)
{
//map the channel if it's private and we haven't seen it
// map the channel if it's private and we haven't seen it
lock (providers)
{
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)
@@ -219,11 +222,11 @@ namespace Tgstation.Server.Host.Components.Chat
}
else
{
//need to add tag and isAdminChannel
// need to add tag and isAdminChannel
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,8 +239,8 @@ namespace Tgstation.Server.Host.Components.Chat
var addressed = address == CommonMention.ToUpperInvariant() || address == provider.BotMention.ToUpperInvariant();
if (!addressed && !message.User.Channel.IsPrivate)
//no mention
// no mention
if (!addressed && !message.User.Channel.IsPrivateChannel)
return;
logger.LogTrace("Chat command: {0}. User (True provider Id): {1}", message.Content, JsonConvert.SerializeObject(message.User));
@@ -247,7 +250,7 @@ namespace Tgstation.Server.Host.Components.Chat
if (splits.Count == 0)
{
//just a mention
// just a mention
await SendMessage("Hi!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
return;
}
@@ -266,8 +269,9 @@ namespace Tgstation.Server.Host.Components.Chat
await Task.WhenAll(tasks).ConfigureAwait(false);
handler = tasks.SelectMany(x => x.Result).Where(x => x.Name.ToUpperInvariant() == commandName).FirstOrDefault();
}
return handler;
};
}
const string UnknownCommandMessage = "Unknown command! Type '?' or 'help' for available commands.";
@@ -290,6 +294,7 @@ namespace Tgstation.Server.Host.Components.Chat
else
helpText = UnknownCommandMessage;
}
await SendMessage(helpText, new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
return;
}
@@ -302,7 +307,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;
@@ -318,7 +323,7 @@ namespace Tgstation.Server.Host.Components.Chat
}
catch (Exception e)
{
//error bc custom commands should reply about why it failed
// error bc custom commands should reply about why it failed
logger.LogError("Error processing chat command: {0}", e);
await SendMessage("Internal error processing command!", new List<ulong> { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false);
}
@@ -336,11 +341,11 @@ namespace Tgstation.Server.Host.Components.Chat
{
while (!cancellationToken.IsCancellationRequested)
{
//prune disconnected providers
// prune disconnected providers
foreach (var I in messageTasks.Where(x => !x.Key.Connected).ToList())
messageTasks.Remove(I.Key);
//add new ones
// add new ones
Task updatedTask;
lock (this)
updatedTask = connectionsUpdated.Task;
@@ -355,10 +360,10 @@ namespace Tgstation.Server.Host.Components.Chat
continue;
}
//wait for a message
// wait for a message
await Task.WhenAny(updatedTask, Task.WhenAny(messageTasks.Select(x => x.Value))).ConfigureAwait(false);
//process completed ones
// process completed ones
foreach (var I in messageTasks.Where(x => x.Value.IsCompleted).ToList())
{
var message = await I.Value.ConfigureAwait(false);
@@ -385,7 +390,7 @@ namespace Tgstation.Server.Host.Components.Chat
if (provider == null)
return;
var results = await provider.MapChannels(newChannels, cancellationToken).ConfigureAwait(false);
if (results == null) //aborted
if (results == null) // aborted
return;
var mappings = Enumerable.Zip(newChannels, results, (x, y) => new ChannelMapping
{
@@ -407,7 +412,7 @@ namespace Tgstation.Server.Host.Components.Chat
lock (mappedChannels)
{
lock (providers)
if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) //aborted again
if (!providers.TryGetValue(connectionId, out IProvider verify) || verify != provider) // aborted again
return;
foreach (var I in mappings)
{
@@ -419,6 +424,7 @@ namespace Tgstation.Server.Host.Components.Chat
lock (trackingContexts)
task = Task.WhenAll(trackingContexts.Select(x => x.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken)));
}
await task.ConfigureAwait(false);
}
@@ -444,7 +450,7 @@ namespace Tgstation.Server.Host.Components.Chat
Task disconnectTask;
lock (providers)
{
//raw settings changes forces a rebuild of the provider
// raw settings changes forces a rebuild of the provider
if (providers.TryGetValue(newSettings.Id, out provider))
{
providers.Remove(newSettings.Id);
@@ -471,7 +477,7 @@ namespace Tgstation.Server.Host.Components.Chat
await provider.Connect(cancellationToken).ConfigureAwait(false);
lock (this)
{
//same thread shennanigans
// same thread shennanigans
var oldOne = connectionsUpdated;
connectionsUpdated = new TaskCompletionSource<object>();
oldOne.SetResult(null);
@@ -506,7 +512,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
List<ulong> wdChannels;
message = String.Format(CultureInfo.InvariantCulture, "WD: {0}", message);
lock (mappedChannels) //so it doesn't change while we're using it
lock (mappedChannels) // so it doesn't change while we're using it
wdChannels = mappedChannels.Where(x => x.Value.IsWatchdogChannel).Select(x => x.Key).ToList();
return SendMessage(message, wdChannels, cancellationToken);
}
@@ -516,7 +522,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
List<ulong> wdChannels;
message = String.Format(CultureInfo.InvariantCulture, "DM: {0}", message);
lock (mappedChannels) //so it doesn't change while we're using it
lock (mappedChannels) // so it doesn't change while we're using it
wdChannels = mappedChannels.Where(x => x.Value.IsUpdatesChannel).Select(x => x.Key).ToList();
return SendMessage(message, wdChannels, cancellationToken);
}
@@ -547,11 +553,17 @@ namespace Tgstation.Server.Host.Components.Chat
if (customCommandHandler == null)
throw new InvalidOperationException("RegisterCommandHandler() hasn't been called!");
JsonTrackingContext context = null;
context = new JsonTrackingContext(ioManager, customCommandHandler, loggerFactory.CreateLogger<JsonTrackingContext>(), () =>
{
lock (trackingContexts)
trackingContexts.Remove(context);
}, ioManager.ConcatPath(basePath, commandsJsonName), ioManager.ConcatPath(basePath, channelsJsonName));
context = new JsonTrackingContext(
ioManager,
customCommandHandler,
loggerFactory.CreateLogger<JsonTrackingContext>(),
() =>
{
lock (trackingContexts)
trackingContexts.Remove(context);
},
ioManager.ConcatPath(basePath, commandsJsonName),
ioManager.ConcatPath(basePath, channelsJsonName));
Task task;
lock (trackingContexts)
{
@@ -559,6 +571,7 @@ namespace Tgstation.Server.Host.Components.Chat
lock (mappedChannels)
task = context.SetChannels(mappedChannels.Select(y => y.Value.Channel), cancellationToken);
}
await task.ConfigureAwait(false);
return context;
}
@@ -598,7 +611,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion);
List<ulong> wdChannels;
lock (mappedChannels) //so it doesn't change while we're using it
lock (mappedChannels) // so it doesn't change while we're using it
wdChannels = mappedChannels.Select(x => x.Key).ToList();
return SendMessage(message, wdChannels, cancellationToken);
}
@@ -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; }
@@ -61,6 +61,8 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
}
/// <inheritdoc />
// TODO: Decomplexify
#pragma warning disable CA1506
public async Task<string> Invoke(string arguments, User user, CancellationToken cancellationToken)
{
IEnumerable<Models.TestMerge> results = null;
@@ -73,6 +75,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
return "Repository unavailable!";
head = repo.Head;
}
await databaseContextFactory.UseContext(async db => results = await db.RevisionInformations.Where(x => x.Instance.Id == instance.Id && x.CommitSha == head)
.SelectMany(x => x.ActiveTestMerges)
.Select(x => x.TestMerge)
@@ -89,10 +92,8 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
results = watchdog.ActiveCompileJob?.RevisionInformation.ActiveTestMerges.Select(x => x.TestMerge).ToList() ?? new List<Models.TestMerge>();
}
if (!results.Any())
return "None!";
return String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7))));
return !results.Any() ? "None!" : String.Join(", ", results.Select(x => String.Format(CultureInfo.InvariantCulture, "#{0} at {1}", x.Number, x.PullRequestRevision.Substring(0, 7))));
}
#pragma warning restore CA1506
}
}
@@ -27,7 +27,7 @@ namespace Tgstation.Server.Host.Components.Chat.Commands
/// <summary>
/// Construct a <see cref="VersionCommand"/>
/// </summary>
/// <param name="application"></param>
/// <param name="application">The value of <see cref="application"/></param>
public VersionCommand(IApplication application)
{
this.application = application ?? throw new ArgumentNullException(nameof(application));
@@ -38,6 +38,15 @@ namespace Tgstation.Server.Host.Components.Chat
bool active;
/// <summary>
/// Construct a <see cref="JsonTrackingContext"/>
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/></param>
/// <param name="customCommandHandler">The value of <see cref="customCommandHandler"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="onDispose">The value of <see cref="onDispose"/></param>
/// <param name="commandsPath">The value of <see cref="commandsPath"/></param>
/// <param name="channelsPath">The value of <see cref="channelsPath"/></param>
public JsonTrackingContext(IIOManager ioManager, ICustomCommandHandler customCommandHandler, ILogger<JsonTrackingContext> logger, Action onDispose, string commandsPath, string channelsPath)
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
@@ -90,6 +99,7 @@ namespace Tgstation.Server.Host.Components.Chat
{
logger.LogWarning("Error retrieving custom commands! Exception: {0}", e);
}
return new List<CustomCommand>();
}
@@ -54,7 +54,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// </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);
static string NormalizeMention(string fromDiscord) => fromDiscord.Replace("!", String.Empty, StringComparison.Ordinal);
/// <summary>
/// Construct a <see cref="DiscordProvider"/>
@@ -97,10 +97,11 @@ 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
// isAdmin and Tag populated by manager
},
FriendlyName = e.Author.Username,
Mention = NormalizeMention(e.Author.Mention)
@@ -182,19 +183,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (!channel.DiscordChannelId.HasValue)
throw new InvalidOperationException("ChatChannel missing DiscordChannelId!");
if (!(client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel))
return null;
if (client.GetChannel(channel.DiscordChannelId.Value) is ITextChannel discordChannel)
return new Channel
{
RealId = discordChannel.Id,
IsAdminChannel = channel.IsAdminChannel == true,
ConnectionName = discordChannel.Guild.Name,
FriendlyName = discordChannel.Name,
IsPrivateChannel = false,
Tag = channel.Tag
};
return new Channel
{
RealId = discordChannel.Id,
IsAdmin = channel.IsAdminChannel == true,
ConnectionName = discordChannel.Guild.Name,
FriendlyName = discordChannel.Name,
IsPrivate = false,
Tag = channel.Tag
};
};
return null;
}
var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null).ToList();
@@ -44,18 +44,22 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
/// Address of the server to connect to
/// </summary>
readonly string address;
/// <summary>
/// Port of the server to connect to
/// </summary>
readonly ushort port;
/// <summary>
/// IRC nickname
/// </summary>
readonly string nickname;
/// <summary>
/// Password which will used for authentication
/// </summary>
readonly string password;
/// <summary>
/// The <see cref="IrcPasswordType"/> of <see cref="password"/>
/// </summary>
@@ -134,7 +138,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
UseSsl = useSsl
};
if (useSsl)
client.ValidateServerCertificate = true; //dunno if it defaults to that or what
client.ValidateServerCertificate = true; // dunno if it defaults to that or what
client.OnChannelMessage += Client_OnChannelMessage;
client.OnQueryMessage += Client_OnQueryMessage;
@@ -151,7 +155,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (Connected)
{
disconnecting = true;
client.Disconnect(); //just closes the socket
client.Disconnect(); // just closes the socket
}
}
@@ -184,8 +188,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (dicToCheck == queryChannelIdMap)
channelIdMap.Add(resultId.Value, null);
}
return resultId.Value;
};
}
ulong userId, channelId;
lock (this)
@@ -204,8 +209,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
ConnectionName = address,
FriendlyName = isPrivate ? String.Format(CultureInfo.InvariantCulture, "PM: {0}", channelName) : channelName,
RealId = channelId,
IsPrivate = isPrivate
//isAdmin and Tag populated by manager
IsPrivateChannel = isPrivate
// isAdmin and Tag populated by manager
},
FriendlyName = username,
RealId = userId,
@@ -247,9 +253,10 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
if (passwordType == IrcPasswordType.Sasl)
{
client.WriteLine("CAP REQ :sasl", Priority.Critical); //needs to be put in the buffer before anything else
client.WriteLine("CAP REQ :sasl", Priority.Critical); // needs to be put in the buffer before anything else
cancellationToken.ThrowIfCancellationRequested();
}
client.Login(nickname, nickname, 0, nickname);
}
@@ -260,7 +267,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
}
else if (passwordType == IrcPasswordType.Sasl)
{
//wait for the sasl ack or timeout
// wait for the sasl ack or timeout
var recievedAck = false;
var recievedPlus = false;
client.OnReadLine += (sender, e) =>
@@ -285,7 +292,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
for (; !recievedPlus && DateTimeOffset.Now <= endTime; asyncDelayer.Delay(listenTimeSpan, cancellationToken).GetAwaiter().GetResult())
client.Listen(false);
//Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
// Stolen! https://github.com/znc/znc/blob/1e697580155d5a38f8b5a377f3b1d94aaa979539/modules/sasl.cpp#L196
var authString = String.Format(CultureInfo.InvariantCulture, "{0}{1}{0}{1}{2}", nickname, '\0', password);
var b64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(authString));
var authLine = String.Format(CultureInfo.InvariantCulture, "AUTHENTICATE {0}", b64);
@@ -306,10 +313,12 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
if (disconnecting || !client.IsConnected)
break;
client.Listen(false);
//ensure we have the correct nick
// ensure we have the correct nick
if (client.GetIrcUser(nickname) == null)
client.RfcNick(nickname);
}
client.Listen();
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
}
@@ -322,6 +331,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
logger.LogWarning("Unable to connect to IRC: {0}", e);
return false;
}
return true;
}, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
@@ -336,7 +346,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
{
try
{
client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); //priocritical otherwise it wont go through
client.RfcQuit("Mr. Stark, I don't feel so good...", Priority.Critical); // priocritical otherwise it wont go through
}
catch (Exception e)
{
@@ -363,7 +373,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
throw new InvalidOperationException("ChatChannel missing IrcChannel!");
lock (this)
{
var hs = new HashSet<string>(); //for unique inserts
var hs = new HashSet<string>(); // for unique inserts
foreach (var I in channels)
hs.Add(I.IrcChannel);
var toPart = new List<string>();
@@ -390,13 +400,14 @@ namespace Tgstation.Server.Host.Components.Chat.Providers
id = channelIdCounter++;
channelIdMap.Add(id.Value, x.IrcChannel);
}
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();
@@ -33,10 +33,12 @@ namespace Tgstation.Server.Host.Components.Compiler
/// The <see cref="IDatabaseContextFactory"/> for the <see cref="DmbFactory"/>
/// </summary>
readonly IDatabaseContextFactory databaseContextFactory;
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="DmbFactory"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="DmbFactory"/>
/// </summary>
@@ -46,6 +48,7 @@ namespace Tgstation.Server.Host.Components.Compiler
/// The <see cref="Api.Models.Instance"/> for the <see cref="DmbFactory"/>
/// </summary>
readonly Api.Models.Instance instance;
/// <summary>
/// The <see cref="CancellationTokenSource"/> for <see cref="cleanupTask"/>
/// </summary>
@@ -55,10 +58,12 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <see cref="Task"/> representing calls to <see cref="CleanJob(CompileJob)"/>
/// </summary>
Task cleanupTask;
/// <summary>
/// <see cref="TaskCompletionSource{TResult}"/> resulting in the latest <see cref="DmbProvider"/> yet to exist
/// </summary>
TaskCompletionSource<object> newerDmbTcs;
/// <summary>
/// The latest <see cref="DmbProvider"/>
/// </summary>
@@ -87,7 +92,7 @@ namespace Tgstation.Server.Host.Components.Compiler
}
/// <inheritdoc />
public void Dispose() => cleanupCts.Dispose(); //we don't dispose nextDmbProvider here, since it might be the only thing we have
public void Dispose() => cleanupCts.Dispose(); // we don't dispose nextDmbProvider here, since it might be the only thing we have
/// <summary>
/// Delete the <see cref="Api.Models.Internal.CompileJob.DirectoryName"/> of <paramref name="job"/>
@@ -99,12 +104,13 @@ namespace Tgstation.Server.Host.Components.Compiler
{
var deleteJob = ioManager.DeleteDirectory(job.DirectoryName.ToString(), cleanupCts.Token);
Task otherTask;
//lock (this) //already locked below
// lock (this) //already locked below
otherTask = cleanupTask;
await Task.WhenAll(otherTask, deleteJob).ConfigureAwait(false);
}
lock (this)
{
if (!jobLockCounts.TryGetValue(job.Id, out var currentVal) || currentVal == 1)
{
jobLockCounts.Remove(job.Id);
@@ -116,7 +122,6 @@ namespace Tgstation.Server.Host.Components.Compiler
var decremented = --jobLockCounts[job.Id];
logger.LogTrace("Compile job {0} lock count now: {1}", job.Id, decremented);
}
}
}
/// <inheritdoc />
@@ -156,13 +161,14 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <inheritdoc />
public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) =>
{
//where complete clause not necessary, only successful COMPILEjobs get in the db
// where complete clause not necessary, only successful COMPILEjobs get in the db
var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id)
.OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false);
if (cj == default(CompileJob))
return;
await LoadCompileJob(cj, cancellationToken).ConfigureAwait(false);
//we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
// we dont do CleanUnusedCompileJobs here because the watchdog may have plans for them yet
});
/// <inheritdoc />
@@ -173,17 +179,18 @@ namespace Tgstation.Server.Host.Components.Compiler
}
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task<IDmbProvider> FromCompileJob(CompileJob compileJob, CancellationToken cancellationToken)
{
if (compileJob == null)
throw new ArgumentNullException(nameof(compileJob));
//ensure we have the entire compile job tree
// ensure we have the entire compile job tree
await databaseContextFactory.UseContext(async db => compileJob = await db.CompileJobs.Where(x => x.Id == compileJob.Id)
.Include(x => x.Job).ThenInclude(x => x.StartedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.PrimaryTestMerge).ThenInclude(x => x.MergedBy)
.Include(x => x.RevisionInformation).ThenInclude(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).ThenInclude(x => x.MergedBy)
.FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query
.FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); // can't wait to see that query
logger.LogTrace("Loading compile job {0}...", compileJob.Id);
var providerSubmitted = false;
@@ -201,7 +208,7 @@ namespace Tgstation.Server.Host.Components.Compiler
if (!(await primaryCheckTask.ConfigureAwait(false) && await secondaryCheckTask.ConfigureAwait(false)))
{
logger.LogWarning("Error loading compile job, .dmb missing!");
return null; //omae wa mou shinderu
return null; // omae wa mou shinderu
}
lock (this)
@@ -226,28 +233,31 @@ namespace Tgstation.Server.Host.Components.Compiler
newProvider.Dispose();
}
}
#pragma warning restore CA1506
/// <inheritdoc />
#pragma warning disable CA1506 // TODO: Decomplexify
public async Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken)
{
List<long> jobIdsToSkip;
//don't clean locked directories
// don't clean locked directories
lock (this)
jobIdsToSkip = jobLockCounts.Select(x => x.Key).ToList();
List<string> jobUidsToNotErase = null;
//find the uids of locked directories
// find the uids of locked directories
await databaseContextFactory.UseContext(async db =>
{
jobUidsToNotErase = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.Id && jobIdsToSkip.Contains(x.Id)).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false);
}).ConfigureAwait(false);
//add the other exemption
// add the other exemption
if (exceptThisOne != null)
jobUidsToNotErase.Add(exceptThisOne.DirectoryName.Value.ToString().ToUpperInvariant());
//cleanup
// cleanup
await ioManager.CreateDirectory(".", cancellationToken).ConfigureAwait(false);
var directories = await ioManager.GetDirectories(".", cancellationToken).ConfigureAwait(false);
int deleting = 0;
@@ -276,5 +286,6 @@ namespace Tgstation.Server.Host.Components.Compiler
await Task.WhenAll().ConfigureAwait(false);
}
}
#pragma warning restore CA1506
}
}
@@ -16,9 +16,6 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <inheritdoc />
public string SecondaryDirectory => ioManager.ResolvePath(ioManager.ConcatPath(CompileJob.DirectoryName.ToString(), DreamMaker.BDirectoryName));
/// <inheritdoc />
public RevisionInformation RevisionInformation => CompileJob.RevisionInformation;
/// <summary>
/// The <see cref="CompileJob"/> for the <see cref="DmbProvider"/>
/// </summary>
@@ -28,6 +25,7 @@ namespace Tgstation.Server.Host.Components.Compiler
/// The <see cref="IIOManager"/> for the <see cref="DmbProvider"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="Action"/> to run when <see cref="Dispose"/> is called
/// </summary>
@@ -24,14 +24,17 @@ namespace Tgstation.Server.Host.Components.Compiler
/// Name of the primary directory used for compilation
/// </summary>
public const string ADirectoryName = "A";
/// <summary>
/// Name of the secondary directory used for compilation
/// </summary>
public const string BDirectoryName = "B";
/// <summary>
/// Extension for .dmbs
/// </summary>
public const string DmbExtension = ".dmb";
/// <summary>
/// Extension for .dmes
/// </summary>
@@ -41,42 +44,52 @@ namespace Tgstation.Server.Host.Components.Compiler
/// The <see cref="IByondManager"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IByondManager byond;
/// <summary>
/// The <see cref="IIOManager"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IIOManager ioManager;
/// <summary>
/// The <see cref="StaticFiles.IConfiguration"/> for <see cref="DreamMaker"/>
/// </summary>
readonly StaticFiles.IConfiguration configuration;
/// <summary>
/// The <see cref="ISessionControllerFactory"/> for <see cref="DreamMaker"/>
/// </summary>
readonly ISessionControllerFactory sessionControllerFactory;
/// <summary>
/// The <see cref="ICompileJobConsumer"/> for <see cref="DreamMaker"/>
/// </summary>
readonly ICompileJobConsumer compileJobConsumer;
/// <summary>
/// The <see cref="IApplication"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IApplication application;
/// <summary>
/// The <see cref="IEventConsumer"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IEventConsumer eventConsumer;
/// <summary>
/// The <see cref="IChat"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IChat chat;
/// <summary>
/// The <see cref="IProcessExecutor"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IProcessExecutor processExecutor;
/// <summary>
/// The <see cref="IWatchdog"/> for <see cref="DreamMaker"/>
/// </summary>
readonly IWatchdog watchdog;
/// <summary>
/// The <see cref="ILogger"/> for <see cref="DreamMaker"/>
/// </summary>
@@ -133,13 +146,13 @@ namespace Tgstation.Server.Host.Components.Compiler
{
AllowWebClient = false,
PrimaryPort = portToUse,
SecurityLevel = securityLevel, //all it needs to read the file and exit
SecurityLevel = securityLevel,
StartupTimeout = timeout
};
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
job.MinimumSecurityLevel = securityLevel; //needed for the TempDmbProvider
job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
var provider = new TemporaryDmbProvider(ioManager.ResolvePath(dirA), String.Concat(job.DmeName, DmbExtension), job);
var timeoutAt = DateTimeOffset.Now.AddSeconds(timeout);
@@ -184,13 +197,13 @@ namespace Tgstation.Server.Host.Components.Compiler
throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus));
}
}
throw new JobException("DMAPI validation timed out!");
}
}
/// <summary>
/// Compiles a .dme with DreamMaker
/// Compiles a .dme with DreamMaker
/// </summary>
/// <param name="dreamMakerPath">The path to the DreamMaker executable</param>
/// <param name="job">The <see cref="Models.CompileJob"/> for the operation</param>
@@ -267,6 +280,7 @@ namespace Tgstation.Server.Host.Components.Compiler
}
/// <inheritdoc />
#pragma warning disable CA1506 //TODO: Decomplexify
public async Task<Models.CompileJob> Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, Action<int> progressReporter, TimeSpan? estimatedDuration, CancellationToken cancellationToken)
{
if (revisionInformation == null)
@@ -327,8 +341,6 @@ namespace Tgstation.Server.Host.Components.Compiler
var progressTask = ProgressTask();
try
{
var commitInsert = revisionInformation.CommitSha.Substring(0, 7);
string remoteCommitInsert;
if (revisionInformation.CommitSha == revisionInformation.OriginCommitSha)
@@ -366,7 +378,7 @@ namespace Tgstation.Server.Host.Components.Compiler
}
await chatTask.ConfigureAwait(false);
};
}
try
{
@@ -375,18 +387,18 @@ namespace Tgstation.Server.Host.Components.Compiler
var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName);
var dirB = ioManager.ConcatPath(job.DirectoryName.ToString(), BDirectoryName);
// copy the repository
logger.LogTrace("Copying repository to game directory...");
//copy the repository
var fullDirA = ioManager.ResolvePath(dirA);
var repoOrigin = repository.Origin;
using (repository)
await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false);
//run precompile scripts
// run precompile scripts
var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName));
await eventConsumer.HandleEvent(EventType.CompileStart, new List<string> { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false);
//determine the dme
// determine the dme
if (job.DmeName == null)
{
logger.LogTrace("Searching for available .dmes...");
@@ -403,11 +415,12 @@ namespace Tgstation.Server.Host.Components.Compiler
await ModifyDme(job, cancellationToken).ConfigureAwait(false);
//run compiler, verify api
// run compiler
job.ByondVersion = byondLock.Version.ToString();
var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false);
// verify api
try
{
if (exitCode != 0)
@@ -417,7 +430,7 @@ namespace Tgstation.Server.Host.Components.Compiler
}
catch (JobException)
{
//server never validated or compile failed
// DD never validated or compile failed
await eventConsumer.HandleEvent(EventType.CompileFailure, new List<string> { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false);
throw;
}
@@ -427,12 +440,12 @@ namespace Tgstation.Server.Host.Components.Compiler
logger.LogTrace("Duplicating compiled game...");
//duplicate the dmb et al
// duplicate the dmb et al
await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false);
logger.LogTrace("Applying static game file symlinks...");
//symlink in the static data
// symlink in the static data
var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken);
var symBTask = configuration.SymlinkStaticFilesTo(ioManager.ResolvePath(dirB), cancellationToken);
@@ -463,5 +476,6 @@ namespace Tgstation.Server.Host.Components.Compiler
}
}
}
#pragma warning restore CA1506
}
}
@@ -43,6 +43,5 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
Task CleanUnusedCompileJobs(CompileJob exceptThisOne, CancellationToken cancellationToken);
}
}
@@ -36,6 +36,7 @@ namespace Tgstation.Server.Host.Components.Compiler
/// <inheritdoc />
public void Dispose() { }
/// <inheritdoc />
public void KeepAlive() => throw new NotSupportedException();
}
}
@@ -16,7 +16,7 @@ namespace Tgstation.Server.Host.Components
readonly IConfiguration configuration;
/// <summary>
/// The <see cref="IWatchdog"/> for the
/// The <see cref="IWatchdog"/> for the <see cref="EventConsumer"/>
/// </summary>
IWatchdog watchdog;
@@ -46,8 +46,10 @@ namespace Tgstation.Server.Host.Components
/// <param name="watchdog">The value of <see cref="watchdog"/></param>
public void SetWatchdog(IWatchdog watchdog)
{
#pragma warning disable IDE0016 // Use 'throw' expression
if (watchdog == null)
throw new ArgumentNullException(nameof(watchdog));
#pragma warning restore IDE0016 // Use 'throw' expression
if (this.watchdog != null)
throw new InvalidOperationException("watchdog already set!");
this.watchdog = watchdog;
@@ -9,18 +9,22 @@
/// Parameters: Reference name, commit sha
/// </summary>
RepoResetOrigin = 0,
/// <summary>
/// Parameters: Checkout target
/// </summary>
RepoCheckout = 1,
/// <summary>
/// No parameters
/// </summary>
RepoFetch = 2,
/// <summary>
/// Parameters: Pull request number, pull request sha, merger message
/// </summary>
RepoMergePullRequest = 3,
/// <summary>
/// Parameters: Absolute path to repository root
/// </summary>
@@ -30,26 +34,32 @@
/// Parameters: Version being installed
/// </summary>
ByondInstallStart = 5,
/// <summary>
/// Parameters: Error string
/// </summary>
ByondInstallFail = 6,
/// <summary>
/// Parameters: Old active version, new active version
/// </summary>
ByondActiveVersionChange = 7,
/// <summary>
/// Parameters: Game directory path, origin commit sha
/// </summary>
CompileStart = 8,
/// <summary>
/// No parameters
/// </summary>
CompileCancelled = 9,
/// <summary>
/// Parameters: Game directory path, "1" if compile succeeded and api validation failed, "0" otherwise
/// </summary>
CompileFailure = 10,
/// <summary>
/// Parameters: Game directory path
/// </summary>
@@ -25,7 +25,7 @@ namespace Tgstation.Server.Host.Components
/// The <see cref="IByondManager"/> for the <see cref="IInstance"/>
/// </summary>
IByondManager ByondManager { get; }
/// <summary>
/// The <see cref="IWatchdog"/> for the <see cref="IInstance"/>
/// </summary>
@@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components
IChat Chat { get; }
/// <summary>
/// The <see cref="StaticFiles.IConfiguration"/> for the <see cref="IInstance"/>
/// The <see cref="IConfiguration"/> for the <see cref="IInstance"/>
/// </summary>
IConfiguration Configuration { get; }

Some files were not shown because too many files have changed in this diff Show More