diff --git a/.dockerignore b/.dockerignore index 24e31346b4..1dfe466b59 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ packages */bin */obj +tests diff --git a/.travis.yml b/.travis.yml index 50230a0ab3..94d49a8e0d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,20 @@ matrix: - libstdc++6:i386 - env: - DMAPI=false - name: "Linux Build" + - CONFIG=Debug + name: "Test Server Debug" + language: csharp + mono: none + dotnet: 2.1.300 + services: + - mysql + cache: + directories: + - $HOME/.nuget/packages: + - env: + - DMAPI=false + - CONFIG=Release + name: "Test Server Release" language: csharp mono: none dotnet: 2.1.300 @@ -31,9 +44,6 @@ matrix: cache: directories: - $HOME/.nuget/packages: - os: - - linux - - osx install: - if [ $DMAPI = true ]; then build/install_byond.sh; fi diff --git a/README.md b/README.md index 603fa83bbc..1280ec519c 100644 --- a/README.md +++ b/README.md @@ -25,13 +25,35 @@ Generally, updates force a live tracking of the configured git repo, resetting l 3. Extract the .zip file to where you want the server to run from. Note the account running the server must have write access to the `lib` subdirectory. 4. If using the ServerService package, run `Tgstation.Server.Host.Service.exe`. It should prompt you to install the service. Click `Yes` and accept a potential UAC elevation prompt. You should now be able to control the service using the Windows service control commandlet. +#### Linux + +[We recommend using Docker for Linux installations](https://github.com/tgstation/tgstation-server#docker). The content of this parent section may be skipped if you choose to do so + +The following dependencies are required to run tgstation-server on Linux alongside the .NET Core runtime + +- gcc-multilib (on 64-bit systems for running BYOND) + +Note that tgstation-server has only ever been tested on Linux via it's [docker environment](https://github.com/tgstation/tgstation-server/blob/master/build/Dockerfile#L22). If you are having trouble with something, or figure out a required workaround, please contact project maintainers so this documentation may be better updated. + #### Docker -tgstation-server supports running in a docker container on linux systems and is the preferred deployment method to avoid [native dependency hell with libgit2](https://github.com/libgit2/libgit2sharp/issues/1533). The official image repository is located at https://hub.docker.com/r/tgstation/server it can be built locally, however, by running `docker build . -f build/Dockerfile` in the repository root. +tgstation-server supports running in a docker container and is the recommended deployment method for Linux systems due being the only tested environment. The official image repository is located at https://hub.docker.com/r/tgstation/server. It can also be built locally by running `docker build . -f build/Dockerfile` in the repository root. -To create a container run `docker create --restart=always -p :80 -v /path/to/your/appsettings.Production.json:/config_data -v path/to/your/log/folder:/tgs_logs tgstation/server` with any additional options you desire (i.e. You'll have to expose more ports in order to actually host servers, add a volume to create instances on, and create a volume for the SQLite database if that is what you're using). +To create a container run +``` +docker create \ + --restart=always \ #if you want maximum uptime + --network="host" \ #if your sql server is on the same machine + -p :80 \ + -p 0.0.0.0:: \ + -v /path/to/store/instances:/tgs4_instances \ + -v /path/to/your/appsettings.Production.json:/config_data \ + -v path/to/your/log/folder:/tgs_logs \ + tgstation/server +``` +with any additional options you desire (i.e. You'll have to expose more game ports in order to host more than one instance). -Note that due to the nature of docker. If the container restarts, you will be sent back to the version of TGS you installed with the initial command. Server updates will have to be reapplied. For this reason it is NOT RECOMMENDED to use the live update feature with a docker host. +Note although `/app/lib` is specified as a volume mount point in the `Dockerfile`, unless you REALLY know what you're doing. Do not mount any volumes over this for fear of breaking your container. ### Configuring @@ -41,7 +63,11 @@ Create an `appsettings.Production.json` file next to `appsettings.json`. This wi - `General:MinimumPasswordLength`: Minimum password length requirement for database users -- `Logging:LogLevel:Default`: Can be one of `Trace`, `Debug`, `Information`, `Warning`, `Error`, or `Critical`. Restricts what is put into the log files. Currently `Debug` is reccommended for help with error reporting. +- `General:GitHubAccessToken`: Specify a GitHub personal access token with no scopes here to highly mitigate the possiblity of 429 response codes from GitHub requests + +- `General:LogFileLevel`: Can be one of `Trace`, `Debug`, `Information`, `Warning`, `Error`, or `Critical`. Restricts what is put into the log files. Currently `Debug` is reccommended for help with error reporting. + +- `Kestrel:Endpoints:Http:Url`: The URL (i.e. interface and ports) your application should listen on. General use case should be `http://localhost:` for restricted local connections. See the Remote Access section for configuring public access to the World Wide Web. This doesn't need to be changed using the docker setup and should be mapped with the `-p` option instead - `Database:DatabaseType`: Can be one of `SqlServer`, `MariaDB`, or `MySql` @@ -55,6 +81,8 @@ If using MySQL, our provider library [recommends you set 'utf8mb4' as your defau The user created for the application will need the privilege to create databases on the first run. Once the initial set of migrations is run, the create right may be revoked. The user should maintain DDL rights though for applying future migrations +Note that the ratio of application installations to databases is 1:1. Do not attempt to share a database amongst multiple TGS installations. + ### Starting For the Windows service version start the `tgstation-server-4` service @@ -79,9 +107,117 @@ A breaking change from V3: tgstation-server 4 now REQUIRES the DMAPI to be integ The DMAPI is fully backwards compatible and should function with any tgstation-server version to date. Updates can be performed in the same manner. Using the `TGS_EXTERNAL_CONFIGURATION` is recommended in order to make the process as easy as replacing `tgs.dm` and the `tgs` folder with a new version +### Example + +Here is a bare minimum example project that implements the essential code changes for integrating the DMAPI + +Before `tgs.dm`: +``` +//Remember, every codebase is different, you probably have better methods for these defines than the ones given here +#define TGS_EXTERNAL_CONFIGURATION +#define TGS_DEFINE_AND_SET_GLOBAL(Name, Value) var/global/##Name = ##Value +#define TGS_READ_GLOBAL(Name) global.##Name +#define TGS_WRITE_GLOBAL(Name, Value) global.##Name = ##Value +#define TGS_WORLD_ANNOUNCE(message) world << ##message +#define TGS_INFO_LOG(message) world.log << "TGS Info: [##message]" +#define TGS_ERROR_LOG(message) world.log << "TGS Error: [##message]" +#define TGS_NOTIFY_ADMINS(event) world.log << "TGS Admin Message: [##event]" +#define TGS_CLIENT_COUNT global.client_cout +#define TGS_PROTECT_DATUM(Path) // Leave blank if your codebase doesn't give administrators code reflection capabilities +``` + +Anywhere else: +```dm +var/global/client_count = 0 + +/world/New() + ..() + TgsNew() + TgsInitializationsComplete() + +/world/Reboot() + TgsReboot() + ..() + +/world/Topic() + TGS_TOPIC + ..() + +/client/New() + ..() + ++global.client_count + +/client/Del() + ..() + --global.client_count + +``` + +## Remote Access + +tgstation-server is an [ASP.Net Core](https://docs.microsoft.com/en-us/aspnet/core/) based on the Kestrel web server. This section is meant to serve as a general use case overview, but the entire Kestrel configuration can be modified to your liking with the configuration JSON. See [the official documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel) for details. + +Exposing the builtin kestrel server to the internet directly over HTTP is highly not reccommended due to the lack of security. The recommended way to expose tgstation-server to the internet is to host it through a reverse proxy with HTTPS support. Here are some step by step examples to achieve this for major web servers. + +System administrators will most likely have their own configuration plans, but here are some basic guides for beginners. + +Once complete, test that your configuration worked by visiting your proxy site from a different computer. You should recieve a 401 Unauthorized response. + +### IIS (Reccommended for Windows) + +1. Acquire an HTTPS certificate. The easiet free way for Windows is [win-acme](https://github.com/PKISharp/win-acme) (requires you to set up the website first) +2. Install the [Web Platform Installer](https://www.microsoft.com/web/downloads/platform.aspx) +3. Open the web platform installer in the IIS Manager and install the Application Request Routing 3.0 module +4. Create a new website, bind it to HTTPS only with your chosen certificate and exposed port. The physical path won't matter since it won't be used. Use `Require Server Name Indication` if you want to limit requests to a specific URL prefix. +5. Close and reopen the IIS Manager +5. Open the site and navigate to the `URL Rewrite` module +6. In the `Actions` Pane on the right click `Add Rule(s)...` +7. For the rule template, select `Reverse Proxy` under `Inbound and Outbound Rules` and click `OK` +8. You may get a prompt about enabling proxy functionality. Click `OK` +9. In the window that appears set the `Inbound Rules` textbox to the URL of your tgstation-server i.e. `http://localhost:5000`. Ensure `Enable SSL Offloading` is checked, then click `OK` + +### Caddy (Reccommended for Linux, or those unfamilar with configuring NGINX or Apache) + +1. Setup a basic website configuration. Instructions on how to do so are out of scope. +2. In your Caddyfile, under a server entry, add the following (replace 8080 with the port TGS is hosted on): +``` +proxy /tgs localhost:8080 { + transparent +} +``` + +See https://caddyserver.com/docs/proxy + +### NGINX (Reccommended for Linux) + +1. Setup a basic website configuration. Instructions on how to do so are out of scope. +2. Acquire an HTTPS certificate, likely via Let's Encrypt, and configure NGINX to use it. +3. Setup a path under a server like the following (replace 8080 with the port TGS is hosted on): +``` +location /tgs { + proxy_pass http://127.0.0.1:8080; + break; +} +``` + +See https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/ + +### Apache + +1. Ensure the `mod_proxy` extension is installed. +2. Setup a basic website configuration. Instructions on how to do so are out of scope. +3. Acquire an HTTPS certificate, likely via Let's Encrypt, and configure Apache to use it. +4. Under a VirtualHost entry, setup the following (replace 8080 with the port TGS is hosted on): +``` +ProxyPass / http://127.0.0.1:8080 +ProxyPassReverse / http://127.0.0.1:8080 +``` + +See https://httpd.apache.org/docs/2.4/howto/reverse_proxy.html + ## Usage -tgstation-sever v4 is controlled via a RESTful HTTP json API. Documentation on this API can be found [here](https://tgstation.github.io/tgstation-server/api.html). This section serves to document the concepts of the server. +tgstation-server v4 is controlled via a RESTful HTTP json API. Documentation on this API can be found [here](https://tgstation.github.io/tgstation-server/api.html). This section serves to document the concepts of the server. ### Users diff --git a/appveyor.yml b/appveyor.yml index 9bd18e311d..a44341a0dd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -4,7 +4,7 @@ pull_requests: environment: TGS4_TEST_DATABASE_TYPE: SqlServer TGS4_TEST_CONNECTION_STRING: Server=(local)\SQL2017;Initial Catalog=TGS_Test;User ID=sa;Password=Password12! - repo_token: + TGS4_TEST_GITHUB_TOKEN: secure: lJNGAXwiB5HlWdthz3K4PetqpTG5IEAyRgKaiKxFMQ8HW8CcOjRtB97B05op7BsK branches: only: @@ -43,7 +43,7 @@ test_script: - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Client.Tests*]*" -output:".\client_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Client.Tests\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Tests*]*" -output:".\host_coverage.xml" -oldstyle + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Models.Migrations*" -output:".\host_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Tests\TestResults\results.trx)) - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Console.Tests*]*" -output:".\console_coverage.xml" -oldstyle @@ -57,7 +57,7 @@ test_script: - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Watchdog.Tests*]*" -output:".\watchdog_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Watchdog.Tests\TestResults\results.trx)) - - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Tests*]*" -output:".\server_coverage.xml" -oldstyle + - OpenCover.Console.exe -returntargetcode -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Tests*]* -[Tgstation.Server.Host]Tgstation.Server.Host.Models.Migrations..*" -output:".\server_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Tests\TestResults\results.trx)) after_test: diff --git a/build/BuildDox.ps1 b/build/BuildDox.ps1 index 81e88b9666..9087e16101 100644 --- a/build/BuildDox.ps1 +++ b/build/BuildDox.ps1 @@ -30,6 +30,6 @@ if($publish_dox){ echo "" > .nojekyll git add --all 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:repo_token@$github_url" 2>&1 | out-null + git push -f "https://$Env:TGS4_TEST_GITHUB_TOKEN@$github_url" 2>&1 | out-null cd "$bf" } diff --git a/build/Dockerfile b/build/Dockerfile index e01c962a85..240aeb77f9 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -1,4 +1,5 @@ FROM microsoft/dotnet:2.1-sdk AS build + WORKDIR /src COPY tgstation-server.sln ./ @@ -8,11 +9,6 @@ COPY src/Tgstation.Server.Host.Watchdog/Tgstation.Server.Host.Watchdog.csproj sr COPY src/Tgstation.Server.Host/Tgstation.Server.Host.csproj src/Tgstation.Server.Host/ COPY src/Tgstation.Server.Api/Tgstation.Server.Api.csproj src/Tgstation.Server.Api/ -COPY tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj tests/Tgstation.Server.Api.Tests/ -COPY tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj tests/Tgstation.Server.Host.Tests/ -COPY tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj tests/Tgstation.Server.Host.Watchdog.Tests/ -COPY tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj tests/Tgstation.Server.Host.Console.Tests/ - RUN dotnet restore -nowarn:MSB3202,nu1503 -p:RestoreUseSkipNonexistentTargets=false COPY . . @@ -26,13 +22,18 @@ RUN dotnet publish -c Release -o /app/lib/Default && mv /app/lib/Default/appsett FROM microsoft/dotnet:2.1-aspnetcore-runtime EXPOSE 80 +#needed for byond +RUN apt-get update \ + && apt-get install -y \ + gcc-multilib \ + && rm -rf /var/lib/apt/lists/* + WORKDIR /app COPY --from=build /app . COPY --from=build /src/build/tgs.docker.sh tgs.sh -COPY --from=build /src/src/Tgstation.Server.Host/appsettings.Docker.json . -RUN mkdir /config_data && mv appsettings.Docker.json /config_data/appsettings.Production.json -VOLUME ["/config_data", "/tgs_logs"] +RUN mkdir /config_data +VOLUME ["/config_data", "/tgs_logs", "/app/lib"] ENTRYPOINT ["./tgs.sh"] diff --git a/build/test_core.sh b/build/test_core.sh index a9477dfb24..071071c3ca 100755 --- a/build/test_core.sh +++ b/build/test_core.sh @@ -1,28 +1,47 @@ #!/bin/bash set -e +dotnet tool install --global coverlet.console + +mkdir TestResults + cd tests/Tgstation.Server.Api.Tests -dotnet test +dotnet build -c $CONFIG +$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp2.1/Tgstation.Server.Api.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/api.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Api.Tests*]*" cd ../Tgstation.Server.Client.Tests -dotnet test +dotnet build -c $CONFIG +$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp2.1/Tgstation.Server.Client.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/client.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Client.Tests*]*" cd ../Tgstation.Server.Host.Tests -dotnet test +dotnet build -c $CONFIG +$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp2.1/Tgstation.Server.Host.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/host.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Host.Tests*]*" --exclude "[Tgstation.Server.Host]Tgstation.Server.Host.Models.Migrations.*" cd ../Tgstation.Server.Host.Watchdog.Tests -dotnet test +dotnet build -c $CONFIG +$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp2.1/Tgstation.Server.Host.Watchdog.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/watchdog.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Host.Watchdog.Tests*]*" cd ../Tgstation.Server.Host.Console.Tests -dotnet test +dotnet build -c $CONFIG +$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp2.1/Tgstation.Server.Host.Console.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/console.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Host.Console.Tests*]*" cd ../Tgstation.Server.Tests - export TGS4_TEST_DATABASE_TYPE=MySql export TGS4_TEST_CONNECTION_STRING="server=127.0.0.1;uid=root;pwd=;database=tgs_test" -dotnet test +#token set in CI settings +dotnet build -c $CONFIG +$HOME/.dotnet/tools/coverlet bin/$CONFIG/netcoreapp2.1/Tgstation.Server.Tests.dll --target "dotnet" --targetargs "test -c $CONFIG --no-build" --format opencover --output "../../TestResults/server.xml" --include "[Tgstation.Server*]*" --exclude "[Tgstation.Server.Tests*]*" --exclude "[Tgstation.Server.Host]Tgstation.Server.Host.Models.Migrations.*" + +cd ../../TestResults + +bash <(curl -s https://codecov.io/bash) -f api.xml -F unittests +bash <(curl -s https://codecov.io/bash) -f client.xml -F unittests +bash <(curl -s https://codecov.io/bash) -f host.xml -F unittests +bash <(curl -s https://codecov.io/bash) -f watchdog.xml -F unittests +bash <(curl -s https://codecov.io/bash) -f console.xml -F unittests +bash <(curl -s https://codecov.io/bash) -f server.xml -F integration diff --git a/build/tgs.docker.sh b/build/tgs.docker.sh index f397e9b9b5..690f9e1c03 100755 --- a/build/tgs.docker.sh +++ b/build/tgs.docker.sh @@ -1,6 +1,5 @@ #!/bin/sh -mkdir /config_data -cp -r /config_data/* ./ +ln -s /config_data/appsettings.Production.json /app/appsettings.Production.json exec dotnet Tgstation.Server.Host.Console.dll "$@" diff --git a/docs/API.dox b/docs/API.dox index 49c19ce3ee..2eb1a0f76a 100644 --- a/docs/API.dox +++ b/docs/API.dox @@ -24,7 +24,6 @@ Last off, if anything in this API doesn't seem to hold true when tested against This document will reference the canonical C# models in the @ref Tgstation.Server.Api.Models namespace. Note that these models are built to mirror the JSON requests and responses with a couple caveats. - The first letter of every field name will/must be lowercase in JSON models -- Fields in the C# model marked with "DenyWrite = true" cannot be changed using POST requests - Fields marked 'Required' should be ignored, this is a semantic for the backing SQL database - Id fields must be specified when making POST requests - All other fields are optional and may be absent from responses or requests unless otherwise specified @@ -37,7 +36,7 @@ TGS4 expects this set of headers. Failure to provide them may result in 400 erro - Accept: application/json - Api: Another product header value representing the version of the API to use. Currently this must be: Tgstation.Server.Api/4.0.0.0 -For POST and PUT requests you must also include the content type. Currently only json is supported +For POST, PATCH, and PUT requests you must also include the content type. Currently only json is supported - Content-Type: application/json @@ -63,6 +62,7 @@ TGS will only every return the response codes listed here - 409: Conflict. Documented in the requests that use them - 410: Gone. Attempted to access/modify a resource that ideally should have been ready, but isn't or no longer is - 422: Unprocessable Entity: Used specifically when an operation that requires a server restart is unable to be performed due to the @ref Tgstation.Server.Host.Watchdog not being present in the deployment. Blame MSO. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage +- 424: Failed Dependency: When a request that depends on the GitHub API fails for a reason other than rate limiting. Check server logs, usually this indicates a bad access token. - 426: Upgrade required: Used when the client's API version is not compatible with the server's. Response body contains an @ref Tgstation.Server.Api.Models.ErrorMessage - 429: Rate limited. Used with operations that rely on GitHub.com. If a rate limit is hit for an operation this will be returned. Response will contain a Retry-After header - 500: Server error. Please report the request and response body to the code repository @@ -232,26 +232,26 @@ I DELETE "/Job/{JobId}" => OK @subsection api_chat Chat Bots -Each chat bot is represented by a @ref Tgstation.Server.Api.Models.ChatSettings object +Each chat bot is represented by a @ref Tgstation.Server.Api.Models.ChatBot object Chat bots can be created/updated/deleted with the following requests respectively -I PUT "/Chat" @ref Tgstation.Server.Api.Models.ChatSettings => Tgstation.Server.Api.Models.ChatSettings -I POST "/Chat" @ref Tgstation.Server.Api.Models.ChatSettings => Tgstation.Server.Api.Models.ChatSettings -I DELETE "/Chat/{ChatSettingsId}" => OK +I PUT "/Chat" @ref Tgstation.Server.Api.Models.ChatBot => Tgstation.Server.Api.Models.ChatBot +I POST "/Chat" @ref Tgstation.Server.Api.Models.ChatBot => Tgstation.Server.Api.Models.ChatBot +I DELETE "/Chat/{ChatBotId}" => OK -The @ref Tgstation.Server.Api.Models.Internal.ChatSettings.ConnectionString must differ based on what kind of chat bot you wish to create +The @ref Tgstation.Server.Api.Models.Internal.ChatBot.ConnectionString must differ based on what kind of chat bot you wish to create. Each @ref Tgstation.Server.Api.Models.ChatProvider has a @ref Tgstation.Server.Api.Models.Internal.ChatConnectionStringBuilder that dictates how to form it -For IRC chat bots it should be in the following format: -`";;;<1 to use SSL, 0 otherwise>[;<`The @ref Tgstation.Server.Api.Models.IrcPasswordType`;]"` +For IRC chat bots see @ref Tgstation.Server.Api.Models.IrcConnectionStringBuilder +For Discord chat bots see @ref Tgstation.Server.Api.Models.DiscordConnectionStringBuilder For Discord chat bots it should be the bot's Token A specific bot's settings may be retrieved with: -I GET "/Chat/{ChatSettingsId}" => @ref Tgstation.Server.Api.Models.ChatSettings +I GET "/Chat/{ChatBotId}" => @ref Tgstation.Server.Api.Models.ChatBot -Also note that if the @ref Tgstation.Server.Api.Models.ChatSettings.Channels is present in a POST request, the list will fully replace any active channels +Also note that if the @ref Tgstation.Server.Api.Models.ChatBot.Channels is present in a POST request, the list will fully replace any active channels @subsection api_byond Byond Version Management @@ -287,7 +287,7 @@ Modifications to the repository are done with the following request: I POST "/Repository" => @ref Tgstation.Server.Api.Models.Repository => @ref Tgstation.Server.Api.Models.Repository -Each update creates a job specified in the @ref Tgstation.Server.Api.Models.Repository.ActiveJob field jobs will be queued in succession. See below for post examples. +Each update that requires git changes creates a job specified in the @ref Tgstation.Server.Api.Models.Repository.ActiveJob field jobs will be queued in succession. See below for POST examples. @subsubsection api_repopost Repository Commands for Git Aliases @@ -448,7 +448,11 @@ To start the watchdog use the following request: I PUT "/DreamDaemon" Empty => @ref Tgstation.Server.Api.Models.Job -The returned job represents the startup process for the watchdog +To restart the watchdog use the following request: + +I PATCH "/DreamDaemon" Empty => @ref Tgstation.Server.Api.Models.Job + +The returned jobs represents the startup process for the watchdog To stop the watchdog use the following request: diff --git a/src/DMAPI/tgs.dm b/src/DMAPI/tgs.dm index 6e55567544..6645ce51ff 100644 --- a/src/DMAPI/tgs.dm +++ b/src/DMAPI/tgs.dm @@ -55,11 +55,16 @@ #define TGS_REBOOT_MODE_SHUTDOWN 1 #define TGS_REBOOT_MODE_RESTART 2 +#define TGS_SECURITY_TRUSTED 0 +#define TGS_SECURITY_SAFE 1 +#define TGS_SECURITY_ULTRASAFE 2 + //REQUIRED HOOKS //Call this somewhere in /world/New() that is always run //event_handler: optional user defined event handler. The default behaviour is to broadcast the event in english to all connected admin channels -/world/proc/TgsNew(datum/tgs_event_handler/event_handler) +//minimum_required_security_level: The minimum required security level to run the game in which the DMAPI is integrated +/world/proc/TgsNew(datum/tgs_event_handler/event_handler, minimum_required_security_level = TGS_SECURITY_ULTRASAFE) return //Call this when your initializations are complete and your game is ready to play before any player interactions happen @@ -155,6 +160,10 @@ /world/proc/TgsRevision() return +//Get the current BYOND security level +/world/proc/TgsSecurityLevel() + return + //Gets a list of active `/datum/tgs_revision_information/test_merge`s /world/proc/TgsTestMerges() return diff --git a/src/DMAPI/tgs/core/core.dm b/src/DMAPI/tgs/core/core.dm index 1158fdbd34..e0495aba4e 100644 --- a/src/DMAPI/tgs/core/core.dm +++ b/src/DMAPI/tgs/core/core.dm @@ -1,4 +1,4 @@ -/world/TgsNew(datum/tgs_event_handler/event_handler) +/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])!") @@ -18,7 +18,7 @@ TGS_WRITE_GLOBAL(tgs, new_api) - var/result = new_api.OnWorldNew(event_handler ? event_handler : new /datum/tgs_event_handler/tgs_default) + var/result = new_api.OnWorldNew(event_handler ? event_handler : new /datum/tgs_event_handler/tgs_default, minimum_required_security_level) if(!result || result == TGS_UNIMPLEMENTED) TGS_WRITE_GLOBAL(tgs, null) TGS_ERROR_LOG("Failed to activate API!") @@ -127,6 +127,11 @@ if(api) api.ChatPrivateMessage(message, user) +/world/TgsSecurityLevel() + var/datum/tgs_api/api = TGS_READ_GLOBAL(tgs) + if(api) + api.SecurityLevel() + /* The MIT License diff --git a/src/DMAPI/tgs/core/datum.dm b/src/DMAPI/tgs/core/datum.dm index f81569136c..b2f9b19cdd 100644 --- a/src/DMAPI/tgs/core/datum.dm +++ b/src/DMAPI/tgs/core/datum.dm @@ -46,6 +46,9 @@ TGS_PROTECT_DATUM(/datum/tgs_api) /datum/tgs_api/proc/ChatPrivateMessage(message, admin_only) return TGS_UNIMPLEMENTED +/datum/tgs_api/proc/SecurityLevel() + return TGS_UNIMPLEMENTED + /* The MIT License diff --git a/src/DMAPI/tgs/v3210/api.dm b/src/DMAPI/tgs/v3210/api.dm index 1c04be9212..63bc0beb2b 100644 --- a/src/DMAPI/tgs/v3210/api.dm +++ b/src/DMAPI/tgs/v3210/api.dm @@ -56,7 +56,7 @@ /datum/tgs_api/v3210/proc/file2list(filename) return splittext(trim_left(trim_right(file2text(filename))), "\n") -/datum/tgs_api/v3210/OnWorldNew(datum/tgs_event_handler/event_handler) //don't use event handling in this version +/datum/tgs_api/v3210/OnWorldNew(datum/tgs_event_handler/event_handler, minimum_required_security_level) //don't use event handling in this version . = FALSE comms_key = world.params[SERVICE_WORLD_PARAM] @@ -191,6 +191,9 @@ /datum/tgs_api/v3210/ChatPrivateMessage(message, datum/tgs_chat_user/user) return TGS_UNIMPLEMENTED +/datum/tgs_api/v3210/SecurityLevel() + return TGS_SECURITY_TRUSTED + #undef REBOOT_MODE_NORMAL #undef REBOOT_MODE_HARD #undef REBOOT_MODE_SHUTDOWN diff --git a/src/DMAPI/tgs/v4/api.dm b/src/DMAPI/tgs/v4/api.dm index 92251d1e43..aedcf1429e 100644 --- a/src/DMAPI/tgs/v4/api.dm +++ b/src/DMAPI/tgs/v4/api.dm @@ -32,6 +32,9 @@ var/chat_commands_json_path var/server_commands_json_path var/reboot_mode = TGS_REBOOT_MODE_NORMAL + var/security_level + + var/requesting_new_port = FALSE var/list/intercepted_message_queue @@ -48,7 +51,7 @@ /datum/tgs_api/v4/ApiVersion() return "4.0.0.0" -/datum/tgs_api/v4/OnWorldNew(datum/tgs_event_handler/event_handler) +/datum/tgs_api/v4/OnWorldNew(datum/tgs_event_handler/event_handler, minimum_required_security_level) json_path = world.params[TGS4_PARAM_INFO_JSON] if(!json_path) TGS_ERROR_LOG("Missing [TGS4_PARAM_INFO_JSON] world parameter!") @@ -63,14 +66,14 @@ return access_identifier = cached_json["accessIdentifier"] - instance_name = text2num(cached_json["instanceName"]) server_commands_json_path = cached_json["serverCommandsJson"] if(cached_json["apiValidateOnly"]) TGS_INFO_LOG("Validating API and exiting...") - Export(TGS4_COMM_VALIDATE) + Export(TGS4_COMM_VALIDATE, list(TGS4_PARAMETER_DATA = "[minimum_required_security_level]")) del(world) + security_level = cached_json["securityLevel"] chat_channels_json_path = cached_json["chatChannelsJson"] chat_commands_json_path = cached_json["chatCommandsJson"] src.event_handler = event_handler @@ -165,6 +168,7 @@ event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) if(!world.OpenPort(new_port)) return "Port change failed!" + return if(TGS4_TOPIC_CHANGE_REBOOT_MODE) var/new_reboot_mode = text2num(params[TGS4_PARAMETER_DATA]) event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) @@ -173,27 +177,31 @@ return "Unknown command: [command]" -/datum/tgs_api/v4/proc/Export(command, list/data) +/datum/tgs_api/v4/proc/Export(command, list/data, override_requesting_new_port = FALSE) if(!data) data = list() data[TGS4_PARAMETER_COMMAND] = command var/json = json_encode(data) + while(requesting_new_port && !override_requesting_new_port) + sleep(1) + //we need some port open at this point to facilitate return communication if(!world.port) + requesting_new_port = TRUE if(!world.OpenPort(0)) //open any port TGS_ERROR_LOG("Unable to open random port to retrieve new port![TGS4_PORT_CRITFAIL_MESSAGE]") del(world) //request a new port export_lock = FALSE - var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list("current_port" = "[world.port]")) //stringify this on purpose + var/list/new_port_json = Export(TGS4_COMM_NEW_PORT, list(TGS4_PARAMETER_DATA = "[world.port]"), TRUE) //stringify this on purpose if(!new_port_json) TGS_ERROR_LOG("No new port response from server![TGS4_PORT_CRITFAIL_MESSAGE]") del(world) - var/new_port = new_port_json["port"] + var/new_port = new_port_json[TGS4_PARAMETER_DATA] if(!isnum(new_port) || new_port <= 0) TGS_ERROR_LOG("Malformed new port json ([json_encode(new_port_json)])![TGS4_PORT_CRITFAIL_MESSAGE]") del(world) @@ -201,6 +209,7 @@ if(new_port != world.port && !world.OpenPort(new_port)) TGS_ERROR_LOG("Unable to open port [new_port]![TGS4_PORT_CRITFAIL_MESSAGE]") del(world) + requesting_new_port = FALSE while(export_lock) sleep(1) @@ -221,14 +230,13 @@ export_lock = FALSE /datum/tgs_api/v4/OnReboot() - var/json = Export(TGS4_COMM_WORLD_REBOOT) - var/list/result = json_decode(json) + var/list/result = Export(TGS4_COMM_WORLD_REBOOT) if(!result) return //okay so the standard TGS4 proceedure is: right before rebooting change the port to whatever was sent to us in the above json's data parameter - var/port = json[TGS4_PARAMETER_DATA] + var/port = result[TGS4_PARAMETER_DATA] if(!isnum(port)) return //this is valid, server may just want use to reboot @@ -295,6 +303,9 @@ channel.custom_tag = channel_json["tag"] return channel +/datum/tgs_api/v4/SecurityLevel() + return security_level + /* The MIT License diff --git a/src/Tgstation.Server.Api/Models/ChatBot.cs b/src/Tgstation.Server.Api/Models/ChatBot.cs index 716469fd6e..97ce91ce3a 100644 --- a/src/Tgstation.Server.Api/Models/ChatBot.cs +++ b/src/Tgstation.Server.Api/Models/ChatBot.cs @@ -15,15 +15,15 @@ namespace Tgstation.Server.Api.Models /// /// Validates are correct for the /// - /// + /// if the are valid for the , otherwise public bool ValidateProviderChannelTypes() { switch (Provider) { case ChatProvider.Discord: - return Channels.Select(x => x.DiscordChannelId.HasValue && x.IrcChannel == null).All(x => x); + return Channels?.Select(x => x.DiscordChannelId.HasValue && x.IrcChannel == null).All(x => x) ?? true; case ChatProvider.Irc: - return Channels.Select(x => !x.DiscordChannelId.HasValue && x.IrcChannel != null).All(x => x); + return Channels?.Select(x => !x.DiscordChannelId.HasValue && x.IrcChannel != null).All(x => x) ?? true; default: throw new InvalidOperationException("Invalid provider type!"); } diff --git a/src/Tgstation.Server.Api/Models/CompilerStatus.cs b/src/Tgstation.Server.Api/Models/CompilerStatus.cs deleted file mode 100644 index 9cb0a7a5ed..0000000000 --- a/src/Tgstation.Server.Api/Models/CompilerStatus.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace Tgstation.Server.Api.Models -{ - /// - /// Status of the for an - /// -#pragma warning disable CA1717 // Only FlagsAttribute enums should have plural names - public enum CompilerStatus -#pragma warning restore CA1717 // Only FlagsAttribute enums should have plural names - { - /// - /// The is idle - /// - Idle, - /// - /// The is being copied - /// - Copying, - /// - /// Pre-compile scripts are running - /// - PreCompile, - /// - /// The .dme is having it's server side modifications applied - /// - Modifying, - /// - /// DreamMaker is running - /// - Compiling, - /// - /// The DMAPI is being verified - /// - Verifying, - /// - /// Post-compile scripts are running - /// - PostCompile, - /// - /// The compile results are being duplicated - /// - Duplicating, - /// - /// The configuration is being linked to the compile results - /// - Symlinking, - /// - /// A failed compile job is being erased - /// - Cleanup - } -} \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs new file mode 100644 index 0000000000..a919e18380 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/DiscordConnectionStringBuilder.cs @@ -0,0 +1,37 @@ +using System; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Api.Models +{ + /// + /// for + /// + public sealed class DiscordConnectionStringBuilder : ChatConnectionStringBuilder + { + /// + public override bool Valid => !String.IsNullOrEmpty(BotToken); + + /// + /// The Discord bot token + /// + /// See https://discordapp.com/developers/docs/topics/oauth2#bots + public string BotToken { get; set; } + + /// + /// Construct a + /// + public DiscordConnectionStringBuilder() { } + + /// + /// Construct a from a + /// + /// The connection string + public DiscordConnectionStringBuilder(string connectionString) + { + BotToken = connectionString ?? throw new ArgumentNullException(nameof(connectionString)); + } + + /// + public override string ToString() => BotToken; + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/DreamDaemon.cs b/src/Tgstation.Server.Api/Models/DreamDaemon.cs index f78ffd2f15..ac982cf3d0 100644 --- a/src/Tgstation.Server.Api/Models/DreamDaemon.cs +++ b/src/Tgstation.Server.Api/Models/DreamDaemon.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Api.Models public bool? Running { get; set; } /// - /// The current of + /// The current of . May be downgraded due to requirements of /// public DreamDaemonSecurity? CurrentSecurity { get; set; } diff --git a/src/Tgstation.Server.Api/Models/DreamMaker.cs b/src/Tgstation.Server.Api/Models/DreamMaker.cs index 93c44bf71e..bb821fdc63 100644 --- a/src/Tgstation.Server.Api/Models/DreamMaker.cs +++ b/src/Tgstation.Server.Api/Models/DreamMaker.cs @@ -1,15 +1,27 @@ -using Tgstation.Server.Api.Models.Internal; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models { /// /// Represents the state of the DreamMaker compiler. Create action starts a new compile. Delete action cancels the current compile /// - public sealed class DreamMaker : DreamMakerSettings + public class DreamMaker { /// - /// The of the compiler + /// The .dme file tries to compile with without the extension /// - public CompilerStatus Status { get; set; } + public string ProjectName { get; set; } + + /// + /// The port used during compilation to validate the DMAPI + /// + [Required] + public ushort? ApiValidationPort { get; set; } + + /// + /// The level used to validate the DMAPI + /// + [Required] + public DreamDaemonSecurity? ApiValidationSecurityLevel { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs index 784133ffd2..577321132d 100644 --- a/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs +++ b/src/Tgstation.Server.Api/Models/Internal/ChatBot.cs @@ -1,4 +1,6 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; namespace Tgstation.Server.Api.Models.Internal { @@ -33,5 +35,33 @@ namespace Tgstation.Server.Api.Models.Internal /// [Required] public string ConnectionString { get; set; } + + /// + /// The which maps to the + /// + [NotMapped] + public ChatConnectionStringBuilder ConnectionStringBuilder + { + get + { + if (ConnectionString == null) + return null; + switch (Provider) + { + case ChatProvider.Discord: + return new DiscordConnectionStringBuilder(ConnectionString); + case ChatProvider.Irc: + return new IrcConnectionStringBuilder(ConnectionString); + default: + throw new InvalidOperationException("Invalid Provider!"); + } + } + set + { + if (value?.Valid == false) + throw new InvalidOperationException("Cannot set invalid ChatConnectionStringBuilder!"); + ConnectionString = value?.ToString(); + } + } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs new file mode 100644 index 0000000000..b5d3b5f9a3 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/Internal/ChatConnectionStringBuilder.cs @@ -0,0 +1,19 @@ +namespace Tgstation.Server.Api.Models.Internal +{ + /// + /// Helper for building s + /// + public abstract class ChatConnectionStringBuilder + { + /// + /// If the evaluates to a valid + /// + public abstract bool Valid { get; } + + /// + /// Gets the associated with the + /// + /// + public abstract override string ToString(); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs index bcd2801a51..c659c49737 100644 --- a/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs +++ b/src/Tgstation.Server.Api/Models/Internal/CompileJob.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models.Internal { @@ -15,16 +16,25 @@ namespace Tgstation.Server.Api.Models.Internal /// /// The .dme file used for compilation /// + [Required] public string DmeName { get; set; } /// /// Textual output of DM /// + [Required] public string Output { get; set; } /// /// The Game folder the results were compiled into /// + [Required] public Guid? DirectoryName { get; set; } + + /// + /// The minimum required to run the 's output + /// + [Required] + public DreamDaemonSecurity? MinimumSecurityLevel { get; set; } } } diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs index b1f3496329..5e79d35cff 100644 --- a/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs +++ b/src/Tgstation.Server.Api/Models/Internal/DreamDaemonLaunchParameters.cs @@ -1,4 +1,5 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; namespace Tgstation.Server.Api.Models.Internal { @@ -36,5 +37,17 @@ namespace Tgstation.Server.Api.Models.Internal /// [Required] public uint? StartupTimeout { get; set; } + + /// + /// Check if we match a given set of + /// + /// The to compare against + /// if they match, otherwise + public bool Match(DreamDaemonLaunchParameters otherParameters) => + AllowWebClient == otherParameters.AllowWebClient + && SecurityLevel == otherParameters.SecurityLevel + && PrimaryPort == otherParameters.PrimaryPort + && SecondaryPort == otherParameters.SecondaryPort + && StartupTimeout == otherParameters.StartupTimeout; } } \ No newline at end of file diff --git a/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs b/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs deleted file mode 100644 index a7b649d80c..0000000000 --- a/src/Tgstation.Server.Api/Models/Internal/DreamMakerSettings.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace Tgstation.Server.Api.Models.Internal -{ - /// - /// Configurable settings for - /// - public class DreamMakerSettings - { - /// - /// The .dme file tries to compile with without the extension - /// - public string ProjectName { get; set; } - - /// - /// The port used during compilation to validate the DMAPI - /// - [Required] - public ushort? ApiValidationPort { get; set; } - } -} diff --git a/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs b/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs new file mode 100644 index 0000000000..55df172e51 --- /dev/null +++ b/src/Tgstation.Server.Api/Models/IrcConnectionStringBuilder.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Tgstation.Server.Api.Models.Internal; + +namespace Tgstation.Server.Api.Models +{ + /// + /// for + /// + public sealed class IrcConnectionStringBuilder : ChatConnectionStringBuilder + { + /// + public override bool Valid => Address != null && Port.HasValue && Port != 0 && UseSsl.HasValue && (PasswordType.HasValue ^ Password == null); + + /// + /// The IP address or URL of the IRC server + /// + public string Address { get; set; } + + /// + /// The port the server runs on + /// + public ushort? Port { get; set; } + + /// + /// The nickname for the bot to use + /// + public string Nickname { get; set; } + + /// + /// If the connection should be made using SSL + /// + public bool? UseSsl { get; set; } + + /// + /// The optional to use + /// + public IrcPasswordType? PasswordType { get; set; } + + /// + /// The optional password to use + /// + public string Password { get; set; } + + /// + /// Construct an + /// + public IrcConnectionStringBuilder() { } + + /// + /// Construct a from a + /// + /// The connection string + public IrcConnectionStringBuilder(string connectionString) + { + if (connectionString == null) + throw new ArgumentNullException(nameof(connectionString)); + var splits = connectionString.Split(';'); + + Address = splits[0]; + + if (splits.Length < 2) + return; + + if (UInt16.TryParse(splits[1], out var port)) + Port = port; + + if (splits.Length < 3) + return; + + Nickname = splits[2]; + + if (splits.Length < 4) + return; + + if (Int32.TryParse(splits[3], out var intSsl)) + UseSsl = Convert.ToBoolean(intSsl); + + if (splits.Length < 5) + return; + if (Enum.TryParse(splits[4], out var passwordType)) + switch (passwordType) + { + case IrcPasswordType.NickServ: + case IrcPasswordType.Sasl: + case IrcPasswordType.Server: + PasswordType = passwordType; + break; + } + + if (splits.Length < 6) + return; + + var rest = new List(splits); + rest.RemoveRange(0, 5); + Password = String.Join(";", rest); + } + + /// + public override string ToString() + { + var sb = new StringBuilder(); + sb.Append(Address); + sb.Append(';'); + sb.Append(Port); + sb.Append(';'); + sb.Append(Nickname); + sb.Append(';'); + if(UseSsl.HasValue) + sb.Append(Convert.ToInt32(UseSsl.Value)); + if (PasswordType.HasValue) + { + sb.Append(';'); + sb.Append((int)PasswordType); + sb.Append(';'); + sb.Append(Password); + } + return sb.ToString(); + } + } +} diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs similarity index 60% rename from src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs rename to src/Tgstation.Server.Api/Models/IrcPasswordType.cs index d31a3aba73..bb3451bfbd 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcPasswordType.cs +++ b/src/Tgstation.Server.Api/Models/IrcPasswordType.cs @@ -1,9 +1,9 @@ -namespace Tgstation.Server.Host.Components.Chat.Providers +namespace Tgstation.Server.Api.Models { /// - /// Represents the type of a password passed to the constructor of + /// Represents the type of a password for a /// - enum IrcPasswordType + public enum IrcPasswordType { /// /// Use server authentication diff --git a/src/Tgstation.Server.Api/Models/Repository.cs b/src/Tgstation.Server.Api/Models/Repository.cs index 83f610ba5a..ee50701502 100644 --- a/src/Tgstation.Server.Api/Models/Repository.cs +++ b/src/Tgstation.Server.Api/Models/Repository.cs @@ -23,9 +23,14 @@ namespace Tgstation.Server.Api.Models public RevisionInformation RevisionInformation { get; set; } /// - /// If the repository was cloned from GitHub.com. If this enables test merge functionality + /// If the repository was cloned from GitHub.com this will be set with the owner of the repository /// - public bool? IsGitHub { get; set; } + public string GitHubOwner { get; set; } + + /// + /// If the repository was cloned from GitHub.com this will be set with the name of the repository + /// + public string GitHubName { get; set; } /// /// The started by the if any diff --git a/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs b/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs index 439a13d81c..e9b04645f4 100644 --- a/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs +++ b/src/Tgstation.Server.Api/Rights/DreamMakerRights.cs @@ -25,16 +25,20 @@ namespace Tgstation.Server.Api.Rights /// CancelCompile = 4, /// - /// User may modify + /// User may modify /// SetDme = 8, /// - /// User may modify + /// User may modify /// SetApiValidationPort = 16, /// /// User may list and read all s /// - CompileJobs = 32 + CompileJobs = 32, + /// + /// User may modify + /// + SetSecurityLevel = 64 } } diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index da1bfc9bd9..2ba4b4b19b 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -48,6 +48,16 @@ namespace Tgstation.Server.Api /// public const string Configuration = Root + "Config"; + /// + /// To be paired with for accessing s + /// + public const string File = "File"; + + /// + /// Full combination of and + /// + public const string ConfigurationFile = Configuration + "/" + File; + /// /// The controller /// diff --git a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj index 20370a3c63..6f2d970b73 100644 --- a/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj +++ b/src/Tgstation.Server.Api/Tgstation.Server.Api.csproj @@ -17,7 +17,7 @@ 4.0.0.0 json web api tgstation-server tgstation ss13 byond Prototype release - 4.0.0.0-preview6001 + 4.0.0.0-preview6007 diff --git a/src/Tgstation.Server.Client/ApiClient.cs b/src/Tgstation.Server.Client/ApiClient.cs index e20a143c47..8107fc1913 100644 --- a/src/Tgstation.Server.Client/ApiClient.cs +++ b/src/Tgstation.Server.Client/ApiClient.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Serialization; using System; using System.Collections.Generic; @@ -32,7 +33,7 @@ namespace Tgstation.Server.Client /// /// The for the /// - readonly HttpClient httpClient; + readonly IHttpClient httpClient; /// /// The s used by the @@ -42,14 +43,15 @@ namespace Tgstation.Server.Client /// /// Construct an /// + /// The value of /// The value of /// The value of - public ApiClient(Uri url, ApiHeaders apiHeaders) + public ApiClient(IHttpClient httpClient, Uri url, ApiHeaders apiHeaders) { + this.httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); Url = url ?? throw new ArgumentNullException(nameof(url)); Headers = apiHeaders ?? throw new ArgumentNullException(nameof(apiHeaders)); - - httpClient = new HttpClient(); + requestLoggers = new List(); } @@ -80,7 +82,8 @@ namespace Tgstation.Server.Client var serializerSettings = new JsonSerializerSettings { - ContractResolver = new CamelCasePropertyNamesContractResolver() + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new[] { new VersionConverter() } }; if (body != null) @@ -161,7 +164,14 @@ namespace Tgstation.Server.Client if (String.IsNullOrWhiteSpace(json)) json = JsonConvert.SerializeObject(new object()); - return JsonConvert.DeserializeObject(json, serializerSettings); + try + { + return JsonConvert.DeserializeObject(json, serializerSettings); + } + catch (JsonException) + { + throw new UnrecognizedResponseException(json, response.StatusCode); + } } /// @@ -206,6 +216,9 @@ namespace Tgstation.Server.Client /// public Task Create(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), HttpMethod.Put, instanceId, cancellationToken); + /// + public Task Patch(string route, long instanceId, CancellationToken cancellationToken) => RunRequest(route, new object(), new HttpMethod("PATCH"), instanceId, cancellationToken); + /// public void AddRequestLogger(IRequestLogger requestLogger) => requestLoggers.Add(requestLogger ?? throw new ArgumentNullException(nameof(requestLogger))); } diff --git a/src/Tgstation.Server.Client/ApiClientFactory.cs b/src/Tgstation.Server.Client/ApiClientFactory.cs index 64dae0469f..0b686b77c2 100644 --- a/src/Tgstation.Server.Client/ApiClientFactory.cs +++ b/src/Tgstation.Server.Client/ApiClientFactory.cs @@ -7,6 +7,6 @@ namespace Tgstation.Server.Client sealed class ApiClientFactory : IApiClientFactory { /// - public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders) => new ApiClient(url, apiHeaders); + public IApiClient CreateApiClient(Uri url, ApiHeaders apiHeaders) => new ApiClient(new HttpClient(), url, apiHeaders); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Client/AssemblyInfo.cs b/src/Tgstation.Server.Client/AssemblyInfo.cs new file mode 100644 index 0000000000..0dffa25c06 --- /dev/null +++ b/src/Tgstation.Server.Client/AssemblyInfo.cs @@ -0,0 +1,4 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tgstation.Server.Client.Tests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] diff --git a/src/Tgstation.Server.Client/Components/ByondClient.cs b/src/Tgstation.Server.Client/Components/ByondClient.cs index e75c47110c..92c2f8aba5 100644 --- a/src/Tgstation.Server.Client/Components/ByondClient.cs +++ b/src/Tgstation.Server.Client/Components/ByondClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -30,9 +31,12 @@ namespace Tgstation.Server.Client.Components } /// - public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.Byond, instance.Id, cancellationToken); + public Task ActiveVersion(CancellationToken cancellationToken) => apiClient.Read(Routes.Byond, instance.Id, cancellationToken); /// - public Task Update(Byond byond, CancellationToken cancellationToken) => apiClient.Update(Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, cancellationToken); + public Task> InstalledVersions(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Byond), instance.Id, cancellationToken); + + /// + public Task SetActiveVersion(Byond byond, CancellationToken cancellationToken) => apiClient.Update(Routes.Byond, byond ?? throw new ArgumentNullException(nameof(byond)), instance.Id, cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs index 73cdd02680..5c303f5f77 100644 --- a/src/Tgstation.Server.Client/Components/ChatBotsClient.cs +++ b/src/Tgstation.Server.Client/Components/ChatBotsClient.cs @@ -37,9 +37,12 @@ namespace Tgstation.Server.Client.Components public Task Delete(ChatBot settings, CancellationToken cancellationToken) => apiClient.Delete(Routes.SetID(Routes.Chat, settings?.Id ?? throw new ArgumentNullException(nameof(settings))), instance.Id, cancellationToken); /// - public Task> List(CancellationToken cancellationToken) => apiClient.Create>(Routes.List(Routes.Chat), instance.Id, cancellationToken); + public Task> List(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Chat), instance.Id, cancellationToken); /// public Task Update(ChatBot settings, CancellationToken cancellationToken) => apiClient.Update(Routes.Chat, settings ?? throw new ArgumentNullException(nameof(settings)), instance.Id, cancellationToken); + + /// + public Task GetId(ChatBot settings, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.Chat, (settings ?? throw new ArgumentNullException(nameof(settings))).Id), instance.Id, cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs index d3f1a5bccf..1f1af74469 100644 --- a/src/Tgstation.Server.Client/Components/ConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/ConfigurationClient.cs @@ -47,6 +47,9 @@ namespace Tgstation.Server.Client.Components /// public Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Delete(Routes.Configuration, directory, instance.Id, cancellationToken); + /// + public Task CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken) => apiClient.Create(Routes.Configuration, directory, instance.Id, cancellationToken); + /// public Task> List(string directory, CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.Configuration) + SanitizeGetPath(directory), instance.Id, cancellationToken); @@ -55,7 +58,7 @@ namespace Tgstation.Server.Client.Components { if (file == null) throw new ArgumentNullException(nameof(file)); - return apiClient.Read(Routes.Configuration + SanitizeGetPath(file.Path), instance.Id, cancellationToken); + return apiClient.Read(Routes.ConfigurationFile + SanitizeGetPath(file.Path), instance.Id, cancellationToken); } /// diff --git a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs index e0815a873b..e077ea142d 100644 --- a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs @@ -33,7 +33,10 @@ namespace Tgstation.Server.Client.Components public Task Shutdown(CancellationToken cancellationToken) => apiClient.Delete(Routes.DreamDaemon, instance.Id, cancellationToken); /// - public Task Start(CancellationToken cancellationToken) => apiClient.Create(Routes.DreamDaemon, instance.Id, cancellationToken); + public Task Start(CancellationToken cancellationToken) => apiClient.Create(Routes.DreamDaemon, instance.Id, cancellationToken); + + /// + public Task Restart(CancellationToken cancellationToken) => apiClient.Patch(Routes.DreamDaemon, instance.Id, cancellationToken); /// public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamDaemon, instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs index 5e8d2af1b9..942d252812 100644 --- a/src/Tgstation.Server.Client/Components/DreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamMakerClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; @@ -12,11 +13,12 @@ namespace Tgstation.Server.Client.Components /// /// The for the /// - private IApiClient apiClient; + readonly IApiClient apiClient; + /// /// The for the /// - private Instance instance; + readonly Instance instance; /// /// Construct a @@ -32,6 +34,12 @@ namespace Tgstation.Server.Client.Components /// public Task Compile(CancellationToken cancellationToken) => apiClient.Create(Routes.DreamMaker, instance.Id, cancellationToken); + /// + public Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken) => apiClient.Read(Routes.SetID(Routes.DreamMaker, compileJob?.Id ?? throw new ArgumentNullException(nameof(compileJob))), instance.Id, cancellationToken); + + /// + public Task> GetJobIds(CancellationToken cancellationToken) => apiClient.Read>(Routes.List(Routes.DreamMaker), instance.Id, cancellationToken); + /// public Task Read(CancellationToken cancellationToken) => apiClient.Read(Routes.DreamMaker, instance.Id, cancellationToken); diff --git a/src/Tgstation.Server.Client/Components/IByondClient.cs b/src/Tgstation.Server.Client/Components/IByondClient.cs index bfe9e8f003..da4f4d45a3 100644 --- a/src/Tgstation.Server.Client/Components/IByondClient.cs +++ b/src/Tgstation.Server.Client/Components/IByondClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -10,11 +11,18 @@ namespace Tgstation.Server.Client.Components public interface IByondClient { /// - /// Get the information + /// Get the active information /// /// The for the operation - /// A resulting in the information - Task Read(CancellationToken cancellationToken); + /// A resulting in the active information + Task ActiveVersion(CancellationToken cancellationToken); + + /// + /// Get all installed s + /// + /// The for the operation + /// A resulting in an of installed s + Task> InstalledVersions(CancellationToken cancellationToken); /// /// Updates the information @@ -22,6 +30,6 @@ namespace Tgstation.Server.Client.Components /// The information to update /// The for the operation /// A resulting in the updated information - Task Update(Byond byond, CancellationToken cancellationToken); + Task SetActiveVersion(Byond byond, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/Components/IChatBotsClient.cs b/src/Tgstation.Server.Client/Components/IChatBotsClient.cs index 538fdf72bc..c3f223938e 100644 --- a/src/Tgstation.Server.Client/Components/IChatBotsClient.cs +++ b/src/Tgstation.Server.Client/Components/IChatBotsClient.cs @@ -11,7 +11,7 @@ namespace Tgstation.Server.Client.Components public interface IChatBotsClient { /// - /// List the + /// List the s /// /// The for the operation /// A resulting in a of the of the server @@ -26,13 +26,21 @@ namespace Tgstation.Server.Client.Components Task Create(ChatBot settings, CancellationToken cancellationToken); /// - /// Updates a setttings + /// Updates a 's setttings /// /// The to update /// The for the operation /// A resulting in the updated Task Update(ChatBot settings, CancellationToken cancellationToken); + /// + /// Get a 's setttings + /// + /// The to get + /// The for the operation + /// A resulting in the + Task GetId(ChatBot settings, CancellationToken cancellationToken); + /// /// Delete a /// diff --git a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs index 2d2337ff50..d61e426af3 100644 --- a/src/Tgstation.Server.Client/Components/IConfigurationClient.cs +++ b/src/Tgstation.Server.Client/Components/IConfigurationClient.cs @@ -41,5 +41,13 @@ namespace Tgstation.Server.Client.Components /// The for the operation /// A representing the running operation Task DeleteEmptyDirectory(ConfigurationFile directory, CancellationToken cancellationToken); + + /// + /// Creates an empty + /// + /// The representing the directory to create + /// The for the operation + /// A resulting in the new + Task CreateDirectory(ConfigurationFile directory, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs index 0bb8a1d72b..7b858cba6b 100644 --- a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs +++ b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs @@ -20,8 +20,15 @@ namespace Tgstation.Server.Client.Components /// Start /// /// The for the operation - /// A resulting in the information - Task Start(CancellationToken cancellationToken); + /// A resulting in the of the running operation + Task Start(CancellationToken cancellationToken); + + /// + /// Restart + /// + /// The for the operation + /// A resulting in the of the running operation + Task Restart(CancellationToken cancellationToken); /// /// Shutdown diff --git a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs index dd18119024..011475d5a5 100644 --- a/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs +++ b/src/Tgstation.Server.Client/Components/IDreamMakerClient.cs @@ -1,4 +1,5 @@ -using System.Threading; +using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; @@ -30,5 +31,20 @@ namespace Tgstation.Server.Client.Components /// The for the operation /// A resulting in the for the compile Task Compile(CancellationToken cancellationToken); + + /// + /// Gets the s of all s for the instance + /// + /// The for the operation + /// A resulting in a of s with only the field populated + Task> GetJobIds(CancellationToken cancellationToken); + + /// + /// Get a + /// + /// The to get + /// The for the operation + /// A resulting in the + Task GetCompileJob(CompileJob compileJob, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Client/HttpClient.cs b/src/Tgstation.Server.Client/HttpClient.cs new file mode 100644 index 0000000000..c01042ae2d --- /dev/null +++ b/src/Tgstation.Server.Client/HttpClient.cs @@ -0,0 +1,37 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Client +{ + /// + sealed class HttpClient : IHttpClient + { + /// + public TimeSpan Timeout + { + get => httpClient.Timeout; + set => httpClient.Timeout = value; + } + + /// + /// The real + /// + readonly System.Net.Http.HttpClient httpClient; + + /// + /// Construct an + /// + public HttpClient() + { + httpClient = new System.Net.Http.HttpClient(); + } + + /// + public void Dispose() => httpClient.Dispose(); + + /// + public Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) => httpClient.SendAsync(request, cancellationToken); + } +} diff --git a/src/Tgstation.Server.Client/IApiClient.cs b/src/Tgstation.Server.Client/IApiClient.cs index 82b1298122..48c7d93234 100644 --- a/src/Tgstation.Server.Client/IApiClient.cs +++ b/src/Tgstation.Server.Client/IApiClient.cs @@ -32,6 +32,7 @@ namespace Tgstation.Server.Client Task Create(string route, TBody body, long instanceId, CancellationToken cancellationToken); Task Create(string route, long instanceId, CancellationToken cancellationToken); + Task Patch(string route, long instanceId, CancellationToken cancellationToken); Task Read(string route, long instanceId, CancellationToken cancellationToken); Task Update(string route, TBody body, long instanceId, CancellationToken cancellationToken); Task Delete(string route, long instanceId, CancellationToken cancellationToken); diff --git a/src/Tgstation.Server.Client/IHttpClient.cs b/src/Tgstation.Server.Client/IHttpClient.cs new file mode 100644 index 0000000000..a1e146624e --- /dev/null +++ b/src/Tgstation.Server.Client/IHttpClient.cs @@ -0,0 +1,26 @@ +using System; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Client +{ + /// + /// For sending HTTP requests + /// + interface IHttpClient : IDisposable + { + /// + /// The request timeout + /// + TimeSpan Timeout { get; set; } + + /// + /// Send an HTTP request + /// + /// The + /// The for the operation + /// A resulting in the of the request + Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj index 7e0b60fb65..3f96200434 100644 --- a/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj +++ b/src/Tgstation.Server.Client/Tgstation.Server.Client.csproj @@ -3,7 +3,7 @@ netstandard2.0 Full - 4.0.0.0-preview9102 + 4.0.0.0-preview9116 true Cyberboss /tg/station 13 @@ -24,6 +24,7 @@ true bin\Release\netstandard2.0\Tgstation.Server.Client.xml + 0 diff --git a/src/Tgstation.Server.Client/UnrecognizedResponseException.cs b/src/Tgstation.Server.Client/UnrecognizedResponseException.cs new file mode 100644 index 0000000000..24dfa9227d --- /dev/null +++ b/src/Tgstation.Server.Client/UnrecognizedResponseException.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; +using System.Net; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client +{ + sealed class UnrecognizedResponseException : ClientException + { + /// + /// Construct an with the of a response body and the + /// + /// The body of the response + /// The for the + public UnrecognizedResponseException(string data, HttpStatusCode statusCode) : base(new ErrorMessage + { + Message = String.Format(CultureInfo.InvariantCulture, "Unrecognized response body: {0}", data), + SeverApiVersion = null + }, statusCode) + { } + + /// + /// Construct a + /// + public UnrecognizedResponseException() { } + + /// + /// Construct an with a + /// + /// The message for the + public UnrecognizedResponseException(string message) : base(message) { } + + /// + /// Construct an with a and + /// + /// The message for the + /// The inner for the base + public UnrecognizedResponseException(string message, Exception innerException) : base(message, innerException) { } + } +} diff --git a/src/Tgstation.Server.Host.Console/Properties/launchSettings.json b/src/Tgstation.Server.Host.Console/Properties/launchSettings.json index d8b2b440e6..6bfbf6e27c 100644 --- a/src/Tgstation.Server.Host.Console/Properties/launchSettings.json +++ b/src/Tgstation.Server.Host.Console/Properties/launchSettings.json @@ -11,7 +11,7 @@ "Tgstation.Server.Host.Console": { "commandName": "Project", "commandLineArgs": "--attach-host-debugger", - "workingDirectory": "bin\\Debug\\netcoreapp2.0", + "workingDirectory": "bin\\Debug\\netcoreapp2.1", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" diff --git a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj index dbaae93de8..99e069d7fb 100644 --- a/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj +++ b/src/Tgstation.Server.Host.Console/Tgstation.Server.Host.Console.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.0 + netcoreapp2.1 Full 4.0.0.0 @@ -11,7 +11,7 @@ latest true - bin\Release\netcoreapp2.0\Tgstation.Server.Host.Console.xml + bin\Release\netcoreapp2.1\Tgstation.Server.Host.Console.xml diff --git a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs index 2f8905d7ad..8038984135 100644 --- a/src/Tgstation.Server.Host.Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host.Watchdog/Watchdog.cs @@ -35,36 +35,39 @@ namespace Tgstation.Server.Host.Watchdog { logger.LogInformation("Host watchdog starting..."); - var enviromentPath = Environment.GetEnvironmentVariable("PATH"); - var paths = enviromentPath.Split(';'); - var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - - var exeName = "dotnet"; - IEnumerable enumerator; - if (isWindows) + string updateDirectory = null; + try { - exeName += ".exe"; - enumerator = paths; - } - else - enumerator = paths.Select(x => x.Split(':')).SelectMany(x => x); + var enviromentPath = Environment.GetEnvironmentVariable("PATH"); + var paths = enviromentPath.Split(';'); + var isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows); - enumerator = enumerator.Select(x => Path.Combine(x, exeName)); + var exeName = "dotnet"; + IEnumerable enumerator; + if (isWindows) + { + exeName += ".exe"; + enumerator = paths; + } + else + enumerator = paths.Select(x => x.Split(':')).SelectMany(x => x); - var dotnetPath = enumerator - .Where(x => - { - logger.LogTrace("Checking for dotnet at {0}", x); - return File.Exists(x); - }) - .FirstOrDefault(); + enumerator = enumerator.Select(x => Path.Combine(x, exeName)); - if (dotnetPath == default) - { - 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 dotnetPath = enumerator + .Where(x => + { + logger.LogTrace("Checking for dotnet at {0}", x); + return File.Exists(x); + }) + .FirstOrDefault(); + + if (dotnetPath == default) + { + 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); @@ -74,41 +77,38 @@ namespace Tgstation.Server.Host.Watchdog #endif var defaultAssemblyPath = Path.GetFullPath(Path.Combine(assemblyStoragePath, "Default")); #if DEBUG - //just copy the shit where it belongs - Directory.Delete(assemblyStoragePath, true); - Directory.CreateDirectory(defaultAssemblyPath); + //just copy the shit where it belongs + Directory.Delete(assemblyStoragePath, true); + Directory.CreateDirectory(defaultAssemblyPath); - var sourcePath = "../../../../Tgstation.Server.Host/bin/Debug/netcoreapp2.0"; - foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) - Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath)); + var sourcePath = "../../../../Tgstation.Server.Host/bin/Debug/netcoreapp2.1"; + foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) + Directory.CreateDirectory(dirPath.Replace(sourcePath, defaultAssemblyPath)); - foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories)) - File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath), true); + foreach (string newPath in Directory.GetFiles(sourcePath, "*.*", SearchOption.AllDirectories)) + File.Copy(newPath, newPath.Replace(sourcePath, defaultAssemblyPath), true); - const string AppSettingsJson = "appsettings.json"; - var rootJson = Path.Combine(rootLocation, AppSettingsJson); - File.Delete(rootJson); - File.Move(Path.Combine(defaultAssemblyPath, AppSettingsJson), rootJson); + const string AppSettingsJson = "appsettings.json"; + var rootJson = Path.Combine(rootLocation, AppSettingsJson); + File.Delete(rootJson); + File.Move(Path.Combine(defaultAssemblyPath, AppSettingsJson), rootJson); #endif - var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll"); - var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName); + var assemblyName = String.Join(".", nameof(Tgstation), nameof(Server), nameof(Host), "dll"); + var assemblyPath = Path.Combine(defaultAssemblyPath, assemblyName); - if (assemblyPath.Contains("\"")) - { - logger.LogCritical("Running from paths with \"'s in the name is not supported!"); - return; - } + if (assemblyPath.Contains("\"")) + { + logger.LogCritical("Running from paths with \"'s in the name is not supported!"); + return; + } - if (!File.Exists(assemblyPath)) - { - logger.LogCritical("Unable to locate host assembly!"); - return; - } + if (!File.Exists(assemblyPath)) + { + logger.LogCritical("Unable to locate host assembly!"); + return; + } - string updateDirectory = null; - try - { while (!cancellationToken.IsCancellationRequested) using (logger.BeginScope("Host invocation")) { @@ -117,12 +117,12 @@ namespace Tgstation.Server.Host.Watchdog using (var process = new Process()) { process.StartInfo.FileName = dotnetPath; - process.StartInfo.WorkingDirectory = Environment.CurrentDirectory; //for appsettings + process.StartInfo.WorkingDirectory = rootLocation; //for appsettings var arguments = new List { '"' + assemblyPath + '"', - updateDirectory + '"' + updateDirectory + '"' }; if (Environment.GetCommandLineArgs().Any(x => x == "--attach-host-debugger")) @@ -142,7 +142,7 @@ namespace Tgstation.Server.Host.Watchdog logger.LogInformation("Launching host..."); - var iShotTheSheriff = false; + var killedHostProcess = false; try { process.Start(); @@ -176,7 +176,7 @@ namespace Tgstation.Server.Host.Watchdog { if (!process.HasExited) { - iShotTheSheriff = true; + killedHostProcess = true; process.Kill(); process.WaitForExit(); } @@ -210,7 +210,7 @@ namespace Tgstation.Server.Host.Watchdog } throw new Exception(String.Format(CultureInfo.InvariantCulture, "Host propagated exception: {0}", data)); default: - if (iShotTheSheriff) + if (killedHostProcess) { logger.LogWarning("Watchdog forced to kill host process!"); cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs index f051d38dd1..1623f115cb 100644 --- a/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/ByondManager.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; +using System.Globalization; using System.Linq; +using System.Net; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -13,11 +15,14 @@ namespace Tgstation.Server.Host.Components.Byond /// sealed class ByondManager : IByondManager { + /// + /// The path to the BYOND bin folder + /// + public const string BinPath = "byond/bin"; + const string VersionFileName = "Version.txt"; const string ActiveVersionFileName = "ActiveVersion.txt"; - const string BinPath = "byond/bin"; - /// public Version ActiveVersion { get; private set; } @@ -120,6 +125,11 @@ namespace Tgstation.Server.Host.Components.Byond //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 + throw new JobException(String.Format(CultureInfo.InvariantCulture, "Error downloading BYOND version: {0}", e.Message)); + } catch (OperationCanceledException) { throw; diff --git a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs index ded0d06a1e..e26aab8da4 100644 --- a/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/PosixByondInstaller.cs @@ -2,8 +2,10 @@ 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 @@ -23,16 +25,21 @@ namespace Tgstation.Server.Host.Components.Byond const string ByondCachePath = "~/.byond/cache"; /// - public string DreamDaemonName => "DreamDaemon"; + public string DreamDaemonName => "DreamDaemon.sh"; /// - public string DreamMakerName => "DreamMaker"; + public string DreamMakerName => "DreamMaker.sh"; /// /// The for the /// readonly IIOManager ioManager; + /// + /// The for the + /// + readonly IPostWriteHandler postWriteHandler; + /// /// The for the /// @@ -42,10 +49,12 @@ namespace Tgstation.Server.Host.Components.Byond /// Construct a /// /// The value of + /// The value of /// The value of - public PosixByondInstaller(IIOManager ioManager, ILogger logger) + public PosixByondInstaller(IIOManager ioManager, IPostWriteHandler postWriteHandler, ILogger logger) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -88,6 +97,32 @@ namespace Tgstation.Server.Host.Components.Byond } /// - public Task InstallByond(string path, Version version, CancellationToken cancellationToken) => Task.CompletedTask; + public Task InstallByond(string path, Version version, CancellationToken cancellationToken) + { + //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"; + + const string DreamDaemonExecutableName = "DreamDaemon"; + const string DreamMakerExecutableName = "DreamMaker"; + + var dreamDaemonScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamDaemonExecutableName); + var dreamMakerScript = String.Format(CultureInfo.InvariantCulture, StandardScript, DreamMakerExecutableName); + + async Task WriteAndMakeExecutable(string fullPath, string script) + { + await ioManager.WriteAllBytes(fullPath, Encoding.ASCII.GetBytes(script), cancellationToken).ConfigureAwait(false); + postWriteHandler.HandleWrite(fullPath); + } + + var basePath = ioManager.ConcatPath(path, ByondManager.BinPath); + + var task = Task.WhenAll(WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamDaemonName), dreamDaemonScript), WriteAndMakeExecutable(ioManager.ConcatPath(basePath, DreamMakerName), dreamMakerScript)); + + postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamDaemonExecutableName)); + postWriteHandler.HandleWrite(ioManager.ConcatPath(basePath, DreamMakerExecutableName)); + + return task; + } } } diff --git a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs index a17aa4eece..99ac434050 100644 --- a/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs +++ b/src/Tgstation.Server.Host/Components/Byond/WindowsByondInstaller.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.Components.Byond /// /// Directory to byond installation configuration /// - const string ByondConfigDir = "byond/config"; + const string ByondConfigDir = "byond/cfg"; /// /// BYOND's DreamDaemon config file /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Chat.cs b/src/Tgstation.Server.Host/Components/Chat/Chat.cs index e5653dc5e0..af7da0a334 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Chat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Chat.cs @@ -9,12 +9,13 @@ using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Chat.Providers; +using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Chat { /// - sealed class Chat : IChat + sealed class Chat : IChat, IRestartHandler { const string CommonMention = "!tgs"; @@ -33,6 +34,11 @@ namespace Tgstation.Server.Host.Components.Chat /// readonly ICommandFactory commandFactory; + /// + /// The for the + /// + readonly IRestartRegistration restartRegistration; + /// /// The for the /// @@ -100,15 +106,20 @@ namespace Tgstation.Server.Host.Components.Chat /// The value of /// The value of /// The value of + /// The to populate with /// The used to populate - public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, ILogger logger, IEnumerable initialChatBots) + public Chat(IProviderFactory providerFactory, IIOManager ioManager, ICommandFactory commandFactory, IServerControl serverControl, ILogger logger, IEnumerable initialChatBots) { this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); + if (serverControl == null) + throw new ArgumentNullException(nameof(serverControl)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.initialChatBots = initialChatBots?.ToList() ?? throw new ArgumentNullException(nameof(initialChatBots)); + restartRegistration = serverControl.RegisterForRestart(this); + builtinCommands = new Dictionary(); providers = new Dictionary(); mappedChannels = new Dictionary(); @@ -121,6 +132,7 @@ namespace Tgstation.Server.Host.Components.Chat /// public void Dispose() { + restartRegistration.Dispose(); handlerCts.Dispose(); foreach (var I in providers) I.Value.Dispose(); @@ -195,6 +207,7 @@ namespace Tgstation.Server.Host.Components.Chat { //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; } @@ -269,18 +282,18 @@ namespace Tgstation.Server.Host.Components.Chat var commandHandler = await GetCommand(command).ConfigureAwait(false); - if (commandHandler.AdminOnly && !message.User.Channel.IsAdmin) - { - await SendMessage("Use this command in an admin channel!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); - return; - } - if (commandHandler == default) { await SendMessage(UnknownCommandMessage, new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); return; } + if (commandHandler.AdminOnly && !message.User.Channel.IsAdmin) + { + await SendMessage("Use this command in an admin channel!", new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); + return; + } + var result = await commandHandler.Invoke(arguments, message.User, cancellationToken).ConfigureAwait(false); if (result != null) await SendMessage(result, new List { message.User.Channel.RealId }, cancellationToken).ConfigureAwait(false); @@ -563,8 +576,9 @@ namespace Tgstation.Server.Host.Components.Chat } /// - public Task SendBroadcast(string message, CancellationToken cancellationToken) + public Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) { + var message = updateVersion == null ? "TGS: Restart requested..." : String.Format(CultureInfo.InvariantCulture, "TGS: Updating to version {0}...", updateVersion); List wdChannels; lock (mappedChannels) //so it doesn't change while we're using it wdChannels = mappedChannels.Select(x => x.Key).ToList(); diff --git a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs index 4fc1329766..8b3db001ce 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ChatFactory.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Chat @@ -9,42 +10,35 @@ namespace Tgstation.Server.Host.Components.Chat /// sealed class ChatFactory : IChatFactory { - /// - /// The for the - /// - readonly IIOManager ioManager; - /// /// The for the /// readonly ILoggerFactory loggerFactory; - /// - /// The for the - /// - readonly ICommandFactory commandFactory; - /// /// The for the /// readonly IProviderFactory providerFactory; + /// + /// The for the + /// + readonly IServerControl serverControl; + /// /// Construct a /// - /// The value of /// The value of - /// The value of /// The value of - public ChatFactory(IIOManager ioManager, ILoggerFactory loggerFactory, ICommandFactory commandFactory, IProviderFactory providerFactory) + /// The value of + public ChatFactory(ILoggerFactory loggerFactory, IProviderFactory providerFactory, IServerControl serverControl) { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - this.commandFactory = commandFactory ?? throw new ArgumentNullException(nameof(commandFactory)); this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); } /// - public IChat CreateChat(IEnumerable initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, loggerFactory.CreateLogger(), initialChatBots); + public IChat CreateChat(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots) => new Chat(providerFactory, ioManager, commandFactory, serverControl, loggerFactory.CreateLogger(), initialChatBots); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/IChat.cs b/src/Tgstation.Server.Host/Components/Chat/IChat.cs index fb8a1166df..59ecc05587 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChat.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChat.cs @@ -75,14 +75,6 @@ namespace Tgstation.Server.Host.Components.Chat /// A representing the running operation Task SendUpdateMessage(string message, CancellationToken cancellationToken); - /// - /// Send a chat to all channels - /// - /// The message being sent - /// The for the operation - /// A representing the running operation - Task SendBroadcast(string message, CancellationToken cancellationToken); - /// /// Start tracking json files for commands and channels /// diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs index 6950552eb7..a47c2c13d2 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatFactory.cs @@ -1,4 +1,6 @@ using System.Collections.Generic; +using Tgstation.Server.Host.Components.Chat.Commands; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Host.Components.Chat { @@ -10,8 +12,10 @@ namespace Tgstation.Server.Host.Components.Chat /// /// Create a /// + /// The for the + /// The for the /// The initial for the /// A new - IChat CreateChat(IEnumerable initialChatBots); + IChat CreateChat(IIOManager ioManager, ICommandFactory commandFactory, IEnumerable initialChatBots); } } diff --git a/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs index 928e64f387..aee9d43965 100644 --- a/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs +++ b/src/Tgstation.Server.Host/Components/Chat/ProviderFactory.cs @@ -37,52 +37,17 @@ namespace Tgstation.Server.Host.Components.Chat { if (settings == null) throw new ArgumentNullException(nameof(settings)); + var builder = settings.ConnectionStringBuilder; + if (builder == null || !builder.Valid) + throw new InvalidOperationException("Invalid ChatConnectionStringBuilder!"); switch (settings.Provider) { case ChatProvider.Irc: - //Connection string semicolon delimited until the password field - if (settings.ConnectionString == null) - throw new InvalidOperationException("ConnectionString cannot be null!"); - var splits = settings.ConnectionString.Split(';'); - if (splits.Length < 4) - throw new InvalidOperationException("Invalid connection string!"); - - var address = splits[0]; - if (!UInt16.TryParse(splits[1], out var port)) - throw new InvalidOperationException("Unable to parse port!"); - var nick = splits[2]; - if (!Int32.TryParse(splits[3], out var intSsl)) - throw new InvalidOperationException("Unable to parse ssl option!"); - - IrcPasswordType? passwordType = null; - string password = null; - if (splits.Length > 4) - { - if (splits.Length < 6) - throw new InvalidOperationException("Invalid connection string!"); - if (!Int32.TryParse(splits[4], out var intPasswordType)) - throw new InvalidOperationException("Unable to parse password type!"); - - passwordType = (IrcPasswordType)intPasswordType; - switch (passwordType) - { - case IrcPasswordType.NickServ: - case IrcPasswordType.Sasl: - case IrcPasswordType.Server: - break; - default: - throw new InvalidOperationException("Invalid password type!"); - } - - var rest = new List(splits); - rest.RemoveRange(0, 5); - password = String.Join(";", rest); - } - - return new IrcProvider(loggerFactory.CreateLogger(), application, address, port, nick, password, passwordType, intSsl != 0); + var ircBuilder = (IrcConnectionStringBuilder)builder; + return new IrcProvider(loggerFactory.CreateLogger(), application, ircBuilder.Address, ircBuilder.Port.Value, ircBuilder.Nickname, ircBuilder.Password, ircBuilder.PasswordType, ircBuilder.UseSsl.Value); case ChatProvider.Discord: - //discord is just the bot token - return new DiscordProvider(loggerFactory.CreateLogger(), settings.ConnectionString); + var discordBuilder = (DiscordConnectionStringBuilder)builder; + return new DiscordProvider(loggerFactory.CreateLogger(), discordBuilder.BotToken); default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid ChatProvider: {0}", settings.Provider)); } diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 3ee2e682ad..d1a33aac23 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -92,7 +92,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Channel = new Channel { RealId = e.Channel.Id, - IsPrivate = true, + IsPrivate = pm, ConnectionName = pm ? e.Author.Username : (e.Channel as ITextChannel)?.Guild.Name ?? "UNKNOWN", FriendlyName = e.Channel.Name //isAdmin and Tag populated by manager @@ -122,7 +122,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var channelsAvailable = new TaskCompletionSource(); client.Ready += () => { - channelsAvailable.SetResult(null); + channelsAvailable.TrySetResult(null); return Task.CompletedTask; }; using (cancellationToken.Register(() => channelsAvailable.SetCanceled())) @@ -182,7 +182,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers }; }; - var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null); + var enumerator = channels.Select(x => GetChannelForChatChannel(x)).Where(x => x != null).ToList(); lock (this) { @@ -190,7 +190,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers mappedChannels.AddRange(enumerator.Select(x => x.RealId)); } - return Task.FromResult>(enumerator.ToList()); + return Task.FromResult>(enumerator); } /// diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index f8f6197396..12374f26fd 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -172,7 +172,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return true; })) { - resultId = ++channelIdCounter; + resultId = channelIdCounter++; dicToCheck.Add(resultId.Value, channelName); if (dicToCheck == queryChannelIdMap) channelIdMap.Add(resultId.Value, null); @@ -258,9 +258,9 @@ namespace Tgstation.Server.Host.Components.Chat.Providers var recievedPlus = false; client.OnReadLine += (sender, e) => { - if (e.Line.Contains("ACK :sasl")) + if (e.Line.Contains("ACK :sasl", StringComparison.Ordinal)) recievedAck = true; - else if (e.Line.Contains("AUTHENTICATE +")) + else if (e.Line.Contains("AUTHENTICATE +", StringComparison.Ordinal)) recievedPlus = true; }; @@ -291,16 +291,17 @@ namespace Tgstation.Server.Host.Components.Chat.Providers listenTask = Task.Factory.StartNew(() => { - while (!disconnecting && client.IsConnected) + while (!disconnecting && client.IsConnected && client.Nickname != nickname) { client.ListenOnce(true); if (disconnecting || !client.IsConnected) break; client.Listen(false); //ensure we have the correct nick - if (client.Nickname != nickname && client.GetIrcUser(nickname) == null) + if (client.GetIrcUser(nickname) == null) client.RfcNick(nickname); } + client.Listen(); }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); } catch (Exception e) @@ -359,7 +360,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return (IReadOnlyList)channels.Select(x => { - var id = channelIdCounter; + ulong? id = null; if (!channelIdMap.Any(y => { if (y.Value != x.IrcChannel) @@ -368,15 +369,15 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return true; })) { - channelIdMap.Add(id, x.IrcChannel); - ++channelIdCounter; + id = channelIdCounter++; + channelIdMap.Add(id.Value, x.IrcChannel); } return new Channel { - RealId = id, + RealId = id.Value, IsAdmin = x.IsAdminChannel == true, ConnectionName = address, - FriendlyName = channelIdMap[id], + FriendlyName = channelIdMap[id.Value], IsPrivate = false, Tag = x.Tag }; diff --git a/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs index 045e495fa0..1c3a909b98 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DmbFactory.cs @@ -131,11 +131,7 @@ namespace Tgstation.Server.Host.Components.Compiler .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) - .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query - - if (finalCompileJob == null) - //lol git fucked - return; + .FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); //can't wait to see that query var newProvider = await FromCompileJob(finalCompileJob, cancellationToken).ConfigureAwait(false); if (newProvider == null) @@ -169,7 +165,7 @@ namespace Tgstation.Server.Host.Components.Compiler public Task StartAsync(CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (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 && !x.Job.Cancelled.Value && x.Job.ExceptionDetails == null && x.Job.StoppedAt != null) + var cj = await db.CompileJobs.Where(x => x.Job.Instance.Id == instance.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) @@ -245,7 +241,7 @@ namespace Tgstation.Server.Host.Components.Compiler //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) && x.DirectoryName.HasValue).Select(x => x.DirectoryName.Value.ToString().ToUpperInvariant()).ToListAsync(cancellationToken).ConfigureAwait(false); + 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 diff --git a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs index 1088833d24..adaf2a55ef 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/DreamMaker.cs @@ -37,9 +37,6 @@ namespace Tgstation.Server.Host.Components.Compiler /// const string DmeExtension = "dme"; - /// - public CompilerStatus Status { get; private set; } - /// /// The for /// @@ -85,6 +82,11 @@ namespace Tgstation.Server.Host.Components.Compiler /// readonly ILogger logger; + /// + /// If a compile job is running + /// + bool compiling; + /// /// Construct /// @@ -123,8 +125,8 @@ namespace Tgstation.Server.Host.Components.Compiler /// The current /// The port to use for API validation /// The for the operation - /// A resulting in if the DMAPI was successfully validated, otherwise - async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken) + /// A representing the running operation + async Task VerifyApi(uint timeout, DreamDaemonSecurity securityLevel, Models.CompileJob job, IByondExecutableLock byondLock, ushort portToUse, CancellationToken cancellationToken) { logger.LogTrace("Verifying DMAPI..."); var launchParameters = new DreamDaemonLaunchParameters @@ -136,6 +138,8 @@ namespace Tgstation.Server.Host.Components.Compiler }; var dirA = ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName); + + 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); @@ -152,15 +156,36 @@ namespace Tgstation.Server.Host.Components.Compiler cancellationToken.ThrowIfCancellationRequested(); } - if (!controller.Lifetime.IsCompleted) + if (controller.Lifetime.IsCompleted) { - logger.LogDebug("API validation timed out!"); - return false; + var validationStatus = controller.ApiValidationStatus; + logger.LogTrace("API validation status: {0}", validationStatus); + switch (validationStatus) + { + case ApiValidationStatus.RequiresUltrasafe: + job.MinimumSecurityLevel = DreamDaemonSecurity.Ultrasafe; + return; + case ApiValidationStatus.RequiresSafe: + if (securityLevel == DreamDaemonSecurity.Ultrasafe) + throw new JobException("This game must be run with at least the 'Safe' DreamDaemon security level!"); + job.MinimumSecurityLevel = DreamDaemonSecurity.Safe; + return; + case ApiValidationStatus.RequiresTrusted: + if (securityLevel != DreamDaemonSecurity.Trusted) + throw new JobException("This game must be run with at least the 'Trusted' DreamDaemon security level!"); + job.MinimumSecurityLevel = DreamDaemonSecurity.Trusted; + return; + case ApiValidationStatus.NeverValidated: + break; + case ApiValidationStatus.BadValidationRequest: + throw new JobException("Recieved an unrecognized API validation request from DreamDaemon!"); + case ApiValidationStatus.UnaskedValidationRequest: + default: + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Session controller returned unexpected ApiValidationStatus: {0}", validationStatus)); + } } - - var validated = controller.ApiValidated; - logger.LogTrace("API valid: {0}", validated); - return validated; + + throw new JobException("DMAPI validation timed out!"); } } @@ -224,12 +249,12 @@ namespace Tgstation.Server.Host.Components.Compiler for (var I = 0; I < dmeLines.Count; ++I) { var line = dmeLines[I]; - if (line.Contains("BEGIN_INCLUDE") && dmeModifications.HeadIncludeLine != null) + if (line.Contains("BEGIN_INCLUDE", StringComparison.Ordinal) && dmeModifications.HeadIncludeLine != null) { dmeLines.Insert(I + 1, dmeModifications.HeadIncludeLine); ++I; } - else if (line.Contains("END_INCLUDE") && dmeModifications.TailIncludeLine != null) + else if (line.Contains("END_INCLUDE", StringComparison.Ordinal) && dmeModifications.TailIncludeLine != null) { dmeLines.Insert(I, dmeModifications.TailIncludeLine); break; @@ -241,7 +266,7 @@ namespace Tgstation.Server.Host.Components.Compiler } /// - public async Task Compile(Models.RevisionInformation revisionInformation, DreamMakerSettings dreamMakerSettings, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken) + public async Task Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken) { if (revisionInformation == null) throw new ArgumentNullException(nameof(revisionInformation)); @@ -252,8 +277,8 @@ namespace Tgstation.Server.Host.Components.Compiler if (repository == null) throw new ArgumentNullException(nameof(repository)); - if (securityLevel == DreamDaemonSecurity.Ultrasafe) - throw new ArgumentOutOfRangeException(nameof(securityLevel), securityLevel, "Cannot compile with ultrasafe security!"); + if (dreamMakerSettings.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe) + throw new ArgumentOutOfRangeException(nameof(dreamMakerSettings), dreamMakerSettings, "Cannot compile with ultrasafe security!"); logger.LogTrace("Begin Compile"); @@ -268,10 +293,9 @@ namespace Tgstation.Server.Host.Components.Compiler lock (this) { - if (Status != CompilerStatus.Idle) - throw new JobException("There is already a compile in progress!"); - - Status = CompilerStatus.Copying; + if (compiling) + throw new JobException("There is already a compile job in progress!"); + compiling = true; } try @@ -302,7 +326,6 @@ namespace Tgstation.Server.Host.Components.Compiler async Task CleanupFailedCompile(bool cancelled) { logger.LogTrace("Cleaning compile directory..."); - Status = CompilerStatus.Cleanup; var chatTask = chat.SendUpdateMessage(cancelled ? "Deploy cancelled!" : "Deploy failed!", cancellationToken); try { @@ -330,13 +353,11 @@ namespace Tgstation.Server.Host.Components.Compiler using (repository) await repository.CopyTo(fullDirA, cancellationToken).ConfigureAwait(false); - Status = CompilerStatus.PreCompile; - + //run precompile scripts var resolvedGameDirectory = ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)); await eventConsumer.HandleEvent(EventType.CompileStart, new List { resolvedGameDirectory, repoOrigin }, cancellationToken).ConfigureAwait(false); - Status = CompilerStatus.Modifying; - + //determine the dme if (job.DmeName == null) { logger.LogTrace("Searching for available .dmes..."); @@ -346,45 +367,41 @@ namespace Tgstation.Server.Host.Components.Compiler var dmeWithExtension = ioManager.GetFileName(path); job.DmeName = dmeWithExtension.Substring(0, dmeWithExtension.Length - DmeExtension.Length - 1); } + else if (!await ioManager.FileExists(ioManager.ConcatPath(dirA, String.Join('.', job.DmeName, DmeExtension)), cancellationToken).ConfigureAwait(false)) + throw new JobException("Unable to locate specified .dme!"); logger.LogDebug("Selected {0}.dme for compilation!", job.DmeName); await ModifyDme(job, cancellationToken).ConfigureAwait(false); - Status = CompilerStatus.Compiling; - //run compiler, verify api job.ByondVersion = byondLock.Version.ToString(); var exitCode = await RunDreamMaker(byondLock.DreamMakerPath, job, cancellationToken).ConfigureAwait(false); - var apiValidated = false; - if (exitCode == 0) + try { - Status = CompilerStatus.Verifying; + if (exitCode != 0) + throw new JobException(String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); - apiValidated = await VerifyApi(apiValidateTimeout, securityLevel, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); + await VerifyApi(apiValidateTimeout, dreamMakerSettings.ApiValidationSecurityLevel.Value, job, byondLock, dreamMakerSettings.ApiValidationPort.Value, cancellationToken).ConfigureAwait(false); } - - if (!apiValidated) + catch (JobException) { //server never validated or compile failed await eventConsumer.HandleEvent(EventType.CompileFailure, new List { resolvedGameDirectory, exitCode == 0 ? "1" : "0" }, cancellationToken).ConfigureAwait(false); - throw new JobException(exitCode == 0 ? "Validation of the TGS api failed!" : String.Format(CultureInfo.InvariantCulture, "DM exited with a non-zero code: {0}{1}{2}", exitCode, Environment.NewLine, job.Output)); + throw; } logger.LogTrace("Running post compile event..."); - Status = CompilerStatus.PostCompile; await eventConsumer.HandleEvent(EventType.CompileComplete, new List { ioManager.ResolvePath(ioManager.ConcatPath(job.DirectoryName.ToString(), ADirectoryName)) }, cancellationToken).ConfigureAwait(false); logger.LogTrace("Duplicating compiled game..."); - Status = CompilerStatus.Duplicating; //duplicate the dmb et al await ioManager.CopyDirectory(dirA, dirB, null, cancellationToken).ConfigureAwait(false); logger.LogTrace("Applying static game file symlinks..."); - Status = CompilerStatus.Symlinking; //symlink in the static data var symATask = configuration.SymlinkStaticFilesTo(fullDirA, cancellationToken); @@ -411,7 +428,7 @@ namespace Tgstation.Server.Host.Components.Compiler } finally { - Status = CompilerStatus.Idle; + compiling = false; } } } diff --git a/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs b/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs index c2d1f3fb6c..83f83849fa 100644 --- a/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs +++ b/src/Tgstation.Server.Host/Components/Compiler/IDreamMaker.cs @@ -1,7 +1,6 @@ using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Repository; namespace Tgstation.Server.Host.Components.Compiler @@ -11,21 +10,15 @@ namespace Tgstation.Server.Host.Components.Compiler /// public interface IDreamMaker { - /// - /// The of - /// - CompilerStatus Status { get; } - /// /// Starts a compile /// /// The being compiled from the - /// The for the compile - /// The level allowed for API validation + /// The for the compile /// The time in seconds to wait while validating the API /// The to copy from /// The for the operation /// A resulting in the partially populated for the operation. In particular, note the field will only have it's field populated - Task Compile(Models.RevisionInformation revisionInformation, DreamMakerSettings dreamMakerSettings, DreamDaemonSecurity securityLevel, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken); + Task Compile(Models.RevisionInformation revisionInformation, Api.Models.DreamMaker dreamMakerSettings, uint apiValidateTimeout, IRepository repository, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/EventType.cs b/src/Tgstation.Server.Host/Components/EventType.cs index add5b3c21a..a46b4ddb4e 100644 --- a/src/Tgstation.Server.Host/Components/EventType.cs +++ b/src/Tgstation.Server.Host/Components/EventType.cs @@ -22,7 +22,7 @@ /// RepoMergePullRequest = 3, /// - /// Parameters: Absolute path to repository root, committer name, committer email + /// Parameters: Absolute path to repository root /// RepoPreSynchronize = 4, diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 8e5b2edf80..d90826f9b4 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Hosting; using System; +using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; -using Tgstation.Server.Host.Components.Compiler; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; @@ -25,12 +25,7 @@ namespace Tgstation.Server.Host.Components /// The for the /// IByondManager ByondManager { get; } - - /// - /// The for the - /// - IDreamMaker DreamMaker { get; } - + /// /// The for the /// @@ -41,11 +36,6 @@ namespace Tgstation.Server.Host.Components /// IChat Chat { get; } - /// - /// The for the - /// - ICompileJobConsumer CompileJobConsumer { get; } - /// /// The for the /// @@ -57,12 +47,6 @@ namespace Tgstation.Server.Host.Components /// The latest if it exists CompileJob LatestCompileJob(); - /// - /// Get the associated with the - /// - /// The associated with the - Api.Models.Instance GetMetadata(); - /// /// Rename the /// @@ -75,5 +59,15 @@ namespace Tgstation.Server.Host.Components /// The new auto update inteval /// A representing the running operation Task SetAutoUpdateInterval(uint newInterval); + + /// + /// Run the compile job and insert it into the database. Meant to be called by a + /// + /// The running + /// The for the operation + /// The to report compilation progress + /// The for the operation + /// A representing the running operation + Task CompileProcess(Job job, IDatabaseContext databaseContext, Action progressReporter, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Instance.cs b/src/Tgstation.Server.Host/Components/Instance.cs index d3faceb58c..50077a86db 100644 --- a/src/Tgstation.Server.Host/Components/Instance.cs +++ b/src/Tgstation.Server.Host/Components/Instance.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Compiler; @@ -49,6 +50,11 @@ namespace Tgstation.Server.Host.Components /// readonly IDmbFactory dmbFactory; + /// + /// The for the + /// + readonly IJobManager jobManager; + /// /// The for the /// @@ -81,8 +87,9 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of + /// The value of /// The value of - public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, ILogger logger) + public Instance(Api.Models.Instance metadata, IRepositoryManager repositoryManager, IByondManager byondManager, IDreamMaker dreamMaker, IWatchdog watchdog, IChat chat, StaticFiles.IConfiguration configuration, ICompileJobConsumer compileJobConsumer, IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, IJobManager jobManager, ILogger logger) { this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); RepositoryManager = repositoryManager ?? throw new ArgumentNullException(nameof(repositoryManager)); @@ -94,6 +101,7 @@ namespace Tgstation.Server.Host.Components CompileJobConsumer = compileJobConsumer ?? throw new ArgumentNullException(nameof(compileJobConsumer)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -108,6 +116,64 @@ namespace Tgstation.Server.Host.Components RepositoryManager.Dispose(); } + /// + public async Task CompileProcess(Job job, IDatabaseContext databaseContext, Action progressReporter, CancellationToken cancellationToken) + { + //DO NOT FOLLOW THE SUGGESTION FOR A THROW EXPRESSION HERE + if (job == null) + throw new ArgumentNullException(nameof(job)); + if (databaseContext == null) + throw new ArgumentNullException(nameof(databaseContext)); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + + var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == metadata.Id).Select(x => new DreamDaemonSettings + { + StartupTimeout = x.StartupTimeout, + }).FirstOrDefaultAsync(cancellationToken); + + var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false); + if (dreamMakerSettings == default) + throw new JobException("Missing DreamMakerSettings in DB!"); + var ddSettings = await ddSettingsTask.ConfigureAwait(false); + if (ddSettings == default) + throw new JobException("Missing DreamDaemonSettings in DB!"); + + CompileJob compileJob; + RevisionInformation revInfo; + using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + { + if (repo == null) + throw new JobException("Missing Repository!"); + + var repoSha = repo.Head; + revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false); + + if (revInfo == default) + { + revInfo = new RevisionInformation + { + CommitSha = repoSha, + OriginCommitSha = repoSha, + Instance = new Models.Instance + { + Id = metadata.Id + } + }; + logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha); + databaseContext.Instances.Attach(revInfo.Instance); + } + + compileJob = await DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); + } + + compileJob.Job = job; + + databaseContext.CompileJobs.Add(compileJob); //will be saved by job context + + job.PostComplete = ct => CompileJobConsumer.LoadCompileJob(compileJob, ct); + } + /// /// Pull the repository and compile for every set of given /// @@ -119,91 +185,203 @@ namespace Tgstation.Server.Host.Components while (true) try { - await Task.Delay(new TimeSpan(0, minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes, 0), cancellationToken).ConfigureAwait(false); - + await Task.Delay(TimeSpan.FromMinutes(minutes > Int32.MaxValue ? Int32.MaxValue : (int)minutes), cancellationToken).ConfigureAwait(false); + logger.LogDebug("Beginning auto update..."); try { - CompileJob job = null; - //need this the whole time - await databaseContextFactory.UseContext(async (db) => + Models.User user = null; + await databaseContextFactory.UseContext(async (db) => user = await db.Users.FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + var repositoryUpdateJob = new Job { - //start up queries we'll need in the future - var instanceQuery = db.Instances.Where(x => x.Id == metadata.Id); - var ddSettingsTask = instanceQuery.Select(x => x.DreamDaemonSettings).Select(x => new DreamDaemonSettings + Instance = new Models.Instance { - StartupTimeout = x.StartupTimeout, - SecurityLevel = x.SecurityLevel - }).FirstAsync(cancellationToken); - var dmSettingsTask = instanceQuery.Select(x => x.DreamMakerSettings).FirstAsync(cancellationToken); - var repositorySettingsTask = instanceQuery.Select(x => x.RepositorySettings).FirstAsync(cancellationToken); + Id = metadata.Id + }, + Description = "Scheduled repository update", + CancelRightsType = RightsType.Repository, + CancelRight = (ulong)RepositoryRights.CancelPendingChanges, + StartedBy = user + }; - using (var repo = await RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + string deploySha = null; + await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) => + { + var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken); + + //assume 5 steps with synchronize + const int ProgressSections = 7; + const int ProgressStep = 100 / ProgressSections; + + + const int NumSteps = 3; + var doneSteps = 0; + + Action NextProgressReporter() + { + var tmpDoneSteps = doneSteps; + ++doneSteps; + return progress => progressReporter((progress + 100 * tmpDoneSteps) / NumSteps); + }; + + using (var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false)) { if (repo == null) + { + logger.LogTrace("Aborting repo update, no repository!"); return; + } - //start the rev info query var startSha = repo.Head; - var revInfoTask = instanceQuery.SelectMany(x => x.RevisionInformations).Where(x => x.CommitSha == startSha).FirstOrDefaultAsync(cancellationToken); + if (!repo.Tracking) + { + logger.LogTrace("Aborting repo update, not tracking origin!"); + deploySha = startSha; + return; + } - //need repo setting to fetch var repositorySettings = await repositorySettingsTask.ConfigureAwait(false); - await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, null, cancellationToken).ConfigureAwait(false); + + //the main point of auto update is to pull the remote + await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); + + RevisionInformation currentRevInfo = null; + bool hasDbChanges = false; + + Task LoadRevInfo() => databaseContext.RevisionInformations + .Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id) + .Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge) + .FirstOrDefaultAsync(cancellationToken); + + async Task UpdateRevInfo(string currentHead, bool onOrigin) + { + if(currentRevInfo == null) + currentRevInfo = await LoadRevInfo().ConfigureAwait(false); + + if (currentRevInfo == default) + { + logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead); + onOrigin = true; + } + + var attachedInstance = new Models.Instance + { + Id = metadata.Id + }; + var oldRevInfo = currentRevInfo; + currentRevInfo = new RevisionInformation + { + CommitSha = currentHead, + OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha, + Instance = attachedInstance + }; + if (!onOrigin) + currentRevInfo.ActiveTestMerges = new List(oldRevInfo.ActiveTestMerges); + + databaseContext.Instances.Attach(attachedInstance); + databaseContext.RevisionInformations.Add(currentRevInfo); + hasDbChanges = true; + } //take appropriate auto update actions bool shouldSyncTracked; if (repositorySettings.AutoUpdatesKeepTestMerges.Value) { - var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, cancellationToken).ConfigureAwait(false); + logger.LogTrace("Preserving test merges..."); + + var currentRevInfoTask = LoadRevInfo(); + + var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); + if (!result.HasValue) - return; - shouldSyncTracked = result.Value; + throw new JobException("Merge conflict while preserving test merges!"); + + currentRevInfo = await currentRevInfoTask.ConfigureAwait(false); + + var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha; + var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit; + + var currentHead = repo.Head; + if (currentHead != startSha) + { + await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false); + shouldSyncTracked = stillOnOrigin; + } + else + shouldSyncTracked = false; } else { - await repo.ResetToOrigin(cancellationToken).ConfigureAwait(false); + logger.LogTrace("Not preserving test merges..."); + await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false); + + var currentHead = repo.Head; + + currentRevInfo = await databaseContext.RevisionInformations + .Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id) + .FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false); + + if (currentHead != startSha && currentRevInfo != default) + await UpdateRevInfo(currentHead, true).ConfigureAwait(false); + shouldSyncTracked = true; } //synch if necessary if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head) - await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, shouldSyncTracked, cancellationToken).ConfigureAwait(false); - - //finish other queries - var dmSettings = await dmSettingsTask.ConfigureAwait(false); - var ddSettings = await ddSettingsTask.ConfigureAwait(false); - var revInfo = await revInfoTask.ConfigureAwait(false); - - //null rev info handling - if (revInfo == default) { - var currentSha = repo.Head; - revInfo = new RevisionInformation - { - CommitSha = currentSha, - OriginCommitSha = currentSha, - Instance = new Models.Instance - { - Id = metadata.Id - }, - ActiveTestMerges = new List(), - CompileJobs = new List() - }; - db.Instances.Attach(revInfo.Instance); + var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false); + var currentHead = repo.Head; + if (currentHead != currentRevInfo.CommitSha) + await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false); } - //finally start compile - job = await DreamMaker.Compile(revInfo, dmSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); + if(hasDbChanges) + try + { + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + } + catch + { + await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false); + throw; + } + + progressReporter(5 * ProgressStep); + deploySha = repo.Head; } + }, cancellationToken).ConfigureAwait(false); - db.CompileJobs.Add(job); - await db.Save(cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); + await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false); - await CompileJobConsumer.LoadCompileJob(job, cancellationToken).ConfigureAwait(false); + if (deploySha == null) + { + logger.LogTrace("Aborting auto update, repository error!"); + continue; + } + + if(deploySha == LatestCompileJob()?.RevisionInformation.CommitSha) + { + logger.LogTrace("Aborting auto update, same revision as latest CompileJob"); + continue; + } + + //finally set up the job + var compileProcessJob = new Job + { + StartedBy = user, + Instance = repositoryUpdateJob.Instance, + Description = "Scheduled code deployment", + CancelRightsType = RightsType.DreamMaker, + CancelRight = (ulong)DreamMakerRights.CancelCompile + }; + + await jobManager.RegisterOperation(compileProcessJob, CompileProcess, cancellationToken).ConfigureAwait(false); + + await jobManager.WaitForJobCompletion(compileProcessJob, user, cancellationToken, default).ConfigureAwait(false); } catch (OperationCanceledException) { + logger.LogDebug("Cancelled auto update job!"); throw; } catch (Exception e) @@ -216,11 +394,9 @@ namespace Tgstation.Server.Host.Components { break; } + logger.LogTrace("Leaving auto update loop..."); } - - /// - public Api.Models.Instance GetMetadata() => metadata.CloneMetadata(); - + /// public void Rename(string newName) { @@ -240,7 +416,7 @@ namespace Tgstation.Server.Host.Components CompileJob latestCompileJob = null; await databaseContextFactory.UseContext(async db => { - latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id && x.Job.ExceptionDetails == null).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + latestCompileJob = await db.CompileJobs.Where(x => x.Job.Instance.Id == metadata.Id).OrderByDescending(x => x.Job.StoppedAt).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); }).ConfigureAwait(false); await dmbFactory.CleanUnusedCompileJobs(latestCompileJob, cancellationToken).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index d676351557..c702355b55 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -8,7 +8,6 @@ using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Chat.Commands; using Tgstation.Server.Host.Components.Compiler; using Tgstation.Server.Host.Components.Repository; -using Tgstation.Server.Host.Components.StaticFiles; using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.IO; @@ -44,11 +43,6 @@ namespace Tgstation.Server.Host.Components /// readonly IByondTopicSender byondTopicSender; - /// - /// The for the - /// - readonly IServerControl serverUpdater; - /// /// The for the /// @@ -72,7 +66,7 @@ namespace Tgstation.Server.Host.Components /// /// The for the /// - readonly IProviderFactory providerFactory; + readonly IChatFactory chatFactory; /// /// The for the @@ -84,6 +78,26 @@ namespace Tgstation.Server.Host.Components /// readonly IPostWriteHandler postWriteHandler; + /// + /// The for the + /// + readonly IWatchdogFactory watchdogFactory; + + /// + /// The for the + /// + readonly IJobManager jobManager; + + /// + /// The for the + /// + readonly ICredentialsProvider credentialsProvider; + + /// + /// The for the + /// + readonly INetworkPromptReaper networkPromptReaper; + /// /// Construct an /// @@ -92,29 +106,35 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The value of /// The value of /// The value of /// The value of /// The value of - /// The value of + /// The value of /// The value of /// The value of - public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, IServerControl serverUpdater, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IProviderFactory providerFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler) + /// The value of + /// The value of + /// The value of + /// The value of + public InstanceFactory(IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, ILoggerFactory loggerFactory, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, ISynchronousIOManager synchronousIOManager, ISymlinkFactory symlinkFactory, IByondInstaller byondInstaller, IChatFactory chatFactory, IProcessExecutor processExecutor, IPostWriteHandler postWriteHandler, IWatchdogFactory watchdogFactory, IJobManager jobManager, ICredentialsProvider credentialsProvider, INetworkPromptReaper networkPromptReaper) { this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.application = application ?? throw new ArgumentNullException(nameof(application)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); - this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager)); this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller)); - this.providerFactory = providerFactory ?? throw new ArgumentNullException(nameof(providerFactory)); + this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory)); this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler)); + this.watchdogFactory = watchdogFactory ?? throw new ArgumentNullException(nameof(watchdogFactory)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider)); + this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper)); } /// @@ -135,28 +155,26 @@ namespace Tgstation.Server.Host.Components var dmbFactory = new DmbFactory(databaseContextFactory, gameIoManager, loggerFactory.CreateLogger(), metadata.CloneMetadata()); try { - var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer); + var repoManager = new RepositoryManager(metadata.RepositorySettings, repoIoManager, eventConsumer, credentialsProvider, loggerFactory.CreateLogger(), loggerFactory.CreateLogger()); try { var byond = new ByondManager(byondIOManager, byondInstaller, loggerFactory.CreateLogger()); var commandFactory = new CommandFactory(application, byond, repoManager, databaseContextFactory, metadata); - var chatFactory = new ChatFactory(instanceIoManager, loggerFactory, commandFactory, providerFactory); - var chat = chatFactory.CreateChat(metadata.ChatSettings); + var chat = chatFactory.CreateChat(instanceIoManager, commandFactory, metadata.ChatSettings); try { - var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, loggerFactory, metadata.CloneMetadata()); - var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, metadata.CloneMetadata()); - var watchdogFactory = new WatchdogFactory(chat, sessionControllerFactory, serverUpdater, loggerFactory, reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, metadata.CloneMetadata()); - var watchdog = watchdogFactory.CreateWatchdog(dmbFactory, metadata.DreamDaemonSettings); + var sessionControllerFactory = new SessionControllerFactory(processExecutor, byond, byondTopicSender, cryptographySuite, application, gameIoManager, chat, networkPromptReaper, loggerFactory, metadata.CloneMetadata()); + var reattachInfoHandler = new ReattachInfoHandler(databaseContextFactory, dmbFactory, loggerFactory.CreateLogger(), metadata.CloneMetadata()); + var watchdog = watchdogFactory.CreateWatchdog(chat, dmbFactory, reattachInfoHandler, configuration, sessionControllerFactory, metadata.CloneMetadata(), metadata.DreamDaemonSettings); eventConsumer.SetWatchdog(watchdog); commandFactory.SetWatchdog(watchdog); try { var dreamMaker = new DreamMaker(byond, gameIoManager, configuration, sessionControllerFactory, dmbFactory, application, eventConsumer, chat, processExecutor, watchdog, loggerFactory.CreateLogger()); - return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, loggerFactory.CreateLogger()); + return new Instance(metadata.CloneMetadata(), repoManager, byond, dreamMaker, watchdog, chat, configuration, dmbFactory, databaseContextFactory, dmbFactory, jobManager, loggerFactory.CreateLogger()); } catch { diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index 1165c0c390..00b4e42e3c 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -49,16 +49,6 @@ namespace Tgstation.Server.Host.Components /// readonly Dictionary instances; - /// - /// of s to finish in - /// - readonly List shutdownTasks; - - /// - /// Used as a temporary for - /// - readonly CancellationTokenSource shutdownCancellationTokenSource; - /// /// Construct an /// @@ -67,31 +57,17 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of /// The value of - /// The for the /// The value of - public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, IServerControl serverControl, ILogger logger) + public InstanceManager(IInstanceFactory instanceFactory, IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IApplication application, IJobManager jobManager, ILogger logger) { this.instanceFactory = instanceFactory ?? throw new ArgumentNullException(nameof(instanceFactory)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.application = application ?? throw new ArgumentNullException(nameof(application)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); - - if (serverControl == null) - throw new ArgumentNullException(nameof(serverControl)); - - shutdownCancellationTokenSource = new CancellationTokenSource(); - var cancellationToken = shutdownCancellationTokenSource.Token; - serverControl.RegisterForRestart(() => - { - lock (this) - shutdownTasks.AddRange(instances.Select(x => x.Value.Chat.SendBroadcast("TGS: Restart requested...", cancellationToken))); - }); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); instances = new Dictionary(); - shutdownTasks = new List(); } /// @@ -99,7 +75,6 @@ namespace Tgstation.Server.Host.Components { foreach (var I in instances) I.Value.Dispose(); - shutdownCancellationTokenSource.Dispose(); } /// @@ -237,11 +212,7 @@ namespace Tgstation.Server.Host.Components public async Task StopAsync(CancellationToken cancellationToken) { await jobManager.StopAsync(cancellationToken).ConfigureAwait(false); - - using (cancellationToken.Register(() => shutdownCancellationTokenSource.Cancel())) - await Task.WhenAll(shutdownTasks).ConfigureAwait(false); await Task.WhenAll(instances.Select(x => x.Value.StopAsync(cancellationToken))).ConfigureAwait(false); - await instanceFactory.StopAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/src/Tgstation.Server.Host/Components/Interop/JsonFile.cs b/src/Tgstation.Server.Host/Components/Interop/JsonFile.cs index 4c318c21c3..10eebe5c13 100644 --- a/src/Tgstation.Server.Host/Components/Interop/JsonFile.cs +++ b/src/Tgstation.Server.Host/Components/Interop/JsonFile.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; namespace Tgstation.Server.Host.Components.Interop @@ -39,9 +40,14 @@ namespace Tgstation.Server.Host.Components.Interop public string ServerCommandsJson { get; set; } /// - /// The of the launch + /// The of the launch /// - public RevisionInformation Revision { get; set; } + public Api.Models.Internal.RevisionInformation Revision { get; set; } + + /// + /// The level of the launch + /// + public DreamDaemonSecurity SecurityLevel { get; set; } /// /// The s in the launch diff --git a/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs b/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs index eac4b39a01..c4f61071a3 100644 --- a/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs +++ b/src/Tgstation.Server.Host/Components/ReattachInfoHandler.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using System; using System.Linq; using System.Threading; @@ -22,6 +23,11 @@ namespace Tgstation.Server.Host.Components /// readonly IDmbFactory dmbFactory; + /// + /// The for the + /// + readonly ILogger logger; + /// /// The for the /// @@ -32,17 +38,24 @@ namespace Tgstation.Server.Host.Components /// /// The value of /// The value of + /// The value of /// The value of - public ReattachInfoHandler(IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, Api.Models.Instance metadata) + public ReattachInfoHandler(IDatabaseContextFactory databaseContextFactory, IDmbFactory dmbFactory, ILogger logger, Api.Models.Instance metadata) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.dmbFactory = dmbFactory ?? throw new ArgumentNullException(nameof(dmbFactory)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); } /// public Task Save(WatchdogReattachInformation reattachInformation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async (db) => { + if (reattachInformation == null) + throw new ArgumentNullException(nameof(reattachInformation)); + + logger.LogDebug("Saving reattach information: {0}...", reattachInformation); + var instance = new Models.Instance { Id = metadata.Id }; db.Instances.Attach(instance); @@ -93,10 +106,15 @@ namespace Tgstation.Server.Host.Components }).ConfigureAwait(false); if (result == default) + { + logger.LogDebug("Reattach information not found!"); return null; + } var bravoDmbTask = dmbFactory.FromCompileJob(result.Bravo.CompileJob, cancellationToken); - return new WatchdogReattachInformation(result, await dmbFactory.FromCompileJob(result.Alpha.CompileJob, cancellationToken).ConfigureAwait(false), await bravoDmbTask.ConfigureAwait(false)); + var info = new WatchdogReattachInformation(result, await dmbFactory.FromCompileJob(result.Alpha.CompileJob, cancellationToken).ConfigureAwait(false), await bravoDmbTask.ConfigureAwait(false)); + logger.LogDebug("Reattach information loaded: {0}", info); + return info; } } } diff --git a/src/Tgstation.Server.Host/Components/Repository/CredentialsProvider.cs b/src/Tgstation.Server.Host/Components/Repository/CredentialsProvider.cs new file mode 100644 index 0000000000..38dd416d11 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/CredentialsProvider.cs @@ -0,0 +1,52 @@ +using LibGit2Sharp; +using LibGit2Sharp.Handlers; +using Microsoft.Extensions.Logging; +using System; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + sealed class CredentialsProvider : ICredentialsProvider + { + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// Construct a + /// + /// The value of + public CredentialsProvider(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + public CredentialsHandler GenerateHandler(string username, string password) => (a, b, supportedCredentialTypes) => + { + var hasCreds = username != null; + var supportsUserPass = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.UsernamePassword); + var supportsAnonymous = supportedCredentialTypes.HasFlag(SupportedCredentialTypes.Default); + + logger.LogTrace("Credentials requested. Present: {0}. Supports anonymous: {1}. Supports user/pass: {2}", hasCreds, supportsAnonymous, supportsUserPass); + if (supportsUserPass) + { + if (hasCreds) + return new UsernamePasswordCredentials + { + Username = username, + Password = password + }; + } + + if (supportsAnonymous) + return new DefaultCredentials(); + + if (hasCreds) + throw new JobException("Remote does not support anonymous authentication!"); + + throw new JobException("Server does not support anonymous or username/password authentication!"); + }; + } +} diff --git a/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs new file mode 100644 index 0000000000..c130b02a78 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Repository/ICredentialsProvider.cs @@ -0,0 +1,18 @@ +using LibGit2Sharp.Handlers; + +namespace Tgstation.Server.Host.Components.Repository +{ + /// + /// For generating s + /// + interface ICredentialsProvider + { + /// + /// Generate a from a given and + /// + /// The optional username to use in the + /// The optional password to use in the + /// A new + CredentialsHandler GenerateHandler(string username, string password); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs index 48f6ff2d06..e993508172 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepository.cs @@ -57,9 +57,10 @@ namespace Tgstation.Server.Host.Components.Repository /// Checks out a given /// /// The sha or reference to checkout + /// to report 0-100 progress of the operation /// The for the operation /// A representing the running operation - Task CheckoutObject(string committish, CancellationToken cancellationToken); + Task CheckoutObject(string committish, Action progressReporter, CancellationToken cancellationToken); /// /// Attempt to merge a GitHub pull request into HEAD @@ -70,8 +71,8 @@ namespace Tgstation.Server.Host.Components.Repository /// The username to fetch from the origin repository /// The password to fetch from the origin repository /// The for the operation - /// Optional function to report 0-100 progress of the clone - /// A resulting in a representing the merge result that is after a fast forward or up to date, on a merge, on a conflict + /// to report 0-100 progress of the operation + /// A resulting in a representing the merge result that is after a fast forward or up to date, on a non-fast-forward, on a conflict Task AddTestMerge(TestMergeParameters testMergeParameters, string committerName, string committerEmail, string username, string password, Action progressReporter, CancellationToken cancellationToken); /// @@ -79,7 +80,7 @@ namespace Tgstation.Server.Host.Components.Repository /// /// The username to fetch from the origin repository /// The password to fetch from the origin repository - /// Optional function to report 0-100 progress of the clone + /// to report 0-100 progress of the operation /// The for the operation /// A representing the running operation Task FetchOrigin(string username, string password, Action progressReporter, CancellationToken cancellationToken); @@ -87,36 +88,42 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Requires the current HEAD to be a tracked reference. Hard resets the reference to what it tracks on the origin repository /// + /// to report 0-100 progress of the operation /// The for the operation /// A resulting in the SHA of the new HEAD - Task ResetToOrigin(CancellationToken cancellationToken); + Task ResetToOrigin(Action progressReporter, CancellationToken cancellationToken); /// /// Requires the current HEAD to be a reference. Hard resets the reference to the given sha /// /// The sha hash to reset to + /// to report 0-100 progress of the operation /// The for the operation /// A resulting in the SHA of the new HEAD - Task ResetToSha(string sha, CancellationToken cancellationToken); + Task ResetToSha(string sha, Action progressReporter, CancellationToken cancellationToken); /// /// Requires the current HEAD to be a tracked reference. Merges the reference to what it tracks on the origin repository /// /// The name of the merge committer /// The e-mail of the merge committer + /// to report 0-100 progress of the operation /// The for the operation - /// A resulting in a representing the merge result that is after a fast forward or up to date, on a merge, on a conflict - Task MergeOrigin(string committerName, string committerEmail, CancellationToken cancellationToken); + /// A resulting in a representing the merge result that is after a fast forward, on a merge or up to date, on a conflict + Task MergeOrigin(string committerName, string committerEmail, Action progressReporter, CancellationToken cancellationToken); /// /// Runs the synchronize event script and attempts to push any changes made to the if on a tracked branch /// /// The username to fetch from the origin repository /// The password to fetch from the origin repository + /// The name of the potential committer + /// The e-mail of the potential committer /// If the synchronizations should be made to the tracked reference as opposed to a temporary branch + /// to report 0-100 progress of the operation /// The for the operation - /// A representing the running operation - Task Sychronize(string username, string password, bool synchronizeTrackedBranch, CancellationToken cancellationToken); + /// A resulting in if commits were pushed to the tracked origin reference, otherwise + Task Sychronize(string username, string password, string committerName, string committerEmail, Action progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken); /// /// Copies the current working directory to a given diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index 0ed716b751..c7652fddfc 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -1,4 +1,6 @@ using LibGit2Sharp; +using LibGit2Sharp.Handlers; +using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Globalization; @@ -18,13 +20,18 @@ namespace Tgstation.Server.Host.Components.Repository /// public const string GitHubUrl = "://github.com/"; - const string UnknownReference = ""; + /// + /// Template error message for when tracking of the most recent origin commit fails + /// + public const string OriginTrackingErrorTemplate = "Unable to determine most recent origin commit of {0}. Marking it as an origin commit. This may result in invalid git metadata until the next hard reset to an origin reference."; /// /// The branch name used for publishing testmerge commits /// public const string RemoteTemporaryBranchName = "___TGSTempBranch"; + const string UnknownReference = ""; + /// public bool IsGitHubRepository { get; } @@ -61,12 +68,22 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly IEventConsumer eventConsumer; + /// + /// The for the + /// + readonly ICredentialsProvider credentialsProvider; + + /// + /// The for the + /// + readonly ILogger logger; + /// /// to be taken when is called /// readonly Action onDispose; - static void GetRepositoryOwnerName(string remote, out string owner, out string name) + void GetRepositoryOwnerName(string remote, out string owner, out string name) { //Assume standard gh format: [(git)|(https)]://github.com/owner/repo(.git)[0-1] //Yes use .git twice in case it was weird @@ -77,22 +94,35 @@ namespace Tgstation.Server.Host.Components.Repository var splits = remote.Split('/'); name = splits[splits.Length - 1]; owner = splits[splits.Length - 2].Split('.')[0]; + + logger.LogTrace("GetRepositoryOwnerName({0}) => {1} / {2}", remote, owner, name); } + /// + /// Converts a given to a + /// + /// to report 0-100 progress of the operation + /// A based on + static CheckoutProgressHandler CheckoutProgressHandler(Action progressReporter) => (a, completedSteps, totalSteps) => progressReporter((int)((((float)completedSteps) / totalSteps) * 100)); + /// /// Construct a /// /// The value of /// The value of /// The value of + /// The value of + /// The value of /// The value if - public Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, Action onDispose) + public Repository(LibGit2Sharp.IRepository repository, IIOManager ioMananger, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger logger, Action onDispose) { this.repository = repository ?? throw new ArgumentNullException(nameof(repository)); this.ioMananger = ioMananger ?? throw new ArgumentNullException(nameof(ioMananger)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); - IsGitHubRepository = Origin.ToUpperInvariant().Contains(GitHubUrl.ToUpperInvariant()); + IsGitHubRepository = Origin.Contains(GitHubUrl, StringComparison.InvariantCultureIgnoreCase); if (IsGitHubRepository) { GetRepositoryOwnerName(Origin, out var owner, out var name); @@ -104,20 +134,57 @@ namespace Tgstation.Server.Host.Components.Repository /// public void Dispose() { + logger.LogTrace("Disposing..."); repository.Dispose(); onDispose.Invoke(); } + /// + /// Generate a standard set of + /// + /// to report 0-100 progress of the operation + /// The username for the + /// The password for the + /// The for the operation + /// A new set of + PushOptions GeneratePushOptions(Action progressReporter, string username, string password, CancellationToken cancellationToken) => new PushOptions + { + OnPackBuilderProgress = (stage, current, total) => + { + var baseProgress = stage == PackBuilderStage.Counting ? 0 : 25; + progressReporter(baseProgress + ((int)(25 * ((float)current) / total))); + return !cancellationToken.IsCancellationRequested; + }, + OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested, + OnPushTransferProgress = (a, sentBytes, totalBytes) => + { + progressReporter(50 + ((int)(50 * ((float)sentBytes) / totalBytes))); + return !cancellationToken.IsCancellationRequested; + }, + CredentialsProvider = credentialsProvider.GenerateHandler(username, password) + }; + /// /// Runs a blocking force checkout to /// /// The committish to checkout - void RawCheckout(string committish) + /// Progress reporter + /// The for the operation + void RawCheckout(string committish, Action progressReporter, CancellationToken cancellationToken) { + logger.LogTrace("Checkout: {0}", committish); + + progressReporter(0); + cancellationToken.ThrowIfCancellationRequested(); + Commands.Checkout(repository, committish, new CheckoutOptions { - CheckoutModifiers = CheckoutModifiers.Force + CheckoutModifiers = CheckoutModifiers.Force, + OnCheckoutProgress = CheckoutProgressHandler(progressReporter) }); + + cancellationToken.ThrowIfCancellationRequested(); + repository.RemoveUntrackedFiles(); } @@ -126,22 +193,25 @@ namespace Tgstation.Server.Host.Components.Repository { if (testMergeParameters == null) throw new ArgumentNullException(nameof(testMergeParameters)); - if (committerName == null) throw new ArgumentNullException(nameof(committerName)); if (committerEmail == null) throw new ArgumentNullException(nameof(committerEmail)); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + + logger.LogDebug("Begin AddTestMerge: #{0} at {1} ({4}) by <{2} ({3})>", testMergeParameters.Number, testMergeParameters.PullRequestRevision?.Substring(0, 7), committerName, committerEmail, testMergeParameters.Comment); if (!IsGitHubRepository) throw new InvalidOperationException("Test merging is only available on GitHub hosted origin repositories!"); var commitMessage = String.Format(CultureInfo.InvariantCulture, "Test merge of pull request #{0}{1}{2}", testMergeParameters.Number.Value, testMergeParameters.Comment != null ? Environment.NewLine : String.Empty, testMergeParameters.Comment ?? String.Empty); - var prBranchName = String.Format(CultureInfo.InvariantCulture, "pr-{0}", testMergeParameters.Number); var localBranchName = String.Format(CultureInfo.InvariantCulture, "pull/{0}/headrefs/heads/{1}", testMergeParameters.Number, prBranchName); - var Refspec = new List { String.Format(CultureInfo.InvariantCulture, "pull/{0}/head:{1}", testMergeParameters.Number, prBranchName) }; + var refSpec = String.Format(CultureInfo.InvariantCulture, "pull/{0}/head:{1}", testMergeParameters.Number, prBranchName); + var refSpecList = new List { refSpec }; var logMessage = String.Format(CultureInfo.InvariantCulture, "Merge remote pull request #{0}", testMergeParameters.Number); var originalCommit = repository.Head; @@ -155,39 +225,45 @@ namespace Tgstation.Server.Host.Components.Repository { try { + logger.LogTrace("Fetching refspec {0}...", refSpec); + var remote = repository.Network.Remotes.First(); - Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, Refspec, new FetchOptions + progressReporter(0); + Commands.Fetch((LibGit2Sharp.Repository)repository, remote.Name, refSpecList, new FetchOptions { Prune = true, OnProgress = (a) => !cancellationToken.IsCancellationRequested, OnTransferProgress = (a) => { - var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2)); - progressReporter?.Invoke((int)percentage); + var percentage = 50 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2)); + progressReporter((int)percentage); return !cancellationToken.IsCancellationRequested; }, OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = (a, b, c) => username != null ? (Credentials)new UsernamePasswordCredentials - { - Username = username, - Password = password - } : new DefaultCredentials() + CredentialsProvider = credentialsProvider.GenerateHandler(username, password) }, logMessage); } catch (UserCancelledException) { } cancellationToken.ThrowIfCancellationRequested(); + repository.RemoveUntrackedFiles(); + + cancellationToken.ThrowIfCancellationRequested(); + testMergeParameters.PullRequestRevision = repository.Lookup(testMergeParameters.PullRequestRevision ?? localBranchName).Sha; cancellationToken.ThrowIfCancellationRequested(); + logger.LogTrace("Merging {0} into {1}...", testMergeParameters.PullRequestRevision.Substring(0, 7), Reference); + result = repository.Merge(testMergeParameters.PullRequestRevision, sig, new MergeOptions { CommitOnSuccess = commitMessage == null, FailOnConflict = true, FastForwardStrategy = FastForwardStrategy.NoFastForward, - SkipReuc = true + SkipReuc = true, + OnCheckoutProgress = (a, completedSteps, totalSteps) => progressReporter(50 + ((int)((((float)completedSteps) / totalSteps) * 50))) }); } finally @@ -199,7 +275,9 @@ namespace Tgstation.Server.Host.Components.Repository if (result.Status == MergeStatus.Conflicts) { - RawCheckout(originalCommit.CanonicalName ?? originalCommit.Tip.Sha); + var revertTo = originalCommit.CanonicalName ?? originalCommit.Tip.Sha; + logger.LogDebug("Merge conflict, aborting and reverting to {0}", revertTo); + RawCheckout(revertTo, progressReporter, cancellationToken); cancellationToken.ThrowIfCancellationRequested(); } @@ -209,31 +287,45 @@ namespace Tgstation.Server.Host.Components.Repository if (result.Status == MergeStatus.Conflicts) { await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List { originalCommit.Tip.Sha, testMergeParameters.PullRequestRevision, originalCommit.FriendlyName ?? UnknownReference, prBranchName }, cancellationToken).ConfigureAwait(false); - return false; + return null; } - if (commitMessage != null) - repository.Commit(commitMessage, sig, sig, new CommitOptions + if (commitMessage != null && result.Status != MergeStatus.UpToDate) + { + logger.LogTrace("Committing merge: \"{0}\"...", commitMessage); + await Task.Factory.StartNew(() => repository.Commit(commitMessage, sig, sig, new CommitOptions { PrettifyMessage = true - }); + }), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + } - return true; + return result.Status != MergeStatus.NonFastForward; } /// - public async Task CheckoutObject(string committish, CancellationToken cancellationToken) + public async Task CheckoutObject(string committish, Action progressReporter, CancellationToken cancellationToken) { if (committish == null) throw new ArgumentNullException(nameof(committish)); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + logger.LogDebug("Checkout object: {0}...", committish); await eventConsumer.HandleEvent(EventType.RepoCheckout, new List { committish }, cancellationToken).ConfigureAwait(false); - await Task.Factory.StartNew(() => RawCheckout(committish), cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + await Task.Factory.StartNew(() => + { + repository.RemoveUntrackedFiles(); + RawCheckout(committish, progressReporter, cancellationToken); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); } /// - public Task FetchOrigin(string username, string password, Action progressReporter, CancellationToken cancellationToken) => Task.WhenAll( - eventConsumer.HandleEvent(EventType.RepoFetch, Array.Empty(), cancellationToken), - Task.Factory.StartNew(() => + public async Task FetchOrigin(string username, string password, Action progressReporter, CancellationToken cancellationToken) + { + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + logger.LogDebug("Fetch origin..."); + await eventConsumer.HandleEvent(EventType.RepoFetch, Array.Empty(), cancellationToken).ConfigureAwait(false); + await Task.Factory.StartNew(() => { var remote = repository.Network.Remotes.First(); try @@ -245,32 +337,31 @@ namespace Tgstation.Server.Host.Components.Repository OnTransferProgress = (a) => { var percentage = 100 * (((float)a.IndexedObjects + a.ReceivedObjects) / (a.TotalObjects * 2)); - progressReporter?.Invoke((int)percentage); + progressReporter((int)percentage); return !cancellationToken.IsCancellationRequested; }, OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = (a, b, c) => username != null ? (Credentials)new UsernamePasswordCredentials - { - Username = username, - Password = password - } : new DefaultCredentials() + CredentialsProvider = credentialsProvider.GenerateHandler(username, password) }, "Fetch origin commits"); } catch (UserCancelledException) { cancellationToken.ThrowIfCancellationRequested(); } - }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current)); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + } /// - /// Force push the current repository HEAD to ; + /// Force push the current repository HEAD to ; /// /// The username to fetch from the origin repository /// The password to fetch from the origin repository + /// to report 0-100 progress of the operation /// The for the operation /// A representing the running operation - Task PushHeadToTemporaryBranch(string username, string password, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + Task PushHeadToTemporaryBranch(string username, string password, Action progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(() => { + logger.LogInformation("Pushing changes to temporary remote branch..."); var branch = repository.CreateBranch(RemoteTemporaryBranchName); try { @@ -278,17 +369,10 @@ namespace Tgstation.Server.Host.Components.Repository var remote = repository.Network.Remotes.First(); try { - repository.Network.Push(remote, String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName), new PushOptions - { - OnPackBuilderProgress = (a, b, c) => !cancellationToken.IsCancellationRequested, - OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested, - OnPushTransferProgress = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = (a, b, c) => username != null ? (Credentials)new UsernamePasswordCredentials - { - Username = username, - Password = password - } : new DefaultCredentials() - }); + var forcePushString = String.Format(CultureInfo.InvariantCulture, "+{0}:{0}", branch.CanonicalName); + repository.Network.Push(remote,forcePushString, GeneratePushOptions(progress => progressReporter((int)(0.9f * progress)), username, password, cancellationToken)); + var removalString = String.Format(CultureInfo.InvariantCulture, ":{0}", branch.CanonicalName); + repository.Network.Push(remote, removalString, GeneratePushOptions(progress => progressReporter(90 + (int)(0.1f * progress)), username, password, cancellationToken)); } catch (UserCancelledException) { @@ -302,21 +386,41 @@ namespace Tgstation.Server.Host.Components.Repository }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public async Task ResetToOrigin(CancellationToken cancellationToken) + public async Task ResetToOrigin(Action progressReporter, CancellationToken cancellationToken) { - if (!repository.Head.IsTracking) - throw new InvalidOperationException("Cannot reset to origin while not on a tracked reference!"); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + if (!Tracking) + throw new JobException("Cannot reset to origin while not on a tracked reference!"); + logger.LogTrace("Reset to origin..."); var trackedBranch = repository.Head.TrackedBranch; await eventConsumer.HandleEvent(EventType.RepoResetOrigin, new List { trackedBranch.FriendlyName, trackedBranch.Tip.Sha }, cancellationToken).ConfigureAwait(false); - await ResetToSha(trackedBranch.Tip.Sha, cancellationToken).ConfigureAwait(false); + await ResetToSha(trackedBranch.Tip.Sha, progressReporter, cancellationToken).ConfigureAwait(false); } /// - public Task ResetToSha(string sha, CancellationToken cancellationToken) => Task.Factory.StartNew(() => + public Task ResetToSha(string sha, Action progressReporter, CancellationToken cancellationToken) => Task.Factory.StartNew(() => { - repository.Reset(ResetMode.Hard, sha); - cancellationToken.ThrowIfCancellationRequested(); + if (sha == null) + throw new ArgumentNullException(nameof(sha)); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + + logger.LogDebug("Reset to sha: {0}", sha.Substring(0, 7)); + repository.RemoveUntrackedFiles(); + cancellationToken.ThrowIfCancellationRequested(); + + var gitObject = repository.Lookup(sha, ObjectType.Commit); + cancellationToken.ThrowIfCancellationRequested(); + + if (gitObject == null) + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Cannot reset to non-existent SHA: {0}", sha)); + + repository.Reset(ResetMode.Hard, gitObject.Peel(), new CheckoutOptions + { + OnCheckoutProgress = CheckoutProgressHandler(progressReporter) + }); }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// @@ -324,36 +428,51 @@ namespace Tgstation.Server.Host.Components.Repository { if (path == null) throw new ArgumentNullException(nameof(path)); + logger.LogTrace("Copying to {0}...", path); await ioMananger.CopyDirectory(".", path, new List { ".git" }, cancellationToken).ConfigureAwait(false); } /// - public async Task MergeOrigin(string committerName, string committerEmail, CancellationToken cancellationToken) + public async Task MergeOrigin(string committerName, string committerEmail, Action progressReporter, CancellationToken cancellationToken) { + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + MergeResult result = null; Branch trackedBranch = null; var oldHead = repository.Head; + var oldTip = oldHead.Tip; await Task.Factory.StartNew(() => { - if (!repository.Head.IsTracking) - throw new InvalidOperationException("Cannot reset to origin while not on a tracked reference!"); - trackedBranch = repository.Head.TrackedBranch; + if (!Tracking) + throw new JobException("Cannot reset to origin while not on a tracked reference!"); + repository.RemoveUntrackedFiles(); + + cancellationToken.ThrowIfCancellationRequested(); + + trackedBranch = repository.Head.TrackedBranch; + logger.LogDebug("Merge origin/{2}: <{0} ({1})>", committerName, committerEmail, trackedBranch.FriendlyName); result = repository.Merge(trackedBranch, new Signature(new Identity(committerName, committerEmail), DateTimeOffset.Now), new MergeOptions { CommitOnSuccess = true, FailOnConflict = true, FastForwardStrategy = FastForwardStrategy.Default, SkipReuc = true, + OnCheckoutProgress = CheckoutProgressHandler(progressReporter) }); cancellationToken.ThrowIfCancellationRequested(); if (result.Status == MergeStatus.Conflicts) { - RawCheckout(oldHead.CanonicalName); + logger.LogDebug("Merge conflict, aborting and reverting to {0}", oldHead.FriendlyName); + repository.Reset(ResetMode.Hard, oldTip, new CheckoutOptions + { + OnCheckoutProgress = CheckoutProgressHandler(progressReporter) + }); cancellationToken.ThrowIfCancellationRequested(); } @@ -362,18 +481,29 @@ namespace Tgstation.Server.Host.Components.Repository if (result.Status == MergeStatus.Conflicts) { - await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List { oldHead.Tip.Sha, trackedBranch.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName }, cancellationToken).ConfigureAwait(false); + await eventConsumer.HandleEvent(EventType.RepoMergeConflict, new List { oldTip.Sha, trackedBranch.Tip.Sha, oldHead.FriendlyName ?? UnknownReference, trackedBranch.FriendlyName }, cancellationToken).ConfigureAwait(false); return null; } - return result.Status != MergeStatus.NonFastForward; + return result.Status == MergeStatus.FastForward; } /// - public async Task Sychronize(string username, string password, bool synchronizeTrackedBranch, CancellationToken cancellationToken) + public async Task Sychronize(string username, string password, string committerName, string committerEmail, Action progressReporter, bool synchronizeTrackedBranch, CancellationToken cancellationToken) { + if (committerName == null) + throw new ArgumentNullException(nameof(committerName)); + if (committerEmail == null) + throw new ArgumentNullException(nameof(committerEmail)); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + if (username == null && password == null) - return; + { + logger.LogTrace("Not synchronizing due to lack of credentials!"); + return false; + } + logger.LogTrace("Begin Synchronize..."); if (username == null) throw new ArgumentNullException(nameof(username)); @@ -382,39 +512,71 @@ namespace Tgstation.Server.Host.Components.Repository var startHead = Head; - if (!await eventConsumer.HandleEvent(EventType.RepoPreSynchronize, new List { ioMananger.ResolvePath(".") }, cancellationToken).ConfigureAwait(false)) - return; + logger.LogTrace("Configuring <{0} ({1})> as author/committer", committerName, committerEmail); + await Task.Factory.StartNew(() => + { + repository.Config.Set("user.name", committerName); + cancellationToken.ThrowIfCancellationRequested(); + repository.Config.Set("user.email", committerEmail); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + + cancellationToken.ThrowIfCancellationRequested(); + try + { + if (!await eventConsumer.HandleEvent(EventType.RepoPreSynchronize, new List { ioMananger.ResolvePath(".") }, cancellationToken).ConfigureAwait(false)) + { + logger.LogDebug("Aborted synchronize due to event handler response!"); + return false; + } + } + finally + { + logger.LogTrace("Resetting and cleaning untracked files..."); + await Task.Factory.StartNew(() => + { + repository.RemoveUntrackedFiles(); + cancellationToken.ThrowIfCancellationRequested(); + repository.Reset(ResetMode.Hard, repository.Head.Tip, new CheckoutOptions + { + OnCheckoutProgress = CheckoutProgressHandler(progress => progressReporter(progress / 10)) + }); + }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); + } + + void FinalReporter(int progress) => progressReporter((int)(((float)progress) / 100 * 90)); if (!synchronizeTrackedBranch) { - await PushHeadToTemporaryBranch(username, password, cancellationToken).ConfigureAwait(false); - return; + await PushHeadToTemporaryBranch(username, password, FinalReporter, cancellationToken).ConfigureAwait(false); + return false; } - if (Head == startHead || !repository.Head.IsTracking) - return; - - await Task.Factory.StartNew(() => + var sameHead = Head == startHead; + if (sameHead || !Tracking) + { + logger.LogTrace("Aborted synchronize due to {0}!", sameHead ? "lack of changes" : "not being on tracked reference"); + return false; + } + + logger.LogInformation("Synchronizing with origin..."); + + return await Task.Factory.StartNew(() => { - cancellationToken.ThrowIfCancellationRequested(); var remote = repository.Network.Remotes.First(); try { - repository.Network.Push(repository.Head, new PushOptions - { - OnPackBuilderProgress = (a, b, c) => !cancellationToken.IsCancellationRequested, - OnNegotiationCompletedBeforePush = (a) => !cancellationToken.IsCancellationRequested, - OnPushTransferProgress = (a, b, c) => !cancellationToken.IsCancellationRequested, - CredentialsProvider = (a, b, c) => username != null ? (Credentials)new UsernamePasswordCredentials - { - Username = username, - Password = password - } : new DefaultCredentials() - }); + repository.Network.Push(repository.Head, GeneratePushOptions(FinalReporter, username, password, cancellationToken)); + return true; } - catch (UserCancelledException) + catch (NonFastForwardException) + { + logger.LogInformation("Synchronize aborted, non-fast forward!"); + return false; + } + catch (UserCancelledException e) { cancellationToken.ThrowIfCancellationRequested(); + throw new InvalidOperationException("Caught UserCancelledException without cancellationToken triggering", e); } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 9b7ef5e95b..2ec1c57468 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -1,4 +1,5 @@ using LibGit2Sharp; +using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; @@ -27,6 +28,21 @@ namespace Tgstation.Server.Host.Components.Repository /// readonly IEventConsumer eventConsumer; + /// + /// The for the + /// + readonly ICredentialsProvider credentialsProvider; + + /// + /// The created s + /// + readonly ILogger repositoryLogger; + + /// + /// The for the + /// + readonly ILogger logger; + /// /// The for the /// @@ -43,20 +59,36 @@ namespace Tgstation.Server.Host.Components.Repository /// The value of /// The value of /// The value of - public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer) + /// The value of + /// The value of + /// The value of + public RepositoryManager(RepositorySettings repositorySettings, IIOManager ioManager, IEventConsumer eventConsumer, ICredentialsProvider credentialsProvider, ILogger repositoryLogger, ILogger logger) { this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.credentialsProvider = credentialsProvider ?? throw new ArgumentNullException(nameof(credentialsProvider)); + this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); semaphore = new SemaphoreSlim(1); } /// - public void Dispose() => semaphore.Dispose(); + public void Dispose() + { + logger.LogTrace("Disposing..."); + semaphore.Dispose(); + } /// public async Task CloneRepository(Uri url, string initialBranch, string username, string password, Action progressReporter, CancellationToken cancellationToken) { + if (url == null) + throw new ArgumentNullException(nameof(url)); + if (progressReporter == null) + throw new ArgumentNullException(nameof(progressReporter)); + + logger.LogInformation("Begin clone {0} (Branch: {1})", url, initialBranch); lock (this) { if (CloneInProgress) @@ -66,6 +98,8 @@ namespace Tgstation.Server.Host.Components.Repository try { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + { + logger.LogTrace("Semaphore acquired"); if (!await ioManager.DirectoryExists(".", cancellationToken).ConfigureAwait(false)) try { @@ -87,11 +121,7 @@ namespace Tgstation.Server.Host.Components.Repository OnUpdateTips = (a, b, c) => !cancellationToken.IsCancellationRequested, RepositoryOperationStarting = (a) => !cancellationToken.IsCancellationRequested, BranchName = initialBranch, - CredentialsProvider = (a, b, c) => username != null ? (Credentials)new UsernamePasswordCredentials - { - Username = username, - Password = password - } : new DefaultCredentials() + CredentialsProvider = credentialsProvider.GenerateHandler(username, password) }); } catch (UserCancelledException) { } @@ -102,13 +132,22 @@ namespace Tgstation.Server.Host.Components.Repository { try { + logger.LogTrace("Deleting partially cloned repository..."); await ioManager.DeleteDirectory(".", default).ConfigureAwait(false); } - catch { } + catch (Exception e) + { + logger.LogDebug("Error deleting partially cloned repository! Exception: {0}", e); + } throw; } else + { + logger.LogDebug("Repository exists, clone aborted!"); return null; + } + } + logger.LogInformation("Clone complete!"); } finally { @@ -120,6 +159,7 @@ namespace Tgstation.Server.Host.Components.Repository /// public async Task LoadRepository(CancellationToken cancellationToken) { + logger.LogTrace("Begin LoadRepository..."); lock (this) if (CloneInProgress) throw new InvalidOperationException("The repository is being cloned!"); @@ -129,28 +169,41 @@ namespace Tgstation.Server.Host.Components.Repository { try { + logger.LogTrace("Creating LibGit2Sharp.Repository..."); repo = new LibGit2Sharp.Repository(ioManager.ResolvePath(".")); } - catch (RepositoryNotFoundException) { } + catch (RepositoryNotFoundException e) + { + logger.LogDebug("Repository not found!"); + logger.LogTrace("Exception: {0}", e); + } + catch + { + semaphore.Release(); + throw; + } }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current).ConfigureAwait(false); if (repo == null) { semaphore.Release(); return null; } - var localSemaphore = semaphore; - return new Repository(repo, ioManager, eventConsumer, () => + return new Repository(repo, ioManager, eventConsumer, credentialsProvider, repositoryLogger, () => { - localSemaphore?.Release(); - localSemaphore = null; + logger.LogTrace("Releasing semaphore due to Repository disposal..."); + semaphore.Release(); }); } /// public async Task DeleteRepository(CancellationToken cancellationToken) { + logger.LogInformation("Deleting repository..."); using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + { + logger.LogTrace("Semaphore acquired, deleting Repository directory..."); await ioManager.DeleteDirectory(".", cancellationToken).ConfigureAwait(false); + } } } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index 69846570a3..1bb59369e4 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -159,6 +159,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles await EnsureDirectories(cancellationToken).ConfigureAwait(false); var path = ValidateConfigRelativePath(configurationRelativePath); + if (configurationRelativePath == null) + configurationRelativePath = "/"; + List result = new List(); void ListImpl() @@ -169,7 +172,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles result.AddRange(enumerator.Select(x => new ConfigurationFile { IsDirectory = true, - Path = ioManager.ConcatPath(path, x), + Path = ioManager.ConcatPath(configurationRelativePath, x), })); } catch (IOException e) @@ -182,7 +185,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles result.AddRange(enumerator.Select(x => new ConfigurationFile { IsDirectory = false, - Path = ioManager.ConcatPath(path, x), + Path = ioManager.ConcatPath(configurationRelativePath, x), })); } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index 29b8b66da0..7c6ec4298b 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -74,7 +74,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// The data to write. If , the file is deleted /// The hash any existing file must match in order for the write to succeed /// The for the operation. Usage may result in partial writes - /// A resulting in the updated + /// A resulting in the updated or if the write failed due to conflicts Task Write(string configurationRelativePath, ISystemIdentity systemIdentity, byte[] data, string previousHash, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ApiValidationStatus.cs b/src/Tgstation.Server.Host/Components/Watchdog/ApiValidationStatus.cs new file mode 100644 index 0000000000..46e282802d --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Watchdog/ApiValidationStatus.cs @@ -0,0 +1,33 @@ +namespace Tgstation.Server.Host.Components.Watchdog +{ + /// + /// Status of DMAPI validation + /// + enum ApiValidationStatus + { + /// + /// The DMAPI never contacted the server for validation + /// + NeverValidated, + /// + /// The server was contacted for validation but it was never requested + /// + UnaskedValidationRequest, + /// + /// The validation request was malformed + /// + BadValidationRequest, + /// + /// Valid API. The game must be run with a minimum security level of + /// + RequiresSafe, + /// + /// Valid API. The game must be run with a security level of + /// + RequiresTrusted, + /// + /// Valid API. The game must be run with a minimum security level of + /// + RequiresUltrasafe + } +} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/INetworkPromptReaper.cs b/src/Tgstation.Server.Host/Components/Watchdog/INetworkPromptReaper.cs new file mode 100644 index 0000000000..eb939218e7 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Watchdog/INetworkPromptReaper.cs @@ -0,0 +1,16 @@ +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Watchdog +{ + /// + /// On Windows, DreamDaemon will show an unskippable prompt when using /world/proc/OpenPort(). This looks out for those prompts and immediately clicks "Yes" if the owning process has registered for it + /// + interface INetworkPromptReaper + { + /// + /// Register a given for network prompt reaping + /// + /// The to register + void RegisterProcess(IProcess process); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs index 3f2e245eba..97e19b271d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionController.cs @@ -25,9 +25,9 @@ namespace Tgstation.Server.Host.Components.Watchdog bool TerminationWasRequested { get; } /// - /// If the DMAPI was validated. This field may only be access once completes + /// The DMAPI /// - bool ApiValidated { get; } + ApiValidationStatus ApiValidationStatus { get; } /// /// The being used diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs index 645af10171..4873ce5b21 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ISessionControllerFactory.cs @@ -13,7 +13,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Create a from a freshly launch DreamDaemon instance /// - /// The to use + /// The to use. will be updated with the minumum required security level for the launch /// The to use /// The current if any /// If the of should be used diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 21a45d3e60..2002597284 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -56,7 +56,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Changes the . If currently triggers a graceful restart /// - /// The new + /// The new . May be modified /// The for the operation /// A representing the running operation Task ChangeSettings(DreamDaemonLaunchParameters launchParameters, CancellationToken cancellationToken); @@ -76,5 +76,12 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the operation /// A representing the running operation Task Terminate(bool graceful, CancellationToken cancellationToken); + + /// + /// Cancels pending graceful actions + /// + /// The for the operation + /// A representing the running operation + Task ResetRebootState(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs index 68717edc9c..7c4091e959 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs @@ -1,4 +1,5 @@ using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Compiler; namespace Tgstation.Server.Host.Components.Watchdog @@ -11,9 +12,14 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Creates a /// + /// The for the /// The for the with + /// The for the + /// The for the + /// The for the + /// The for the /// The initial for the /// A new - IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings); + IWatchdog CreateWatchdog(IChat chat, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IEventConsumer eventConsumer, ISessionControllerFactory sessionControllerFactory, Api.Models.Instance instance, DreamDaemonSettings settings); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/PosixNetworkPromptReaper.cs b/src/Tgstation.Server.Host/Components/Watchdog/PosixNetworkPromptReaper.cs new file mode 100644 index 0000000000..f12cfbc7b9 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Watchdog/PosixNetworkPromptReaper.cs @@ -0,0 +1,12 @@ +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Watchdog +{ + //POSIX BYOND doesn't prompt you when you change the port + /// + sealed class PosixNetworkPromptReaper : INetworkPromptReaper + { + /// + public void RegisterProcess(IProcess process) { } + } +} diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs index f017a51ed1..d6730f4630 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionController.cs @@ -29,13 +29,13 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public bool ApiValidated + public ApiValidationStatus ApiValidationStatus { get { if (!Lifetime.IsCompleted) throw new InvalidOperationException("ApiValidated cannot be checked while Lifetime is incomplete!"); - return apiValidated; + return apiValidationStatus; } } @@ -126,6 +126,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly ILogger logger; + /// + /// The level the was launched with + /// + readonly DreamDaemonSecurity? launchSecurityLevel; + /// /// The waits on when DreamDaemon currently has it's ports closed /// @@ -151,9 +156,9 @@ namespace Tgstation.Server.Host.Components.Watchdog bool disposed; /// - /// If the DMAPI was validated + /// The for the /// - bool apiValidated; + ApiValidationStatus apiValidationStatus; /// /// If should be kept alive instead @@ -171,8 +176,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of + /// The value of /// The optional time to wait before failing the - public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger logger, uint? startupTimeout) + public SessionController(ReattachInformation reattachInformation, IProcess process, IByondExecutableLock byondLock, IByondTopicSender byondTopicSender, IJsonTrackingContext chatJsonTrackingContext, ICommContext interopContext, IChat chat, ILogger logger, DreamDaemonSecurity? launchSecurityLevel, uint? startupTimeout) { this.chatJsonTrackingContext = chatJsonTrackingContext; //null valid this.reattachInformation = reattachInformation ?? throw new ArgumentNullException(nameof(reattachInformation)); @@ -183,11 +189,13 @@ namespace Tgstation.Server.Host.Components.Watchdog this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.launchSecurityLevel = launchSecurityLevel; + interopContext.RegisterHandler(this); portClosedForReboot = false; disposed = false; - apiValidated = false; + apiValidationStatus = ApiValidationStatus.NeverValidated; released = false; rebootTcs = new TaskCompletionSource(); @@ -270,6 +278,7 @@ namespace Tgstation.Server.Host.Components.Watchdog object content; Action postRespond = null; + ushort? overrideResponsePort = null; if (query.TryGetValue(Constants.DMParameterCommand, out var method)) { content = new object(); @@ -289,6 +298,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { /////UHHHH logger.LogWarning("DreamDaemon sent new port command without providing it's own!"); + content = new ErrorMessage { Message = "Missing stringified port as data parameter!" }; break; } @@ -301,6 +311,7 @@ namespace Tgstation.Server.Host.Components.Watchdog //if it fails it'll kill itself content = new Dictionary { { Constants.DMParameterData, nextPort.Value } }; reattachInformation.Port = nextPort.Value; + overrideResponsePort = currentPort; nextPort = null; //we'll also get here from SetPort so complete that task @@ -314,7 +325,30 @@ namespace Tgstation.Server.Host.Components.Watchdog } break; case Constants.DMCommandApiValidate: - apiValidated = true; + if (!launchSecurityLevel.HasValue) + { + logger.LogWarning("DreamDaemon requested API validation but no intial security level was passed to the session controller!"); + apiValidationStatus = ApiValidationStatus.UnaskedValidationRequest; + content = new ErrorMessage { Message = "Invalid API validation request!" }; + break; + } + if (!query.TryGetValue(Constants.DMParameterData, out var stringMinimumSecurityLevel) || !Enum.TryParse(stringMinimumSecurityLevel, out var minimumSecurityLevel)) + apiValidationStatus = ApiValidationStatus.BadValidationRequest; + else + switch (minimumSecurityLevel) + { + case DreamDaemonSecurity.Safe: + apiValidationStatus = ApiValidationStatus.RequiresSafe; + break; + case DreamDaemonSecurity.Ultrasafe: + apiValidationStatus = ApiValidationStatus.RequiresUltrasafe; + break; + case DreamDaemonSecurity.Trusted: + apiValidationStatus = ApiValidationStatus.RequiresTrusted; + break; + default: + throw new InvalidOperationException("Enum.TryParse failed to validate the DreamDaemonSecurity range!"); + } break; case Constants.DMCommandWorldReboot: if (ClosePortOnReboot) @@ -335,7 +369,7 @@ namespace Tgstation.Server.Host.Components.Watchdog content = new ErrorMessage { Message = "Missing command parameter!" }; var json = JsonConvert.SerializeObject(content); - var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), cancellationToken).ConfigureAwait(false); + var response = await SendCommand(String.Format(CultureInfo.InvariantCulture, "{0}&{1}={2}", byondTopicSender.SanitizeString(Constants.DMTopicInteropResponse), byondTopicSender.SanitizeString(Constants.DMParameterData), byondTopicSender.SanitizeString(json)), overrideResponsePort, cancellationToken).ConfigureAwait(false); if (response != Constants.DMResponseSuccess) logger.LogWarning("Recieved error response while responding to interop: {0}", response); @@ -367,7 +401,9 @@ namespace Tgstation.Server.Host.Components.Watchdog } /// - public async Task SendCommand(string command, CancellationToken cancellationToken) + public Task SendCommand(string command, CancellationToken cancellationToken) => SendCommand(command, null, cancellationToken); + + async Task SendCommand(string command, ushort? overridePort, CancellationToken cancellationToken) { try { @@ -379,10 +415,11 @@ namespace Tgstation.Server.Host.Components.Watchdog //intentionally don't sanitize command, that's up to the caller command); - logger.LogTrace("Export to :{0}. Query: {1}", reattachInformation.Port, commandString); + var targetPort = overridePort ?? reattachInformation.Port; + logger.LogTrace("Export to :{0}. Query: {1}", targetPort, commandString); return await byondTopicSender.SendTopic( - new IPEndPoint(IPAddress.Loopback, reattachInformation.Port), + new IPEndPoint(IPAddress.Loopback, targetPort), commandString, cancellationToken).ConfigureAwait(false); } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs index d4ce1cdc68..d4af485ee8 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/SessionControllerFactory.cs @@ -57,6 +57,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IChat chat; + /// + /// The for the + /// + readonly INetworkPromptReaper networkPromptReaper; + /// /// The for the /// @@ -98,8 +103,9 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of + /// The value of /// The value of - public SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, ILoggerFactory loggerFactory, Api.Models.Instance instance) + public SessionControllerFactory(IProcessExecutor processExecutor, IByondManager byond, IByondTopicSender byondTopicSender, ICryptographySuite cryptographySuite, IApplication application, IIOManager ioManager, IChat chat, INetworkPromptReaper networkPromptReaper, ILoggerFactory loggerFactory, Api.Models.Instance instance) { this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor)); this.byond = byond ?? throw new ArgumentNullException(nameof(byond)); @@ -109,6 +115,7 @@ namespace Tgstation.Server.Host.Components.Watchdog this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); + this.networkPromptReaper = networkPromptReaper ?? throw new ArgumentNullException(nameof(networkPromptReaper)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } @@ -131,6 +138,22 @@ namespace Tgstation.Server.Host.Components.Watchdog //i changed this back from guids, hopefully i don't regret that string JsonFile(string name) => String.Format(CultureInfo.InvariantCulture, "{0}.{1}", name, JsonPostfix); + var securityLevelToUse = launchParameters.SecurityLevel.Value; + switch (dmbProvider.CompileJob.MinimumSecurityLevel) + { + case DreamDaemonSecurity.Ultrasafe: + break; + case DreamDaemonSecurity.Safe: + if (securityLevelToUse == DreamDaemonSecurity.Ultrasafe) + securityLevelToUse = DreamDaemonSecurity.Safe; + break; + case DreamDaemonSecurity.Trusted: + securityLevelToUse = DreamDaemonSecurity.Trusted; + break; + default: + throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid DreamDaemonSecurity value: {0}", dmbProvider.CompileJob.MinimumSecurityLevel)); + } + //setup interop files var interopInfo = new JsonFile { @@ -140,6 +163,7 @@ namespace Tgstation.Server.Host.Components.Watchdog ChatCommandsJson = JsonFile("chat_commands"), ServerCommandsJson = JsonFile("server_commands"), InstanceName = instance.Name, + SecurityLevel = securityLevelToUse, Revision = new Api.Models.Internal.RevisionInformation { CommitSha = dmbProvider.CompileJob.RevisionInformation.CommitSha, @@ -182,15 +206,17 @@ namespace Tgstation.Server.Host.Components.Watchdog dmbProvider.DmbName, primaryPort ? launchParameters.PrimaryPort : launchParameters.SecondaryPort, launchParameters.AllowWebClient.Value ? "-webclient " : String.Empty, - SecurityWord(launchParameters.SecurityLevel.Value), + SecurityWord(securityLevelToUse), parameters); //launch dd - var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments); + var process = processExecutor.LaunchProcess(byondLock.DreamDaemonPath, basePath, arguments, noShellExecute: true); try { + networkPromptReaper.RegisterProcess(process); + //return the session controller for it - return new SessionController(new ReattachInformation + var result = new SessionController(new ReattachInformation { AccessIdentifier = accessIdentifier, Dmb = dmbProvider, @@ -200,7 +226,12 @@ namespace Tgstation.Server.Host.Components.Watchdog ChatChannelsJson = interopInfo.ChatChannelsJson, ChatCommandsJson = interopInfo.ChatCommandsJson, ServerCommandsJson = interopInfo.ServerCommandsJson, - }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), launchParameters.StartupTimeout); + }, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), launchParameters.SecurityLevel, launchParameters.StartupTimeout); + + //writeback launch parameter's fixed security level + launchParameters.SecurityLevel = securityLevelToUse; + + return result; } catch { @@ -247,7 +278,8 @@ namespace Tgstation.Server.Host.Components.Watchdog var process = processExecutor.GetProcess(reattachInformation.ProcessId); try { - return new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), null); + networkPromptReaper.RegisterProcess(process); + return new SessionController(reattachInformation, process, byondLock, byondTopicSender, chatJsonTrackingContext, context, chat, loggerFactory.CreateLogger(), null, null); } catch { diff --git a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs index b91af00859..b97eafd733 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/Watchdog.cs @@ -1,4 +1,5 @@ using Byond.TopicSender; +using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; @@ -10,6 +11,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Models.Internal; +using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components.Chat; using Tgstation.Server.Host.Components.Compiler; using Tgstation.Server.Host.Components.Interop; @@ -18,7 +20,7 @@ using Tgstation.Server.Host.Core; namespace Tgstation.Server.Host.Components.Watchdog { /// - sealed class Watchdog : IWatchdog, ICustomCommandHandler + sealed class Watchdog : IWatchdog, ICustomCommandHandler, IRestartHandler { /// /// The time in seconds to wait from starting to start . Does not take responsiveness into account @@ -86,6 +88,16 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IEventConsumer eventConsumer; + /// + /// The for the + /// + readonly IJobManager jobManager; + + /// + /// The for the + /// + readonly IRestartRegistration restartRegistration; + /// /// The for the /// @@ -136,16 +148,17 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The value of /// The value of - /// The for the /// The value of /// The value of /// The value of /// The value of - /// The initial value of + /// The value of + /// The value of + /// The to populate with + /// The initial value of . May be modified /// The value of /// The value of - /// The value of - public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, IServerControl serverUpdater, ILogger logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart) + public Watchdog(IChat chat, ISessionControllerFactory sessionControllerFactory, IDmbFactory dmbFactory, ILogger logger, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, IJobManager jobManager, IServerControl serverControl, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart) { this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); @@ -155,13 +168,14 @@ namespace Tgstation.Server.Host.Components.Watchdog this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); this.autoStart = autoStart; - if (serverUpdater == null) - throw new ArgumentNullException(nameof(serverUpdater)); + if (serverControl == null) + throw new ArgumentNullException(nameof(serverControl)); - serverUpdater.RegisterForRestart(() => releaseServers = true); + restartRegistration = serverControl.RegisterForRestart(this); chat.RegisterCommandHandler(this); @@ -177,6 +191,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { DisposeAndNullControllers(); semaphore.Dispose(); + restartRegistration.Dispose(); } /// @@ -206,6 +221,7 @@ namespace Tgstation.Server.Host.Components.Watchdog var chatTask = announce ? chat.SendWatchdogMessage("Terminating...", cancellationToken) : Task.CompletedTask; await StopMonitor().ConfigureAwait(false); DisposeAndNullControllers(); + LastLaunchParameters = null; await chatTask.ConfigureAwait(false); return; } @@ -431,11 +447,10 @@ namespace Tgstation.Server.Host.Components.Watchdog monitorState.NextAction = MonitorAction.Continue; break; case MonitorActivationReason.NewDmbAvailable: - monitorState.InactiveServerHasStagedDmb = true; - await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); //next case does same thing - break; + monitorState.InactiveServerHasStagedDmb = true; + goto case MonitorActivationReason.ActiveLaunchParametersUpdated; case MonitorActivationReason.ActiveLaunchParametersUpdated: - await UpdateAndRestartInactiveServer(false).ConfigureAwait(false); + await UpdateAndRestartInactiveServer(true).ConfigureAwait(false); break; } } @@ -607,6 +622,8 @@ namespace Tgstation.Server.Host.Components.Watchdog { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { + if (launchParameters.Match(ActiveLaunchParameters)) + return; ActiveLaunchParameters = launchParameters; if (Running) //queue an update @@ -700,15 +717,16 @@ namespace Tgstation.Server.Host.Components.Watchdog await Task.WhenAny(allTask, cancelTcs.Task).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); - //both servers are now running, alpha is the active server, huzzah - AlphaIsActive = doReattach ? reattachInfo.AlphaIsActive : true; + //both servers are now running, alpha is the active server(unless reattach), huzzah + AlphaIsActive = doReattach ? reattachInfo?.AlphaIsActive ?? true : true; LastLaunchResult = alphaLrt.Result; + (AlphaIsActive ? alphaServer : bravoServer).ClosePortOnReboot = true; + logger.LogInformation("Launched servers successfully"); Running = true; if (startMonitor) { - await StopMonitor().ConfigureAwait(false); monitorCts = new CancellationTokenSource(); monitorTask = MonitorLifetimes(monitorCts.Token); } @@ -731,6 +749,13 @@ namespace Tgstation.Server.Host.Components.Watchdog } catch (Exception e) { + var originalChatTask = chatTask; + async Task ChainChatTaskWithErrorMessage() + { + await originalChatTask.ConfigureAwait(false); + await chat.SendWatchdogMessage("Startup failed!", cancellationToken).ConfigureAwait(false); + } + chatTask = ChainChatTaskWithErrorMessage(); logger.LogWarning("Failed to start watchdog: {0}", e.ToString()); throw; } @@ -752,9 +777,23 @@ namespace Tgstation.Server.Host.Components.Watchdog return await LaunchNoLock(true, true, false, cancellationToken).ConfigureAwait(false); } + /// + public async Task ResetRebootState(CancellationToken cancellationToken) + { + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) + { + if (!Running) + return; + var toClear = AlphaIsActive ? alphaServer : bravoServer; + if (toClear != null) + toClear.ResetRebootState(); + } + } + /// public async Task Restart(bool graceful, CancellationToken cancellationToken) { + logger.LogTrace("Begin Restart. Graceful: {0}", graceful); using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) { if (!graceful || !Running) @@ -772,12 +811,10 @@ namespace Tgstation.Server.Host.Components.Watchdog return result; } var toReboot = AlphaIsActive ? alphaServer : bravoServer; - var other = AlphaIsActive ? bravoServer : alphaServer; if (toReboot != null) { if (!await toReboot.SetRebootState(Components.Watchdog.RebootState.Restart, cancellationToken).ConfigureAwait(false)) logger.LogWarning("Unable to send reboot state change event!"); - } return null; } @@ -793,8 +830,27 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public async Task StartAsync(CancellationToken cancellationToken) { - if (autoStart) - await LaunchNoLock(true, true, true, cancellationToken).ConfigureAwait(false); + if (!autoStart) + return; + + long? adminUserId = null; + + await databaseContextFactory.UseContext(async db => adminUserId = await db.Users.Select(x => x.Id).FirstAsync(cancellationToken).ConfigureAwait(false)).ConfigureAwait(false); + var job = new Models.Job + { + StartedBy = new Models.User + { + Id = adminUserId.Value + }, + Instance = new Models.Instance + { + Id = instance.Id + }, + Description = "Instance startup watchdog launch", + CancelRight = (ulong)DreamDaemonRights.Shutdown, + CancelRightsType = RightsType.DreamDaemon + }; + await jobManager.RegisterOperation(job, (j, databaseContext, progressFunction, ct) => Launch(ct), cancellationToken).ConfigureAwait(false); } /// @@ -885,5 +941,13 @@ namespace Tgstation.Server.Host.Components.Watchdog return await activeServer.SendCommand(command, cancellationToken).ConfigureAwait(false) ?? "ERROR: Bad topic exchange!"; } } + + /// + public async Task HandleRestart(Version updateVersion, CancellationToken cancellationToken) + { + releaseServers = true; + if (Running) + await chat.SendWatchdogMessage("Detaching...", cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index 65b0452042..b2d33d838d 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -11,31 +11,16 @@ namespace Tgstation.Server.Host.Components.Watchdog /// sealed class WatchdogFactory : IWatchdogFactory { - /// - /// The for the - /// - readonly IChat chat; - - /// - /// The for the - /// - readonly ISessionControllerFactory sessionControllerFactory; - /// /// The for the /// - readonly IServerControl serverUpdater; + readonly IServerControl serverControl; /// /// The for the /// readonly ILoggerFactory loggerFactory; - /// - /// The for the - /// - readonly IReattachInfoHandler reattachInfoHandler; - /// /// The for the /// @@ -47,42 +32,28 @@ namespace Tgstation.Server.Host.Components.Watchdog readonly IByondTopicSender byondTopicSender; /// - /// The for the + /// The for the /// - readonly IEventConsumer eventConsumer; - - /// - /// The for the - /// - readonly Api.Models.Instance instance; - + readonly IJobManager jobManager; /// /// Construct a /// - /// The value of - /// The value of - /// The value of + /// The value of /// The value of - /// The value of /// The value of /// The value of - /// The value of - /// The value of - public WatchdogFactory(IChat chat, ISessionControllerFactory sessionControllerFactory, IServerControl serverUpdater, ILoggerFactory loggerFactory, IReattachInfoHandler reattachInfoHandler, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IEventConsumer eventConsumer, Api.Models.Instance instance) + /// The value of + public WatchdogFactory(IServerControl serverControl, ILoggerFactory loggerFactory, IDatabaseContextFactory databaseContextFactory, IByondTopicSender byondTopicSender, IJobManager jobManager) { - this.chat = chat ?? throw new ArgumentNullException(nameof(chat)); - this.sessionControllerFactory = sessionControllerFactory ?? throw new ArgumentNullException(nameof(sessionControllerFactory)); - this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); + this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); - this.reattachInfoHandler = reattachInfoHandler ?? throw new ArgumentNullException(nameof(reattachInfoHandler)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.byondTopicSender = byondTopicSender ?? throw new ArgumentNullException(nameof(byondTopicSender)); - this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer)); - this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); + this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); } /// - public IWatchdog CreateWatchdog(IDmbFactory dmbFactory, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, serverUpdater, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, settings, instance, settings.AutoStart.Value); + public IWatchdog CreateWatchdog(IChat chat, IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, IEventConsumer eventConsumer, ISessionControllerFactory sessionControllerFactory, Api.Models.Instance instance, DreamDaemonSettings settings) => new Watchdog(chat, sessionControllerFactory, dmbFactory, loggerFactory.CreateLogger(), reattachInfoHandler, databaseContextFactory, byondTopicSender, eventConsumer, jobManager, serverControl, settings, instance, settings.AutoStart.Value); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogReattachInformation.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogReattachInformation.cs index f340a4cd8b..91ec990b55 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogReattachInformation.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogReattachInformation.cs @@ -1,4 +1,6 @@ -using Tgstation.Server.Host.Models; +using System; +using System.Globalization; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Watchdog { @@ -35,5 +37,8 @@ namespace Tgstation.Server.Host.Components.Watchdog if (copy.Bravo != null) Bravo = new ReattachInformation(copy.Bravo, dmbBravo); } + + /// + public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Alpha: {0}, Bravo {1}", Alpha, Bravo); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsNetworkPromptReaper.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsNetworkPromptReaper.cs new file mode 100644 index 0000000000..2298971e6a --- /dev/null +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsNetworkPromptReaper.cs @@ -0,0 +1,200 @@ +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Components.Watchdog +{ + /// + sealed class WindowsNetworkPromptReaper : IHostedService, INetworkPromptReaper, IDisposable + { + /// + /// Number of times to send the button click message. Should be at least 2 or it may fail to focus the window + /// + const int SendMessageCount = 5; + + /// + /// Check for prompts each time this amount of milliseconds pass + /// + const int RecheckDelayMs = 250; + + /// + /// The for the + /// + readonly ILogger logger; + + /// + /// The for the + /// + readonly CancellationTokenSource cancellationTokenSource; + + /// + /// The list of s registered + /// + readonly List registeredProcesses; + + /// + /// The representing the lifetime of the + /// + Task runTask; + + static bool EnumWindow(IntPtr hWnd, IntPtr lParam) + { + var gcChildhandlesList = GCHandle.FromIntPtr(lParam); + + if (gcChildhandlesList == null || gcChildhandlesList.Target == null) + return false; + + var childHandles = (List )gcChildhandlesList.Target; + childHandles.Add(hWnd); + + return true; + } + + static List GetAllChildHandles(IntPtr main) + { + var childHandles = new List(); + + var gcChildhandlesList = GCHandle.Alloc(childHandles); + var pointerChildHandlesList = GCHandle.ToIntPtr(gcChildhandlesList); + + try + { + NativeMethods.EnumWindowProc childProc = new NativeMethods.EnumWindowProc(EnumWindow); + NativeMethods.EnumChildWindows(main, childProc, pointerChildHandlesList); + } + finally + { + gcChildhandlesList.Free(); + } + + return childHandles; + } + + /// + /// Construct a + /// + /// The value of + public WindowsNetworkPromptReaper(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + + registeredProcesses = new List(); + cancellationTokenSource = new CancellationTokenSource(); + } + + /// + public void Dispose() => cancellationTokenSource.Dispose(); + + async Task Run(CancellationToken cancellationToken) + { + logger.LogDebug("Starting network prompt reaper..."); + try + { + while (!cancellationToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromMilliseconds(RecheckDelayMs), cancellationToken).ConfigureAwait(false); + + IntPtr window; + int processId; + lock (registeredProcesses) + { + if (registeredProcesses.Count == 0) + continue; + + window = NativeMethods.FindWindow(null, "Network Accessibility"); + if (window == IntPtr.Zero) + continue; + + //found a bitch + var threadId = NativeMethods.GetWindowThreadProcessId(window, out processId); + if (!registeredProcesses.Any(x => x.Id == processId)) + //not our bitch + continue; + } + logger.LogTrace("Identified \"Network Accessibility\" window in owned process {0}", processId); + + var found = false; + foreach (var I in GetAllChildHandles(window)) + { + const int MaxLength = 10; + var stringBuilder = new StringBuilder(MaxLength + 1); + + if (NativeMethods.GetWindowText(I, stringBuilder, MaxLength) == 0) + { + logger.LogWarning("Error calling GetWindowText! Exception: {0}", new Win32Exception(Marshal.GetLastWin32Error())); + continue; + } + + var windowText = stringBuilder.ToString(); + if (windowText == "Yes") + { + //smash_button_meme.jpg + logger.LogTrace("Sending \"Yes\" button clicks..."); + for (var J = 0; J < SendMessageCount; ++J) + { + const int BM_CLICK = 0x00F5; + var result = NativeMethods.SendMessage(I, BM_CLICK, IntPtr.Zero, IntPtr.Zero); + } + found = true; + break; + } + } + + if (!found) + logger.LogDebug("Unable to find \"Yes\" button for \"Network Accessibility\" window in owned process {0}!", processId); + } + } + catch (OperationCanceledException) { } + finally + { + logger.LogDebug("Exiting network prompt reaper..."); + } + } + + /// + public Task StartAsync(CancellationToken cancellationToken) + { + runTask = Run(cancellationTokenSource.Token); + return Task.CompletedTask; + } + + /// + public async Task StopAsync(CancellationToken cancellationToken) + { + logger.LogTrace("Stopping network prompt reaper..."); + cancellationTokenSource.Cancel(); + await runTask.ConfigureAwait(false); + registeredProcesses.Clear(); + } + + /// + public void RegisterProcess(IProcess process) + { + if (process == null) + throw new ArgumentNullException(nameof(process)); + + lock (registeredProcesses) + { + if (registeredProcesses.Contains(process)) + throw new InvalidOperationException("This process has already been registered for network prompt reaping!"); + logger.LogTrace("Registering process {0}...", process.Id); + registeredProcesses.Add(process); + } + + process.Lifetime.ContinueWith(x => + { + logger.LogTrace("Unregistering process {0}...", process.Id); + lock (registeredProcesses) + registeredProcesses.Remove(process); + }, TaskScheduler.Current); + } + } +} diff --git a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs index 6b7884b399..fc5cf03f09 100644 --- a/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/GeneralConfiguration.cs @@ -15,6 +15,11 @@ /// public string LogFileDirectory { get; set; } + /// + /// The stringified for file logging + /// + public string LogFileLevel { get; set; } + /// /// If file logging is disabled /// @@ -24,5 +29,10 @@ /// Minimum length of database user passwords /// public uint MinimumPasswordLength { get; set; } + + /// + /// A GitHub personal access token to use for bypassing rate limits on requests. Requires no scopes + /// + public string GitHubAccessToken { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index a401011001..fdb40c2462 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -30,10 +30,12 @@ namespace Tgstation.Server.Host.Controllers { const string RestartNotSupportedException = "This deployment of tgstation-server is lacking the Tgstation.Server.Host.Watchdog component. Restarts and version changes cannot be completed!"; + const string OctokitException = "Bad GitHub API response, check configuration! Exception: {0}"; + /// - /// The for the + /// The for the /// - readonly IGitHubClient gitHubClient; + readonly IGitHubClientFactory gitHubClientFactory; /// /// The for the @@ -55,24 +57,31 @@ namespace Tgstation.Server.Host.Controllers /// readonly UpdatesConfiguration updatesConfiguration; + /// + /// The for the + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Construct an /// /// The for the /// The for the - /// The value of + /// The value of /// The value of /// The value of /// The value of /// The for the /// The containing value of - public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClient gitHubClient, IServerControl serverUpdater, IApplication application, IIOManager ioManager, ILogger logger, IOptions updatesConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false) + /// The containing value of + public AdministrationController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IGitHubClientFactory gitHubClientFactory, IServerControl serverUpdater, IApplication application, IIOManager ioManager, ILogger logger, IOptions updatesConfigurationOptions, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, false) { - this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient)); + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.serverUpdater = serverUpdater ?? throw new ArgumentNullException(nameof(serverUpdater)); this.application = application ?? throw new ArgumentNullException(nameof(application)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } StatusCodeResult RateLimit(RateLimitExceededException exception) @@ -83,6 +92,8 @@ namespace Tgstation.Server.Host.Controllers return StatusCode(429); } + IGitHubClient GetGitHubClient() => String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) ? gitHubClientFactory.CreateClient() : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken); + /// [TgsAuthorize] public override async Task Read(CancellationToken cancellationToken) @@ -93,6 +104,7 @@ namespace Tgstation.Server.Host.Controllers Uri repoUrl = null; try { + var gitHubClient = GetGitHubClient(); var repositoryTask = gitHubClient.Repository.Get(updatesConfiguration.GitHubRepositoryId); var releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)); @@ -118,6 +130,11 @@ namespace Tgstation.Server.Host.Controllers { return RateLimit(e); } + catch (ApiException e) + { + Logger.LogWarning(OctokitException, e); + return StatusCode((int)HttpStatusCode.FailedDependency); + } } /// @@ -137,12 +154,18 @@ namespace Tgstation.Server.Host.Controllers IEnumerable releases; try { + var gitHubClient = GetGitHubClient(); releases = (await gitHubClient.Repository.Release.GetAll(updatesConfiguration.GitHubRepositoryId).ConfigureAwait(false)).Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)); } catch (RateLimitExceededException e) { return RateLimit(e); } + catch (ApiException e) + { + Logger.LogWarning(OctokitException, e); + return StatusCode((int)HttpStatusCode.FailedDependency); + } Logger.LogTrace("Release query complete!"); foreach (var release in releases) @@ -157,7 +180,7 @@ namespace Tgstation.Server.Host.Controllers try { Logger.LogDebug("Extracting server update..."); - if (!await serverUpdater.ApplyUpdate(assetBytes, ioManager, cancellationToken).ConfigureAwait(false)) + if (!await serverUpdater.ApplyUpdate(version, assetBytes, ioManager, cancellationToken).ConfigureAwait(false)) return UnprocessableEntity(new ErrorMessage { Message = RestartNotSupportedException @@ -178,23 +201,23 @@ namespace Tgstation.Server.Host.Controllers /// [HttpDelete] [TgsAuthorize(AdministrationRights.RestartHost)] - public Task Delete() + public async Task Delete() { try { - var result = serverUpdater.Restart(); + var result = await serverUpdater.Restart().ConfigureAwait(false); if (result) Logger.LogInformation("Restarting host by request..."); else Logger.LogDebug("Restart request failed due to lack of host watchdog!"); - return Task.FromResult(result ? (IActionResult)Ok() : UnprocessableEntity(new ErrorMessage + return result ? (IActionResult)Ok() : UnprocessableEntity(new ErrorMessage { Message = RestartNotSupportedException - })); + }); } catch (InvalidOperationException) { - return Task.FromResult(StatusCode((int)HttpStatusCode.ServiceUnavailable)); + return StatusCode((int)HttpStatusCode.ServiceUnavailable); } } } diff --git a/src/Tgstation.Server.Host/Controllers/ApiController.cs b/src/Tgstation.Server.Host/Controllers/ApiController.cs index 16fa1704dd..7b62fa020d 100644 --- a/src/Tgstation.Server.Host/Controllers/ApiController.cs +++ b/src/Tgstation.Server.Host/Controllers/ApiController.cs @@ -1,20 +1,14 @@ -using Microsoft.AspNetCore.Authentication.JwtBearer; -using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Globalization; -using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Net; -using System.Security.Claims; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; -using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -57,64 +51,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly bool requireInstance; - /// - /// Runs after a has been validated. Creates the for the - /// - /// The for the operation - /// A representing the running operation - public static async Task OnTokenValidated(TokenValidatedContext context) - { - var databaseContext = context.HttpContext.RequestServices.GetRequiredService(); - var authenticationContextFactory = context.HttpContext.RequestServices.GetRequiredService(); - - var userIdClaim = context.Principal.FindFirst(JwtRegisteredClaimNames.Sub); - - if (userIdClaim == default(Claim)) - throw new InvalidOperationException("Missing required claim!"); - - long userId; - try - { - userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture); - } - catch (Exception e) - { - throw new InvalidOperationException("Failed to parse user ID!", e); - } - - ApiHeaders apiHeaders; - try - { - apiHeaders = new ApiHeaders(context.HttpContext.Request.GetTypedHeaders()); - } - catch - { - //let OnActionExecutionAsync handle the reponse - return; - } - - await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, context.SecurityToken.ValidFrom, context.HttpContext.RequestAborted).ConfigureAwait(false); - - var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext; - - var enumerator = Enum.GetValues(typeof(RightsType)); - var claims = new List(); - foreach (RightsType I in enumerator) - { - //if there's no instance user, do a weird thing and add all the instance roles - //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid - //if user is null that means they got the token with an expired password - var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I); - var rightEnum = RightsHelper.RightToType(I); - var right = (Enum)Enum.ToObject(rightEnum, rightInt); - foreach (Enum J in Enum.GetValues(rightEnum)) - if (right.HasFlag(J)) - claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J))); - } - - context.Principal.AddIdentity(new ClaimsIdentity(claims)); - } - /// /// Construct an /// @@ -210,6 +146,7 @@ namespace Tgstation.Server.Host.Controllers catch (OperationCanceledException e) { Logger.LogDebug("Request cancelled! Exception: {0}", e); + throw; } } } diff --git a/src/Tgstation.Server.Host/Controllers/ByondController.cs b/src/Tgstation.Server.Host/Controllers/ByondController.cs index 566aaae2b0..f8be5a0174 100644 --- a/src/Tgstation.Server.Host/Controllers/ByondController.cs +++ b/src/Tgstation.Server.Host/Controllers/ByondController.cs @@ -95,7 +95,7 @@ namespace Tgstation.Server.Host.Controllers CancelRight = (ulong)ByondRights.CancelInstall, Instance = Instance }; - await jobManager.RegisterOperation(job, (paramJob, serviceProvicer, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false); result.InstallJob = job.ToApi(); } result.Version = byondManager.ActiveVersion; diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index baca707185..2e6d3a9ebc 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -53,7 +53,8 @@ namespace Tgstation.Server.Host.Controllers IrcChannel = api.IrcChannel, IsAdminChannel = api.IsAdminChannel ?? false, IsWatchdogChannel = api.IsWatchdogChannel ?? false, - IsUpdatesChannel = api.IsUpdatesChannel ?? false + IsUpdatesChannel = api.IsUpdatesChannel ?? false, + Tag = api.Tag }; /// @@ -81,12 +82,11 @@ namespace Tgstation.Server.Host.Controllers return BadRequest(new ErrorMessage { Message = "Invalid provider!" }); } - if (!model.Enabled.HasValue) - return BadRequest(new ErrorMessage { Message = "enabled cannot be null!" }); - if (!model.ValidateProviderChannelTypes()) return BadRequest(new ErrorMessage { Message = "One or more of channels aren't formatted correctly for the given provider!" }); + model.Enabled = model.Enabled ?? false; + //try to update das db first var dbModel = new Models.ChatBot { @@ -210,6 +210,8 @@ namespace Tgstation.Server.Host.Controllers return false; }; + var oldProvider = current.Provider; + if (CheckModified(x => x.ConnectionString, ChatBotRights.WriteConnectionString) || CheckModified(x => x.Enabled, ChatBotRights.WriteEnabled) || CheckModified(x => x.Name, ChatBotRights.WriteName) @@ -217,12 +219,18 @@ namespace Tgstation.Server.Host.Controllers || (model.Channels != null && !userRights.HasFlag(ChatBotRights.WriteChannels))) return Forbid(); - if (model.Channels != null) + var hasChannels = model.Channels != null; + if (hasChannels || (model.Provider.HasValue && model.Provider != oldProvider)) { DatabaseContext.ChatChannels.RemoveRange(current.Channels); - var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList(); - DatabaseContext.ChatChannels.AddRange(dbChannels); - current.Channels = dbChannels; + if (hasChannels) + { + var dbChannels = model.Channels.Select(x => ConvertApiChatChannel(x)).ToList(); + DatabaseContext.ChatChannels.AddRange(dbChannels); + current.Channels = dbChannels; + } + else + current.Channels.Clear(); } await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index d60ae85a35..327f289072 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -68,7 +68,7 @@ namespace Tgstation.Server.Host.Controllers if (newFile == null) return Conflict(new ErrorMessage { - Message = "" + Message = "This file has been updated since you last viewed it!" }); newFile.Content = null; @@ -95,7 +95,7 @@ namespace Tgstation.Server.Host.Controllers /// The path of the file to get /// The for the operation /// A resulting in the for the operation - [HttpGet("File/{*filePath}")] + [HttpGet(Routes.File + "/{*filePath}")] [TgsAuthorize(ConfigurationRights.Read)] public async Task File(string filePath, CancellationToken cancellationToken) { @@ -171,6 +171,7 @@ namespace Tgstation.Server.Host.Controllers try { + model.IsDirectory = true; return await instanceManager.GetInstance(Instance).Configuration.CreateDirectory(model.Path, AuthenticationContext.SystemIdentity, cancellationToken).ConfigureAwait(false) ? (IActionResult)Json(model) : StatusCode((int)HttpStatusCode.Created, model); } catch (NotImplementedException) diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 59dfd9016b..af3bb8cc15 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -67,11 +67,11 @@ namespace Tgstation.Server.Host.Controllers StartedBy = AuthenticationContext.User }; await jobManager.RegisterOperation(job, - async (paramJob, serviceProvider, progressHandler, innerCt) => + async (paramJob, databaseContext, progressHandler, innerCt) => { var result = await instance.Watchdog.Launch(innerCt).ConfigureAwait(false); if (result == null) - throw new InvalidOperationException("Watchdog already running!"); + throw new JobException("Watchdog already running!"); }, cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); @@ -119,13 +119,15 @@ namespace Tgstation.Server.Host.Controllers result.SecurityLevel = settings.SecurityLevel; result.SoftRestart = rstate == RebootState.Restart; result.SoftShutdown = rstate == RebootState.Shutdown; + result.StartupTimeout = settings.StartupTimeout; }; if (revision) { - result.ActiveCompileJob = dd.ActiveCompileJob?.ToApi(); - var compileJob = instance.LatestCompileJob(); - result.StagedCompileJob = compileJob?.ToApi(); + var latestCompileJob = instance.LatestCompileJob(); + result.ActiveCompileJob = (dd.ActiveCompileJob ?? latestCompileJob)?.ToApi(); + if (latestCompileJob?.Id != result.ActiveCompileJob?.Id) + result.StagedCompileJob = latestCompileJob?.ToApi(); } return Json(result); @@ -149,6 +151,12 @@ namespace Tgstation.Server.Host.Controllers if (model == null) throw new ArgumentNullException(nameof(model)); + if (model.PrimaryPort == 0) + return BadRequest(new ErrorMessage { Message = "Primary port cannot be 0!" }); + + if (model.SecurityLevel == DreamDaemonSecurity.Ultrasafe) + return BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" }); + //alias for changing DD settings var current = await DatabaseContext.Instances.Where(x => x.Id == Instance.Id).Select(x => x.DreamDaemonSettings).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); @@ -185,22 +193,47 @@ namespace Tgstation.Server.Host.Controllers || CheckModified(x => x.StartupTimeout, DreamDaemonRights.SetStartupTimeout)) return Forbid(); - if (current.SecurityLevel == DreamDaemonSecurity.Ultrasafe) - return BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" }); + if (current.PrimaryPort == current.SecondaryPort) + return BadRequest(new ErrorMessage { Message = "Primary port and secondary port cannot be the same!" }); var wd = instanceManager.GetInstance(Instance).Watchdog; + + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + //run this second because current may be modified by it + await wd.ChangeSettings(current, cancellationToken).ConfigureAwait(false); - //run these in parallel because they are equally as important - await Task.WhenAll(DatabaseContext.Save(cancellationToken), wd.ChangeSettings(current, cancellationToken)).ConfigureAwait(false); - - //soft shutdown/restart can't be cancelled because of how many things rely on them - //They can be alternated though if (!oldSoftRestart.Value && current.SoftRestart.Value) await wd.Restart(true, cancellationToken).ConfigureAwait(false); else if (!oldSoftShutdown.Value && current.SoftShutdown.Value) await wd.Terminate(true, cancellationToken).ConfigureAwait(false); + else if ((oldSoftRestart.Value && !current.SoftRestart.Value) || (oldSoftShutdown.Value && !current.SoftShutdown.Value)) + await wd.ResetRebootState(cancellationToken).ConfigureAwait(false); return await ReadImpl(current, cancellationToken).ConfigureAwait(false); } + + /// + /// Handle a HTTP PATCH to the + /// + /// The for the operation + /// A resulting in the of the request + [HttpPatch] + [TgsAuthorize(DreamDaemonRights.Restart)] + public async Task Restart(CancellationToken cancellationToken) + { + var job = new Models.Job + { + Instance = Instance, + CancelRightsType = RightsType.DreamDaemon, + CancelRight = (ulong)DreamDaemonRights.Shutdown, + StartedBy = AuthenticationContext.User, + Description = "Restart Watchdog" + }; + + var watchdog = instanceManager.GetInstance(Instance).Watchdog; + + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); + return Accepted(job.ToApi()); + } } } diff --git a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs index ace66c7692..8551b58622 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamMakerController.cs @@ -1,14 +1,13 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; -using System.Collections.Generic; using System.Linq; using System.Net; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Core; @@ -51,12 +50,8 @@ namespace Tgstation.Server.Host.Controllers public override async Task Read(CancellationToken cancellationToken) { var instance = instanceManager.GetInstance(Instance); - var projectName = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).Select(x => x.ProjectName).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); - return Json(new Api.Models.DreamMaker - { - ProjectName = projectName, - Status = instance.DreamMaker.Status - }); + var dreamMakerSettings = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); + return Json(dreamMakerSettings.ToApi()); } /// @@ -78,7 +73,7 @@ namespace Tgstation.Server.Host.Controllers [TgsAuthorize(DreamMakerRights.CompileJobs)] public override async Task List(CancellationToken cancellationToken) { - var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StartedAt).Select(x => new Api.Models.CompileJob + var compileJobs = await DatabaseContext.CompileJobs.Where(x => x.Job.Instance.Id == Instance.Id).OrderByDescending(x => x.Job.StoppedAt).Select(x => new Api.Models.CompileJob { Id = x.Id }).ToListAsync(cancellationToken).ConfigureAwait(false); @@ -87,9 +82,9 @@ namespace Tgstation.Server.Host.Controllers /// [TgsAuthorize(DreamMakerRights.Compile)] - public override async Task Create([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken) + public override async Task Create([FromBody] DreamMaker model, CancellationToken cancellationToken) { - var job = new Job + var job = new Models.Job { Description = "Compile active repository code", StartedBy = AuthenticationContext.User, @@ -97,14 +92,20 @@ namespace Tgstation.Server.Host.Controllers CancelRight = (ulong)DreamMakerRights.CancelCompile, Instance = Instance }; - await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => RunCompile(paramJob, serviceProvider, Instance, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, instanceManager.GetInstance(Instance).CompileProcess, cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); } /// - [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort)] - public override async Task Update([FromBody] Api.Models.DreamMaker model, CancellationToken cancellationToken) + [TgsAuthorize(DreamMakerRights.SetDme | DreamMakerRights.SetApiValidationPort | DreamMakerRights.SetApiValidationPort)] + public override async Task Update([FromBody] DreamMaker model, CancellationToken cancellationToken) { + if (model.ApiValidationPort == 0) + return BadRequest(new ErrorMessage { Message = "API Validation port cannot be 0!" }); + + if (model.ApiValidationSecurityLevel == DreamDaemonSecurity.Ultrasafe) + return BadRequest(new ErrorMessage { Message = "This version of TGS does not support the ultrasafe DreamDaemon configuration!" }); + var hostModel = await DatabaseContext.DreamMakerSettings.Where(x => x.InstanceId == Instance.Id).FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); if (hostModel == null) return StatusCode((int)HttpStatusCode.Gone); @@ -126,74 +127,15 @@ namespace Tgstation.Server.Host.Controllers hostModel.ApiValidationPort = model.ApiValidationPort; } + if (model.ApiValidationSecurityLevel.HasValue) + { + if (!AuthenticationContext.InstanceUser.DreamMakerRights.Value.HasFlag(DreamMakerRights.SetSecurityLevel)) + return Forbid(); + hostModel.ApiValidationSecurityLevel = model.ApiValidationSecurityLevel; + } + await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); return await Read(cancellationToken).ConfigureAwait(false); } - - /// - /// Run the compile job and insert it into the database - /// - /// The running - /// The for the operation - /// The for the operation - /// The for the operation - /// A representing the running operation - async Task RunCompile(Job job, IServiceProvider serviceProvider, Models.Instance instanceModel, CancellationToken cancellationToken) - { - var instanceManager = serviceProvider.GetRequiredService(); - var databaseContext = serviceProvider.GetRequiredService(); - - var ddSettingsTask = databaseContext.DreamDaemonSettings.Where(x => x.InstanceId == instanceModel.Id).Select(x => new DreamDaemonSettings - { - StartupTimeout = x.StartupTimeout, - SecurityLevel = x.SecurityLevel - }).FirstOrDefaultAsync(cancellationToken); - - - var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == instanceModel.Id).FirstAsync(cancellationToken).ConfigureAwait(false); - if (dreamMakerSettings == default) - throw new JobException("Missing DreamMakerSettings in DB!"); - var ddSettings = await ddSettingsTask.ConfigureAwait(false); - if (ddSettings == default) - throw new JobException("Missing DreamDaemonSettings in DB!"); - - var instance = instanceManager.GetInstance(instanceModel); - - CompileJob compileJob; - RevisionInformation revInfo; - using (var repo = await instance.RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) - { - if (repo == null) - throw new JobException("Missing Repository!"); - - var repoSha = repo.Head; - revInfo = await databaseContext.RevisionInformations.Where(x => x.CommitSha == repoSha).Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge).FirstOrDefaultAsync().ConfigureAwait(false); - - if (revInfo == default) - { - revInfo = new RevisionInformation - { - CommitSha = repoSha, - OriginCommitSha = repoSha, - Instance = new Models.Instance - { - Id = Instance.Id - }, - ActiveTestMerges = new List(), - CompileJobs = new List() - }; - databaseContext.Instances.Attach(revInfo.Instance); - } - - compileJob = await instance.DreamMaker.Compile(revInfo, dreamMakerSettings, ddSettings.SecurityLevel.Value, ddSettings.StartupTimeout.Value, repo, cancellationToken).ConfigureAwait(false); - } - - compileJob.Job = job; - - databaseContext.CompileJobs.Add(compileJob); - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - - await instance.CompileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false); - } } } diff --git a/src/Tgstation.Server.Host/Controllers/InstanceController.cs b/src/Tgstation.Server.Host/Controllers/InstanceController.cs index 277677d774..28cc4d34e7 100644 --- a/src/Tgstation.Server.Host/Controllers/InstanceController.cs +++ b/src/Tgstation.Server.Host/Controllers/InstanceController.cs @@ -136,7 +136,8 @@ namespace Tgstation.Server.Host.Controllers }, DreamMakerSettings = new DreamMakerSettings { - ApiValidationPort = 1339 + ApiValidationPort = 1339, + ApiValidationSecurityLevel = DreamDaemonSecurity.Safe }, Name = model.Name, Online = false, @@ -294,7 +295,9 @@ namespace Tgstation.Server.Host.Controllers } } + var oldAutoUpdateInterval = originalModel.AutoUpdateInterval.Value; var originalOnline = originalModel.Online.Value; + var renamed = model.Name != null && originalModel.Name != model.Name; if (CheckModified(x => x.AutoUpdateInterval, InstanceManagerRights.SetAutoUpdate) || CheckModified(x => x.ConfigurationType, InstanceManagerRights.SetConfiguration) @@ -311,6 +314,9 @@ namespace Tgstation.Server.Host.Controllers await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + if (renamed) + instanceManager.GetInstance(originalModel).Rename(originalModel.Name); + var oldAutoStart = originalModel.DreamDaemonSettings.AutoStart; try { @@ -347,10 +353,13 @@ namespace Tgstation.Server.Host.Controllers StartedBy = AuthenticationContext.User }; - await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false); api.MoveJob = job.ToApi(); } + if (originalModel.Online.Value && model.AutoUpdateInterval.HasValue && oldAutoUpdateInterval != model.AutoUpdateInterval) + await instanceManager.GetInstance(originalModel).SetAutoUpdateInterval(model.AutoUpdateInterval.Value).ConfigureAwait(false); + return Json(api); } diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 721e8b6bd7..40f8837f7d 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -1,7 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Globalization; @@ -15,6 +15,7 @@ using Tgstation.Server.Api; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -42,6 +43,11 @@ namespace Tgstation.Server.Host.Controllers /// readonly IJobManager jobManager; + /// + /// The for the + /// + readonly GeneralConfiguration generalConfiguration; + /// /// Construct a /// @@ -51,14 +57,16 @@ namespace Tgstation.Server.Host.Controllers /// The value of /// The value of /// The for the - public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger logger) : base(databaseContext, authenticationContextFactory, logger, true) + /// The containing value of + public RepositoryController(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, IInstanceManager instanceManager, IGitHubClientFactory gitHubClientFactory, IJobManager jobManager, ILogger logger, IOptions generalConfigurationOptions) : base(databaseContext, authenticationContextFactory, logger, true) { this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); } - static async Task LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action revInfoSink, CancellationToken cancellationToken) + async Task LoadRevisionInformation(Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action revInfoSink, CancellationToken cancellationToken) { var repoSha = repository.Head; @@ -70,7 +78,7 @@ namespace Tgstation.Server.Host.Controllers .FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); //search every rev info because LOL SHA COLLISIONS if (revisionInfo == default) - revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha).FirstOrDefault(); + revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id).FirstOrDefault(); var needsDbUpdate = revisionInfo == default; if (needsDbUpdate) @@ -87,14 +95,23 @@ namespace Tgstation.Server.Host.Controllers lock (databaseContext) //cleaner this way databaseContext.RevisionInformations.Add(revisionInfo); } - revisionInfo.OriginCommitSha = revisionInfo.OriginCommitSha ?? lastOriginCommitSha ?? repository.Head; + revisionInfo.OriginCommitSha = revisionInfo.OriginCommitSha ?? lastOriginCommitSha; + if (revisionInfo.OriginCommitSha == null) + { + revisionInfo.OriginCommitSha = repoSha; + Logger.LogWarning(Components.Repository.Repository.OriginTrackingErrorTemplate, repoSha); + } revInfoSink?.Invoke(revisionInfo); return needsDbUpdate; } - static async Task PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken) + async Task PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken) { - model.IsGitHub = repository.IsGitHubRepository; + if (repository.IsGitHubRepository) + { + model.GitHubOwner = repository.GitHubOwner; + model.GitHubName = repository.GitHubRepoName; + } model.Origin = repository.Origin; model.Reference = repository.Reference; @@ -128,7 +145,7 @@ namespace Tgstation.Server.Host.Controllers var uiOrigin = model.Origin.ToUpperInvariant(); var uiBad = BadGitHubUrl.ToUpperInvariant(); var uiGitHub = Components.Repository.Repository.GitHubUrl.ToUpperInvariant(); - if (uiOrigin.Contains(uiBad)) + if (uiOrigin.Contains(uiBad, StringComparison.Ordinal)) model.Origin = uiOrigin.Replace(uiBad, uiGitHub, StringComparison.Ordinal); currentModel.AccessToken = model.AccessToken; @@ -168,26 +185,24 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, progressReporter, ct) => + await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false)) { if (repos == null) throw new JobException("Filesystem conflict while cloning repository!"); - var db = serviceProvider.GetRequiredService(); var instance = new Models.Instance { Id = Instance.Id }; - db.Instances.Attach(instance); - if (await PopulateApi(api, repos, db, instance, ct).ConfigureAwait(false)) - await db.Save(ct).ConfigureAwait(false); + databaseContext.Instances.Attach(instance); + if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false)) + await databaseContext.Save(ct).ConfigureAwait(false); } }, cancellationToken).ConfigureAwait(false); api.Origin = model.Origin; api.Reference = model.Reference; - api.IsGitHub = model.Origin.ToUpperInvariant().Contains(uiGitHub); api.ActiveJob = job.ToApi(); return StatusCode((int)HttpStatusCode.Created, api); @@ -221,7 +236,7 @@ namespace Tgstation.Server.Host.Controllers Instance = Instance }; var api = currentModel.ToApi(); - await jobManager.RegisterOperation(job, (paramJob, serviceProvider, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); + await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false); api.ActiveJob = job.ToApi(); return Accepted(api); } @@ -287,6 +302,12 @@ namespace Tgstation.Server.Host.Controllers if (model.NewTestMerges?.Any(x => model.NewTestMerges.Any(y => x != y && x.Number == y.Number)) == true) return BadRequest(new ErrorMessage { Message = "Cannot test merge the same PR twice in one job!" }); + if (model.CommitterName?.Length == 0) + return BadRequest(new ErrorMessage { Message = "Cannot set empty committer name!" }); + + if (model.CommitterEmail?.Length == 0) + return BadRequest(new ErrorMessage { Message = "Cannot set empty committer e=mail!" }); + var newTestMerges = model.NewTestMerges != null && model.NewTestMerges.Count > 0; var userRights = (RepositoryRights)AuthenticationContext.GetRight(RightsType.Repository); if (newTestMerges && !userRights.HasFlag(RepositoryRights.MergePullRequest)) @@ -363,16 +384,40 @@ namespace Tgstation.Server.Host.Controllers //this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); + //format the job description + string description = null; + if (model.UpdateFromOrigin == true) + if (model.Reference != null) + description = String.Format(CultureInfo.InvariantCulture, "Fetch and hard reset repository to origin/{0}", model.Reference); + else if (model.CheckoutSha != null) + description = String.Format(CultureInfo.InvariantCulture, "Fetch and checkout {0} in repository", model.CheckoutSha); + else + description = "Pull current repository reference"; + else if (model.Reference != null || model.CheckoutSha != null) + description = String.Format(CultureInfo.InvariantCulture, "Checkout repository {0} {1}", model.Reference != null ? "reference" : "SHA", model.Reference ?? model.CheckoutSha); + + if (newTestMerges) + description = String.Format(CultureInfo.InvariantCulture, "{0}est merge pull request(s) {1}{2}", + description != null ? String.Format(CultureInfo.InvariantCulture, "{0} and t", description) : "T", + String.Join(", ", model.NewTestMerges.Select(x => + String.Format(CultureInfo.InvariantCulture, "#{0}{1}", x.Number, + x.PullRequestRevision != null ? String.Format(CultureInfo.InvariantCulture, " at {0}", x.PullRequestRevision.Substring(0, 7)) : String.Empty))), + description != null ? String.Empty : " in repository"); + + if (description == null) + //no git changes + return Json(api); + var job = new Models.Job { - Description = "Apply repository changes", + Description = description, StartedBy = AuthenticationContext.User, Instance = Instance, CancelRightsType = RightsType.Repository, CancelRight = (ulong)RepositoryRights.CancelPendingChanges, }; - await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, progressReporter, ct) => + await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) => { using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false)) { @@ -383,21 +428,30 @@ namespace Tgstation.Server.Host.Controllers var startReference = repo.Reference; var startSha = repo.Head; + string postUpdateSha = null; if (newTestMerges && !repo.IsGitHubRepository) throw new JobException("Cannot test merge on a non GitHub based repository!"); var committerName = currentModel.ShowTestMergeCommitters.Value ? AuthenticationContext.User.Name : currentModel.CommitterName; - var numFetches = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0); - var doneFetches = 0; - if (numFetches > 0) - progressReporter(0); + var hardResettingToOriginReference = model.UpdateFromOrigin == true && model.Reference != null; + + var numSteps = (model.NewTestMerges?.Count ?? 0) + (model.UpdateFromOrigin == true ? 1 : 0) + (!modelHasShaOrReference ? 2 : (hardResettingToOriginReference ? 3 : 1)); + var doneSteps = 0; + + Action NextProgressReporter() + { + var tmpDoneSteps = doneSteps; + ++doneSteps; + return progress => progressReporter((progress + 100 * tmpDoneSteps) / numSteps); + }; + + progressReporter(0); //get a base line for where we are Models.RevisionInformation lastRevisionInfo = null; - - var databaseContext = serviceProvider.GetRequiredService(); + var attachedInstance = new Models.Instance { Id = Instance.Id @@ -421,19 +475,22 @@ namespace Tgstation.Server.Host.Controllers { if (!repo.Tracking) throw new JobException("Not on an updatable reference!"); - await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, x => progressReporter(x / numFetches), ct).ConfigureAwait(false); - doneFetches = 1; + await repo.FetchOrigin(currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); + doneSteps = 1; if (!modelHasShaOrReference) { - var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, ct).ConfigureAwait(false); + var fastForward = await repo.MergeOrigin(committerName, currentModel.CommitterEmail, NextProgressReporter(), ct).ConfigureAwait(false); if (!fastForward.HasValue) throw new JobException("Merge conflict occurred during origin update!"); await UpdateRevInfo().ConfigureAwait(false); if (fastForward.Value) { lastRevisionInfo.OriginCommitSha = repo.Head; - await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, true, ct).ConfigureAwait(false); + await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); + postUpdateSha = repo.Head; } + else + NextProgressReporter()(100); } } @@ -449,16 +506,18 @@ namespace Tgstation.Server.Host.Controllers if ((isSha && model.Reference != null) || (!isSha && model.CheckoutSha != null)) throw new JobException("Attempted to checkout a SHA or reference that was actually the opposite!"); - await repo.CheckoutObject(committish, ct).ConfigureAwait(false); + await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); //we've either seen origin before or what we're checking out is on origin } + else + NextProgressReporter()(100); - if (model.UpdateFromOrigin == true && model.Reference != null) + if (hardResettingToOriginReference) { if (!repo.Tracking) throw new JobException("Checked out reference does not track a remote object!"); - await repo.ResetToOrigin(ct).ConfigureAwait(false); - await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, true, ct).ConfigureAwait(false); + await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false); + await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false); await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); //repo head is on origin so force this //will update the db if necessary @@ -474,7 +533,11 @@ namespace Tgstation.Server.Host.Controllers foreach (var I in model.NewTestMerges.Where(x => String.IsNullOrWhiteSpace(x.PullRequestRevision))) I.PullRequestRevision = null; - var gitHubClient = currentModel.AccessToken != null ? gitHubClientFactory.CreateClient(currentModel.AccessToken) : gitHubClientFactory.CreateClient(); + var gitHubClient = currentModel.AccessToken != null + ? gitHubClientFactory.CreateClient(currentModel.AccessToken) + : (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken) + ? gitHubClientFactory.CreateClient() + : gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken)); var repoOwner = repo.GitHubOwner; var repoName = repo.GitHubRepoName; @@ -575,7 +638,7 @@ namespace Tgstation.Server.Host.Controllers if (revInfoWereLookingFor != null) { //goteem - await repo.ResetToSha(revInfoWereLookingFor.CommitSha, cancellationToken).ConfigureAwait(false); + await repo.ResetToSha(revInfoWereLookingFor.CommitSha, NextProgressReporter(), cancellationToken).ConfigureAwait(false); lastRevisionInfo = revInfoWereLookingFor; } @@ -591,6 +654,10 @@ namespace Tgstation.Server.Host.Controllers { Octokit.PullRequest pr = null; string errorMessage = null; + + if (lastRevisionInfo.ActiveTestMerges.Any(x => x.TestMerge.Number == I.Number.Value)) + throw new JobException("Cannot test merge the same PR twice in one HEAD!"); + try { //load from cache if possible @@ -602,6 +669,10 @@ namespace Tgstation.Server.Host.Controllers //you look at your anonymous access and sigh errorMessage = "P.R.E. RATE LIMITED"; } + catch (Octokit.AuthorizationException) + { + errorMessage = "P.R.E. BAD CREDENTIALS"; + } catch (Octokit.NotFoundException) { //you look at your shithub and sigh @@ -612,12 +683,12 @@ namespace Tgstation.Server.Host.Controllers if (I.PullRequestRevision == null && pr != null) I.PullRequestRevision = pr.Head.Sha; - var mergeResult = await repo.AddTestMerge(I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, x => progressReporter((x + 100 * doneFetches) / numFetches), ct).ConfigureAwait(false); + var mergeResult = await repo.AddTestMerge(I, committerName, currentModel.CommitterEmail, currentModel.AccessUser, currentModel.AccessToken, NextProgressReporter(), ct).ConfigureAwait(false); - if (!mergeResult.HasValue) //conflict, we don't care, dd already knows - continue; + if (!mergeResult.HasValue) + throw new JobException(String.Format(CultureInfo.InvariantCulture, "Merge of PR #{0} at {1} conflicted!", I.Number, I.PullRequestRevision.Substring(0, 7))); - ++doneFetches; + ++doneSteps; var revInfoUpdateTask = UpdateRevInfo(); @@ -645,19 +716,24 @@ namespace Tgstation.Server.Host.Controllers } } - if (startSha != repo.Head) + var currentHead = repo.Head; + if (startSha != currentHead || (postUpdateSha != null && postUpdateSha != currentHead)) { - await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, false, ct).ConfigureAwait(false); + await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false); await UpdateRevInfo().ConfigureAwait(false); } await databaseContext.Save(ct).ConfigureAwait(false); } catch { + doneSteps = 0; + numSteps = 2; //the stuff didn't make it into the db, forget what we've done and abort - await repo.CheckoutObject(startReference ?? startSha, default).ConfigureAwait(false); + await repo.CheckoutObject(startReference ?? startSha, NextProgressReporter(), default).ConfigureAwait(false); if (startReference != null && repo.Head != startSha) - await repo.ResetToSha(startSha, default).ConfigureAwait(false); + await repo.ResetToSha(startSha, NextProgressReporter(), default).ConfigureAwait(false); + else + progressReporter(100); throw; } } diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index a397f43b17..3c654d2c52 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -3,15 +3,16 @@ using Cyberboss.AspNetCore.AsyncInitializer; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.AspNetCore.Hosting.Server.Features; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; using Microsoft.IdentityModel.Tokens; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; @@ -21,9 +22,9 @@ using System.Threading.Tasks; using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Components.Chat; -using Tgstation.Server.Host.Components.StaticFiles; +using Tgstation.Server.Host.Components.Repository; +using Tgstation.Server.Host.Components.Watchdog; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Controllers; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; @@ -45,7 +46,7 @@ namespace Tgstation.Server.Host.Core /// /// The for the /// - readonly Microsoft.Extensions.Configuration.IConfiguration configuration; + readonly IConfiguration configuration; /// /// The for the @@ -53,13 +54,19 @@ namespace Tgstation.Server.Host.Core readonly Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment; readonly TaskCompletionSource startupTcs; + static LogLevel GetMinimumLogLevel(string stringLevel) + { + if (String.IsNullOrWhiteSpace(stringLevel) || !Enum.TryParse(stringLevel, out var minimumLevel)) + minimumLevel = LogLevel.Information; + return minimumLevel; + } /// /// Construct an /// /// The value of /// The value of - public Application(Microsoft.Extensions.Configuration.IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment) + public Application(IConfiguration configuration, Microsoft.AspNetCore.Hosting.IHostingEnvironment hostingEnvironment) { this.configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); this.hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment)); @@ -74,9 +81,7 @@ namespace Tgstation.Server.Host.Core /// Configure dependency injected services /// /// The to configure -#pragma warning disable CA1822 // Mark members as static public void ConfigureServices(IServiceCollection services) -#pragma warning restore CA1822 // Mark members as static { if (services == null) throw new ArgumentNullException(nameof(services)); @@ -95,11 +100,14 @@ namespace Tgstation.Server.Host.Core if (generalConfiguration?.DisableFileLogging != true) { var logPath = !String.IsNullOrEmpty(generalConfiguration?.LogFileDirectory) ? generalConfiguration.LogFileDirectory : ioManager.ConcatPath(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), VersionPrefix, "Logs"); - services.AddLogging(builder => builder.AddFile(ioManager.ConcatPath(logPath, "tgs-{Date}.log"))); + + services.AddLogging(builder => builder.AddFile(ioManager.ConcatPath(logPath, "tgs-{Date}.log"), GetMinimumLogLevel(generalConfiguration?.LogFileLevel))); } services.AddOptions(); + services.AddScoped(); + const string scheme = "JwtBearer"; services.AddAuthentication((options) => { @@ -127,9 +135,11 @@ namespace Tgstation.Server.Host.Core }; jwtBearerOptions.Events = new JwtBearerEvents { - OnTokenValidated = ApiController.OnTokenValidated + //Application is our composition root so this monstrosity of a line is okay + OnTokenValidated = ctx => ctx.HttpContext.RequestServices.GetRequiredService().InjectClaimsIntoContext(ctx, ctx.HttpContext.RequestAborted) }; }); + JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); //fucking converts 'sub' to M$ bs services.AddMvc().AddJsonOptions(options => @@ -139,6 +149,7 @@ namespace Tgstation.Server.Host.Core options.SerializerSettings.CheckAdditionalContent = true; options.SerializerSettings.MissingMemberHandling = MissingMemberHandling.Error; options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; + options.SerializerSettings.Converters = new[] { new VersionConverter() }; }); var databaseConfiguration = databaseConfigurationSection.Get(); @@ -149,17 +160,21 @@ namespace Tgstation.Server.Host.Core builder.EnableSensitiveDataLogging(); }; + void AddTypedContext() where TContext : DatabaseContext + { + services.AddDbContext(ConfigureDatabase); + services.AddScoped(x => x.GetRequiredService()); + } + var dbType = databaseConfiguration?.DatabaseType; - switch (dbType) + switch (databaseConfiguration?.DatabaseType) { case DatabaseType.MySql: case DatabaseType.MariaDB: - services.AddDbContext(ConfigureDatabase); - services.AddScoped(x => x.GetRequiredService()); + AddTypedContext(); break; case DatabaseType.SqlServer: - services.AddDbContext(ConfigureDatabase); - services.AddScoped(x => x.GetRequiredService()); + AddTypedContext(); break; default: throw new InvalidOperationException(String.Format(CultureInfo.InvariantCulture, "Invalid {0}: {1}!", nameof(DatabaseType), dbType)); @@ -173,9 +188,9 @@ namespace Tgstation.Server.Host.Core services.AddSingleton, PasswordHasher>(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(x => x.GetRequiredService().CreateClient()); if (isWindows) { @@ -183,6 +198,10 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(x => x.GetRequiredService()); } else { @@ -190,6 +209,7 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); } services.AddSingleton(); @@ -200,8 +220,10 @@ namespace Tgstation.Server.Host.Core SendTimeout = 5000 }); - services.AddSingleton(); - services.AddSingleton(x => x.GetRequiredService()); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(x => x.GetRequiredService()); @@ -221,7 +243,8 @@ namespace Tgstation.Server.Host.Core /// /// The to configure /// The for the - public void Configure(IApplicationBuilder applicationBuilder, ILogger logger) + /// The for the application + public void Configure(IApplicationBuilder applicationBuilder, ILogger logger, IServerControl serverControl) { if (applicationBuilder == null) throw new ArgumentNullException(nameof(applicationBuilder)); @@ -229,6 +252,9 @@ namespace Tgstation.Server.Host.Core throw new ArgumentNullException(nameof(logger)); logger.LogInformation(VersionString); + + //attempt to restart the server if the configuration changes + ChangeToken.OnChange(configuration.GetReloadToken, () => serverControl.Restart()); applicationBuilder.UseDeveloperExceptionPage(); //it is not worth it to limit this, you should only ever get it if you're an authorized user @@ -250,12 +276,15 @@ namespace Tgstation.Server.Host.Core /// public void Ready(Exception initializationError) { - if (startupTcs.Task.IsCompleted) - throw new InvalidOperationException("Ready has already been called!"); - if (initializationError == null) - startupTcs.SetResult(null); - else - startupTcs.SetException(initializationError); + lock (startupTcs) + { + if (startupTcs.Task.IsCompleted) + throw new InvalidOperationException("Ready has already been called!"); + if (initializationError == null) + startupTcs.SetResult(null); + else + startupTcs.SetException(initializationError); + } } } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs b/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs index f3d64f355c..69c5287aa9 100644 --- a/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs +++ b/src/Tgstation.Server.Host/Core/DatabaseContextFactory.cs @@ -9,20 +9,26 @@ namespace Tgstation.Server.Host.Core sealed class DatabaseContextFactory : IDatabaseContextFactory { /// - /// The for the + /// The for the /// - readonly IServiceProvider serviceProvider; + readonly IServiceScopeFactory scopeFactory; /// /// Construct a /// - /// The value of - public DatabaseContextFactory(IServiceProvider serviceProvider) => this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + /// The value of . Created scopes must be able to provide instances of + public DatabaseContextFactory(IServiceScopeFactory scopeFactory) + { + this.scopeFactory = scopeFactory ?? throw new ArgumentNullException(nameof(scopeFactory)); + + using (var scope = scopeFactory.CreateScope()) + scope.ServiceProvider.GetRequiredService(); + } /// public async Task UseContext(Func operation) { - using (var scope = serviceProvider.CreateScope()) + using (var scope = scopeFactory.CreateScope()) await operation(scope.ServiceProvider.GetRequiredService()).ConfigureAwait(false); } } diff --git a/src/Tgstation.Server.Host/Core/IJobManager.cs b/src/Tgstation.Server.Host/Core/IJobManager.cs index 0d5a16842e..12ecf7666f 100644 --- a/src/Tgstation.Server.Host/Core/IJobManager.cs +++ b/src/Tgstation.Server.Host/Core/IJobManager.cs @@ -20,10 +20,22 @@ namespace Tgstation.Server.Host.Core /// Registers a given and begins running it /// /// The - /// The operation to run taking the started , a progress reporter and a + /// The operation to run taking the started , a , progress reporter and a /// The for the operation /// A representing a running operation - Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); + Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken); + + /// + /// Wait for a given to complete + /// + /// The to wait for + /// The to cancel the + /// A that will cancel the + /// The for the operation + /// A representing the +#pragma warning disable CA1068 // CancellationToken parameters must come last https://github.com/dotnet/roslyn-analyzers/issues/1816 + Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken); +#pragma warning restore CA1068 // CancellationToken parameters must come last /// /// Cancels a give diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IPostWriteHandler.cs b/src/Tgstation.Server.Host/Core/IPostWriteHandler.cs similarity index 81% rename from src/Tgstation.Server.Host/Components/StaticFiles/IPostWriteHandler.cs rename to src/Tgstation.Server.Host/Core/IPostWriteHandler.cs index a1db4b8d37..7209b44761 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IPostWriteHandler.cs +++ b/src/Tgstation.Server.Host/Core/IPostWriteHandler.cs @@ -1,4 +1,4 @@ -namespace Tgstation.Server.Host.Components.StaticFiles +namespace Tgstation.Server.Host.Core { interface IPostWriteHandler { diff --git a/src/Tgstation.Server.Host/Core/IRestartHandler.cs b/src/Tgstation.Server.Host/Core/IRestartHandler.cs new file mode 100644 index 0000000000..f3b0c3afca --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IRestartHandler.cs @@ -0,0 +1,20 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Handler for server restarts + /// + public interface IRestartHandler + { + /// + /// Handle a restart of the server + /// + /// The being updated to, if not being changed + /// The for the operation + /// A representing the running operation + Task HandleRestart(Version updateVersion, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Core/IRestartRegistration.cs b/src/Tgstation.Server.Host/Core/IRestartRegistration.cs new file mode 100644 index 0000000000..78aa00ffc5 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/IRestartRegistration.cs @@ -0,0 +1,11 @@ +using System; + +namespace Tgstation.Server.Host.Core +{ + /// + /// Represents the lifetime of a registration + /// + public interface IRestartRegistration : IDisposable + { + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Core/IServerControl.cs b/src/Tgstation.Server.Host/Core/IServerControl.cs index dc97c4bb56..4c20880dd7 100644 --- a/src/Tgstation.Server.Host/Core/IServerControl.cs +++ b/src/Tgstation.Server.Host/Core/IServerControl.cs @@ -13,22 +13,24 @@ namespace Tgstation.Server.Host.Core /// /// Run a new assembly and stop the current one. This will likely trigger all active s /// + /// The the is updating to /// The s of the .zip file that contains the new assembly /// The for the operation /// The for the operation /// A resulting in if live updates are supported, otherwise - Task ApplyUpdate(byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken); + Task ApplyUpdate(Version version, byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken); /// - /// Register a given to run before stopping the server for a restart + /// Register a given to run before stopping the server for a restart /// - /// The to run - void RegisterForRestart(Action action); + /// The to register + /// A new representing the scope of the registration + IRestartRegistration RegisterForRestart(IRestartHandler handler); /// /// Restarts the /// - /// if live restarts are supported, otherwise - bool Restart(); + /// A resulting in if live restarts are supported, otherwise + Task Restart(); } } diff --git a/src/Tgstation.Server.Host/Core/JobManager.cs b/src/Tgstation.Server.Host/Core/JobManager.cs index 47ac244289..2900dc28f4 100644 --- a/src/Tgstation.Server.Host/Core/JobManager.cs +++ b/src/Tgstation.Server.Host/Core/JobManager.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; @@ -16,7 +15,7 @@ namespace Tgstation.Server.Host.Core /// /// The for the /// - readonly IServiceProvider serviceProvider; + readonly IDatabaseContextFactory databaseContextFactory; /// /// The for the @@ -31,11 +30,11 @@ namespace Tgstation.Server.Host.Core /// /// Construct a /// - /// The value of + /// The value of /// The value of - public JobManager(IServiceProvider serviceProvider, ILogger logger) + public JobManager(IDatabaseContextFactory databaseContextFactory, ILogger logger) { - this.serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); jobs = new Dictionary(); } @@ -69,37 +68,59 @@ namespace Tgstation.Server.Host.Core /// The operation for the /// The for the operation /// A representing the running operation - async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) - { + async Task RunJob(Job job, Func operation, CancellationToken cancellationToken) + { try { - using (var scope = serviceProvider.CreateScope()) + await databaseContextFactory.UseContext(async databaseContext => { - IDatabaseContext databaseContext = null; - try + async Task HandleExceptions(Task task) + { + try + { + await task.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + logger.LogDebug("Job {0} cancelled!", job.Id); + job.Cancelled = true; + } + catch (Exception e) + { + job.ExceptionDetails = e is JobException ? e.Message : e.ToString(); + logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); + } + finally + { + job.StoppedAt = DateTimeOffset.Now; + } + } + + async Task RunJobInternal() { var oldJob = job; job = new Job { Id = oldJob.Id }; - databaseContext = scope.ServiceProvider.GetRequiredService(); databaseContext.Jobs.Attach(job); - await operation(job, scope.ServiceProvider, cancellationToken).ConfigureAwait(false); + await operation(job, databaseContext, cancellationToken).ConfigureAwait(false); logger.LogDebug("Job {0} completed!", job.Id); - } - catch (OperationCanceledException) - { - logger.LogDebug("Job {0} cancelled!", job.Id); - job.Cancelled = true; - } - catch (Exception e) - { - job.ExceptionDetails = e is JobException ? e.Message : e.ToString(); - logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails); - } - job.StoppedAt = DateTimeOffset.Now; + }; + + await HandleExceptions(RunJobInternal()).ConfigureAwait(false); + await databaseContext.Save(default).ConfigureAwait(false); - } + + bool JobErroredOrCancelled() => job.ExceptionDetails != null || job.Cancelled == true; + + //ok so, now it's time for the post commit step if it exists + if (!JobErroredOrCancelled() && job.PostComplete != null) + { + await HandleExceptions(job.PostComplete(cancellationToken)).ConfigureAwait(false); + if (JobErroredOrCancelled()) + await databaseContext.Save(default).ConfigureAwait(false); + } + }).ConfigureAwait(false); } finally { @@ -113,50 +134,44 @@ namespace Tgstation.Server.Host.Core } /// - public async Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken) + public Task RegisterOperation(Job job, Func, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext => { - using (var scope = serviceProvider.CreateScope()) + job.StartedAt = DateTimeOffset.Now; + job.Cancelled = false; + job.Instance = new Instance { - var databaseContext = scope.ServiceProvider.GetRequiredService(); - job.StartedAt = DateTimeOffset.Now; - job.Cancelled = false; - job.Instance = new Instance + Id = job.Instance.Id + }; + databaseContext.Instances.Attach(job.Instance); + if (job.StartedBy != null) + { + job.StartedBy = new User { - Id = job.Instance.Id + Id = job.StartedBy.Id }; - databaseContext.Instances.Attach(job.Instance); - if (job.StartedBy != null) - { - job.StartedBy = new User - { - Id = job.StartedBy.Id - }; - databaseContext.Users.Attach(job.StartedBy); - } - databaseContext.Jobs.Add(job); - await databaseContext.Save(cancellationToken).ConfigureAwait(false); - logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description); - var jobHandler = JobHandler.Create(x => RunJob(job, (jobParam, serviceProvider, ct) => - operation(jobParam, serviceProvider, y => - { - lock (this) - if (jobs.TryGetValue(job.Id, out var handler)) - handler.Progress = y; - }, ct), - x)); - lock (this) - jobs.Add(job.Id, jobHandler); + databaseContext.Users.Attach(job.StartedBy); } - } + databaseContext.Jobs.Add(job); + await databaseContext.Save(cancellationToken).ConfigureAwait(false); + logger.LogDebug("Starting job {0}: {1}...", job.Id, job.Description); + var jobHandler = JobHandler.Create(x => RunJob(job, (jobParam, serviceProvider, ct) => + operation(jobParam, serviceProvider, y => + { + lock (this) + if (jobs.TryGetValue(job.Id, out var handler)) + handler.Progress = y; + }, ct), + x)); + lock (this) + jobs.Add(job.Id, jobHandler); + }); /// public async Task StartAsync(CancellationToken cancellationToken) { logger.LogTrace("Starting job manager..."); - using (var scope = serviceProvider.CreateScope()) + await databaseContextFactory.UseContext(async databaseContext => { - var databaseContext = scope.ServiceProvider.GetRequiredService(); - //mark all jobs as cancelled var badJobs = await databaseContext.Jobs.Where(y => !y.StoppedAt.HasValue).Select(y => y.Id).ToListAsync(cancellationToken).ConfigureAwait(false); if (badJobs.Count > 0) @@ -171,7 +186,7 @@ namespace Tgstation.Server.Host.Core } await databaseContext.Save(cancellationToken).ConfigureAwait(false); } - } + }).ConfigureAwait(false); logger.LogDebug("Job manager started!"); } @@ -204,9 +219,8 @@ namespace Tgstation.Server.Host.Core return false; } handler.Cancel(); //this will ensure the db update is only done once - using (var scope = serviceProvider.CreateScope()) + await databaseContextFactory.UseContext(async databaseContext => { - var databaseContext = scope.ServiceProvider.GetRequiredService(); job = new Job { Id = job.Id }; databaseContext.Jobs.Attach(job); user = new User { Id = user.Id }; @@ -214,7 +228,7 @@ namespace Tgstation.Server.Host.Core job.CancelledBy = user; //let either startup or cancellation set job.cancelled await databaseContext.Save(cancellationToken).ConfigureAwait(false); - } + }).ConfigureAwait(false); if (blocking) await handler.Wait(cancellationToken).ConfigureAwait(false); return true; @@ -223,6 +237,8 @@ namespace Tgstation.Server.Host.Core /// public int? JobProgress(Job job) { + if (job == null) + throw new ArgumentNullException(nameof(job)); lock (this) { if (!jobs.TryGetValue(job.Id, out var handler)) @@ -230,5 +246,26 @@ namespace Tgstation.Server.Host.Core return handler.Progress; } } + + /// + public async Task WaitForJobCompletion(Job job, User canceller, CancellationToken jobCancellationToken, CancellationToken cancellationToken) + { + if (job == null) + throw new ArgumentNullException(nameof(job)); + if (canceller == null) + throw new ArgumentNullException(nameof(canceller)); + JobHandler handler; + lock (this) + { + if (!jobs.TryGetValue(job.Id, out handler)) + return; + } + Task cancelTask = null; + using (jobCancellationToken.Register(() => cancelTask = CancelJob(job, canceller, true, cancellationToken))) + await handler.Wait(cancellationToken).ConfigureAwait(false); + + if (cancelTask != null) + await cancelTask.ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/PosixPostWriteHandler.cs b/src/Tgstation.Server.Host/Core/PosixPostWriteHandler.cs similarity index 92% rename from src/Tgstation.Server.Host/Components/StaticFiles/PosixPostWriteHandler.cs rename to src/Tgstation.Server.Host/Core/PosixPostWriteHandler.cs index a028fe1c03..d121d2bca8 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/PosixPostWriteHandler.cs +++ b/src/Tgstation.Server.Host/Core/PosixPostWriteHandler.cs @@ -1,7 +1,7 @@ using Mono.Unix; using Mono.Unix.Native; -namespace Tgstation.Server.Host.Components.StaticFiles +namespace Tgstation.Server.Host.Core { /// /// for POSIX systems diff --git a/src/Tgstation.Server.Host/Core/Process.cs b/src/Tgstation.Server.Host/Core/Process.cs index 15296ebb2f..7ea9434c74 100644 --- a/src/Tgstation.Server.Host/Core/Process.cs +++ b/src/Tgstation.Server.Host/Core/Process.cs @@ -1,4 +1,5 @@ -using System; +using Microsoft.Extensions.Logging; +using System; using System.Text; using System.Threading.Tasks; @@ -22,7 +23,12 @@ namespace Tgstation.Server.Host.Core readonly StringBuilder errorStringBuilder; readonly StringBuilder combinedStringBuilder; - public Process(System.Diagnostics.Process handle, Task lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder) + /// + /// The for the + /// + readonly ILogger logger; + + public Process(System.Diagnostics.Process handle, Task lifetime, StringBuilder outputStringBuilder, StringBuilder errorStringBuilder, StringBuilder combinedStringBuilder, ILogger logger) { this.handle = handle ?? throw new ArgumentNullException(nameof(handle)); Lifetime = lifetime ?? throw new ArgumentNullException(nameof(lifetime)); @@ -31,6 +37,8 @@ namespace Tgstation.Server.Host.Core this.errorStringBuilder = errorStringBuilder; this.combinedStringBuilder = combinedStringBuilder; + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + Id = handle.Id; Startup = Task.Factory.StartNew(() => { @@ -40,6 +48,8 @@ namespace Tgstation.Server.Host.Core } catch (InvalidOperationException) { } }, default, TaskCreationOptions.LongRunning, TaskScheduler.Current); + + logger.LogTrace("Created proces ID: {0}", Id); } /// @@ -72,12 +82,18 @@ namespace Tgstation.Server.Host.Core /// public void Terminate() { + if (handle.HasExited) + return; try { + logger.LogTrace("Terminating process..."); handle.Kill(); handle.WaitForExit(); } - catch (InvalidOperationException) { } + catch (Exception e) + { + logger.LogDebug("Process termination exception: {0}", e); + } } public void SetHighPriority() @@ -85,8 +101,12 @@ namespace Tgstation.Server.Host.Core try { handle.PriorityClass = System.Diagnostics.ProcessPriorityClass.AboveNormal; + logger.LogTrace("Set to above normal priority", handle.Id); + } + catch (Exception e) + { + logger.LogWarning("Unable to raise process priority! Exception: {0}", e); } - catch (InvalidOperationException) { } } } } diff --git a/src/Tgstation.Server.Host/Core/ProcessExecutor.cs b/src/Tgstation.Server.Host/Core/ProcessExecutor.cs index 32d00b207d..5aad8a9e8b 100644 --- a/src/Tgstation.Server.Host/Core/ProcessExecutor.cs +++ b/src/Tgstation.Server.Host/Core/ProcessExecutor.cs @@ -1,6 +1,5 @@ using Microsoft.Extensions.Logging; using System; -using System.Diagnostics; using System.Text; using System.Threading.Tasks; @@ -14,6 +13,11 @@ namespace Tgstation.Server.Host.Core /// readonly ILogger logger; + /// + /// The for the + /// + readonly ILoggerFactory loggerFactory; + /// /// Create a resulting in the exit code of a given /// @@ -43,9 +47,11 @@ namespace Tgstation.Server.Host.Core /// Construct a /// /// The value of - public ProcessExecutor(ILogger logger) + /// The value of + public ProcessExecutor(ILogger logger, ILoggerFactory loggerFactory) { this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); } /// @@ -55,7 +61,7 @@ namespace Tgstation.Server.Host.Core var handle = System.Diagnostics.Process.GetProcessById(id); try { - return new Process(handle, AttachExitHandler(handle), null, null, null); + return new Process(handle, AttachExitHandler(handle), null, null, null, loggerFactory.CreateLogger()); } catch { @@ -85,31 +91,25 @@ namespace Tgstation.Server.Host.Core { outputStringBuilder = new StringBuilder(); handle.StartInfo.RedirectStandardOutput = true; - var eventHandler = new DataReceivedEventHandler( - delegate (object sender, DataReceivedEventArgs e) - { - combinedStringBuilder.Append(Environment.NewLine); - combinedStringBuilder.Append(e.Data); - outputStringBuilder.Append(Environment.NewLine); - outputStringBuilder.Append(e.Data); - } - ); - handle.OutputDataReceived += eventHandler; + handle.OutputDataReceived += (sender, e) => + { + combinedStringBuilder.Append(Environment.NewLine); + combinedStringBuilder.Append(e.Data); + outputStringBuilder.Append(Environment.NewLine); + outputStringBuilder.Append(e.Data); + }; } if (readError) { errorStringBuilder = new StringBuilder(); handle.StartInfo.RedirectStandardError = true; - var eventHandler = new DataReceivedEventHandler( - delegate (object sender, DataReceivedEventArgs e) - { - combinedStringBuilder.Append(Environment.NewLine); - combinedStringBuilder.Append(e.Data); - errorStringBuilder.Append(Environment.NewLine); - errorStringBuilder.Append(e.Data); - } - ); - handle.ErrorDataReceived += eventHandler; + handle.ErrorDataReceived += (sender, e) => + { + combinedStringBuilder.Append(Environment.NewLine); + combinedStringBuilder.Append(e.Data); + errorStringBuilder.Append(Environment.NewLine); + errorStringBuilder.Append(e.Data); + }; } } @@ -129,7 +129,7 @@ namespace Tgstation.Server.Host.Core } catch (InvalidOperationException) { } - return new Process(handle, lifetimeTask, outputStringBuilder, errorStringBuilder, combinedStringBuilder); + return new Process(handle, lifetimeTask, outputStringBuilder, errorStringBuilder, combinedStringBuilder, loggerFactory.CreateLogger()); } catch { @@ -138,4 +138,4 @@ namespace Tgstation.Server.Host.Core } } } -} \ No newline at end of file +} diff --git a/src/Tgstation.Server.Host/Core/RestartRegistration.cs b/src/Tgstation.Server.Host/Core/RestartRegistration.cs new file mode 100644 index 0000000000..8df2cf3ad2 --- /dev/null +++ b/src/Tgstation.Server.Host/Core/RestartRegistration.cs @@ -0,0 +1,26 @@ +using System; +using Tgstation.Server.Host.Core; + +namespace Tgstation.Server.Host.Core +{ + /// + sealed class RestartRegistration : IRestartRegistration + { + /// + /// The + /// + readonly Action onDispose; + + /// + /// Construct a + /// + /// The value of + public RestartRegistration(Action onDispose) + { + this.onDispose = onDispose ?? throw new ArgumentNullException(nameof(onDispose)); + } + + /// + public void Dispose() => onDispose(); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs b/src/Tgstation.Server.Host/Core/ServerSideModifications.cs similarity index 100% rename from src/Tgstation.Server.Host/Components/StaticFiles/ServerSideModifications.cs rename to src/Tgstation.Server.Host/Core/ServerSideModifications.cs diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/WindowsPostWriteHandler.cs b/src/Tgstation.Server.Host/Core/WindowsPostWriteHandler.cs similarity index 79% rename from src/Tgstation.Server.Host/Components/StaticFiles/WindowsPostWriteHandler.cs rename to src/Tgstation.Server.Host/Core/WindowsPostWriteHandler.cs index 61c8431662..42ed2cdf1b 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/WindowsPostWriteHandler.cs +++ b/src/Tgstation.Server.Host/Core/WindowsPostWriteHandler.cs @@ -1,4 +1,4 @@ -namespace Tgstation.Server.Host.Components.StaticFiles +namespace Tgstation.Server.Host.Core { /// /// for Windows systems diff --git a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs index d31e652e5d..c3937d5a9c 100644 --- a/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs +++ b/src/Tgstation.Server.Host/IO/SynchronousIOManager.cs @@ -42,7 +42,7 @@ namespace Tgstation.Server.Host.IO { foreach (var I in Directory.EnumerateDirectories(path)) { - yield return I; + yield return Path.GetFileName(I); cancellationToken.ThrowIfCancellationRequested(); } } @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.IO { foreach (var I in Directory.EnumerateFiles(path)) { - yield return I; + yield return Path.GetFileName(I); cancellationToken.ThrowIfCancellationRequested(); } } diff --git a/src/Tgstation.Server.Host/Models/ChatChannel.cs b/src/Tgstation.Server.Host/Models/ChatChannel.cs index 17e1d0c367..99bdedf7a6 100644 --- a/src/Tgstation.Server.Host/Models/ChatChannel.cs +++ b/src/Tgstation.Server.Host/Models/ChatChannel.cs @@ -30,7 +30,8 @@ namespace Tgstation.Server.Host.Models IsAdminChannel = IsAdminChannel, IsWatchdogChannel = IsWatchdogChannel, IsUpdatesChannel = IsUpdatesChannel, - IrcChannel = IrcChannel + IrcChannel = IrcChannel, + Tag = Tag }; } } diff --git a/src/Tgstation.Server.Host/Models/CompileJob.cs b/src/Tgstation.Server.Host/Models/CompileJob.cs index 22e97759dd..3fadc5b182 100644 --- a/src/Tgstation.Server.Host/Models/CompileJob.cs +++ b/src/Tgstation.Server.Host/Models/CompileJob.cs @@ -9,8 +9,14 @@ namespace Tgstation.Server.Host.Models /// /// See /// + [Required] public Job Job { get; set; } + /// + /// The of + /// + public long JobId { get; set; } + /// /// See /// @@ -35,7 +41,8 @@ namespace Tgstation.Server.Host.Models Job = Job.ToApi(), Output = Output, RevisionInformation = RevisionInformation.ToApi(), - ByondVersion = Version.Parse(ByondVersion) + ByondVersion = Version.Parse(ByondVersion), + MinimumSecurityLevel = MinimumSecurityLevel }; } } diff --git a/src/Tgstation.Server.Host/Models/DatabaseContext.cs b/src/Tgstation.Server.Host/Models/DatabaseContext.cs index c79f697839..7578c21c8a 100644 --- a/src/Tgstation.Server.Host/Models/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Models/DatabaseContext.cs @@ -107,11 +107,13 @@ namespace Tgstation.Server.Host.Models var revInfo = modelBuilder.Entity(); revInfo.HasMany(x => x.CompileJobs).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade); revInfo.HasMany(x => x.ActiveTestMerges).WithOne(x => x.RevisionInformation).OnDelete(DeleteBehavior.Cascade); - revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.SetNull); + revInfo.HasOne(x => x.PrimaryTestMerge).WithOne(x => x.PrimaryRevisionInformation).OnDelete(DeleteBehavior.Restrict); revInfo.HasIndex(x => x.CommitSha).IsUnique(); modelBuilder.Entity().HasIndex(x => x.DirectoryName); + modelBuilder.Entity().HasOne().WithOne(x => x.Job).OnDelete(DeleteBehavior.Restrict); + var chatChannel = modelBuilder.Entity(); chatChannel.HasIndex(x => new { x.ChatSettingsId, x.IrcChannel }).IsUnique(); chatChannel.HasIndex(x => new { x.ChatSettingsId, x.DiscordChannelId }).IsUnique(); @@ -128,6 +130,7 @@ namespace Tgstation.Server.Host.Models instanceModel.HasMany(x => x.RevisionInformations).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); instanceModel.HasMany(x => x.InstanceUsers).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); instanceModel.HasMany(x => x.Jobs).WithOne(x => x.Instance).OnDelete(DeleteBehavior.Cascade); + instanceModel.HasOne(x => x.WatchdogReattachInformation).WithOne().OnDelete(DeleteBehavior.Cascade); } /// diff --git a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs index 81f8312926..0137be1678 100644 --- a/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs +++ b/src/Tgstation.Server.Host/Models/DreamMakerSettings.cs @@ -3,7 +3,7 @@ namespace Tgstation.Server.Host.Models { /// - public sealed class DreamMakerSettings : Api.Models.Internal.DreamMakerSettings + public sealed class DreamMakerSettings : Api.Models.DreamMaker { /// /// The row Id @@ -20,5 +20,16 @@ namespace Tgstation.Server.Host.Models /// [Required] public Instance Instance { get; set; } + + /// + /// Convert the to it's API form + /// + /// A new + public Api.Models.DreamMaker ToApi() => new Api.Models.DreamMaker + { + ProjectName = ProjectName, + ApiValidationPort = ApiValidationPort, + ApiValidationSecurityLevel = ApiValidationSecurityLevel + }; } } diff --git a/src/Tgstation.Server.Host/Models/Job.cs b/src/Tgstation.Server.Host/Models/Job.cs index aeb31c0d0e..b601ecc46b 100644 --- a/src/Tgstation.Server.Host/Models/Job.cs +++ b/src/Tgstation.Server.Host/Models/Job.cs @@ -1,4 +1,8 @@ -using System.ComponentModel.DataAnnotations; +using System; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Threading; +using System.Threading.Tasks; namespace Tgstation.Server.Host.Models { @@ -22,6 +26,13 @@ namespace Tgstation.Server.Host.Models [Required] public Instance Instance { get; set; } + /// + /// A to run after the job completes. This will not affect the time, unless it is cancelled or errors + /// + /// This should only be used where there are database dependencies that also rely on the Job itself completing A.K.A. manually initiated s + [NotMapped] + public Func PostComplete { get; set; } + /// /// Convert the to it's API form /// diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180906143029_MYInitialCreate.Designer.cs b/src/Tgstation.Server.Host/Models/Migrations/20180906143029_MYInitialCreate.Designer.cs index 73566bba2d..3c1a424266 100644 --- a/src/Tgstation.Server.Host/Models/Migrations/20180906143029_MYInitialCreate.Designer.cs +++ b/src/Tgstation.Server.Host/Models/Migrations/20180906143029_MYInitialCreate.Designer.cs @@ -3,7 +3,6 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; -using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Tgstation.Server.Host.Models.Migrations { diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918020726_MYAddMinimumSecurity.Designer.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918020726_MYAddMinimumSecurity.Designer.cs new file mode 100644 index 0000000000..0c4538790b --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918020726_MYAddMinimumSecurity.Designer.cs @@ -0,0 +1,647 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Models.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20180918020726_MYAddMinimumSecurity")] + partial class MYAddMinimumSecurity + { + /// + /// Builds the target model + /// + /// The to use + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConnectionString") + .IsRequired(); + + b.Property("Enabled"); + + b.Property("InstanceId"); + + b.Property("Name") + .IsRequired(); + + b.Property("Provider"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChatSettingsId"); + + b.Property("DiscordChannelId"); + + b.Property("IrcChannel"); + + b.Property("IsAdminChannel") + .IsRequired(); + + b.Property("IsUpdatesChannel") + .IsRequired(); + + b.Property("IsWatchdogChannel") + .IsRequired(); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ByondVersion") + .IsRequired(); + + b.Property("DirectoryName"); + + b.Property("DmeName"); + + b.Property("JobId"); + + b.Property("MinimumSecurityLevel"); + + b.Property("Output"); + + b.Property("RevisionInformationId"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId"); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessToken"); + + b.Property("AllowWebClient") + .IsRequired(); + + b.Property("AutoStart") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PrimaryPort") + .IsRequired(); + + b.Property("ProcessId"); + + b.Property("SecondaryPort") + .IsRequired(); + + b.Property("SecurityLevel"); + + b.Property("SoftRestart") + .IsRequired(); + + b.Property("SoftShutdown") + .IsRequired(); + + b.Property("StartupTimeout") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiValidationPort") + .IsRequired(); + + b.Property("ApiValidationSecurityLevel"); + + b.Property("InstanceId"); + + b.Property("ProjectName"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoUpdateInterval") + .IsRequired(); + + b.Property("ConfigurationType"); + + b.Property("Name") + .IsRequired(); + + b.Property("Online") + .IsRequired(); + + b.Property("Path") + .IsRequired(); + + b.Property("WatchdogReattachInformationId"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.HasIndex("WatchdogReattachInformationId"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ByondRights"); + + b.Property("ChatBotRights"); + + b.Property("ConfigurationRights"); + + b.Property("DreamDaemonRights"); + + b.Property("DreamMakerRights"); + + b.Property("InstanceId"); + + b.Property("InstanceUserRights"); + + b.Property("RepositoryRights"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CancelRight"); + + b.Property("CancelRightsType"); + + b.Property("Cancelled") + .IsRequired(); + + b.Property("CancelledById"); + + b.Property("Description") + .IsRequired(); + + b.Property("ExceptionDetails"); + + b.Property("InstanceId"); + + b.Property("StartedAt") + .IsRequired(); + + b.Property("StartedById"); + + b.Property("StoppedAt"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessIdentifier") + .IsRequired(); + + b.Property("ChatChannelsJson") + .IsRequired(); + + b.Property("ChatCommandsJson") + .IsRequired(); + + b.Property("CompileJobId"); + + b.Property("IsPrimary"); + + b.Property("Port"); + + b.Property("ProcessId"); + + b.Property("RebootState"); + + b.Property("ServerCommandsJson") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessToken"); + + b.Property("AccessUser"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired(); + + b.Property("AutoUpdatesSynchronize") + .IsRequired(); + + b.Property("CommitterEmail") + .IsRequired(); + + b.Property("CommitterName") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PushTestMergeCommits") + .IsRequired(); + + b.Property("ShowTestMergeCommitters") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("RevisionInformationId"); + + b.Property("TestMergeId"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40); + + b.Property("InstanceId"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("CommitSha") + .IsUnique(); + + b.HasIndex("InstanceId"); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author") + .IsRequired(); + + b.Property("BodyAtMerge") + .IsRequired(); + + b.Property("Comment"); + + b.Property("MergedAt"); + + b.Property("MergedById"); + + b.Property("Number") + .IsRequired(); + + b.Property("PrimaryRevisionInformationId"); + + b.Property("PullRequestRevision") + .IsRequired(); + + b.Property("TitleAtMerge") + .IsRequired(); + + b.Property("Url") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AdministrationRights"); + + b.Property("CanonicalName") + .IsRequired(); + + b.Property("CreatedAt") + .IsRequired(); + + b.Property("CreatedById"); + + b.Property("Enabled") + .IsRequired(); + + b.Property("InstanceManagerRights"); + + b.Property("LastPasswordUpdate"); + + b.Property("Name") + .IsRequired(); + + b.Property("PasswordHash"); + + b.Property("SystemIdentifier"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AlphaId"); + + b.Property("AlphaIsActive"); + + b.Property("BravoId"); + + b.HasKey("Id"); + + b.HasIndex("AlphaId"); + + b.HasIndex("BravoId"); + + b.ToTable("WatchdogReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.HasOne("Tgstation.Server.Host.Models.WatchdogReattachInformation", "WatchdogReattachInformation") + .WithMany() + .HasForeignKey("WatchdogReattachInformationId"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User") + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha") + .WithMany() + .HasForeignKey("AlphaId"); + + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") + .WithMany() + .HasForeignKey("BravoId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918020726_MYAddMinimumSecurity.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918020726_MYAddMinimumSecurity.cs new file mode 100644 index 0000000000..95950e251f --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918020726_MYAddMinimumSecurity.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Models.Migrations +{ + /// + /// Add the and columns for MySQL/MariaDB + /// + public partial class MYAddMinimumSecurity : Migration + { + /// + /// Applies the migration + /// + /// The to use + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ApiValidationSecurityLevel", + table: "DreamMakerSettings", + nullable: false, + defaultValue: (int)DreamDaemonSecurity.Safe); + + migrationBuilder.AddColumn( + name: "MinimumSecurityLevel", + table: "CompileJobs", + nullable: false, + defaultValue: (int)DreamDaemonSecurity.Safe); + } + + /// + /// Unapplies the migration + /// + /// The to use + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ApiValidationSecurityLevel", + table: "DreamMakerSettings"); + + migrationBuilder.DropColumn( + name: "MinimumSecurityLevel", + table: "CompileJobs"); + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918021228_MSAddMinimumSecurity.Designer.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918021228_MSAddMinimumSecurity.Designer.cs new file mode 100644 index 0000000000..6223542618 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918021228_MSAddMinimumSecurity.Designer.cs @@ -0,0 +1,675 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Tgstation.Server.Host.Models.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20180918021228_MSAddMinimumSecurity")] + partial class MSAddMinimumSecurity + { + /// + /// Builds the target model + /// + /// The to use + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ConnectionString") + .IsRequired(); + + b.Property("Enabled"); + + b.Property("InstanceId"); + + b.Property("Name") + .IsRequired(); + + b.Property("Provider"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChatSettingsId"); + + b.Property("DiscordChannelId") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("IrcChannel"); + + b.Property("IsAdminChannel") + .IsRequired(); + + b.Property("IsUpdatesChannel") + .IsRequired(); + + b.Property("IsWatchdogChannel") + .IsRequired(); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondVersion") + .IsRequired(); + + b.Property("DirectoryName"); + + b.Property("DmeName"); + + b.Property("JobId"); + + b.Property("MinimumSecurityLevel"); + + b.Property("Output"); + + b.Property("RevisionInformationId"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId"); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken"); + + b.Property("AllowWebClient") + .IsRequired(); + + b.Property("AutoStart") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PrimaryPort"); + + b.Property("ProcessId"); + + b.Property("SecondaryPort"); + + b.Property("SecurityLevel"); + + b.Property("SoftRestart") + .IsRequired(); + + b.Property("SoftShutdown") + .IsRequired(); + + b.Property("StartupTimeout"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ApiValidationPort"); + + b.Property("ApiValidationSecurityLevel"); + + b.Property("InstanceId"); + + b.Property("ProjectName"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AutoUpdateInterval"); + + b.Property("ConfigurationType"); + + b.Property("Name") + .IsRequired(); + + b.Property("Online") + .IsRequired(); + + b.Property("Path") + .IsRequired(); + + b.Property("WatchdogReattachInformationId"); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.HasIndex("WatchdogReattachInformationId"); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("ChatBotRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("ConfigurationRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("DreamDaemonRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("DreamMakerRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("InstanceId"); + + b.Property("InstanceUserRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("RepositoryRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CancelRight") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("CancelRightsType") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("Cancelled") + .IsRequired(); + + b.Property("CancelledById"); + + b.Property("Description") + .IsRequired(); + + b.Property("ExceptionDetails"); + + b.Property("InstanceId"); + + b.Property("StartedAt") + .IsRequired(); + + b.Property("StartedById"); + + b.Property("StoppedAt"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessIdentifier") + .IsRequired(); + + b.Property("ChatChannelsJson") + .IsRequired(); + + b.Property("ChatCommandsJson") + .IsRequired(); + + b.Property("CompileJobId"); + + b.Property("IsPrimary"); + + b.Property("Port"); + + b.Property("ProcessId"); + + b.Property("RebootState"); + + b.Property("ServerCommandsJson") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken"); + + b.Property("AccessUser"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired(); + + b.Property("AutoUpdatesSynchronize") + .IsRequired(); + + b.Property("CommitterEmail") + .IsRequired(); + + b.Property("CommitterName") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PushTestMergeCommits") + .IsRequired(); + + b.Property("ShowTestMergeCommitters") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RevisionInformationId"); + + b.Property("TestMergeId"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40); + + b.Property("InstanceId"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("CommitSha") + .IsUnique(); + + b.HasIndex("InstanceId"); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired(); + + b.Property("BodyAtMerge") + .IsRequired(); + + b.Property("Comment"); + + b.Property("MergedAt"); + + b.Property("MergedById"); + + b.Property("Number") + .IsRequired(); + + b.Property("PrimaryRevisionInformationId"); + + b.Property("PullRequestRevision") + .IsRequired(); + + b.Property("TitleAtMerge") + .IsRequired(); + + b.Property("Url") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique() + .HasFilter("[PrimaryRevisionInformationId] IS NOT NULL"); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("CanonicalName") + .IsRequired(); + + b.Property("CreatedAt") + .IsRequired(); + + b.Property("CreatedById"); + + b.Property("Enabled") + .IsRequired(); + + b.Property("InstanceManagerRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("LastPasswordUpdate"); + + b.Property("Name") + .IsRequired(); + + b.Property("PasswordHash"); + + b.Property("SystemIdentifier"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AlphaId"); + + b.Property("AlphaIsActive"); + + b.Property("BravoId"); + + b.HasKey("Id"); + + b.HasIndex("AlphaId"); + + b.HasIndex("BravoId"); + + b.ToTable("WatchdogReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.HasOne("Tgstation.Server.Host.Models.WatchdogReattachInformation", "WatchdogReattachInformation") + .WithMany() + .HasForeignKey("WatchdogReattachInformationId"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User") + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.SetNull); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha") + .WithMany() + .HasForeignKey("AlphaId"); + + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") + .WithMany() + .HasForeignKey("BravoId"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918021228_MSAddMinimumSecurity.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918021228_MSAddMinimumSecurity.cs new file mode 100644 index 0000000000..9ac5b22b9f --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918021228_MSAddMinimumSecurity.cs @@ -0,0 +1,45 @@ +using Microsoft.EntityFrameworkCore.Migrations; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Host.Models.Migrations +{ + /// + /// Add the and columns for MSSQL + /// + public partial class MSAddMinimumSecurity : Migration + { + /// + /// Applies the migration + /// + /// The to use + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ApiValidationSecurityLevel", + table: "DreamMakerSettings", + nullable: false, + defaultValue: (int)DreamDaemonSecurity.Safe); + + migrationBuilder.AddColumn( + name: "MinimumSecurityLevel", + table: "CompileJobs", + nullable: false, + defaultValue: (int)DreamDaemonSecurity.Safe); + } + + /// + /// Unapplies the migration + /// + /// The to use + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "ApiValidationSecurityLevel", + table: "DreamMakerSettings"); + + migrationBuilder.DropColumn( + name: "MinimumSecurityLevel", + table: "CompileJobs"); + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918204520_MYNullableAndForeignKeyCleanup.Designer.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918204520_MYNullableAndForeignKeyCleanup.Designer.cs new file mode 100644 index 0000000000..c267dc1782 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918204520_MYNullableAndForeignKeyCleanup.Designer.cs @@ -0,0 +1,652 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Models.Migrations +{ + [DbContext(typeof(MySqlDatabaseContext))] + [Migration("20180918204520_MYNullableAndForeignKeyCleanup")] + partial class MYNullableAndForeignKeyCleanup + { + /// + /// Builds the target model + /// + /// The to use + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ConnectionString") + .IsRequired(); + + b.Property("Enabled"); + + b.Property("InstanceId"); + + b.Property("Name") + .IsRequired(); + + b.Property("Provider"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ChatSettingsId"); + + b.Property("DiscordChannelId"); + + b.Property("IrcChannel"); + + b.Property("IsAdminChannel") + .IsRequired(); + + b.Property("IsUpdatesChannel") + .IsRequired(); + + b.Property("IsWatchdogChannel") + .IsRequired(); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique(); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique(); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ByondVersion") + .IsRequired(); + + b.Property("DirectoryName") + .IsRequired(); + + b.Property("DmeName") + .IsRequired(); + + b.Property("JobId"); + + b.Property("MinimumSecurityLevel"); + + b.Property("Output") + .IsRequired(); + + b.Property("RevisionInformationId"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessToken"); + + b.Property("AllowWebClient") + .IsRequired(); + + b.Property("AutoStart") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PrimaryPort") + .IsRequired(); + + b.Property("ProcessId"); + + b.Property("SecondaryPort") + .IsRequired(); + + b.Property("SecurityLevel"); + + b.Property("SoftRestart") + .IsRequired(); + + b.Property("SoftShutdown") + .IsRequired(); + + b.Property("StartupTimeout") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ApiValidationPort") + .IsRequired(); + + b.Property("ApiValidationSecurityLevel"); + + b.Property("InstanceId"); + + b.Property("ProjectName"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AutoUpdateInterval") + .IsRequired(); + + b.Property("ConfigurationType"); + + b.Property("Name") + .IsRequired(); + + b.Property("Online") + .IsRequired(); + + b.Property("Path") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("ByondRights"); + + b.Property("ChatBotRights"); + + b.Property("ConfigurationRights"); + + b.Property("DreamDaemonRights"); + + b.Property("DreamMakerRights"); + + b.Property("InstanceId"); + + b.Property("InstanceUserRights"); + + b.Property("RepositoryRights"); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CancelRight"); + + b.Property("CancelRightsType"); + + b.Property("Cancelled") + .IsRequired(); + + b.Property("CancelledById"); + + b.Property("Description") + .IsRequired(); + + b.Property("ExceptionDetails"); + + b.Property("InstanceId"); + + b.Property("StartedAt") + .IsRequired(); + + b.Property("StartedById"); + + b.Property("StoppedAt"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessIdentifier") + .IsRequired(); + + b.Property("ChatChannelsJson") + .IsRequired(); + + b.Property("ChatCommandsJson") + .IsRequired(); + + b.Property("CompileJobId"); + + b.Property("IsPrimary"); + + b.Property("Port"); + + b.Property("ProcessId"); + + b.Property("RebootState"); + + b.Property("ServerCommandsJson") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AccessToken"); + + b.Property("AccessUser"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired(); + + b.Property("AutoUpdatesSynchronize") + .IsRequired(); + + b.Property("CommitterEmail") + .IsRequired(); + + b.Property("CommitterName") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PushTestMergeCommits") + .IsRequired(); + + b.Property("ShowTestMergeCommitters") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("RevisionInformationId"); + + b.Property("TestMergeId"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40); + + b.Property("InstanceId"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("CommitSha") + .IsUnique(); + + b.HasIndex("InstanceId"); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("Author") + .IsRequired(); + + b.Property("BodyAtMerge") + .IsRequired(); + + b.Property("Comment"); + + b.Property("MergedAt"); + + b.Property("MergedById"); + + b.Property("Number") + .IsRequired(); + + b.Property("PrimaryRevisionInformationId") + .IsRequired(); + + b.Property("PullRequestRevision") + .IsRequired(); + + b.Property("TitleAtMerge") + .IsRequired(); + + b.Property("Url") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AdministrationRights"); + + b.Property("CanonicalName") + .IsRequired(); + + b.Property("CreatedAt") + .IsRequired(); + + b.Property("CreatedById"); + + b.Property("Enabled") + .IsRequired(); + + b.Property("InstanceManagerRights"); + + b.Property("LastPasswordUpdate"); + + b.Property("Name") + .IsRequired(); + + b.Property("PasswordHash"); + + b.Property("SystemIdentifier"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd(); + + b.Property("AlphaId"); + + b.Property("AlphaIsActive"); + + b.Property("BravoId"); + + b.Property("InstanceId"); + + b.HasKey("Id"); + + b.HasIndex("AlphaId"); + + b.HasIndex("BravoId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("WatchdogReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User") + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha") + .WithMany() + .HasForeignKey("AlphaId"); + + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") + .WithMany() + .HasForeignKey("BravoId"); + + b.HasOne("Tgstation.Server.Host.Models.Instance") + .WithOne("WatchdogReattachInformation") + .HasForeignKey("Tgstation.Server.Host.Models.WatchdogReattachInformation", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918204520_MYNullableAndForeignKeyCleanup.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918204520_MYNullableAndForeignKeyCleanup.cs new file mode 100644 index 0000000000..c5bf5fa166 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918204520_MYNullableAndForeignKeyCleanup.cs @@ -0,0 +1,219 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Models.Migrations +{ + /// + /// Cleans up of nullable columns and foreign keys MySQL/MariaDB + /// + public partial class MYNullableAndForeignKeyCleanup : Migration + { + /// + /// Applies the migration + /// + /// The to use + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInfor~", + table: "Instances"); + + migrationBuilder.DropForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges"); + + migrationBuilder.DropForeignKey( + name: "FK_CompileJobs_Jobs_JobId", + table: "CompileJobs"); + + migrationBuilder.DropIndex( + name: "IX_Instances_WatchdogReattachInformationId", + table: "Instances"); + + migrationBuilder.DropIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs"); + + migrationBuilder.DropColumn( + name: "WatchdogReattachInformationId", + table: "Instances"); + + migrationBuilder.AddColumn( + name: "InstanceId", + table: "WatchdogReattachInformations", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AlterColumn( + name: "PrimaryRevisionInformationId", + table: "TestMerges", + nullable: false, + oldClrType: typeof(long), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Output", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "JobId", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(long), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DmeName", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DirectoryName", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(Guid), + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_WatchdogReattachInformations_InstanceId", + table: "WatchdogReattachInformations", + column: "InstanceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs", + column: "JobId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_CompileJobs_Jobs_JobId", + table: "CompileJobs", + column: "JobId", + principalTable: "Jobs", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges", + column: "PrimaryRevisionInformationId", + principalTable: "RevisionInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_WatchdogReattachInformations_Instances_InstanceId", + table: "WatchdogReattachInformations", + column: "InstanceId", + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + /// Unapplies the migration + /// + /// The to use + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges"); + + migrationBuilder.DropForeignKey( + name: "FK_WatchdogReattachInformations_Instances_InstanceId", + table: "WatchdogReattachInformations"); + + migrationBuilder.DropForeignKey( + name: "FK_CompileJobs_Jobs_JobId", + table: "CompileJobs"); + + migrationBuilder.DropIndex( + name: "IX_WatchdogReattachInformations_InstanceId", + table: "WatchdogReattachInformations"); + + migrationBuilder.DropIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs"); + + migrationBuilder.DropColumn( + name: "InstanceId", + table: "WatchdogReattachInformations"); + + migrationBuilder.AlterColumn( + name: "PrimaryRevisionInformationId", + table: "TestMerges", + nullable: true, + oldClrType: typeof(long)); + + migrationBuilder.AddColumn( + name: "WatchdogReattachInformationId", + table: "Instances", + nullable: true); + + migrationBuilder.AlterColumn( + name: "Output", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(string)); + + migrationBuilder.AlterColumn( + name: "JobId", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(long)); + + migrationBuilder.AlterColumn( + name: "DmeName", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(string)); + + migrationBuilder.AlterColumn( + name: "DirectoryName", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(Guid)); + + migrationBuilder.CreateIndex( + name: "IX_Instances_WatchdogReattachInformationId", + table: "Instances", + column: "WatchdogReattachInformationId"); + + migrationBuilder.CreateIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs", + column: "JobId"); + + migrationBuilder.AddForeignKey( + name: "FK_CompileJobs_Jobs_JobId", + table: "CompileJobs", + column: "JobId", + principalTable: "Jobs", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInfor~", + table: "Instances", + column: "WatchdogReattachInformationId", + principalTable: "WatchdogReattachInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges", + column: "PrimaryRevisionInformationId", + principalTable: "RevisionInformations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918205224_MSNullableAndForeignKeyCleanup.Designer.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918205224_MSNullableAndForeignKeyCleanup.Designer.cs new file mode 100644 index 0000000000..d8bf97f528 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918205224_MSNullableAndForeignKeyCleanup.Designer.cs @@ -0,0 +1,679 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Tgstation.Server.Host.Models.Migrations +{ + [DbContext(typeof(SqlServerDatabaseContext))] + [Migration("20180918205224_MSNullableAndForeignKeyCleanup")] + partial class MSNullableAndForeignKeyCleanup + { + /// + /// Builds the target model + /// + /// The to use + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") + .HasAnnotation("Relational:MaxIdentifierLength", 128) + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ConnectionString") + .IsRequired(); + + b.Property("Enabled"); + + b.Property("InstanceId"); + + b.Property("Name") + .IsRequired(); + + b.Property("Provider"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("ChatBots"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ChatSettingsId"); + + b.Property("DiscordChannelId") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("IrcChannel"); + + b.Property("IsAdminChannel") + .IsRequired(); + + b.Property("IsUpdatesChannel") + .IsRequired(); + + b.Property("IsWatchdogChannel") + .IsRequired(); + + b.Property("Tag"); + + b.HasKey("Id"); + + b.HasIndex("ChatSettingsId", "DiscordChannelId") + .IsUnique() + .HasFilter("[DiscordChannelId] IS NOT NULL"); + + b.HasIndex("ChatSettingsId", "IrcChannel") + .IsUnique() + .HasFilter("[IrcChannel] IS NOT NULL"); + + b.ToTable("ChatChannels"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondVersion") + .IsRequired(); + + b.Property("DirectoryName") + .IsRequired(); + + b.Property("DmeName") + .IsRequired(); + + b.Property("JobId"); + + b.Property("MinimumSecurityLevel"); + + b.Property("Output") + .IsRequired(); + + b.Property("RevisionInformationId"); + + b.HasKey("Id"); + + b.HasIndex("DirectoryName"); + + b.HasIndex("JobId") + .IsUnique(); + + b.HasIndex("RevisionInformationId"); + + b.ToTable("CompileJobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken"); + + b.Property("AllowWebClient") + .IsRequired(); + + b.Property("AutoStart") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PrimaryPort"); + + b.Property("ProcessId"); + + b.Property("SecondaryPort"); + + b.Property("SecurityLevel"); + + b.Property("SoftRestart") + .IsRequired(); + + b.Property("SoftShutdown") + .IsRequired(); + + b.Property("StartupTimeout"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamDaemonSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ApiValidationPort"); + + b.Property("ApiValidationSecurityLevel"); + + b.Property("InstanceId"); + + b.Property("ProjectName"); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("DreamMakerSettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AutoUpdateInterval"); + + b.Property("ConfigurationType"); + + b.Property("Name") + .IsRequired(); + + b.Property("Online") + .IsRequired(); + + b.Property("Path") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("Path") + .IsUnique(); + + b.ToTable("Instances"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("ByondRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("ChatBotRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("ConfigurationRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("DreamDaemonRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("DreamMakerRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("InstanceId"); + + b.Property("InstanceUserRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("RepositoryRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("UserId") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId"); + + b.HasIndex("UserId", "InstanceId") + .IsUnique(); + + b.ToTable("InstanceUsers"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CancelRight") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("CancelRightsType") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("Cancelled") + .IsRequired(); + + b.Property("CancelledById"); + + b.Property("Description") + .IsRequired(); + + b.Property("ExceptionDetails"); + + b.Property("InstanceId"); + + b.Property("StartedAt") + .IsRequired(); + + b.Property("StartedById"); + + b.Property("StoppedAt"); + + b.HasKey("Id"); + + b.HasIndex("CancelledById"); + + b.HasIndex("InstanceId"); + + b.HasIndex("StartedById"); + + b.ToTable("Jobs"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessIdentifier") + .IsRequired(); + + b.Property("ChatChannelsJson") + .IsRequired(); + + b.Property("ChatCommandsJson") + .IsRequired(); + + b.Property("CompileJobId"); + + b.Property("IsPrimary"); + + b.Property("Port"); + + b.Property("ProcessId"); + + b.Property("RebootState"); + + b.Property("ServerCommandsJson") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("CompileJobId"); + + b.ToTable("ReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AccessToken"); + + b.Property("AccessUser"); + + b.Property("AutoUpdatesKeepTestMerges") + .IsRequired(); + + b.Property("AutoUpdatesSynchronize") + .IsRequired(); + + b.Property("CommitterEmail") + .IsRequired(); + + b.Property("CommitterName") + .IsRequired(); + + b.Property("InstanceId"); + + b.Property("PushTestMergeCommits") + .IsRequired(); + + b.Property("ShowTestMergeCommitters") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("RepositorySettings"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("RevisionInformationId"); + + b.Property("TestMergeId"); + + b.HasKey("Id"); + + b.HasIndex("RevisionInformationId"); + + b.HasIndex("TestMergeId"); + + b.ToTable("RevInfoTestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("CommitSha") + .IsRequired() + .HasMaxLength(40); + + b.Property("InstanceId"); + + b.Property("OriginCommitSha") + .IsRequired() + .HasMaxLength(40); + + b.HasKey("Id"); + + b.HasIndex("CommitSha") + .IsUnique(); + + b.HasIndex("InstanceId"); + + b.ToTable("RevisionInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("Author") + .IsRequired(); + + b.Property("BodyAtMerge") + .IsRequired(); + + b.Property("Comment"); + + b.Property("MergedAt"); + + b.Property("MergedById"); + + b.Property("Number") + .IsRequired(); + + b.Property("PrimaryRevisionInformationId") + .IsRequired(); + + b.Property("PullRequestRevision") + .IsRequired(); + + b.Property("TitleAtMerge") + .IsRequired(); + + b.Property("Url") + .IsRequired(); + + b.HasKey("Id"); + + b.HasIndex("MergedById"); + + b.HasIndex("PrimaryRevisionInformationId") + .IsUnique(); + + b.ToTable("TestMerges"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AdministrationRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("CanonicalName") + .IsRequired(); + + b.Property("CreatedAt") + .IsRequired(); + + b.Property("CreatedById"); + + b.Property("Enabled") + .IsRequired(); + + b.Property("InstanceManagerRights") + .HasConversion(new ValueConverter(v => default(decimal), v => default(decimal), new ConverterMappingHints(precision: 20, scale: 0))); + + b.Property("LastPasswordUpdate"); + + b.Property("Name") + .IsRequired(); + + b.Property("PasswordHash"); + + b.Property("SystemIdentifier"); + + b.HasKey("Id"); + + b.HasIndex("CanonicalName") + .IsUnique(); + + b.HasIndex("CreatedById"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); + + b.Property("AlphaId"); + + b.Property("AlphaIsActive"); + + b.Property("BravoId"); + + b.Property("InstanceId"); + + b.HasKey("Id"); + + b.HasIndex("AlphaId"); + + b.HasIndex("BravoId"); + + b.HasIndex("InstanceId") + .IsUnique(); + + b.ToTable("WatchdogReattachInformations"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("ChatSettings") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ChatChannel", b => + { + b.HasOne("Tgstation.Server.Host.Models.ChatBot", "ChatSettings") + .WithMany("Channels") + .HasForeignKey("ChatSettingsId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => + { + b.HasOne("Tgstation.Server.Host.Models.Job", "Job") + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("CompileJobs") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamDaemonSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamDaemonSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamDaemonSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.DreamMakerSettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("DreamMakerSettings") + .HasForeignKey("Tgstation.Server.Host.Models.DreamMakerSettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("InstanceUsers") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User") + .WithMany("InstanceUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.Job", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CancelledBy") + .WithMany() + .HasForeignKey("CancelledById"); + + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("Jobs") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.User", "StartedBy") + .WithMany() + .HasForeignKey("StartedById") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.ReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.CompileJob", "CompileJob") + .WithMany() + .HasForeignKey("CompileJobId"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RepositorySettings", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithOne("RepositorySettings") + .HasForeignKey("Tgstation.Server.Host.Models.RepositorySettings", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevInfoTestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") + .WithMany("ActiveTestMerges") + .HasForeignKey("RevisionInformationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Tgstation.Server.Host.Models.TestMerge", "TestMerge") + .WithMany("RevisonInformations") + .HasForeignKey("TestMergeId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.RevisionInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") + .WithMany("RevisionInformations") + .HasForeignKey("InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.TestMerge", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "MergedBy") + .WithMany("TestMerges") + .HasForeignKey("MergedById") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") + .WithOne("PrimaryTestMerge") + .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") + .OnDelete(DeleteBehavior.Restrict); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => + { + b.HasOne("Tgstation.Server.Host.Models.User", "CreatedBy") + .WithMany("CreatedUsers") + .HasForeignKey("CreatedById"); + }); + + modelBuilder.Entity("Tgstation.Server.Host.Models.WatchdogReattachInformation", b => + { + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Alpha") + .WithMany() + .HasForeignKey("AlphaId"); + + b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") + .WithMany() + .HasForeignKey("BravoId"); + + b.HasOne("Tgstation.Server.Host.Models.Instance") + .WithOne("WatchdogReattachInformation") + .HasForeignKey("Tgstation.Server.Host.Models.WatchdogReattachInformation", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/20180918205224_MSNullableAndForeignKeyCleanup.cs b/src/Tgstation.Server.Host/Models/Migrations/20180918205224_MSNullableAndForeignKeyCleanup.cs new file mode 100644 index 0000000000..3cc60483d6 --- /dev/null +++ b/src/Tgstation.Server.Host/Models/Migrations/20180918205224_MSNullableAndForeignKeyCleanup.cs @@ -0,0 +1,216 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Tgstation.Server.Host.Models.Migrations +{ + /// + /// Cleans up of nullable columns and foreign keys MSSQL + /// + public partial class MSNullableAndForeignKeyCleanup : Migration + { + /// + /// Applies the migration + /// + /// The to use + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInformationId", + table: "Instances"); + + migrationBuilder.DropForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges"); + + migrationBuilder.DropIndex( + name: "IX_TestMerges_PrimaryRevisionInformationId", + table: "TestMerges"); + + migrationBuilder.DropIndex( + name: "IX_Instances_WatchdogReattachInformationId", + table: "Instances"); + + migrationBuilder.DropIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs"); + + migrationBuilder.DropColumn( + name: "WatchdogReattachInformationId", + table: "Instances"); + + migrationBuilder.AddColumn( + name: "InstanceId", + table: "WatchdogReattachInformations", + nullable: false, + defaultValue: 0L); + + migrationBuilder.AlterColumn( + name: "PrimaryRevisionInformationId", + table: "TestMerges", + nullable: false, + oldClrType: typeof(long), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "Output", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "JobId", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(long), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DmeName", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(string), + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "DirectoryName", + table: "CompileJobs", + nullable: false, + oldClrType: typeof(Guid), + oldNullable: true); + + migrationBuilder.CreateIndex( + name: "IX_WatchdogReattachInformations_InstanceId", + table: "WatchdogReattachInformations", + column: "InstanceId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_TestMerges_PrimaryRevisionInformationId", + table: "TestMerges", + column: "PrimaryRevisionInformationId", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs", + column: "JobId", + unique: true); + + migrationBuilder.AddForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges", + column: "PrimaryRevisionInformationId", + principalTable: "RevisionInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_WatchdogReattachInformations_Instances_InstanceId", + table: "WatchdogReattachInformations", + column: "InstanceId", + principalTable: "Instances", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + /// Unapplies the migration + /// + /// The to use + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges"); + + migrationBuilder.DropForeignKey( + name: "FK_WatchdogReattachInformations_Instances_InstanceId", + table: "WatchdogReattachInformations"); + + migrationBuilder.DropIndex( + name: "IX_WatchdogReattachInformations_InstanceId", + table: "WatchdogReattachInformations"); + + migrationBuilder.DropIndex( + name: "IX_TestMerges_PrimaryRevisionInformationId", + table: "TestMerges"); + + migrationBuilder.DropIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs"); + + migrationBuilder.DropColumn( + name: "InstanceId", + table: "WatchdogReattachInformations"); + + migrationBuilder.AlterColumn( + name: "PrimaryRevisionInformationId", + table: "TestMerges", + nullable: true, + oldClrType: typeof(long)); + + migrationBuilder.AddColumn( + name: "WatchdogReattachInformationId", + table: "Instances", + nullable: true); + + migrationBuilder.AlterColumn( + name: "Output", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(string)); + + migrationBuilder.AlterColumn( + name: "JobId", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(long)); + + migrationBuilder.AlterColumn( + name: "DmeName", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(string)); + + migrationBuilder.AlterColumn( + name: "DirectoryName", + table: "CompileJobs", + nullable: true, + oldClrType: typeof(Guid)); + + migrationBuilder.CreateIndex( + name: "IX_TestMerges_PrimaryRevisionInformationId", + table: "TestMerges", + column: "PrimaryRevisionInformationId", + unique: true, + filter: "[PrimaryRevisionInformationId] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_Instances_WatchdogReattachInformationId", + table: "Instances", + column: "WatchdogReattachInformationId"); + + migrationBuilder.CreateIndex( + name: "IX_CompileJobs_JobId", + table: "CompileJobs", + column: "JobId"); + + migrationBuilder.AddForeignKey( + name: "FK_Instances_WatchdogReattachInformations_WatchdogReattachInformationId", + table: "Instances", + column: "WatchdogReattachInformationId", + principalTable: "WatchdogReattachInformations", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + + migrationBuilder.AddForeignKey( + name: "FK_TestMerges_RevisionInformations_PrimaryRevisionInformationId", + table: "TestMerges", + column: "PrimaryRevisionInformationId", + principalTable: "RevisionInformations", + principalColumn: "Id", + onDelete: ReferentialAction.SetNull); + } + } +} diff --git a/src/Tgstation.Server.Host/Models/Migrations/MySqlDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Models/Migrations/MySqlDatabaseContextModelSnapshot.cs index 7c82e23050..e273129437 100644 --- a/src/Tgstation.Server.Host/Models/Migrations/MySqlDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Models/Migrations/MySqlDatabaseContextModelSnapshot.cs @@ -3,6 +3,7 @@ using System; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Models.Migrations { @@ -13,7 +14,7 @@ namespace Tgstation.Server.Host.Models.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.1.2-rtm-30932") + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") .HasAnnotation("Relational:MaxIdentifierLength", 64); modelBuilder.Entity("Tgstation.Server.Host.Models.ChatBot", b => @@ -84,13 +85,18 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("ByondVersion") .IsRequired(); - b.Property("DirectoryName"); + b.Property("DirectoryName") + .IsRequired(); - b.Property("DmeName"); + b.Property("DmeName") + .IsRequired(); - b.Property("JobId"); + b.Property("JobId"); - b.Property("Output"); + b.Property("MinimumSecurityLevel"); + + b.Property("Output") + .IsRequired(); b.Property("RevisionInformationId"); @@ -98,7 +104,8 @@ namespace Tgstation.Server.Host.Models.Migrations b.HasIndex("DirectoryName"); - b.HasIndex("JobId"); + b.HasIndex("JobId") + .IsUnique(); b.HasIndex("RevisionInformationId"); @@ -155,6 +162,8 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("ApiValidationPort") .IsRequired(); + b.Property("ApiValidationSecurityLevel"); + b.Property("InstanceId"); b.Property("ProjectName"); @@ -186,15 +195,11 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("Path") .IsRequired(); - b.Property("WatchdogReattachInformationId"); - b.HasKey("Id"); b.HasIndex("Path") .IsUnique(); - b.HasIndex("WatchdogReattachInformationId"); - b.ToTable("Instances"); }); @@ -405,7 +410,8 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("Number") .IsRequired(); - b.Property("PrimaryRevisionInformationId"); + b.Property("PrimaryRevisionInformationId") + .IsRequired(); b.Property("PullRequestRevision") .IsRequired(); @@ -476,12 +482,17 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("BravoId"); + b.Property("InstanceId"); + b.HasKey("Id"); b.HasIndex("AlphaId"); b.HasIndex("BravoId"); + b.HasIndex("InstanceId") + .IsUnique(); + b.ToTable("WatchdogReattachInformations"); }); @@ -504,8 +515,9 @@ namespace Tgstation.Server.Host.Models.Migrations modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => { b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithMany() - .HasForeignKey("JobId"); + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Restrict); b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") .WithMany("CompileJobs") @@ -529,13 +541,6 @@ namespace Tgstation.Server.Host.Models.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.HasOne("Tgstation.Server.Host.Models.WatchdogReattachInformation", "WatchdogReattachInformation") - .WithMany() - .HasForeignKey("WatchdogReattachInformationId"); - }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -612,7 +617,7 @@ namespace Tgstation.Server.Host.Models.Migrations b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") .WithOne("PrimaryTestMerge") .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.SetNull); + .OnDelete(DeleteBehavior.Restrict); }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => @@ -631,6 +636,11 @@ namespace Tgstation.Server.Host.Models.Migrations b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") .WithMany() .HasForeignKey("BravoId"); + + b.HasOne("Tgstation.Server.Host.Models.Instance") + .WithOne("WatchdogReattachInformation") + .HasForeignKey("Tgstation.Server.Host.Models.WatchdogReattachInformation", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); }); #pragma warning restore 612, 618 } diff --git a/src/Tgstation.Server.Host/Models/Migrations/SqlServerDatabaseContextModelSnapshot.cs b/src/Tgstation.Server.Host/Models/Migrations/SqlServerDatabaseContextModelSnapshot.cs index a17096d672..0174e55a09 100644 --- a/src/Tgstation.Server.Host/Models/Migrations/SqlServerDatabaseContextModelSnapshot.cs +++ b/src/Tgstation.Server.Host/Models/Migrations/SqlServerDatabaseContextModelSnapshot.cs @@ -4,6 +4,7 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Models.Migrations { @@ -14,7 +15,7 @@ namespace Tgstation.Server.Host.Models.Migrations { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "2.1.2-rtm-30932") + .HasAnnotation("ProductVersion", "2.1.3-rtm-32065") .HasAnnotation("Relational:MaxIdentifierLength", 128) .HasAnnotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn); @@ -92,13 +93,18 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("ByondVersion") .IsRequired(); - b.Property("DirectoryName"); + b.Property("DirectoryName") + .IsRequired(); - b.Property("DmeName"); + b.Property("DmeName") + .IsRequired(); - b.Property("JobId"); + b.Property("JobId"); - b.Property("Output"); + b.Property("MinimumSecurityLevel"); + + b.Property("Output") + .IsRequired(); b.Property("RevisionInformationId"); @@ -106,7 +112,8 @@ namespace Tgstation.Server.Host.Models.Migrations b.HasIndex("DirectoryName"); - b.HasIndex("JobId"); + b.HasIndex("JobId") + .IsUnique(); b.HasIndex("RevisionInformationId"); @@ -161,6 +168,8 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("ApiValidationPort"); + b.Property("ApiValidationSecurityLevel"); + b.Property("InstanceId"); b.Property("ProjectName"); @@ -192,15 +201,11 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("Path") .IsRequired(); - b.Property("WatchdogReattachInformationId"); - b.HasKey("Id"); b.HasIndex("Path") .IsUnique(); - b.HasIndex("WatchdogReattachInformationId"); - b.ToTable("Instances"); }); @@ -427,7 +432,8 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("Number") .IsRequired(); - b.Property("PrimaryRevisionInformationId"); + b.Property("PrimaryRevisionInformationId") + .IsRequired(); b.Property("PullRequestRevision") .IsRequired(); @@ -443,8 +449,7 @@ namespace Tgstation.Server.Host.Models.Migrations b.HasIndex("MergedById"); b.HasIndex("PrimaryRevisionInformationId") - .IsUnique() - .HasFilter("[PrimaryRevisionInformationId] IS NOT NULL"); + .IsUnique(); b.ToTable("TestMerges"); }); @@ -503,12 +508,17 @@ namespace Tgstation.Server.Host.Models.Migrations b.Property("BravoId"); + b.Property("InstanceId"); + b.HasKey("Id"); b.HasIndex("AlphaId"); b.HasIndex("BravoId"); + b.HasIndex("InstanceId") + .IsUnique(); + b.ToTable("WatchdogReattachInformations"); }); @@ -531,8 +541,9 @@ namespace Tgstation.Server.Host.Models.Migrations modelBuilder.Entity("Tgstation.Server.Host.Models.CompileJob", b => { b.HasOne("Tgstation.Server.Host.Models.Job", "Job") - .WithMany() - .HasForeignKey("JobId"); + .WithOne() + .HasForeignKey("Tgstation.Server.Host.Models.CompileJob", "JobId") + .OnDelete(DeleteBehavior.Restrict); b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "RevisionInformation") .WithMany("CompileJobs") @@ -556,13 +567,6 @@ namespace Tgstation.Server.Host.Models.Migrations .OnDelete(DeleteBehavior.Cascade); }); - modelBuilder.Entity("Tgstation.Server.Host.Models.Instance", b => - { - b.HasOne("Tgstation.Server.Host.Models.WatchdogReattachInformation", "WatchdogReattachInformation") - .WithMany() - .HasForeignKey("WatchdogReattachInformationId"); - }); - modelBuilder.Entity("Tgstation.Server.Host.Models.InstanceUser", b => { b.HasOne("Tgstation.Server.Host.Models.Instance", "Instance") @@ -639,7 +643,7 @@ namespace Tgstation.Server.Host.Models.Migrations b.HasOne("Tgstation.Server.Host.Models.RevisionInformation", "PrimaryRevisionInformation") .WithOne("PrimaryTestMerge") .HasForeignKey("Tgstation.Server.Host.Models.TestMerge", "PrimaryRevisionInformationId") - .OnDelete(DeleteBehavior.SetNull); + .OnDelete(DeleteBehavior.Restrict); }); modelBuilder.Entity("Tgstation.Server.Host.Models.User", b => @@ -658,6 +662,11 @@ namespace Tgstation.Server.Host.Models.Migrations b.HasOne("Tgstation.Server.Host.Models.ReattachInformation", "Bravo") .WithMany() .HasForeignKey("BravoId"); + + b.HasOne("Tgstation.Server.Host.Models.Instance") + .WithOne("WatchdogReattachInformation") + .HasForeignKey("Tgstation.Server.Host.Models.WatchdogReattachInformation", "InstanceId") + .OnDelete(DeleteBehavior.Cascade); }); #pragma warning restore 612, 618 } diff --git a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs index 3d808a7f96..115b0ae6fe 100644 --- a/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs +++ b/src/Tgstation.Server.Host/Models/ReattachInformationBase.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using System.Globalization; using Tgstation.Server.Host.Components.Interop; using Tgstation.Server.Host.Components.Watchdog; @@ -34,7 +35,6 @@ namespace Tgstation.Server.Host.Models /// /// The current DreamDaemon reboot state /// - [Required] public RebootState RebootState { get; set; } /// @@ -56,5 +56,8 @@ namespace Tgstation.Server.Host.Models ProcessId = copy.ProcessId; RebootState = copy.RebootState; } + + /// + public override string ToString() => String.Format(CultureInfo.InvariantCulture, "Process ID: {3}, Access Identifier {4}, Primary: {0}, RebootState: {1}, Port: {2}", IsPrimary, RebootState, Port, ProcessId, AccessIdentifier); } } diff --git a/src/Tgstation.Server.Host/Models/TestMerge.cs b/src/Tgstation.Server.Host/Models/TestMerge.cs index bbab03bea0..754951c355 100644 --- a/src/Tgstation.Server.Host/Models/TestMerge.cs +++ b/src/Tgstation.Server.Host/Models/TestMerge.cs @@ -15,6 +15,7 @@ namespace Tgstation.Server.Host.Models /// /// The initial the was merged with /// + [Required] public RevisionInformation PrimaryRevisionInformation { get; set; } /// diff --git a/src/Tgstation.Server.Host/Models/WatchdogReattachInformation.cs b/src/Tgstation.Server.Host/Models/WatchdogReattachInformation.cs index db27049745..204baed3b3 100644 --- a/src/Tgstation.Server.Host/Models/WatchdogReattachInformation.cs +++ b/src/Tgstation.Server.Host/Models/WatchdogReattachInformation.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace Tgstation.Server.Host.Models +namespace Tgstation.Server.Host.Models { /// /// Database representation of @@ -14,6 +10,11 @@ namespace Tgstation.Server.Host.Models /// public long Id { get; set; } + /// + /// The of the the belongs to + /// + public long InstanceId { get; set; } + /// /// The for the Alpha server /// diff --git a/src/Tgstation.Server.Host/NativeMethods.cs b/src/Tgstation.Server.Host/NativeMethods.cs index 398185ab2a..afe778312a 100644 --- a/src/Tgstation.Server.Host/NativeMethods.cs +++ b/src/Tgstation.Server.Host/NativeMethods.cs @@ -1,5 +1,6 @@ using System; using System.Runtime.InteropServices; +using System.Text; namespace Tgstation.Server.Host { @@ -8,6 +9,42 @@ namespace Tgstation.Server.Host /// static class NativeMethods { + /// + /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid + /// + [DllImport("user32.dll")] + public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId); + + /// + /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-findwindoww + /// + [DllImport("user32.dll", CharSet = CharSet.Unicode)] + public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); + + /// + /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-sendmessage + /// + [DllImport("user32.dll")] + public static extern int SendMessage(IntPtr hWnd, int wMsg, IntPtr wParam, IntPtr lParam); + + /// + /// See https://msdn.microsoft.com/en-us/library/ms633493(v=VS.85).aspx + /// + public delegate bool EnumWindowProc(IntPtr hwnd, IntPtr lParam); + + /// + /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-enumchildwindows + /// + [DllImport("user32")] + [return: MarshalAs(UnmanagedType.Bool)] + public static extern bool EnumChildWindows(IntPtr window, EnumWindowProc callback, IntPtr lParam); + + /// + /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowtextw + /// + [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount); + /// /// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa378184(v=vs.85).aspx /// diff --git a/src/Tgstation.Server.Host/Security/ClaimsInjector.cs b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs new file mode 100644 index 0000000000..3e7f202b2e --- /dev/null +++ b/src/Tgstation.Server.Host/Security/ClaimsInjector.cs @@ -0,0 +1,95 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Http; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Models; + +namespace Tgstation.Server.Host.Security +{ + /// + sealed class ClaimsInjector : IClaimsInjector + { + /// + /// The for the + /// + readonly IDatabaseContext databaseContext; + + /// + /// The for the + /// + readonly IAuthenticationContextFactory authenticationContextFactory; + + /// + /// Construct a + /// + /// The value of + /// The value of + public ClaimsInjector(IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory) + { + this.databaseContext = databaseContext ?? throw new ArgumentNullException(nameof(databaseContext)); + this.authenticationContextFactory = authenticationContextFactory ?? throw new ArgumentNullException(nameof(authenticationContextFactory)); + } + + /// + public async Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken) + { + if (tokenValidatedContext == null) + throw new ArgumentNullException(nameof(tokenValidatedContext)); + + //Find the user id in the token + var userIdClaim = tokenValidatedContext.Principal.FindFirst(JwtRegisteredClaimNames.Sub); + if (userIdClaim == default) + throw new InvalidOperationException("Missing required claim!"); + + long userId; + try + { + userId = Int64.Parse(userIdClaim.Value, CultureInfo.InvariantCulture); + } + catch (Exception e) + { + throw new InvalidOperationException("Failed to parse user ID!", e); + } + + ApiHeaders apiHeaders; + try + { + apiHeaders = new ApiHeaders(tokenValidatedContext.HttpContext.Request.GetTypedHeaders()); + } + catch (InvalidOperationException) + { + //we are not responsible for handling header validation issues + return; + } + + //This populates the CurrentAuthenticationContext field for use by us and subsequent controllers + await authenticationContextFactory.CreateAuthenticationContext(userId, apiHeaders.InstanceId, tokenValidatedContext.SecurityToken.ValidFrom, cancellationToken).ConfigureAwait(false); + + var authenticationContext = authenticationContextFactory.CurrentAuthenticationContext; + + var enumerator = Enum.GetValues(typeof(RightsType)); + var claims = new List(); + foreach (RightsType I in enumerator) + { + //if there's no instance user, do a weird thing and add all the instance roles + //we need it so we can get to OnActionExecutionAsync where we can properly decide between BadRequest and Forbid + //if user is null that means they got the token with an expired password + var rightInt = authenticationContext.User == null || (RightsHelper.IsInstanceRight(I) && authenticationContext.InstanceUser == null) ? ~0U : authenticationContext.GetRight(I); + var rightEnum = RightsHelper.RightToType(I); + var right = (Enum)Enum.ToObject(rightEnum, rightInt); + foreach (Enum J in Enum.GetValues(rightEnum)) + if (right.HasFlag(J)) + claims.Add(new Claim(ClaimTypes.Role, RightsHelper.RoleName(I, J))); + } + + tokenValidatedContext.Principal.AddIdentity(new ClaimsIdentity(claims)); + } + } +} diff --git a/src/Tgstation.Server.Host/Security/IClaimsInjector.cs b/src/Tgstation.Server.Host/Security/IClaimsInjector.cs new file mode 100644 index 0000000000..216b03b0f0 --- /dev/null +++ b/src/Tgstation.Server.Host/Security/IClaimsInjector.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Authentication.JwtBearer; +using System.Threading; +using System.Threading.Tasks; + +namespace Tgstation.Server.Host.Security +{ + /// + /// For injecting s that can look for + /// + interface IClaimsInjector + { + /// + /// Setup the s for a given + /// + /// The containing the and of the request and the to add s to + /// The for the operation + /// A representing the running operation + Task InjectClaimsIntoContext(TokenValidatedContext tokenValidatedContext, CancellationToken cancellationToken); + } +} \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Server.cs b/src/Tgstation.Server.Host/Server.cs index 7bc3192638..18a5749478 100644 --- a/src/Tgstation.Server.Host/Server.cs +++ b/src/Tgstation.Server.Host/Server.cs @@ -1,8 +1,10 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using System; +using System.Collections.Generic; using System.IO; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Host.Core; @@ -30,6 +32,11 @@ namespace Tgstation.Server.Host /// The absolute path to install updates to /// readonly string updatePath; + + /// + /// The s to run when the restarts + /// + readonly List restartHandlers; /// /// If a server update has been applied @@ -51,6 +58,7 @@ namespace Tgstation.Server.Host this.webHostBuilder = webHostBuilder ?? throw new ArgumentNullException(nameof(webHostBuilder)); this.updatePath = updatePath; + restartHandlers = new List(); semaphore = new SemaphoreSlim(1); updated = false; RestartRequested = false; @@ -85,8 +93,15 @@ namespace Tgstation.Server.Host } /// - public async Task ApplyUpdate(byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken) + public async Task ApplyUpdate(Version version, byte[] updateZipData, IIOManager ioManager, CancellationToken cancellationToken) { + if (version == null) + throw new ArgumentNullException(nameof(version)); + if (updateZipData == null) + throw new ArgumentNullException(nameof(updateZipData)); + if (ioManager == null) + throw new ArgumentNullException(nameof(ioManager)); + if (updatePath == null) return false; using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false)) @@ -98,44 +113,82 @@ namespace Tgstation.Server.Host { await ioManager.ZipToDirectory(updatePath, updateZipData, cancellationToken).ConfigureAwait(false); } - catch + catch (Exception e) { + updated = false; try { //important to not leave this directory around if possible await ioManager.DeleteDirectory(updatePath, default).ConfigureAwait(false); } - catch { } - updated = false; + catch (Exception e2) + { + throw new AggregateException(e, e2); + } throw; } - Restart(); + await Restart(version).ConfigureAwait(false); return true; } } /// - public void RegisterForRestart(Action action) + public IRestartRegistration RegisterForRestart(IRestartHandler handler) { - if (action == null) - throw new ArgumentNullException(nameof(action)); + if (handler == null) + throw new ArgumentNullException(nameof(handler)); if (cancellationTokenSource == null) throw new InvalidOperationException("Tried to register an update action on a non-running Server!"); - cancellationTokenSource.Token.Register(() => - { - if (RestartRequested) - action(); - }); + lock (this) + if (!RestartRequested) + { + restartHandlers.Add(handler); + return new RestartRegistration(() => + { + lock (this) + if(!RestartRequested) + restartHandlers.Remove(handler); + }); + } + return new RestartRegistration(() => { }); } /// - public bool Restart() + public Task Restart() => Restart(null); + + /// + /// Implements + /// + /// The of any potential updates being applied + /// + async Task Restart(Version newVersion) { if (updatePath == null) return false; if (cancellationTokenSource == null) throw new InvalidOperationException("Tried to restart a non-running Server!"); - RestartRequested = true; + lock (this) + { + if (RestartRequested) + return true; + RestartRequested = true; + } + + using (var cts = new CancellationTokenSource()) + { + var cancellationToken = cts.Token; + var eventsTask = Task.WhenAll(restartHandlers.Select(x => x.HandleRestart(newVersion, cancellationToken))); + //YA GOT 10 SECONDS + var expiryTask = Task.Delay(TimeSpan.FromSeconds(10)); + await Task.WhenAny(eventsTask, expiryTask).ConfigureAwait(false); + cts.Cancel(); + try + { + await eventsTask.ConfigureAwait(false); + } + catch (OperationCanceledException) { } + } + cancellationTokenSource.Cancel(); return true; } diff --git a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj index fe16fe434e..3c20773be9 100644 --- a/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj +++ b/src/Tgstation.Server.Host/Tgstation.Server.Host.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp2.1 Full 4.0.0.0 4.0.0.0 @@ -12,7 +12,7 @@ latest true - bin\Release\netstandard2.0\Tgstation.Server.Host.xml + bin\Release\netcoreapp2.1\Tgstation.Server.Host.xml 1701;1702;1705;CA2227 @@ -35,18 +35,18 @@ all compile; build; native; contentfiles; analyzers - - - - + + + + - - + + - + diff --git a/src/Tgstation.Server.Host/appsettings.Docker.json b/src/Tgstation.Server.Host/appsettings.Docker.json deleted file mode 100644 index d90c93f5a5..0000000000 --- a/src/Tgstation.Server.Host/appsettings.Docker.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "General": { - "LogFileDirectory": "/tgs_logs" - }, - "Database": { - "DatabaseType": "SqlServer or MySQL or MariaDB", - "ConnectionString": "", - "MySqlServerVersion": "" - } -} diff --git a/src/Tgstation.Server.Host/appsettings.json b/src/Tgstation.Server.Host/appsettings.json index d16d0bb4f5..0504931d30 100644 --- a/src/Tgstation.Server.Host/appsettings.json +++ b/src/Tgstation.Server.Host/appsettings.json @@ -2,7 +2,16 @@ "General": { "LogFileDirectory": null, //use the default path "DisableFileLogging": false, - "MinimumPasswordLength": 15 + "LogFileLevel": "Debug", + "MinimumPasswordLength": 15, + "GitHubAccessToken": null + }, + "Kestrel": { + "EndPoints": { + "Http": { + "Url": "http://0.0.0.0:5000" + } + } }, "Logging": { "IncludeScopes": false, @@ -17,10 +26,6 @@ "Default": "Trace", "Microsoft": "Warning" } - }, - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning" } }, "Updates": { diff --git a/tests/Tgstation.Server.Api.Tests/Models/TestIrcConnectionStringBuilder.cs b/tests/Tgstation.Server.Api.Tests/Models/TestIrcConnectionStringBuilder.cs new file mode 100644 index 0000000000..4eca51f170 --- /dev/null +++ b/tests/Tgstation.Server.Api.Tests/Models/TestIrcConnectionStringBuilder.cs @@ -0,0 +1,25 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tgstation.Server.Api.Models.Tests +{ + [TestClass] + public sealed class TestIrcConnectionStringBuilder + { + [TestMethod] + public void TestBasicParseAndBuild() + { + const string exampleString = "server;1234;nick;1;2;asdf"; + var builder = new IrcConnectionStringBuilder(exampleString); + + Assert.IsTrue(builder.Valid); + Assert.AreEqual("server", builder.Address); + Assert.AreEqual((ushort)1234, builder.Port); + Assert.AreEqual("nick", builder.Nickname); + Assert.IsTrue(builder.UseSsl.Value); + Assert.AreEqual(IrcPasswordType.NickServ, builder.PasswordType); + Assert.AreEqual("asdf", builder.Password); + + Assert.AreEqual(exampleString, builder.ToString()); + } + } +} diff --git a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj index e4e77805e2..c395583e37 100644 --- a/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj +++ b/tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj @@ -1,16 +1,16 @@ - netcoreapp2.0 + netcoreapp2.1 false latest - - - + + + diff --git a/tests/Tgstation.Server.Client.Tests/Components/TestDreamDaemonClient.cs b/tests/Tgstation.Server.Client.Tests/Components/TestDreamDaemonClient.cs new file mode 100644 index 0000000000..a70423e97c --- /dev/null +++ b/tests/Tgstation.Server.Client.Tests/Components/TestDreamDaemonClient.cs @@ -0,0 +1,39 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client.Components.Tests +{ + [TestClass] + public sealed class TestDreamDaemonClient + { + [TestMethod] + public async Task TestStart() + { + var example = new Job + { + Id = 347, + StartedAt = DateTimeOffset.Now + }; + + var inst = new Instance + { + Id = 4958 + }; + + var mockApiClient = new Mock(); + mockApiClient.Setup(x => x.Create(Routes.DreamDaemon, inst.Id, It.IsAny())).Returns(Task.FromResult(example)); + + var client = new DreamDaemonClient(mockApiClient.Object, inst); + + var result = await client.Start(default).ConfigureAwait(false); + Assert.AreSame(example, result); + } + } +} diff --git a/tests/Tgstation.Server.Client.Tests/TestApiClient.cs b/tests/Tgstation.Server.Client.Tests/TestApiClient.cs new file mode 100644 index 0000000000..2a544b5863 --- /dev/null +++ b/tests/Tgstation.Server.Client.Tests/TestApiClient.cs @@ -0,0 +1,71 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; +using System; +using System.Net; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Api; +using Tgstation.Server.Api.Models; + +namespace Tgstation.Server.Client.Tests +{ + [TestClass] + public sealed class TestApiClient + { + [TestMethod] + public async Task TestDeserializingByondModelsWork() + { + var sample = new Byond + { + Version = new Version(511, 1385) + }; + + var sampleJson = JsonConvert.SerializeObject(sample, new JsonSerializerSettings + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + Converters = new[] { new VersionConverter() } + }); + + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(sampleJson) + }; + + var httpClient = new Mock(); + httpClient.Setup(x => x.SendAsync(It.IsNotNull(), It.IsAny())).Returns(Task.FromResult(response)); + + var client = new ApiClient(httpClient.Object, new Uri("http://fake.com"), new ApiHeaders(new ProductHeaderValue("fake"), "fake")); + + var result = await client.Read(Routes.Byond, default).ConfigureAwait(false); + Assert.AreEqual(sample.Version, result.Version); + } + + [TestMethod] + public async Task TestUnrecognizedResponse() + { + var sample = new Byond + { + Version = new Version(511, 1385) + }; + + var fakeJson = "asdfasd <>F#(*)U*#JLI"; + + var response = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(fakeJson) + }; + + var httpClient = new Mock(); + httpClient.Setup(x => x.SendAsync(It.IsNotNull(), It.IsAny())).Returns(Task.FromResult(response)); + + var client = new ApiClient(httpClient.Object, new Uri("http://fake.com"), new ApiHeaders(new ProductHeaderValue("fake"), "fake")); + + await Assert.ThrowsExceptionAsync(() => client.Read(Routes.Byond, default)).ConfigureAwait(false); + } + } +} diff --git a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj index caacb5743a..6eaed1daed 100644 --- a/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj +++ b/tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj @@ -1,16 +1,17 @@ - netcoreapp2.0 + netcoreapp2.1 false latest - - - + + + + diff --git a/tests/Tgstation.Server.CommandLine.Tests/Tgstation.Server.CommandLine.Tests.csproj b/tests/Tgstation.Server.CommandLine.Tests/Tgstation.Server.CommandLine.Tests.csproj deleted file mode 100644 index 43a487525f..0000000000 --- a/tests/Tgstation.Server.CommandLine.Tests/Tgstation.Server.CommandLine.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - netcoreapp2.0 - - false - latest - - - - - - - - - - - - - diff --git a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj index 2e6e921228..1648915ec8 100644 --- a/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj +++ b/tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj @@ -1,17 +1,17 @@ - netcoreapp2.0 + netcoreapp2.1 false latest - - - - + + + + diff --git a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj index 6baddb56cb..b2b93b20f3 100644 --- a/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj +++ b/tests/Tgstation.Server.Host.Service.Tests/Tgstation.Server.Host.Service.Tests.csproj @@ -1,6 +1,5 @@  - Debug AnyCPU @@ -40,94 +39,21 @@ 7.1 - - ..\..\packages\Castle.Core.4.2.1\lib\net45\Castle.Core.dll - - - ..\..\packages\Microsoft.Extensions.Configuration.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.dll - - - ..\..\packages\Microsoft.Extensions.Configuration.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Abstractions.dll - - - ..\..\packages\Microsoft.Extensions.Configuration.Binder.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Configuration.Binder.dll - - - ..\..\packages\Microsoft.Extensions.DependencyInjection.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.DependencyInjection.Abstractions.dll - - - ..\..\packages\Microsoft.Extensions.Logging.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.dll - - - ..\..\packages\Microsoft.Extensions.Logging.Abstractions.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Logging.Abstractions.dll - - - ..\..\packages\Microsoft.Extensions.Logging.EventLog.2.1.1\lib\net461\Microsoft.Extensions.Logging.EventLog.dll - - - ..\..\packages\Microsoft.Extensions.Options.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Options.dll - - - ..\..\packages\Microsoft.Extensions.Primitives.2.1.1\lib\netstandard2.0\Microsoft.Extensions.Primitives.dll - - - ..\..\packages\MSTest.TestFramework.1.2.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.dll - - - ..\..\packages\MSTest.TestFramework.1.2.1\lib\net45\Microsoft.VisualStudio.TestPlatform.TestFramework.Extensions.dll - - - ..\..\packages\Moq.4.8.2\lib\net45\Moq.dll - - - ..\..\packages\System.Buffers.4.4.0\lib\netstandard2.0\System.Buffers.dll - - - ..\..\packages\System.Diagnostics.EventLog.4.5.0\lib\net461\System.Diagnostics.EventLog.dll - - - ..\..\packages\System.Memory.4.5.1\lib\netstandard2.0\System.Memory.dll - - - ..\..\packages\System.Numerics.Vectors.4.4.0\lib\net46\System.Numerics.Vectors.dll - - - ..\..\packages\System.Runtime.CompilerServices.Unsafe.4.5.1\lib\netstandard2.0\System.Runtime.CompilerServices.Unsafe.dll - - - ..\..\packages\System.Security.AccessControl.4.5.0\lib\net461\System.Security.AccessControl.dll - - - ..\..\packages\System.Security.Permissions.4.5.0\lib\net461\System.Security.Permissions.dll - - - ..\..\packages\System.Security.Principal.Windows.4.5.0\lib\net461\System.Security.Principal.Windows.dll - - - ..\..\packages\System.Threading.Tasks.Extensions.4.3.0\lib\portable-net45+win8+wp8+wpa81\System.Threading.Tasks.Extensions.dll - - - ..\..\packages\System.ValueTuple.4.4.0\lib\net47\System.ValueTuple.dll - True - - - - {29927416-3b78-49a7-a560-5ccaa638b6b4} @@ -138,14 +64,20 @@ Tgstation.Server.Host.Watchdog + + + 2.1.1 + + + 4.10.0 + + + 1.3.2 + + + 1.3.2 + + - - - This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. - - - - - \ No newline at end of file diff --git a/tests/Tgstation.Server.Host.Service.Tests/packages.config b/tests/Tgstation.Server.Host.Service.Tests/packages.config deleted file mode 100644 index 8a479af186..0000000000 --- a/tests/Tgstation.Server.Host.Service.Tests/packages.config +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj index 6481ec2802..3226a9a749 100644 --- a/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj +++ b/tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj @@ -1,17 +1,17 @@ - netcoreapp2.0 + netcoreapp2.1 false latest - - - - + + + + diff --git a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj index d69d3cbdf8..d2102ff8cf 100644 --- a/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj +++ b/tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp2.1 false @@ -18,10 +18,10 @@ - - - - + + + + diff --git a/tests/Tgstation.Server.Tests/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs new file mode 100644 index 0000000000..248a7c4f79 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Instance/ByondTest.cs @@ -0,0 +1,69 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Threading; +using System.Threading.Tasks; +using Tgstation.Server.Client.Components; + +namespace Tgstation.Server.Tests.Instance +{ + sealed class ByondTest + { + readonly IByondClient byondClient; + readonly IJobsClient jobsClient; + + public ByondTest(IByondClient byondClient, IJobsClient jobsClient) + { + this.byondClient = byondClient ?? throw new ArgumentNullException(nameof(byondClient)); + this.jobsClient = jobsClient ?? throw new ArgumentNullException(nameof(jobsClient)); + } + + public async Task Run(CancellationToken cancellationToken) + { + await TestNoVersion(cancellationToken).ConfigureAwait(false); + await TestInstall511(cancellationToken).ConfigureAwait(false); + } + + async Task TestInstall511(CancellationToken cancellationToken) + { + var newModel = new Api.Models.Byond + { + Version = new Version(511, 1385) + }; + var test = await byondClient.SetActiveVersion(newModel, cancellationToken).ConfigureAwait(false); + Assert.IsNotNull(test.InstallJob); + Assert.IsNull(test.Version); + var job = test.InstallJob; + var maxWait = 60; //it's 10MB max give me a break + do + { + await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); + job = await jobsClient.GetId(job, cancellationToken).ConfigureAwait(false); + --maxWait; + } + while (!job.StoppedAt.HasValue && maxWait > 0); + if (!job.StoppedAt.HasValue) + { + await jobsClient.Cancel(job, cancellationToken).ConfigureAwait(false); + Assert.Fail("Byond installation job timed out!"); + } + + if (job.ExceptionDetails != null) + Assert.Fail(job.ExceptionDetails); + + var currentShit = await byondClient.ActiveVersion(cancellationToken).ConfigureAwait(false); + Assert.AreEqual(newModel.Version, currentShit.Version); + } + + async Task TestNoVersion(CancellationToken cancellationToken) + { + var allVersionsTask = byondClient.InstalledVersions(cancellationToken); + var currentShit = await byondClient.ActiveVersion(cancellationToken).ConfigureAwait(false); + Assert.IsNotNull(currentShit); + Assert.IsNull(currentShit.InstallJob); + Assert.IsNull(currentShit.Version); + var otherShit = await allVersionsTask.ConfigureAwait(false); + Assert.IsNotNull(otherShit); + Assert.AreEqual(0, otherShit.Count); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs index cd65f015f2..8ca9076a30 100644 --- a/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/InstanceTest.cs @@ -1,6 +1,4 @@ using System; -using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Client.Components; @@ -18,8 +16,12 @@ namespace Tgstation.Server.Tests.Instance public async Task RunTests(CancellationToken cancellationToken) { + var byondTests = new ByondTest(instanceClient.Byond, instanceClient.Jobs); var configTests = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata); + + var byondTest = byondTests.Run(cancellationToken); await configTests.Run(cancellationToken).ConfigureAwait(false); + await byondTest.ConfigureAwait(false); } } } diff --git a/tests/Tgstation.Server.Tests/TestingServer.cs b/tests/Tgstation.Server.Tests/TestingServer.cs index 3e30fbfa06..50a2691860 100644 --- a/tests/Tgstation.Server.Tests/TestingServer.cs +++ b/tests/Tgstation.Server.Tests/TestingServer.cs @@ -1,5 +1,6 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Threading; @@ -28,6 +29,7 @@ namespace Tgstation.Server.Tests //we have to rely on env vars var databaseType = Environment.GetEnvironmentVariable("TGS4_TEST_DATABASE_TYPE"); var connectionString = Environment.GetEnvironmentVariable("TGS4_TEST_CONNECTION_STRING"); + var gitHubAccessToken = Environment.GetEnvironmentVariable("TGS4_TEST_GITHUB_TOKEN"); if (String.IsNullOrEmpty(databaseType)) Assert.Fail("No database type configured in env var TGS4_TEST_DATABASE_TYPE!"); @@ -35,14 +37,18 @@ namespace Tgstation.Server.Tests if (String.IsNullOrEmpty(connectionString)) Assert.Fail("No connection string configured in env var TGS4_TEST_CONNECTION_STRING!"); - realServer = new ServerFactory().CreateServer(new string[] + var args = new List() { - "--urls", - Url.ToString(), + String.Format(CultureInfo.InvariantCulture, "Kestrel:EndPoints:Http:Url={0}", Url), String.Format(CultureInfo.InvariantCulture, "Database:DatabaseType={0}", databaseType), String.Format(CultureInfo.InvariantCulture, "Database:ConnectionString={0}", connectionString), "Database:DropDatabase=true" - }, null); + }; + + if (!String.IsNullOrEmpty(gitHubAccessToken)) + args.Add(String.Format(CultureInfo.InvariantCulture, "General:GitHubAccessToken={0}", gitHubAccessToken)); + + realServer = new ServerFactory().CreateServer(args.ToArray(), null); } public void Dispose() diff --git a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj index 1a89f581ca..044446ae80 100644 --- a/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj +++ b/tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj @@ -1,7 +1,7 @@ - netcoreapp2.0 + netcoreapp2.1 false @@ -15,9 +15,9 @@ - - - + + +