diff --git a/.github/workflows/ci-suite.yml b/.github/workflows/ci-suite.yml index 660e363a6d..6641c4cd5c 100644 --- a/.github/workflows/ci-suite.yml +++ b/.github/workflows/ci-suite.yml @@ -110,6 +110,13 @@ jobs: sudo apt-get update sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 libgcc-s1:i386 + - name: Restore BYOND cache + uses: actions/cache@v3 + id: cache-byond + with: + path: $HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }} + key: ${{ env.BYOND_MAJOR }}-${{ env.BYOND_MINOR }}-dmapibyond + - name: Install BYOND if: steps.cache-byond.outputs.cache-hit != 'true' run: | @@ -318,12 +325,30 @@ jobs: watchdog-type: [ 'Basic', 'System' ] configuration: [ 'Debug', 'Release' ] runs-on: windows-2019 + env: + BYOND_MAJOR: 514 + BYOND_MINOR: 1588 steps: - name: Setup dotnet uses: actions/setup-dotnet@v2 with: dotnet-version: ${{ env.TGS_DOTNET_VERSION }} + - name: Restore BYOND cache + uses: actions/cache@v3 + id: cache-byond + with: + path: $HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }} + key: ${{ env.BYOND_MAJOR }}-${{ env.BYOND_MINOR }}-livebyond + + - name: Download BYOND + if: steps.cache-byond.outputs.cache-hit != 'true' + run: | + echo "Downloading BYOND..." + mkdir -p "$HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}" + cd "$HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}" + curl "http://www.byond.com/download/build/${{ env.BYOND_MAJOR }}/${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}_byond.zip" -o byond.zip + - name: Upgrade NPM run: npm install -g npm @@ -405,7 +430,7 @@ jobs: - name: Store Code Coverage uses: actions/upload-artifact@v3 with: - name: windows-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }} + name: windows-integration-test-coverage-${{ matrix.configuration }}-${{ matrix.watchdog-type }}-${{ matrix.database-type }} path: ./TestResults/ - name: Store OpenAPI Spec @@ -423,6 +448,7 @@ jobs: cd ../Tgstation.Server.Host dotnet publish -c ${{ matrix.configuration }} --no-build -o ../../Artifacts/Service/lib/Default mv ../../Artifacts/Service/lib/Default/appsettings.yml ../../Artifacts/Service/appsettings.yml + rm ../../Artifacts/Service/lib/Default/Tgstation.Server.Host.exe - name: Store Server Service if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Basic' }} @@ -483,6 +509,9 @@ jobs: watchdog-type: [ 'Basic', 'System' ] configuration: [ 'Debug', 'Release' ] runs-on: ubuntu-latest + env: + BYOND_MAJOR: 514 + BYOND_MINOR: 1588 steps: - name: Disable ptrace_scope run: echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope @@ -493,6 +522,21 @@ jobs: sudo apt-get update sudo apt-get install -y -o APT::Immediate-Configure=0 libc6-i386 libstdc++6:i386 gdb libgcc-s1:i386 + - name: Restore BYOND cache + uses: actions/cache@v3 + id: cache-byond + with: + path: $HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }} + key: ${{ env.BYOND_MAJOR }}-${{ env.BYOND_MINOR }}-livebyond + + - name: Download BYOND + if: steps.cache-byond.outputs.cache-hit != 'true' + run: | + echo "Downloading BYOND..." + mkdir -p "$HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}" + cd "$HOME/BYOND-${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}" + curl "http://www.byond.com/download/build/${{ env.BYOND_MAJOR }}/${{ env.BYOND_MAJOR }}.${{ env.BYOND_MINOR }}_byond_linux.zip" -o byond.zip + - name: Install Node 12.X uses: actions/setup-node@v3 with: @@ -568,6 +612,7 @@ jobs: cd ../Tgstation.Server.Host dotnet publish -c ${{ matrix.configuration }}NoService --no-build -o ../../Artifacts/Console/lib/Default mv ../../Artifacts/Console/lib/Default/appsettings.yml ../../Artifacts/Console/appsettings.yml + rm ../../Artifacts/Console/lib/Default/Tgstation.Server.Host - name: Package Server Update Package if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }} @@ -575,6 +620,7 @@ jobs: cd src/Tgstation.Server.Host dotnet publish -c ${{ matrix.configuration }}NoService --no-build -o ../../Artifacts/ServerUpdate rm ../../Artifacts/ServerUpdate/appsettings.yml + rm ../../Artifacts/ServerUpdate/Tgstation.Server.Host - name: Store Server Console if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }} @@ -753,29 +799,125 @@ jobs: name: windows-unit-test-coverage-Release path: ./code_coverage/unit_tests/windows_unit_tests_release - - name: Retrieve Windows Integration Test Coverage (Debug, Basic) + - name: Retrieve Windows Integration Test Coverage (Debug, Basic, SqlServer) uses: actions/download-artifact@v3 with: - name: windows-integration-test-coverage-Debug-Basic - path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic + name: windows-integration-test-coverage-Debug-Basic-SqlServer + path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_sqlserver - - name: Retrieve Windows Integration Test Coverage (Release, Basic) + - name: Retrieve Windows Integration Test Coverage (Release, Basic, SqlServer) uses: actions/download-artifact@v3 with: - name: windows-integration-test-coverage-Release-Basic - path: ./code_coverage/integration_tests/windows_integration_tests_release_basic + name: windows-integration-test-coverage-Release-Basic-SqlServer + path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_sqlserver - - name: Retrieve Windows Integration Test Coverage (Debug, System) + - name: Retrieve Windows Integration Test Coverage (Debug, System, SqlServer) uses: actions/download-artifact@v3 with: - name: windows-integration-test-coverage-Debug-System - path: ./code_coverage/integration_tests/windows_integration_tests_debug_system + name: windows-integration-test-coverage-Debug-System-SqlServer + path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlserver - - name: Retrieve Windows Integration Test Coverage (Release, System) + - name: Retrieve Windows Integration Test Coverage (Release, System, SqlServer) uses: actions/download-artifact@v3 with: - name: windows-integration-test-coverage-Release-System - path: ./code_coverage/integration_tests/windows_integration_tests_release_system + name: windows-integration-test-coverage-Release-System-SqlServer + path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlserver + + - name: Retrieve Windows Integration Test Coverage (Debug, Basic, MariaDB) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-Basic-MariaDB + path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_mariadb + + - name: Retrieve Windows Integration Test Coverage (Release, Basic, MariaDB) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-Basic-MariaDB + path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_mariadb + + - name: Retrieve Windows Integration Test Coverage (Debug, System, MariaDB) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-System-MariaDB + path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mariadb + + - name: Retrieve Windows Integration Test Coverage (Release, System, MariaDB) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-System-MariaDB + path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mariadb + + - name: Retrieve Windows Integration Test Coverage (Debug, Basic, MySql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-Basic-MySql + path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_mysql + + - name: Retrieve Windows Integration Test Coverage (Release, Basic, MySql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-Basic-MySql + path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_mysql + + - name: Retrieve Windows Integration Test Coverage (Debug, System, MySql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-System-MySql + path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mysql + + - name: Retrieve Windows Integration Test Coverage (Release, System, MySql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-System-MySql + path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mysql + + - name: Retrieve Windows Integration Test Coverage (Debug, Basic, PostgresSql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-Basic-PostgresSql + path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_postgressql + + - name: Retrieve Windows Integration Test Coverage (Release, Basic, PostgresSql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-Basic-PostgresSql + path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_postgressql + + - name: Retrieve Windows Integration Test Coverage (Debug, System, PostgresSql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-System-PostgresSql + path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_postgressql + + - name: Retrieve Windows Integration Test Coverage (Release, System, PostgresSql) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-System-PostgresSql + path: ./code_coverage/integration_tests/windows_integration_tests_release_system_postgressql + + - name: Retrieve Windows Integration Test Coverage (Debug, Basic, Sqlite) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-Basic-Sqlite + path: ./code_coverage/integration_tests/windows_integration_tests_debug_basic_sqlite + + - name: Retrieve Windows Integration Test Coverage (Release, Basic, Sqlite) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-Basic-Sqlite + path: ./code_coverage/integration_tests/windows_integration_tests_release_basic_sqlite + + - name: Retrieve Windows Integration Test Coverage (Debug, System, Sqlite) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Debug-System-Sqlite + path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlite + + - name: Retrieve Windows Integration Test Coverage (Release, System, Sqlite) + uses: actions/download-artifact@v3 + with: + name: windows-integration-test-coverage-Release-System-Sqlite + path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlite - name: Upload Coverage to CodeCov uses: codecov/codecov-action@v3 diff --git a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs index 10222483ec..58ab6d7480 100644 --- a/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs +++ b/src/Tgstation.Server.Host/Components/Byond/IByondManager.cs @@ -4,8 +4,6 @@ using System.IO; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Byond @@ -14,7 +12,7 @@ namespace Tgstation.Server.Host.Components.Byond /// For managing the BYOND installation. /// /// When passing in s, ensure they are BYOND format versions unless referring to a custom version. This means should NEVER be 0. - public interface IByondManager : IHostedService, IDisposable + public interface IByondManager : IComponentService, IDisposable { /// /// The currently active BYOND version. diff --git a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs index 59f3e2b1c5..7004bfc592 100644 --- a/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs +++ b/src/Tgstation.Server.Host/Components/Chat/IChatManager.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Interop; @@ -13,7 +11,7 @@ namespace Tgstation.Server.Host.Components.Chat /// /// For managing connected chat services. /// - public interface IChatManager : IHostedService, IAsyncDisposable + public interface IChatManager : IComponentService, IAsyncDisposable { /// /// Registers a to use. diff --git a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs index f57222c8b2..7d57085eff 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/DmbFactory.cs @@ -90,7 +90,7 @@ namespace Tgstation.Server.Host.Components.Deployment IDmbProvider nextDmbProvider; /// - /// If the is "started" via . + /// If the is "started" via . /// bool started; diff --git a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs index 4c4872a61c..0d10b7dcd1 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/IDmbFactory.cs @@ -2,8 +2,6 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Components.Deployment @@ -11,7 +9,7 @@ namespace Tgstation.Server.Host.Components.Deployment /// /// Factory for s. /// - public interface IDmbFactory : ILatestCompileJobProvider, IHostedService, IDisposable + public interface IDmbFactory : ILatestCompileJobProvider, IComponentService, IDisposable { /// /// Get a that completes when the result of a call to will be different than the previous call if any. diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs index 8f9f24105d..d0cf3471ad 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/GitHubRemoteDeploymentManager.cs @@ -8,13 +8,13 @@ using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; + using Octokit; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Components.Deployment.Remote { @@ -29,26 +29,26 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote readonly IDatabaseContextFactory databaseContextFactory; /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubServiceFactory gitHubServiceFactory; /// /// Initializes a new instance of the class. /// /// The value of . - /// The value of . + /// The value of . /// The for the . /// The for the . public GitHubRemoteDeploymentManager( IDatabaseContextFactory databaseContextFactory, - IGitHubClientFactory gitHubClientFactory, + IGitHubServiceFactory gitHubServiceFactory, ILogger logger, Api.Models.Instance metadata) : base(logger, metadata) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); } /// @@ -74,15 +74,23 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote .FirstAsync(cancellationToken)); var instanceAuthenticated = repositorySettings.AccessToken != null; - var gitHubClient = !instanceAuthenticated - ? gitHubClientFactory.CreateClient() - : gitHubClientFactory.CreateClient(repositorySettings.AccessToken); + IAuthenticatedGitHubService authenticatedGitHubService; + IGitHubService gitHubService; + if (instanceAuthenticated) + { + authenticatedGitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken); + gitHubService = authenticatedGitHubService; + } + else + { + authenticatedGitHubService = null; + gitHubService = gitHubServiceFactory.CreateService(); + } - var repositoryTask = gitHubClient - .Repository - .Get( - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName); + var repositoryIdTask = gitHubService.GetRepositoryId( + remoteInformation.RemoteRepositoryOwner, + remoteInformation.RemoteRepositoryName, + cancellationToken); if (!repositorySettings.CreateGitHubDeployments.Value) Logger.LogTrace("Not creating deployment"); @@ -91,59 +99,45 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote else { Logger.LogTrace("Creating deployment..."); - Octokit.Deployment deployment; - try { - deployment = await gitHubClient - .Repository - .Deployment - .Create( - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, - new NewDeployment(compileJob.RevisionInformation.CommitSha) - { - AutoMerge = false, - Description = "TGS Game Deployment", - Environment = $"TGS: {Metadata.Name}", - ProductionEnvironment = true, - RequiredContexts = new Collection(), - }) - .WithToken(cancellationToken); + compileJob.GitHubDeploymentId = await authenticatedGitHubService.CreateDeployment( + new NewDeployment(compileJob.RevisionInformation.CommitSha) + { + AutoMerge = false, + Description = "TGS Game Deployment", + Environment = $"TGS: {Metadata.Name}", + ProductionEnvironment = true, + RequiredContexts = new Collection(), + }, + remoteInformation.RemoteRepositoryOwner, + remoteInformation.RemoteRepositoryName, + cancellationToken); - Logger.LogDebug("Created deployment ID {deploymentId}", deployment.Id); + Logger.LogDebug("Created deployment ID {deploymentId}", compileJob.GitHubDeploymentId); - await gitHubClient - .Repository - .Deployment - .Status - .Create( - remoteInformation.RemoteRepositoryOwner, - remoteInformation.RemoteRepositoryName, - deployment.Id, - new NewDeploymentStatus(DeploymentState.InProgress) - { - Description = "The project is being deployed", - AutoInactive = false, - }) - .WithToken(cancellationToken); + await authenticatedGitHubService.CreateDeploymentStatus( + new NewDeploymentStatus(DeploymentState.InProgress) + { + Description = "The project is being deployed", + AutoInactive = false, + }, + remoteInformation.RemoteRepositoryOwner, + remoteInformation.RemoteRepositoryName, + compileJob.GitHubDeploymentId.Value, + cancellationToken); - compileJob.GitHubDeploymentId = deployment.Id; Logger.LogTrace("In-progress deployment status created"); } catch (ApiException ex) { - Logger.LogError(ex, "Unable to create deployment!"); + Logger.LogWarning(ex, "Unable to create deployment!"); } } try { - var gitHubRepo = await repositoryTask - .WithToken(cancellationToken) - ; - - compileJob.GitHubRepoId = gitHubRepo.Id; + compileJob.GitHubRepoId = await repositoryIdTask; Logger.LogTrace("Set GitHub ID as {gitHubRepoId}", compileJob.GitHubRepoId); } catch (RateLimitExceededException ex) when (!repositorySettings.CreateGitHubDeployments.Value) @@ -206,16 +200,13 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return Array.Empty(); } - var gitHubClient = repositorySettings.AccessToken != null - ? gitHubClientFactory.CreateClient(repositorySettings.AccessToken) - : gitHubClientFactory.CreateClient(); + var gitHubService = repositorySettings.AccessToken != null + ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) + : gitHubServiceFactory.CreateService(); var tasks = revisionInformation .ActiveTestMerges - .Select(x => gitHubClient - .PullRequest - .Get(repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, x.TestMerge.Number) - .WithToken(cancellationToken)); + .Select(x => gitHubService.GetPullRequest(repository.RemoteRepositoryOwner, repository.RemoteRepositoryName, x.TestMerge.Number, cancellationToken)); try { await Task.WhenAll(tasks); @@ -260,13 +251,11 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote int testMergeNumber, CancellationToken cancellationToken) { - var gitHubClient = gitHubClientFactory.CreateClient(repositorySettings.AccessToken); + var gitHubService = gitHubServiceFactory.CreateService(repositorySettings.AccessToken); try { - await gitHubClient.Issue.Comment.Create(remoteRepositoryOwner, remoteRepositoryName, testMergeNumber, comment) - .WithToken(cancellationToken) - ; + await gitHubService.CommentOnIssue(remoteRepositoryOwner, remoteRepositoryName, comment, testMergeNumber, cancellationToken); } catch (ApiException e) { @@ -351,21 +340,16 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote return; } - var gitHubClient = gitHubClientFactory.CreateClient(gitHubAccessToken); + var gitHubService = gitHubServiceFactory.CreateService(gitHubAccessToken); - await gitHubClient - .Repository - .Deployment - .Status - .Create( - compileJob.GitHubRepoId.Value, - compileJob.GitHubDeploymentId.Value, - new NewDeploymentStatus(deploymentState) - { - Description = description, - }) - .WithToken(cancellationToken) - ; + await gitHubService.CreateDeploymentStatus( + new NewDeploymentStatus(deploymentState) + { + Description = description, + }, + compileJob.GitHubRepoId.Value, + compileJob.GitHubDeploymentId.Value, + cancellationToken); } } } diff --git a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs index 21b13b7a4c..4ba8882137 100644 --- a/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs +++ b/src/Tgstation.Server.Host/Components/Deployment/Remote/RemoteDeploymentManagerFactory.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Components.Repository; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Components.Deployment.Remote { @@ -20,7 +20,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubServiceFactory gitHubServiceFactory; /// /// The for the . @@ -41,19 +41,19 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote /// Initializes a new instance of the class. /// /// The value of . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . public RemoteDeploymentManagerFactory( IDatabaseContextFactory databaseContextFactory, - IGitHubClientFactory gitHubClientFactory, + IGitHubServiceFactory gitHubServiceFactory, IGitRemoteFeaturesFactory gitRemoteFeaturesFactory, ILoggerFactory loggerFactory, ILogger logger) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); this.gitRemoteFeaturesFactory = gitRemoteFeaturesFactory ?? throw new ArgumentNullException(nameof(gitRemoteFeaturesFactory)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -70,7 +70,7 @@ namespace Tgstation.Server.Host.Components.Deployment.Remote { RemoteGitProvider.GitHub => new GitHubRemoteDeploymentManager( databaseContextFactory, - gitHubClientFactory, + gitHubServiceFactory, loggerFactory.CreateLogger(), metadata), RemoteGitProvider.GitLab => new GitLabRemoteDeploymentManager( diff --git a/src/Tgstation.Server.Host/Components/IComponentService.cs b/src/Tgstation.Server.Host/Components/IComponentService.cs new file mode 100644 index 0000000000..b094e0a092 --- /dev/null +++ b/src/Tgstation.Server.Host/Components/IComponentService.cs @@ -0,0 +1,11 @@ +using Microsoft.Extensions.Hosting; + +namespace Tgstation.Server.Host.Components +{ + /// + /// Represents a component meant to be started and stopped by its parent component. + /// + public interface IComponentService : IHostedService + { + } +} diff --git a/src/Tgstation.Server.Host/Components/IInstance.cs b/src/Tgstation.Server.Host/Components/IInstance.cs index 1d26b9382d..9d8c34988e 100644 --- a/src/Tgstation.Server.Host/Components/IInstance.cs +++ b/src/Tgstation.Server.Host/Components/IInstance.cs @@ -1,13 +1,11 @@ using System; -using Microsoft.Extensions.Hosting; - namespace Tgstation.Server.Host.Components { /// /// Component version of . /// - interface IInstance : IInstanceCore, IHostedService, IAsyncDisposable + interface IInstance : IInstanceCore, IComponentService, IAsyncDisposable { } } diff --git a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs index 0c37b40778..fda8c9afad 100644 --- a/src/Tgstation.Server.Host/Components/IInstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/IInstanceFactory.cs @@ -1,7 +1,5 @@ using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - using Tgstation.Server.Host.Components.Interop.Bridge; using Tgstation.Server.Host.IO; @@ -10,7 +8,7 @@ namespace Tgstation.Server.Host.Components /// /// Factory for creating s. /// - interface IInstanceFactory : IHostedService + interface IInstanceFactory : IComponentService { /// /// Create an . diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index ad3ee3de51..a0c7f8ceec 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -58,9 +58,9 @@ namespace Tgstation.Server.Host.Components readonly IAssemblyInformationProvider assemblyInformationProvider; /// - /// The for the . + /// The for the . /// - readonly IJobManager jobManager; + readonly IJobService jobService; /// /// The for the . @@ -149,7 +149,7 @@ 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 . @@ -163,7 +163,7 @@ namespace Tgstation.Server.Host.Components IIOManager ioManager, IDatabaseContextFactory databaseContextFactory, IAssemblyInformationProvider assemblyInformationProvider, - IJobManager jobManager, + IJobService jobService, IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, @@ -177,7 +177,7 @@ namespace Tgstation.Server.Host.Components this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); - this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); + this.jobService = jobService ?? throw new ArgumentNullException(nameof(jobService)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); @@ -344,7 +344,7 @@ namespace Tgstation.Server.Host.Components }) .ToListAsync(cancellationToken); foreach (var job in jobs) - tasks.Add(jobManager.CancelJob(job, user, true, cancellationToken)); + tasks.Add(jobService.CancelJob(job, user, true, cancellationToken)); }); await Task.WhenAll(tasks); @@ -444,7 +444,7 @@ namespace Tgstation.Server.Host.Components } var instanceFactoryStopTask = instanceFactory.StopAsync(cancellationToken); - await jobManager.StopAsync(cancellationToken); + await jobService.StopAsync(cancellationToken); async Task OfflineInstanceImmediate(IInstance instance, CancellationToken cancellationToken) { @@ -563,7 +563,7 @@ namespace Tgstation.Server.Host.Components .ToListAsync(cancellationToken)); var factoryStartup = instanceFactory.StartAsync(cancellationToken); - var jobManagerStartup = jobManager.StartAsync(cancellationToken); + var jobManagerStartup = jobService.StartAsync(cancellationToken); await Task.WhenAll(instanceEnumeration, factoryStartup, jobManagerStartup); @@ -582,7 +582,7 @@ namespace Tgstation.Server.Host.Components await Task.WhenAll(instanceOnliningTasks); - jobManager.Activate(this); + jobService.Activate(this); logger.LogInformation("Server ready!"); readyTcs.SetResult(); diff --git a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs index 52d6c6efe4..165047dcae 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitHubRemoteFeatures.cs @@ -7,8 +7,7 @@ using Octokit; using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; -using Tgstation.Server.Host.Extensions; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Components.Repository { @@ -33,20 +32,20 @@ namespace Tgstation.Server.Host.Components.Repository public override string RemoteRepositoryName { get; } /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubServiceFactory gitHubServiceFactory; /// /// Initializes a new instance of the class. /// - /// The value of . + /// The value of . /// The for the . /// The remote repository . - public GitHubRemoteFeatures(IGitHubClientFactory gitHubClientFactory, ILogger logger, Uri remoteUrl) + public GitHubRemoteFeatures(IGitHubServiceFactory gitHubServiceFactory, ILogger logger, Uri remoteUrl) : base(logger, remoteUrl) { - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); if (remoteUrl == null) throw new ArgumentNullException(nameof(remoteUrl)); @@ -63,20 +62,16 @@ namespace Tgstation.Server.Host.Components.Repository RepositorySettings repositorySettings, CancellationToken cancellationToken) { - var gitHubClient = repositorySettings.AccessToken != null - ? gitHubClientFactory.CreateClient(repositorySettings.AccessToken) - : gitHubClientFactory.CreateClient(); + var gitHubService = repositorySettings.AccessToken != null + ? gitHubServiceFactory.CreateService(repositorySettings.AccessToken) + : gitHubServiceFactory.CreateService(); PullRequest pr = null; ApiException exception = null; string errorMessage = null; try { - pr = await gitHubClient - .PullRequest - .Get(RemoteRepositoryOwner, RemoteRepositoryName, parameters.Number) - .WithToken(cancellationToken) - ; + pr = await gitHubService.GetPullRequest(RemoteRepositoryOwner, RemoteRepositoryName, parameters.Number, cancellationToken); } catch (RateLimitExceededException ex) { diff --git a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs index d43747fe68..e19b4c22f0 100644 --- a/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/GitRemoteFeaturesFactory.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Components.Repository { @@ -11,9 +11,9 @@ namespace Tgstation.Server.Host.Components.Repository sealed class GitRemoteFeaturesFactory : IGitRemoteFeaturesFactory { /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubServiceFactory gitHubServiceFactory; /// /// The for the . @@ -28,15 +28,15 @@ namespace Tgstation.Server.Host.Components.Repository /// /// Initializes a new instance of the class. /// - /// The value of . + /// The value of . /// The value of . /// The value of . public GitRemoteFeaturesFactory( - IGitHubClientFactory gitHubClientFactory, + IGitHubServiceFactory gitHubServiceFactory, ILoggerFactory loggerFactory, ILogger logger) { - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -52,7 +52,7 @@ namespace Tgstation.Server.Host.Components.Repository return remoteGitProvider switch { RemoteGitProvider.GitHub => new GitHubRemoteFeatures( - gitHubClientFactory, + gitHubServiceFactory, loggerFactory.CreateLogger(), primaryRemote), RemoteGitProvider.GitLab => new GitLabRemoteFeatures( diff --git a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs index 55c0f7f445..65af4f9721 100644 --- a/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs +++ b/src/Tgstation.Server.Host/Components/Repository/LibGit2RepositoryFactory.cs @@ -46,7 +46,7 @@ namespace Tgstation.Server.Host.Components.Repository var repo = await Task.Factory.StartNew( () => { - logger.LogTrace("Creating libgit2 repostory at {repoPath}...", path); + logger.LogTrace("Creating libgit2 repository at {repoPath}...", path); return new LibGit2Sharp.Repository(path); }, cancellationToken, diff --git a/src/Tgstation.Server.Host/Components/Repository/Repository.cs b/src/Tgstation.Server.Host/Components/Repository/Repository.cs index caa3f4381d..6025be0cff 100644 --- a/src/Tgstation.Server.Host/Components/Repository/Repository.cs +++ b/src/Tgstation.Server.Host/Components/Repository/Repository.cs @@ -967,7 +967,8 @@ namespace Tgstation.Server.Host.Components.Repository OnPackBuilderProgress = (stage, current, total) => { var baseProgress = stage == PackBuilderStage.Counting ? 0 : 0.5; - progressReporter.ReportProgress(baseProgress + (0.5 * ((double)current / total))); + var addon = total > 0 && current <= total ? (0.5 * ((double)current / total)) : 0; + progressReporter.ReportProgress(baseProgress + addon); return !cancellationToken.IsCancellationRequested; }, OnNegotiationCompletedBeforePush = (a) => diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 4ebfd23836..798bff74f4 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using LibGit2Sharp; + using Microsoft.Extensions.Logging; using Tgstation.Server.Api.Models; @@ -142,7 +143,7 @@ namespace Tgstation.Server.Host.Components.Repository { using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) { - logger.LogTrace("Semaphore acquired"); + logger.LogTrace("Semaphore acquired for clone"); var repositoryPath = ioManager.ResolvePath(); if (!await ioManager.DirectoryExists(repositoryPath, cancellationToken)) try @@ -167,8 +168,7 @@ namespace Tgstation.Server.Host.Components.Repository url, cloneOptions, repositoryPath, - cancellationToken) - ; + cancellationToken); } catch { @@ -192,14 +192,14 @@ namespace Tgstation.Server.Host.Components.Repository return null; } } - - logger.LogInformation("Clone complete!"); } finally { + logger.LogTrace("Semaphore released after clone"); CloneInProgress = false; } + logger.LogInformation("Clone complete!"); return await LoadRepository(cancellationToken); } @@ -210,11 +210,13 @@ namespace Tgstation.Server.Host.Components.Repository lock (semaphore) if (CloneInProgress) throw new JobException(ErrorCode.RepoCloning); - await semaphore.WaitAsync(cancellationToken); + try { + await semaphore.WaitAsync(cancellationToken); try { + logger.LogTrace("Semaphore acquired for load"); var libGit2Repo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken); return new Repository( @@ -235,6 +237,7 @@ namespace Tgstation.Server.Host.Components.Repository } catch { + logger.LogTrace("Releasing semaphore as load failed"); semaphore.Release(); throw; } @@ -250,10 +253,17 @@ namespace Tgstation.Server.Host.Components.Repository public async Task DeleteRepository(CancellationToken cancellationToken) { logger.LogInformation("Deleting repository..."); - using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) + try { - logger.LogTrace("Semaphore acquired, deleting Repository directory..."); - await ioManager.DeleteDirectory(ioManager.ResolvePath(), cancellationToken); + using (await SemaphoreSlimContext.Lock(semaphore, cancellationToken)) + { + logger.LogTrace("Semaphore acquired, deleting Repository directory..."); + await ioManager.DeleteDirectory(ioManager.ResolvePath(), cancellationToken); + } + } + finally + { + logger.LogTrace("Semaphore released after delete attempt"); } } } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs index 2758ba15fc..bc8b2be171 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/IConfiguration.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Response; using Tgstation.Server.Host.Components.Events; @@ -15,7 +13,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles /// /// For managing the Configuration directory. /// - public interface IConfiguration : IHostedService, IEventConsumer, IDisposable + public interface IConfiguration : IComponentService, IEventConsumer, IDisposable { /// /// Copies all files in the CodeModifications directory to . diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 68bebfb81c..1e908eb85f 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -2,8 +2,6 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; - using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Internal; using Tgstation.Server.Host.Components.Events; @@ -14,7 +12,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// /// Runs and monitors the twin server controllers. /// - public interface IWatchdog : IHostedService, IAsyncDisposable, IEventConsumer, IRenameNotifyee + public interface IWatchdog : IComponentService, IAsyncDisposable, IEventConsumer, IRenameNotifyee { /// /// The current . diff --git a/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs index fae04d8684..406560f458 100644 --- a/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs +++ b/src/Tgstation.Server.Host/Configuration/UpdatesConfiguration.cs @@ -39,11 +39,5 @@ /// Asset package containing the new assembly in zip form. /// public string UpdatePackageAssetName { get; set; } = DefaultUpdatePackageAssetName; - - /// - /// Dump all retrieved releases from the GitHub API when a requested release is not found. - /// - /// This is an internal config and may be adjusted or removed without a version change. - public bool DumpReleasesOnNotFound { get; set; } } } diff --git a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs index b8e50b458c..61f6842e8c 100644 --- a/src/Tgstation.Server.Host/Controllers/AdministrationController.cs +++ b/src/Tgstation.Server.Host/Controllers/AdministrationController.cs @@ -19,12 +19,11 @@ using Tgstation.Server.Api.Rights; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; using Tgstation.Server.Host.Transfer; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Controllers { @@ -40,9 +39,9 @@ namespace Tgstation.Server.Host.Controllers const string OctokitException = "Bad GitHub API response, check configuration!"; /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubService gitHubService; /// /// The for the . @@ -74,11 +73,6 @@ namespace Tgstation.Server.Host.Controllers /// readonly IFileTransferTicketProvider fileTransferService; - /// - /// The for the . - /// - readonly UpdatesConfiguration updatesConfiguration; - /// /// The for the . /// @@ -89,7 +83,7 @@ namespace Tgstation.Server.Host.Controllers /// /// The for the . /// The for the . - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -97,12 +91,11 @@ namespace Tgstation.Server.Host.Controllers /// The value of . /// The value of . /// The for the . - /// The containing value of . /// The containing value of . public AdministrationController( IDatabaseContext databaseContext, IAuthenticationContextFactory authenticationContextFactory, - IGitHubClientFactory gitHubClientFactory, + IGitHubService gitHubService, IServerControl serverControl, IServerUpdateInitiator serverUpdateInitiator, IAssemblyInformationProvider assemblyInformationProvider, @@ -110,7 +103,6 @@ namespace Tgstation.Server.Host.Controllers IPlatformIdentifier platformIdentifier, IFileTransferTicketProvider fileTransferService, ILogger logger, - IOptions updatesConfigurationOptions, IOptions fileLoggingConfigurationOptions) : base( databaseContext, @@ -118,14 +110,13 @@ namespace Tgstation.Server.Host.Controllers logger, true) { - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.serverUpdateInitiator = serverUpdateInitiator ?? throw new ArgumentNullException(nameof(serverUpdateInitiator)); this.assemblyInformationProvider = assemblyInformationProvider ?? throw new ArgumentNullException(nameof(assemblyInformationProvider)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); this.fileTransferService = fileTransferService ?? throw new ArgumentNullException(nameof(fileTransferService)); - updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); fileLoggingConfiguration = fileLoggingConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(fileLoggingConfigurationOptions)); } @@ -150,26 +141,19 @@ namespace Tgstation.Server.Host.Controllers Uri repoUrl = null; try { - var gitHubClient = gitHubClientFactory.CreateClient(); - var repositoryTask = gitHubClient - .Repository - .Get(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken); - var releases = (await gitHubClient - .Repository - .Release - .GetAll(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken)) - .Where(x => x.TagName.StartsWith( - updatesConfiguration.GitTagPrefix, - StringComparison.InvariantCulture)); + var repositoryUrlTask = gitHubService.GetUpdatesRepositoryUrl(cancellationToken); + var releases = await gitHubService.GetTgsReleases(cancellationToken); - foreach (var release in releases) - if (Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version) - && version.Major > 3 // Forward/backward compatible but not before TGS4 + foreach (var kvp in releases) + { + var version = kvp.Key; + var release = kvp.Value; + if (version.Major > 3 // Forward/backward compatible but not before TGS4 && (greatestVersion == null || version > greatestVersion)) greatestVersion = version; - repoUrl = new Uri((await repositoryTask).HtmlUrl); + } + + repoUrl = await repositoryUrlTask; } catch (NotFoundException e) { diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index fa5897a9e5..4e01f9f750 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -367,13 +367,15 @@ namespace Tgstation.Server.Host.Core // configure misc services services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + services.AddFileDownloader(); services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); + + services.AddGitHub(); // configure root services - services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(x => x.GetRequiredService()); services.AddSingleton(); services.AddSingleton(x => x.GetRequiredService()); diff --git a/src/Tgstation.Server.Host/Core/ServerUpdater.cs b/src/Tgstation.Server.Host/Core/ServerUpdater.cs index 86623be571..625591e4de 100644 --- a/src/Tgstation.Server.Host/Core/ServerUpdater.cs +++ b/src/Tgstation.Server.Host/Core/ServerUpdater.cs @@ -8,10 +8,9 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Extensions; using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Swarm; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Core { @@ -19,9 +18,9 @@ namespace Tgstation.Server.Host.Core sealed class ServerUpdater : IServerUpdater, IServerUpdateExecutor { /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubService gitHubService; /// /// The for the . @@ -61,7 +60,7 @@ namespace Tgstation.Server.Host.Core /// /// Initializes a new instance of the class. /// - /// The value of . + /// The value of . /// The value of . /// The value of . /// The value of . @@ -69,7 +68,7 @@ namespace Tgstation.Server.Host.Core /// The containing the value of . /// The containing the value of . public ServerUpdater( - IGitHubClientFactory gitHubClientFactory, + IGitHubService gitHubService, IIOManager ioManager, IFileDownloader fileDownloader, IServerControl serverControl, @@ -77,7 +76,7 @@ namespace Tgstation.Server.Host.Core IOptions generalConfigurationOptions, IOptions updatesConfigurationOptions) { - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubService = gitHubService ?? throw new ArgumentNullException(nameof(gitHubService)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); @@ -224,64 +223,38 @@ namespace Tgstation.Server.Host.Core async Task BeginUpdateImpl(ISwarmService swarmService, Version newVersion, bool recursed, CancellationToken cancellationToken) { logger.LogDebug("Looking for GitHub releases version {version}...", newVersion); - var gitHubClient = gitHubClientFactory.CreateClient(); - var releases = await gitHubClient - .Repository - .Release - .GetAll(updatesConfiguration.GitHubRepositoryId) - .WithToken(cancellationToken); - logger.LogTrace("Received {releaseCount} total releases from GitHub", releases.Count); - - var filteredReleases = releases - .Where(x => x.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)) - .ToList(); - - logger.LogTrace( - "Filtered to {releaseCount} releases matching the configured tag prefix of \"{tagPrefix}\"", - filteredReleases.Count, - updatesConfiguration.GitTagPrefix); - - foreach (var release in filteredReleases) - if (Version.TryParse( - release.TagName.Replace( - updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), - out var version)) + var releases = await gitHubService.GetTgsReleases(cancellationToken); + foreach (var kvp in releases) + { + var version = kvp.Key; + var release = kvp.Value; + if (version == newVersion) { - if (version == newVersion) + var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault(); + if (asset == default) + continue; + + serverUpdateOperation = new ServerUpdateOperation { - var asset = release.Assets.Where(x => x.Name.Equals(updatesConfiguration.UpdatePackageAssetName, StringComparison.Ordinal)).FirstOrDefault(); - if (asset == default) - continue; + TargetVersion = version, + UpdateZipUrl = new Uri(asset.BrowserDownloadUrl), + SwarmService = swarmService, + }; - serverUpdateOperation = new ServerUpdateOperation - { - TargetVersion = version, - UpdateZipUrl = new Uri(asset.BrowserDownloadUrl), - SwarmService = swarmService, - }; - - try - { - if (!serverControl.TryStartUpdate(this, version)) - return ServerUpdateResult.UpdateInProgress; - } - finally - { - serverUpdateOperation = null; - } - - return ServerUpdateResult.Started; + try + { + if (!serverControl.TryStartUpdate(this, version)) + return ServerUpdateResult.UpdateInProgress; + } + finally + { + serverUpdateOperation = null; } - } - else - logger.LogDebug("Unparsable release tag: {releaseTag}", release.TagName); - if (updatesConfiguration.DumpReleasesOnNotFound) - logger.LogInformation( - "Found releases:{newline}\t{releases}", - Environment.NewLine, - String.Join($"{Environment.NewLine}\t", releases.Select(x => x.TagName).OrderBy(x => x))); + return ServerUpdateResult.Started; + } + } if (!recursed) { diff --git a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs index 4980d732d8..49087b4a50 100644 --- a/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs +++ b/src/Tgstation.Server.Host/Extensions/ServiceCollectionExtensions.cs @@ -14,7 +14,9 @@ using Serilog.Sinks.Elasticsearch; using Tgstation.Server.Host.Components.Chat.Providers; using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Extensions { @@ -26,13 +28,31 @@ namespace Tgstation.Server.Host.Extensions /// /// The implementation used in calls to . /// - static Type chatProviderFactoryType = typeof(ProviderFactory); + static Type chatProviderFactoryType; + + /// + /// The implementation used in calls to . + /// + static Type gitHubServiceFactoryType; + + /// + /// The implementation used in calls to . + /// + static Type fileDownloaderType; /// /// A for an additional to use. /// static ServiceDescriptor additionalLoggerProvider; + /// + /// Initializes static members of the class. + /// + static ServiceCollectionExtensions() + { + UseDefaultServices(); + } + /// /// Change the used as an implementation for calls to . /// @@ -42,6 +62,56 @@ namespace Tgstation.Server.Host.Extensions chatProviderFactoryType = typeof(TProviderFactory); } + /// + /// Change the used as an implementation for calls to . + /// + /// The implementation to use. + public static void UseGitHubServiceFactory() where TGitHubServiceFactory : IGitHubServiceFactory + { + gitHubServiceFactoryType = typeof(TGitHubServiceFactory); + } + + /// + /// Change the used as an implementation for calls to . + /// + /// The implementation to use. + public static void UseFileDownloader() where TFileDownloader : IFileDownloader + { + fileDownloaderType = typeof(TFileDownloader); + } + + /// + /// Adds a implementation to the given . + /// + /// The to configure. + /// . + public static IServiceCollection AddFileDownloader(this IServiceCollection serviceCollection) + { + if (serviceCollection == null) + throw new ArgumentNullException(nameof(serviceCollection)); + + serviceCollection.AddSingleton(typeof(IFileDownloader), fileDownloaderType); + + return serviceCollection; + } + + /// + /// Adds a implementation to the given . + /// + /// The to configure. + /// . + public static IServiceCollection AddGitHub(this IServiceCollection serviceCollection) + { + if (serviceCollection == null) + throw new ArgumentNullException(nameof(serviceCollection)); + + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(typeof(IGitHubServiceFactory), gitHubServiceFactoryType); + serviceCollection.AddSingleton(x => x.GetRequiredService().CreateService()); + + return serviceCollection; + } + /// /// Add an additional to s that call . /// @@ -60,6 +130,9 @@ namespace Tgstation.Server.Host.Extensions /// . public static IServiceCollection AddChatProviderFactory(this IServiceCollection serviceCollection) { + if (serviceCollection == null) + throw new ArgumentNullException(nameof(serviceCollection)); + return serviceCollection.AddSingleton(typeof(IProviderFactory), chatProviderFactoryType); } @@ -154,5 +227,15 @@ namespace Tgstation.Server.Host.Extensions if (additionalLoggerProvider != null) builder.Services.TryAddEnumerable(additionalLoggerProvider); }); + + /// + /// Set the modifiable services to their default types. + /// + static void UseDefaultServices() + { + UseChatProviderFactory(); + UseGitHubServiceFactory(); + UseFileDownloader(); + } } } diff --git a/src/Tgstation.Server.Host/Jobs/IJobManager.cs b/src/Tgstation.Server.Host/Jobs/IJobManager.cs index 30a0a45cc7..f02f765994 100644 --- a/src/Tgstation.Server.Host/Jobs/IJobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/IJobManager.cs @@ -1,9 +1,7 @@ using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Hosting; using Tgstation.Server.Api.Models.Response; -using Tgstation.Server.Host.Components; using Tgstation.Server.Host.Models; namespace Tgstation.Server.Host.Jobs @@ -11,7 +9,7 @@ namespace Tgstation.Server.Host.Jobs /// /// Manages the runtime of s. /// - public interface IJobManager : IHostedService + public interface IJobManager { /// /// Set the and for a given . @@ -47,11 +45,5 @@ namespace Tgstation.Server.Host.Jobs /// The for the operation. /// A resulting in the updated if it was cancelled, if it couldn't be found. Task CancelJob(Job job, User user, bool blocking, CancellationToken cancellationToken); - - /// - /// Activate the . - /// - /// The for the . - void Activate(IInstanceCoreProvider instanceCoreProvider); } } diff --git a/src/Tgstation.Server.Host/Jobs/IJobService.cs b/src/Tgstation.Server.Host/Jobs/IJobService.cs new file mode 100644 index 0000000000..e92ead5e6a --- /dev/null +++ b/src/Tgstation.Server.Host/Jobs/IJobService.cs @@ -0,0 +1,16 @@ +using Tgstation.Server.Host.Components; + +namespace Tgstation.Server.Host.Jobs +{ + /// + /// The service that manages everything to do with jobs. + /// + public interface IJobService : IJobManager, IComponentService + { + /// + /// Activate the . + /// + /// The for the . + void Activate(IInstanceCoreProvider instanceCoreProvider); + } +} diff --git a/src/Tgstation.Server.Host/Jobs/JobManager.cs b/src/Tgstation.Server.Host/Jobs/JobService.cs similarity index 96% rename from src/Tgstation.Server.Host/Jobs/JobManager.cs rename to src/Tgstation.Server.Host/Jobs/JobService.cs index cac6b4fab3..c0d01f2dcf 100644 --- a/src/Tgstation.Server.Host/Jobs/JobManager.cs +++ b/src/Tgstation.Server.Host/Jobs/JobService.cs @@ -17,22 +17,22 @@ using Tgstation.Server.Host.Utils; namespace Tgstation.Server.Host.Jobs { /// - sealed class JobManager : IJobManager, IDisposable + sealed class JobService : IJobService, IDisposable { /// - /// The for the . + /// The for the . /// readonly IDatabaseContextFactory databaseContextFactory; /// - /// The for the . + /// The for the . /// readonly ILoggerFactory loggerFactory; /// - /// The for the . + /// The for the . /// - readonly ILogger logger; + readonly ILogger logger; /// /// of s to running s. @@ -60,15 +60,15 @@ namespace Tgstation.Server.Host.Jobs volatile bool noMoreJobsShouldStart; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The value of . /// The value of . /// The value of . - public JobManager( + public JobService( IDatabaseContextFactory databaseContextFactory, ILoggerFactory loggerFactory, - ILogger logger) + ILogger logger) { this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); diff --git a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs index 3991fc33f9..520baf0484 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/GitHubOAuthValidator.cs @@ -8,7 +8,7 @@ using Octokit; using Tgstation.Server.Api.Models; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Security.OAuth { @@ -21,9 +21,9 @@ namespace Tgstation.Server.Host.Security.OAuth public OAuthProvider Provider => OAuthProvider.GitHub; /// - /// The for the . + /// The for the . /// - readonly IGitHubClientFactory gitHubClientFactory; + readonly IGitHubServiceFactory gitHubServiceFactory; /// /// The for the . @@ -38,15 +38,15 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Initializes a new instance of the class. /// - /// The value of . + /// The value of . /// The value of . /// The value of . public GitHubOAuthValidator( - IGitHubClientFactory gitHubClientFactory, + IGitHubServiceFactory gitHubServiceFactory, ILogger logger, OAuthConfiguration oAuthConfiguration) { - this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.gitHubServiceFactory = gitHubServiceFactory ?? throw new ArgumentNullException(nameof(gitHubServiceFactory)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); this.oAuthConfiguration = oAuthConfiguration ?? throw new ArgumentNullException(nameof(oAuthConfiguration)); } @@ -57,35 +57,21 @@ namespace Tgstation.Server.Host.Security.OAuth if (code == null) throw new ArgumentNullException(nameof(code)); - var client = gitHubClientFactory.CreateClient(); try { logger.LogTrace("Validating response code..."); - var response = await client - .Oauth - .CreateAccessToken( - new OauthTokenRequest( - oAuthConfiguration.ClientId, - oAuthConfiguration.ClientSecret, - code) - { - RedirectUri = oAuthConfiguration.RedirectUrl, - }) - ; - var token = response.AccessToken; + var gitHubService = gitHubServiceFactory.CreateService(); + var token = await gitHubService.CreateOAuthAccessToken(oAuthConfiguration, code, cancellationToken); if (token == null) return null; - var authenticatedClient = gitHubClientFactory.CreateClient(token); + var authenticatedClient = gitHubServiceFactory.CreateService(token); logger.LogTrace("Getting user details..."); - var userDetails = await authenticatedClient - .User - .Current() - ; + var userId = await authenticatedClient.GetCurrentUserId(cancellationToken); - return userDetails.Id.ToString(CultureInfo.InvariantCulture); + return userId.ToString(CultureInfo.InvariantCulture); } catch (RateLimitExceededException) { diff --git a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs index a217de3935..a7f4dc3c0a 100644 --- a/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs +++ b/src/Tgstation.Server.Host/Security/OAuth/OAuthProviders.cs @@ -10,7 +10,7 @@ using Microsoft.Extensions.Options; using Tgstation.Server.Api.Models; using Tgstation.Server.Common; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Utils; +using Tgstation.Server.Host.Utils.GitHub; namespace Tgstation.Server.Host.Security.OAuth { @@ -25,12 +25,12 @@ namespace Tgstation.Server.Host.Security.OAuth /// /// Initializes a new instance of the class. /// - /// The to use. + /// The to use. /// The to use. /// The to use. /// The containing the to use. public OAuthProviders( - IGitHubClientFactory gitHubClientFactory, + IGitHubServiceFactory gitHubServiceFactory, IAbstractHttpClientFactory httpClientFactory, ILoggerFactory loggerFactory, IOptions securityConfigurationOptions) @@ -49,7 +49,7 @@ namespace Tgstation.Server.Host.Security.OAuth if (securityConfiguration.OAuth.TryGetValue(OAuthProvider.GitHub, out var gitHubConfig)) validatorsBuilder.Add( new GitHubOAuthValidator( - gitHubClientFactory, + gitHubServiceFactory, loggerFactory.CreateLogger(), gitHubConfig)); diff --git a/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs similarity index 96% rename from src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs rename to src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs index 5bdb7091e1..5fbd22f2de 100644 --- a/src/Tgstation.Server.Host/Utils/GitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubClientFactory.cs @@ -9,7 +9,7 @@ using Octokit; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; -namespace Tgstation.Server.Host.Utils +namespace Tgstation.Server.Host.Utils.GitHub { /// sealed class GitHubClientFactory : IGitHubClientFactory @@ -67,7 +67,9 @@ namespace Tgstation.Server.Host.Utils public IGitHubClient CreateClient() => GetOrCreateClient(generalConfiguration.GitHubAccessToken); /// - public IGitHubClient CreateClient(string accessToken) => GetOrCreateClient(accessToken ?? throw new ArgumentNullException(nameof(accessToken))); + public IGitHubClient CreateClient(string accessToken) + => GetOrCreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken))); /// /// Retrieve a from the or add a new one based on a given . diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs new file mode 100644 index 0000000000..354f26f9d0 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubService.cs @@ -0,0 +1,280 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; + +using Octokit; + +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; + +namespace Tgstation.Server.Host.Utils.GitHub +{ + /// + /// Service for interacting with the GitHub API. Authenticated or otherwise. + /// + sealed class GitHubService : IAuthenticatedGitHubService + { + /// + /// The for the . + /// + readonly IGitHubClient gitHubClient; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly UpdatesConfiguration updatesConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The value of . + public GitHubService(IGitHubClient gitHubClient, ILogger logger, UpdatesConfiguration updatesConfiguration) + { + this.gitHubClient = gitHubClient ?? throw new ArgumentNullException(nameof(gitHubClient)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + this.updatesConfiguration = updatesConfiguration ?? throw new ArgumentNullException(nameof(updatesConfiguration)); + } + + /// + public async Task CreateOAuthAccessToken(OAuthConfiguration oAuthConfiguration, string code, CancellationToken cancellationToken) + { + if (oAuthConfiguration == null) + throw new ArgumentNullException(nameof(oAuthConfiguration)); + + if (code == null) + throw new ArgumentNullException(nameof(code)); + + logger.LogTrace("CreateOAuthAccessToken"); + + var response = await gitHubClient + .Oauth + .CreateAccessToken( + new OauthTokenRequest( + oAuthConfiguration.ClientId, + oAuthConfiguration.ClientSecret, + code) + { + RedirectUri = oAuthConfiguration.RedirectUrl, + }) + .WithToken(cancellationToken); + + var token = response.AccessToken; + return token; + } + + /// + public async Task> GetTgsReleases(CancellationToken cancellationToken) + { + logger.LogTrace("GetTgsReleases"); + var allReleases = await gitHubClient + .Repository + .Release + .GetAll(updatesConfiguration.GitHubRepositoryId) + .WithToken(cancellationToken); + + logger.LogTrace("{totalReleases} total releases", allReleases.Count); + var releases = allReleases + .Where(release => + { + if (!release.PublishedAt.HasValue) + { + logger.LogDebug("Release tag without PublishedAt: {releaseTag}", release.TagName); + return false; + } + + if (!release.TagName.StartsWith(updatesConfiguration.GitTagPrefix, StringComparison.InvariantCulture)) + return false; + + return true; + }) + .GroupBy(release => + { + if (!Version.TryParse(release.TagName.Replace(updatesConfiguration.GitTagPrefix, String.Empty, StringComparison.Ordinal), out var version)) + { + logger.LogDebug("Unparsable release tag: {releaseTag}", release.TagName); + return null; + } + + return version; + }) + .Where(grouping => grouping.Key != null) + + // GitHub can return the same result twice or some other nonsense + .Select(grouping => Tuple.Create(grouping.Key, grouping.OrderBy(x => x.PublishedAt.Value).First())) + .ToDictionary(tuple => tuple.Item1, tuple => tuple.Item2); + + logger.LogTrace("{parsedReleases} parsed releases", releases.Count); + return releases; + } + + /// + public async Task GetUpdatesRepositoryUrl(CancellationToken cancellationToken) + { + logger.LogTrace("GetUpdatesRepositoryUrl"); + var repository = await gitHubClient + .Repository + .Get(updatesConfiguration.GitHubRepositoryId) + .WithToken(cancellationToken); + + var repoUrl = new Uri(repository.HtmlUrl); + logger.LogTrace("Maps to {repostioryUrl}", repoUrl); + + return repoUrl; + } + + /// + public async Task GetCurrentUserId(CancellationToken cancellationToken) + { + logger.LogTrace("CreateOAuthAccessToken"); + + var userDetails = await gitHubClient.User.Current().WithToken(cancellationToken); + return userDetails.Id; + } + + /// + public Task CommentOnIssue(string repoOwner, string repoName, string comment, int issueNumber, CancellationToken cancellationToken) + { + if (repoOwner == null) + throw new ArgumentNullException(nameof(repoOwner)); + + if (repoName == null) + throw new ArgumentNullException(nameof(repoName)); + + if (comment == null) + throw new ArgumentNullException(nameof(comment)); + + logger.LogTrace("CommentOnIssue"); + + return gitHubClient + .Issue + .Comment + .Create( + repoOwner, + repoName, + issueNumber, + comment) + .WithToken(cancellationToken); + } + + /// + public async Task GetRepositoryId(string repoOwner, string repoName, CancellationToken cancellationToken) + { + if (repoOwner == null) + throw new ArgumentNullException(nameof(repoOwner)); + + if (repoName == null) + throw new ArgumentNullException(nameof(repoName)); + + logger.LogTrace("GetRepositoryId"); + + var repo = await gitHubClient + .Repository + .Get( + repoOwner, + repoName) + .WithToken(cancellationToken); + + return repo.Id; + } + + /// + public async Task CreateDeployment(NewDeployment newDeployment, string repoOwner, string repoName, CancellationToken cancellationToken) + { + if (newDeployment == null) + throw new ArgumentNullException(nameof(newDeployment)); + + if (repoOwner == null) + throw new ArgumentNullException(nameof(repoOwner)); + + if (repoName == null) + throw new ArgumentNullException(nameof(repoName)); + + logger.LogTrace("CreateDeployment"); + + var deployment = await gitHubClient + .Repository + .Deployment + .Create( + repoOwner, + repoName, + newDeployment) + .WithToken(cancellationToken); + + return deployment.Id; + } + + /// + public Task CreateDeploymentStatus(NewDeploymentStatus newDeploymentStatus, string repoOwner, string repoName, int deploymentId, CancellationToken cancellationToken) + { + if (newDeploymentStatus == null) + throw new ArgumentNullException(nameof(newDeploymentStatus)); + + if (repoOwner == null) + throw new ArgumentNullException(nameof(repoOwner)); + + if (repoName == null) + throw new ArgumentNullException(nameof(repoName)); + + logger.LogTrace("CreateDeploymentStatus"); + return gitHubClient + .Repository + .Deployment + .Status + .Create( + repoOwner, + repoName, + deploymentId, + newDeploymentStatus) + .WithToken(cancellationToken); + } + + /// + public Task CreateDeploymentStatus(NewDeploymentStatus newDeploymentStatus, long repoId, int deploymentId, CancellationToken cancellationToken) + { + if (newDeploymentStatus == null) + throw new ArgumentNullException(nameof(newDeploymentStatus)); + + logger.LogTrace("CreateDeploymentStatus"); + return gitHubClient + .Repository + .Deployment + .Status + .Create( + repoId, + deploymentId, + newDeploymentStatus) + .WithToken(cancellationToken); + } + + /// + public Task GetPullRequest(string repoOwner, string repoName, int pullRequestNumber, CancellationToken cancellationToken) + { + if (repoOwner == null) + throw new ArgumentNullException(nameof(repoOwner)); + + if (repoName == null) + throw new ArgumentNullException(nameof(repoName)); + + logger.LogTrace("GetPullRequest"); + return gitHubClient + .Repository + .PullRequest + .Get( + repoOwner, + repoName, + pullRequestNumber) + .WithToken(cancellationToken); + } + } +} diff --git a/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs new file mode 100644 index 0000000000..8ff53440a6 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/GitHub/GitHubServiceFactory.cs @@ -0,0 +1,66 @@ +using System; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Octokit; + +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Utils.GitHub +{ + /// + sealed class GitHubServiceFactory : IGitHubServiceFactory + { + /// + /// The for the . + /// + readonly IGitHubClientFactory gitHubClientFactory; + + /// + /// The for the . + /// + readonly ILoggerFactory loggerFactory; + + /// + /// The for the . + /// + readonly UpdatesConfiguration updatesConfiguration; + + /// + /// Initializes a new instance of the class. + /// + /// The value of . + /// The value of . + /// The containing value of . + public GitHubServiceFactory( + IGitHubClientFactory gitHubClientFactory, + ILoggerFactory loggerFactory, + IOptions updatesConfigurationOptions) + { + this.gitHubClientFactory = gitHubClientFactory ?? throw new ArgumentNullException(nameof(gitHubClientFactory)); + this.loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory)); + updatesConfiguration = updatesConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(updatesConfigurationOptions)); + } + + /// + public IGitHubService CreateService() => CreateServiceImpl(gitHubClientFactory.CreateClient()); + + /// + public IAuthenticatedGitHubService CreateService(string accessToken) + => CreateServiceImpl( + gitHubClientFactory.CreateClient( + accessToken ?? throw new ArgumentNullException(nameof(accessToken)))); + + /// + /// Create a . + /// + /// The for the . + /// A new . + GitHubService CreateServiceImpl(IGitHubClient gitHubClient) + => new ( + gitHubClient, + loggerFactory.CreateLogger(), + updatesConfiguration); + } +} diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IAuthenticatedGitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/IAuthenticatedGitHubService.cs new file mode 100644 index 0000000000..f920f25912 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/GitHub/IAuthenticatedGitHubService.cs @@ -0,0 +1,55 @@ +using System.Threading; +using System.Threading.Tasks; + +using Octokit; + +namespace Tgstation.Server.Host.Utils.GitHub +{ + /// + /// that exposes functions that require authentication. + /// + public interface IAuthenticatedGitHubService : IGitHubService + { + /// + /// Create a comment on a given . + /// + /// The owner of the target repository. + /// The name of the target repository. + /// The text of the comment. + /// The number of the issue to comment on. + /// The for the operation. + /// A representing the running operation. + Task CommentOnIssue(string repoOwner, string repoName, string comment, int issueNumber, CancellationToken cancellationToken); + + /// + /// Create a on a target repostiory. + /// + /// The . + /// The owner of the target repository. + /// The name of the target repository. + /// The for the operation. + /// A resulting in the new deployment's ID. + Task CreateDeployment(NewDeployment newDeployment, string repoOwner, string repoName, CancellationToken cancellationToken); + + /// + /// Create a on a target deployment. + /// + /// The . + /// The owner of the target repository. + /// The name of the target repository. + /// The ID of the parent deployment. + /// The for the operation. + /// A representing the running operation. + Task CreateDeploymentStatus(NewDeploymentStatus newDeploymentStatus, string repoOwner, string repoName, int deploymentId, CancellationToken cancellationToken); + + /// + /// Create a on a target deployment. + /// + /// The . + /// The ID of the target repository. + /// The ID of the parent deployment. + /// The for the operation. + /// A representing the running operation. + Task CreateDeploymentStatus(NewDeploymentStatus newDeploymentStatus, long repoId, int deploymentId, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Utils/IGitHubClientFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs similarity index 93% rename from src/Tgstation.Server.Host/Utils/IGitHubClientFactory.cs rename to src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs index 636796a284..ec95fd16ab 100644 --- a/src/Tgstation.Server.Host/Utils/IGitHubClientFactory.cs +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubClientFactory.cs @@ -1,6 +1,6 @@ using Octokit; -namespace Tgstation.Server.Host.Utils +namespace Tgstation.Server.Host.Utils.GitHub { /// /// For creating s. diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubService.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubService.cs new file mode 100644 index 0000000000..1776529dc1 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubService.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using Octokit; + +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Utils.GitHub +{ + /// + /// Service for interacting with the GitHub API. + /// + public interface IGitHubService + { + /// + /// Gets the of the repository designated as the updates repository. + /// + /// The for the operation. + /// A resulting in the of the designated updates repository. + Task GetUpdatesRepositoryUrl(CancellationToken cancellationToken); + + /// + /// Get all valid TGS s from the configured update source. + /// + /// The for the operation. + /// A resulting in a of TGS s keyed by their . + /// GitHub has been known to return incomplete results from the API with this call. + Task> GetTgsReleases(CancellationToken cancellationToken); + + /// + /// Attempt to get an OAuth token from a given . + /// + /// The . Must have , and set. + /// The OAuth response code. + /// The for the operation. + /// A resulting in a representing the returned OAuth code from GitHub on success, otherwise. + Task CreateOAuthAccessToken(OAuthConfiguration oAuthConfiguration, string code, CancellationToken cancellationToken); + + /// + /// Get a target repostiory's ID. + /// + /// The owner of the target repository. + /// The name of the target repository. + /// The for the operation. + /// A resulting in the target repository's ID. + Task GetRepositoryId(string repoOwner, string repoName, CancellationToken cancellationToken); + + /// + /// Get the current user's ID. + /// + /// The for the operation. + /// A resulting in the current user's ID. + Task GetCurrentUserId(CancellationToken cancellationToken); + + /// + /// Get a given . + /// + /// The owner of the target repository. + /// The name of the target repository. + /// The target . + /// The for the operation. + /// A resulting in the target . + Task GetPullRequest(string repoOwner, string repoName, int pullRequestNumber, CancellationToken cancellationToken); + } +} diff --git a/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs new file mode 100644 index 0000000000..f4e7be70f5 --- /dev/null +++ b/src/Tgstation.Server.Host/Utils/GitHub/IGitHubServiceFactory.cs @@ -0,0 +1,21 @@ +namespace Tgstation.Server.Host.Utils.GitHub +{ + /// + /// Factory for s. + /// + public interface IGitHubServiceFactory + { + /// + /// Create a . + /// + /// A new . + public IGitHubService CreateService(); + + /// + /// Create an . + /// + /// The access token to use for communication with GitHub. + /// A new . + public IAuthenticatedGitHubService CreateService(string accessToken); + } +} diff --git a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs index cdc30c888e..7389acbc81 100644 --- a/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs +++ b/tests/Tgstation.Server.Host.Tests/Components/Chat/Providers/TestDiscordProvider.cs @@ -18,11 +18,11 @@ namespace Tgstation.Server.Host.Components.Chat.Providers.Tests [TestClass] public sealed class TestDiscordProvider { - ChatBot testToken1; - IJobManager mockJobManager; + static ChatBot testToken1; + static IJobManager mockJobManager; - [TestInitialize] - public void Initialize() + [ClassInitialize] + public static void Initialize(TestContext _) { var actualToken = Environment.GetEnvironmentVariable("TGS_TEST_DISCORD_TOKEN"); if (!String.IsNullOrWhiteSpace(actualToken)) diff --git a/tests/Tgstation.Server.Host.Tests/IO/TestSymlinkFactory.cs b/tests/Tgstation.Server.Host.Tests/IO/TestSymlinkFactory.cs index fd5dd76591..27646a9dc7 100644 --- a/tests/Tgstation.Server.Host.Tests/IO/TestSymlinkFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/IO/TestSymlinkFactory.cs @@ -10,10 +10,10 @@ namespace Tgstation.Server.Host.IO.Tests [TestClass] public sealed class TestSymlinkFactory { - ISymlinkFactory symlinkFactory; + static ISymlinkFactory symlinkFactory; - [TestInitialize] - public void SelectFactory() + [ClassInitialize] + public static void SelectFactory(TestContext _) { if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) symlinkFactory = new WindowsSymlinkFactory(); diff --git a/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs b/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs index 63d348080a..722d967521 100644 --- a/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs +++ b/tests/Tgstation.Server.Host.Tests/Swarm/TestSwarmProtocol.cs @@ -13,12 +13,12 @@ namespace Tgstation.Server.Host.Swarm.Tests [TestClass] public sealed class TestSwarmProtocol { - readonly HashSet usedPorts = new (); - ILoggerFactory loggerFactory; - ILogger logger; + static readonly HashSet usedPorts = new (); + static ILoggerFactory loggerFactory; + static ILogger logger; - [TestInitialize] - public void Initialize() + [ClassInitialize] + public static void Initialize(TestContext _) { loggerFactory = LoggerFactory.Create(builder => { @@ -29,8 +29,8 @@ namespace Tgstation.Server.Host.Swarm.Tests logger = loggerFactory.CreateLogger(); } - [TestCleanup] - public void Shutdown() + [ClassCleanup] + public static void Shutdown() { usedPorts.Clear(); loggerFactory.Dispose(); diff --git a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs index f6f680c50d..066370977c 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs @@ -15,10 +15,10 @@ namespace Tgstation.Server.Host.System.Tests [TestClass] public sealed class TestProcessFeatures { - IProcessFeatures features; + static IProcessFeatures features; - [TestInitialize] - public void Init() + [ClassInitialize] + public static void Init(TestContext _) { features = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of>()) diff --git a/tests/Tgstation.Server.Host.Tests/Utils/TestGitHubClientFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs similarity index 94% rename from tests/Tgstation.Server.Host.Tests/Utils/TestGitHubClientFactory.cs rename to tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs index 5d7be76cf3..6f4fac0ae2 100644 --- a/tests/Tgstation.Server.Host.Tests/Utils/TestGitHubClientFactory.cs +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubClientFactory.cs @@ -13,15 +13,15 @@ using Octokit; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.System; -namespace Tgstation.Server.Host.Utils.Tests +namespace Tgstation.Server.Host.Utils.GitHub.Tests { [TestClass] public sealed class TestGitHubClientFactory { - ILoggerFactory loggerFactory; + static ILoggerFactory loggerFactory; - [TestInitialize] - public void Initialize() + [ClassInitialize] + public static void Initialize(TestContext _) { loggerFactory = LoggerFactory.Create(builder => { @@ -30,8 +30,8 @@ namespace Tgstation.Server.Host.Utils.Tests }); } - [TestCleanup] - public void Cleanup() + [ClassCleanup] + public static void Cleanup() { loggerFactory.Dispose(); } diff --git a/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs new file mode 100644 index 0000000000..daa9afdd4c --- /dev/null +++ b/tests/Tgstation.Server.Host.Tests/Utils/GitHub/TestGitHubServiceFactory.cs @@ -0,0 +1,57 @@ +using System; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +using Octokit; + +using Tgstation.Server.Host.Configuration; + +namespace Tgstation.Server.Host.Utils.GitHub.Tests +{ + [TestClass] + public sealed class TestGitHubServiceFactory + { + [TestMethod] + public void TestConstructor() + { + Assert.ThrowsException(() => new GitHubServiceFactory(null, null, null)); + Assert.ThrowsException(() => new GitHubServiceFactory(Mock.Of(), null, null)); + Assert.ThrowsException(() => new GitHubServiceFactory(Mock.Of(), Mock.Of(), null)); + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration()); + + _ = new GitHubServiceFactory(Mock.Of(), Mock.Of(), mockOptions.Object); + } + + [TestMethod] + public void TestCreateService() + { + var mockFactory = new Mock(); + + mockFactory.Setup(x => x.CreateClient()).Returns(Mock.Of()).Verifiable(); + + var mockToken = "asdf"; + mockFactory.Setup(x => x.CreateClient(mockToken)).Returns(Mock.Of()).Verifiable(); + + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.Value).Returns(new UpdatesConfiguration()); + + var factory = new GitHubServiceFactory(mockFactory.Object, Mock.Of(), mockOptions.Object); + + Assert.ThrowsException(() => factory.CreateService(null)); + Assert.AreEqual(0, mockFactory.Invocations.Count); + + var result1 = factory.CreateService(); + Assert.IsNotNull(result1); + + var result2 = factory.CreateService(mockToken); + Assert.IsNotNull(result2); + + mockFactory.VerifyAll(); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/CachingFileDownloader.cs b/tests/Tgstation.Server.Tests/Live/CachingFileDownloader.cs new file mode 100644 index 0000000000..41628f16e9 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/CachingFileDownloader.cs @@ -0,0 +1,153 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Tgstation.Server.Common; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.System; +using Tgstation.Server.Tests.Live.Instance; + +namespace Tgstation.Server.Tests.Live +{ + sealed class CachingFileDownloader : IFileDownloader + { + static readonly Dictionary> cachedPaths = new Dictionary>(); + + readonly ILogger logger; + + public CachingFileDownloader(ILogger logger) + { + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + logger.LogTrace("Created"); + } + + public static async Task InitializeAndInject(CancellationToken cancellationToken) + { + if (LiveTestUtils.RunningInGitHubActions) + { + // actions caches BYOND for us + var url = new Uri( + $"https://secure.byond.com/download/build/{ByondTest.TestVersion.Major}/{ByondTest.TestVersion.Major}.{ByondTest.TestVersion.Minor}_byond{(!new PlatformIdentifier().IsWindows ? "_linux" : String.Empty)}.zip"); + + var path = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), + $"BYOND-{ByondTest.TestVersion.Major}.{ByondTest.TestVersion.Minor}", + "byond.zip"); + Assert.IsTrue(File.Exists(path)); + System.Console.WriteLine($"CACHE PREWARMED: {url}"); + cachedPaths.Add(url.ToString(), Tuple.Create(path, false)); + } + + // predownload the target github release update asset + var gitHubToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"); + if (String.IsNullOrWhiteSpace(gitHubToken)) + gitHubToken = null; + + // this can fail, try a few times + var succeeded = false; + using var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + builder.SetMinimumLevel(LogLevel.Trace); + }); + var logger = loggerFactory.CreateLogger("CachingFileDownloader"); + for (var i = 0; i < 10; ++i) + try + { + var url = new Uri($"https://github.com/tgstation/tgstation-server/releases/download/tgstation-server-v{TestLiveServer.TestUpdateVersion}/ServerUpdatePackage.zip"); + await using var stream = await CacheFile(logger, url, gitHubToken, !LiveTestUtils.RunningInGitHubActions, cancellationToken); + succeeded = true; + break; + } + catch (Exception ex) + { + logger.LogWarning(ex, $"TEST: FAILED TO CACHE GITHUB RELEASE."); + } + + Assert.IsTrue(succeeded); + + ServiceCollectionExtensions.UseFileDownloader(); + } + + public static void Cleanup() + { + lock (cachedPaths) + { + foreach (var pathAndDelete in cachedPaths.Values) + if (pathAndDelete.Item2) + try + { + File.Delete(pathAndDelete.Item1); + } + catch + { + } + + cachedPaths.Clear(); + } + } + + public async Task DownloadFile(Uri url, string bearerToken, CancellationToken cancellationToken) + { + Tuple tuple; + lock (cachedPaths) + if (!cachedPaths.TryGetValue(url.ToString(), out tuple)) + { + logger.LogInformation("Cache miss: {url}", url); + tuple = null; + } + + if (tuple == null) + return await CacheFile(logger, url, bearerToken, true, cancellationToken); + + logger.LogTrace("Cache hit: {url}", url); + var bytes = await new DefaultIOManager().ReadAllBytes(tuple.Item1, cancellationToken); + return new MemoryStream(bytes); + } + + static async Task CacheFile(ILogger logger, Uri url, string bearerToken, bool temporal, CancellationToken cancellationToken) + { + var downloader = new FileDownloader( + new HttpClientFactory( + new AssemblyInformationProvider().ProductInfoHeaderValue), + new Logger( + LiveTestUtils.CreateLoggerFactoryForLogger( + logger, + out _))); + var download = await downloader.DownloadFile(url, bearerToken, cancellationToken); + try + { + var path = Path.GetTempFileName(); + try + { + await using var fs = new DefaultIOManager().CreateAsyncSequentialWriteStream(path); + await download.CopyToAsync(fs, cancellationToken); + + lock (cachedPaths) + cachedPaths.Add(url.ToString(), Tuple.Create(path, temporal)); + + logger.LogTrace("Cached to {path}", path); + } + catch + { + File.Delete(path); + throw; + } + + download.Seek(0, SeekOrigin.Begin); + return download; + } + catch + { + await download.DisposeAsync(); + throw; + } + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs index 972e7acc75..e8478c794b 100644 --- a/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs +++ b/tests/Tgstation.Server.Tests/Live/DummyChatProvider.cs @@ -43,21 +43,6 @@ namespace Tgstation.Server.Tests.Live ulong channelIdAllocator; - public static ILoggerFactory CreateLoggerFactoryForLogger(ILogger logger, out Mock mockLoggerFactory) - { - mockLoggerFactory = new Mock(); - mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(() => - { - var temp = logger; - logger = null; - - Assert.IsNotNull(temp); - return temp; - }) - .Verifiable(); - return mockLoggerFactory.Object; - } - static IAsyncDelayer CreateMockDelayer() { // at time of writing, this is used exclusively for the reconnection interval which works in minutes @@ -79,7 +64,7 @@ namespace Tgstation.Server.Tests.Live ICryptographySuite cryptographySuite, IReadOnlyCollection commands, Random random) - : base(jobManager, CreateMockDelayer(), new Logger(CreateLoggerFactoryForLogger(logger, out var mockLoggerFactory)), chatBot) + : base(jobManager, CreateMockDelayer(), new Logger(LiveTestUtils.CreateLoggerFactoryForLogger(logger, out var mockLoggerFactory)), chatBot) { mockLoggerFactory.VerifyAll(); this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs b/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs new file mode 100644 index 0000000000..c1bcd2903e --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/DummyGitHubService.cs @@ -0,0 +1,134 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +using Moq; + +using Octokit; + +using Tgstation.Server.Host.Configuration; +using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.System; +using Tgstation.Server.Host.Utils.GitHub; + +namespace Tgstation.Server.Tests.Live +{ + sealed class DummyGitHubService : IAuthenticatedGitHubService + { + static Dictionary releasesDictionary; + static PullRequest testPr; + + readonly ICryptographySuite cryptographySuite; + readonly ILogger logger; + + public static async Task InitializeAndInject(CancellationToken cancellationToken) + { + var mockOptions = new Mock>(); + mockOptions.SetupGet(x => x.Value).Returns(new GeneralConfiguration + { + GitHubAccessToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN") + }); + + var gitHubClientFactory = new GitHubClientFactory(new AssemblyInformationProvider(), Mock.Of>(), mockOptions.Object); + var gitHubClient = gitHubClientFactory.CreateClient(); + + Release targetRelease; + do + { + var releases = await gitHubClient + .Repository + .Release + .GetAll("tgstation", "tgstation-server") + .WithToken(cancellationToken); + + targetRelease = releases.FirstOrDefault(release => release.TagName == $"{new UpdatesConfiguration().GitTagPrefix}{TestLiveServer.TestUpdateVersion}"); + } + while (targetRelease == null); + + releasesDictionary = new Dictionary + { + { TestLiveServer.TestUpdateVersion, targetRelease } + }; + + testPr = await gitHubClient + .PullRequest + .Get("Cyberboss", "common_core", 2) + .WithToken(cancellationToken); + + ServiceCollectionExtensions.UseGitHubServiceFactory(); + } + + public DummyGitHubService(ICryptographySuite cryptographySuite, ILogger logger) + { + this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + logger.LogTrace("Created"); + } + + public Task CommentOnIssue(string repoOwner, string repoName, string comment, int issueNumber, CancellationToken cancellationToken) + { + logger.LogTrace("CommentOnIssue"); + return Task.CompletedTask; + } + + public Task CreateDeployment(NewDeployment newDeployment, string repoOwner, string repoName, CancellationToken cancellationToken) + { + logger.LogTrace("CreateDeployment"); + return Task.FromResult(new Random().Next()); ; + } + + public Task CreateDeploymentStatus(NewDeploymentStatus newDeploymentStatus, string repoOwner, string repoName, int deploymentId, CancellationToken cancellationToken) + { + logger.LogTrace("CreateDeploymentStatus"); + return Task.CompletedTask; + } + + public Task CreateDeploymentStatus(NewDeploymentStatus newDeploymentStatus, long repoId, int deploymentId, CancellationToken cancellationToken) + { + logger.LogTrace("CreateDeploymentStatus"); + return Task.CompletedTask; + } + + public Task CreateOAuthAccessToken(OAuthConfiguration oAuthConfiguration, string code, CancellationToken cancellationToken) + { + logger.LogTrace("CreateOAuthAccessToken"); + return Task.FromResult(cryptographySuite.GetSecureString()); + } + + public Task GetCurrentUserId(CancellationToken cancellationToken) + { + logger.LogTrace("GetCurrentUserId"); + return Task.FromResult(new Random().Next()); + } + + public Task GetRepositoryId(string repoOwner, string repoName, CancellationToken cancellationToken) + { + logger.LogTrace("GetRepositoryId"); + return Task.FromResult(new Random().NextInt64()); + } + + public Task GetUpdatesRepositoryUrl(CancellationToken cancellationToken) + { + logger.LogTrace("GetUpdatesRepositoryUrl"); + return Task.FromResult(new Uri("https://github.com/tgstation/tgstation-server")); + } + + public Task GetPullRequest(string repoOwner, string repoName, int pullRequestNumber, CancellationToken cancellationToken) + { + logger.LogTrace("GetPullRequest"); + return Task.FromResult(testPr); + } + + public Task> GetTgsReleases(CancellationToken cancellationToken) + { + logger.LogTrace("GetTgsReleases"); + return Task.FromResult(releasesDictionary); + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs new file mode 100644 index 0000000000..59bef5fdd0 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/DummyGitHubServiceFactory.cs @@ -0,0 +1,33 @@ +using System; + +using Microsoft.Extensions.Logging; + +using Tgstation.Server.Host.Security; +using Tgstation.Server.Host.Utils.GitHub; + +namespace Tgstation.Server.Tests.Live +{ + sealed class DummyGitHubServiceFactory : IGitHubServiceFactory + { + readonly ICryptographySuite cryptographySuite; + readonly ILogger logger; + + public DummyGitHubServiceFactory(ICryptographySuite cryptographySuite, ILogger logger) + { + this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public IGitHubService CreateService() => CreateDummyService(); + + public IAuthenticatedGitHubService CreateService(string accessToken) + { + if (accessToken == null) + throw new ArgumentNullException(nameof(accessToken)); + + return CreateDummyService(); + } + + DummyGitHubService CreateDummyService() => new DummyGitHubService(cryptographySuite, logger); + } +} diff --git a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs index 8eac19aaf9..68e89c0f9f 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/ByondTest.cs @@ -14,7 +14,6 @@ using Tgstation.Server.Api.Models; using Tgstation.Server.Api.Models.Request; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; -using Tgstation.Server.Common; using Tgstation.Server.Host.Components.Byond; using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.IO; @@ -27,13 +26,15 @@ namespace Tgstation.Server.Tests.Live.Instance public static readonly Version TestVersion = new(514, 1588); readonly IByondClient byondClient; + readonly IFileDownloader fileDownloader; readonly Api.Models.Instance metadata; - public ByondTest(IByondClient byondClient, IJobsClient jobsClient, Api.Models.Instance metadata) + public ByondTest(IByondClient byondClient, IJobsClient jobsClient, IFileDownloader fileDownloader, Api.Models.Instance metadata) : base(jobsClient) { this.byondClient = byondClient ?? throw new ArgumentNullException(nameof(byondClient)); + this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); this.metadata = metadata ?? throw new ArgumentNullException(nameof(metadata)); } @@ -159,9 +160,6 @@ namespace Tgstation.Server.Tests.Live.Instance generalConfigOptionsMock.SetupGet(x => x.Value).Returns(new GeneralConfiguration()); var assemblyInformationProvider = new AssemblyInformationProvider(); - var fileDownloader = new FileDownloader( - new HttpClientFactory(assemblyInformationProvider.ProductInfoHeaderValue), - Mock.Of>()); IByondInstaller byondInstaller = new PlatformIdentifier().IsWindows ? new WindowsByondInstaller( diff --git a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs index 0eecae4997..63b8798223 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/InstanceTest.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Tgstation.Server.Client; using Tgstation.Server.Client.Components; using Tgstation.Server.Host.Components; +using Tgstation.Server.Host.IO; namespace Tgstation.Server.Tests.Live.Instance { @@ -12,20 +13,22 @@ namespace Tgstation.Server.Tests.Live.Instance { readonly IInstanceClient instanceClient; readonly IInstanceManagerClient instanceManagerClient; + readonly IFileDownloader fileDownloader; readonly InstanceManager instanceManager; readonly ushort serverPort; - public InstanceTest(IInstanceClient instanceClient, IInstanceManagerClient instanceManagerClient, InstanceManager instanceManager, ushort serverPort) + public InstanceTest(IInstanceClient instanceClient, IInstanceManagerClient instanceManagerClient, IFileDownloader fileDownloader, InstanceManager instanceManager, ushort serverPort) { this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient)); this.instanceManagerClient = instanceManagerClient ?? throw new ArgumentNullException(nameof(instanceManagerClient)); + this.fileDownloader = fileDownloader ?? throw new ArgumentNullException(nameof(fileDownloader)); this.instanceManager = instanceManager ?? throw new ArgumentNullException(nameof(instanceManager)); this.serverPort = serverPort; } public async Task RunTests(CancellationToken cancellationToken, bool highPrioDD, bool lowPrioDeployment) { - var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, instanceClient.Metadata); + var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, fileDownloader, instanceClient.Metadata); var chatTest = new ChatTest(instanceClient.ChatBots, instanceManagerClient, instanceClient.Jobs, instanceClient.Metadata); var configTest = new ConfigurationTest(instanceClient.Configuration, instanceClient.Metadata); var repoTest = new RepositoryTest(instanceClient.Repository, instanceClient.Jobs); @@ -34,7 +37,7 @@ namespace Tgstation.Server.Tests.Live.Instance var byondTask = byondTest.Run(cancellationToken, out var firstInstall); var chatTask = chatTest.RunPreWatchdog(cancellationToken); - var repoLongJob = repoTest.RunLongClone(cancellationToken); + var repoLongJob = await repoTest.RunLongClone(cancellationToken); await dmTest.RunPreRepoClone(cancellationToken); await repoTest.AbortLongCloneAndCloneSomethingQuick(repoLongJob, cancellationToken); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs index d93c50e3d2..c35ccd70dd 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/RepositoryTest.cs @@ -23,7 +23,7 @@ namespace Tgstation.Server.Tests.Live.Instance this.repositoryClient = repositoryClient ?? throw new ArgumentNullException(nameof(repositoryClient)); } - public async Task RunLongClone(CancellationToken cancellationToken) + public async Task> RunLongClone(CancellationToken cancellationToken) { var workingBranch = "master"; @@ -35,24 +35,40 @@ namespace Tgstation.Server.Tests.Live.Instance }; var clone = await repositoryClient.Clone(cloneRequest, cancellationToken); - await ApiAssert.ThrowsException(() => repositoryClient.Read(cancellationToken), ErrorCode.RepoCloning); - Assert.IsNotNull(clone); - Assert.AreEqual(cloneRequest.Origin, clone.Origin); - Assert.AreEqual(workingBranch, clone.Reference); - Assert.IsNull(clone.RevisionInformation); - Assert.IsNotNull(clone.ActiveJob); - // throwing this small jobs consistency test in here - await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); - var activeJobs = await JobsClient.ListActive(null, cancellationToken); - var allJobs = await JobsClient.List(null, cancellationToken); + return Rest(); - Assert.IsTrue(activeJobs.Any(x => x.Id == clone.ActiveJob.Id)); - Assert.IsTrue(allJobs.Any(x => x.Id == clone.ActiveJob.Id)); - Assert.IsTrue(activeJobs.First(x => x.Id == clone.ActiveJob.Id).Progress.HasValue); - Assert.IsTrue(allJobs.First(x => x.Id == clone.ActiveJob.Id).Progress.HasValue); + async Task Rest() + { + await ApiAssert.ThrowsException(() => repositoryClient.Read(cancellationToken), ErrorCode.RepoCloning); + Assert.IsNotNull(clone); + Assert.AreEqual(cloneRequest.Origin, clone.Origin); + Assert.AreEqual(workingBranch, clone.Reference); + Assert.IsNull(clone.RevisionInformation); + Assert.IsNotNull(clone.ActiveJob); - return clone.ActiveJob; + // throwing this small jobs consistency test in here + await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); + var activeJobs = await JobsClient.ListActive(null, cancellationToken); + var allJobs = await JobsClient.List(null, cancellationToken); + + Assert.IsTrue(activeJobs.Any(x => x.Id == clone.ActiveJob.Id)); + Assert.IsTrue(allJobs.Any(x => x.Id == clone.ActiveJob.Id)); + + var targetActiveJob = activeJobs.First(x => x.Id == clone.ActiveJob.Id); + + if (!targetActiveJob.Progress.HasValue) + { + // give it 15 more seconds + targetActiveJob = await WaitForJobProgress(targetActiveJob, 15, cancellationToken); + allJobs = await JobsClient.List(null, cancellationToken); + } + + Assert.IsTrue(targetActiveJob.Progress.HasValue); + Assert.IsTrue(allJobs.First(x => x.Id == clone.ActiveJob.Id).Progress.HasValue); + + return clone.ActiveJob; + } } public async Task AbortLongCloneAndCloneSomethingQuick(Task longCloneJob, CancellationToken cancellationToken) @@ -70,7 +86,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(secondRead); Assert.IsNull(secondRead.ActiveJob); - const string Origin = "https://github.com/tgstation/common_core"; + const string Origin = "https://github.com/Cyberboss/common_core"; var cloneRequest = new RepositoryCreateRequest { Origin = new Uri(Origin), @@ -108,7 +124,20 @@ namespace Tgstation.Server.Tests.Live.Instance // Back updated = await Checkout(new RepositoryUpdateRequest { Reference = "master" }, false, true, cancellationToken); - var prNumber = 37; + // enable the good shit if possible + if (LiveTestUtils.RunningInGitHubActions + || String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN")) + || Environment.MachineName.Equals("CYBERSTATIONXVI", StringComparison.OrdinalIgnoreCase)) + await repositoryClient.Update(new RepositoryUpdateRequest + { + CreateGitHubDeployments = true, + PostTestMergeComment = true, + PushTestMergeCommits = true, + AccessUser = "Cyberboss", + AccessToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"), + }, cancellationToken); + + var prNumber = 2; await TestMergeTests(updated, prNumber, cancellationToken); } @@ -164,7 +193,7 @@ namespace Tgstation.Server.Tests.Live.Instance Assert.IsNotNull(withMerge.RevisionInformation.PrimaryTestMerge.TitleAtMerge); Assert.IsNotNull(withMerge.RevisionInformation.PrimaryTestMerge.BodyAtMerge); if (withMerge.RevisionInformation.PrimaryTestMerge.Url != "GITHUB API ERROR: RATE LIMITED") - Assert.AreEqual($"https://github.com/tgstation/common_core/pull/{prNumber}", withMerge.RevisionInformation.PrimaryTestMerge.Url); + Assert.AreEqual($"https://github.com/Cyberboss/common_core/pull/{prNumber}", withMerge.RevisionInformation.PrimaryTestMerge.Url); Assert.AreEqual(orignCommit, withMerge.RevisionInformation.OriginCommitSha); Assert.AreNotEqual(orignCommit, withMerge.RevisionInformation.CommitSha); diff --git a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs index f827548214..eb5c6946cc 100644 --- a/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Live/Instance/WatchdogTest.cs @@ -582,7 +582,7 @@ namespace Tgstation.Server.Tests.Live.Instance while (!cancellationToken.IsCancellationRequested) { var currentSize = baseSize + (int)Math.Pow(2, nextPow); - var topicRequestResult = await TopicClient.SendTopic( + var topicRequestResult = await TopicClientNoLogger.SendTopic( IPAddress.Loopback, $"tgs_integration_test_tactics4={TopicClient.SanitizeString(currentSize.ToString())}", TestLiveServer.DDPort, @@ -935,7 +935,7 @@ namespace Tgstation.Server.Tests.Live.Instance ReceiveTimeout = TimeSpan.FromSeconds(30), ConnectTimeout = TimeSpan.FromSeconds(30), DisconnectTimeout = TimeSpan.FromSeconds(30) - }, new Logger(DummyChatProvider.CreateLoggerFactoryForLogger(loggerFactory.CreateLogger("WatchdogTest.TopicClient"), out var mockLoggerFactory))); + }, new Logger(LiveTestUtils.CreateLoggerFactoryForLogger(loggerFactory.CreateLogger("WatchdogTest.TopicClient"), out var mockLoggerFactory))); public static readonly TopicClient TopicClientNoLogger = new(new SocketParameters { diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestUtils.cs b/tests/Tgstation.Server.Tests/Live/LiveTestUtils.cs new file mode 100644 index 0000000000..b2e21c6353 --- /dev/null +++ b/tests/Tgstation.Server.Tests/Live/LiveTestUtils.cs @@ -0,0 +1,29 @@ +using System; + +using Microsoft.Extensions.Logging; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +using Moq; + +namespace Tgstation.Server.Tests.Live +{ + static class LiveTestUtils + { + public static bool RunningInGitHubActions { get; } = !String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_RUN_ID")); + + public static ILoggerFactory CreateLoggerFactoryForLogger(ILogger logger, out Mock mockLoggerFactory) + { + mockLoggerFactory = new Mock(); + mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(() => + { + var temp = logger; + logger = null; + + Assert.IsNotNull(temp); + return temp; + }) + .Verifiable(); + return mockLoggerFactory.Object; + } + } +} diff --git a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs index 6a66269efb..66e0d46e90 100644 --- a/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs +++ b/tests/Tgstation.Server.Tests/Live/LiveTestingServer.cs @@ -49,7 +49,7 @@ namespace Tgstation.Server.Tests.Live SerilogContextHelper.AddSwarmNodeIdentifierToTemplate(); } - public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth, ushort port = 5010, bool dumpOnMissingUpdate = true) + public LiveTestingServer(SwarmConfiguration swarmConfiguration, bool enableOAuth, ushort port = 5010) { Directory = Environment.GetEnvironmentVariable("TGS_TEST_TEMP_DIRECTORY"); if (string.IsNullOrWhiteSpace(Directory)) @@ -89,9 +89,8 @@ namespace Tgstation.Server.Tests.Live // neither of these should really matter but it's better that we test them // high prio DD might help with some topic flakiness actually // github doesn't allow nicing on linux though - var runningInGitHubActions = !String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("GITHUB_RUN_ID")); var windows = new Host.System.PlatformIdentifier().IsWindows; - var nicingAllowed = windows || !runningInGitHubActions; + var nicingAllowed = windows || !LiveTestUtils.RunningInGitHubActions; HighPriorityDreamDaemon = nicingAllowed; LowPriorityDeployments = nicingAllowed; @@ -116,9 +115,6 @@ namespace Tgstation.Server.Tests.Live $"Session:LowPriorityDeploymentProcesses={LowPriorityDeployments}", }; - if (dumpOnMissingUpdate) - args.Add("Updates:DumpReleasesOnNotFound=true"); - swarmArgs = new List(); if (swarmConfiguration != null) { diff --git a/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs b/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs index c45c12dbf8..9434730bc1 100644 --- a/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs +++ b/tests/Tgstation.Server.Tests/Live/RateLimitRetryingApiClient.cs @@ -3,6 +3,8 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; + using Tgstation.Server.Api; using Tgstation.Server.Client; using Tgstation.Server.Common; @@ -29,6 +31,8 @@ namespace Tgstation.Server.Tests.Live var now = DateTimeOffset.UtcNow; Console.WriteLine($"TEST ERROR RATE LIMITED: {ex}"); + if (!LiveTestUtils.RunningInGitHubActions) + Assert.Inconclusive("Rate limited by GitHub!"); var sleepTime = ex.RetryAfter.Value - now; Console.WriteLine($"Sleeping for {sleepTime.TotalMinutes} minutes and retrying..."); diff --git a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs index 5b10b5b6d6..bace1d01f8 100644 --- a/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs +++ b/tests/Tgstation.Server.Tests/Live/TestLiveServer.cs @@ -44,7 +44,7 @@ namespace Tgstation.Server.Tests.Live public static ushort DDPort { get; } = FreeTcpPort(); public static ushort DMPort { get; } = GetDMPort(); - readonly Version TestUpdateVersion = new(5, 11, 0); + public static readonly Version TestUpdateVersion = new(5, 11, 0); readonly IServerClientFactory clientFactory = new ServerClientFactory(new ProductHeaderValue(Assembly.GetExecutingAssembly().GetName().Name, Assembly.GetExecutingAssembly().GetName().Version.ToString())); @@ -79,9 +79,14 @@ namespace Tgstation.Server.Tests.Live } } - [TestInitialize] - public async Task Initialize() + [ClassInitialize] + public static async Task Initialize(TestContext _) { + if (LiveTestUtils.RunningInGitHubActions || String.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN"))) + await DummyGitHubService.InitializeAndInject(default); + + await CachingFileDownloader.InitializeAndInject(default); + await DummyChatProvider.RandomDisconnections(true, default); ServerClientFactory.ApiClientFactory = new RateLimitRetryingApiClientFactory(); @@ -146,6 +151,12 @@ namespace Tgstation.Server.Tests.Live } } + [ClassCleanup] + public static void Cleanup() + { + CachingFileDownloader.Cleanup(); + } + [TestMethod] public async Task TestUpdateProtocolAndDisabledOAuth() { @@ -155,7 +166,6 @@ namespace Tgstation.Server.Tests.Live var serverTask = server.Run(cancellationToken); try { - var testUpdateVersion = new Version(4, 3, 0); using (var adminClient = await CreateAdminClient(server.Url, cancellationToken)) { // Disabled OAuth test @@ -178,10 +188,21 @@ namespace Tgstation.Server.Tests.Live //attempt to update to stable await adminClient.Administration.Update(new ServerUpdateRequest { - NewVersion = testUpdateVersion + NewVersion = TestUpdateVersion }, cancellationToken); - var serverInfo = await adminClient.ServerInformation(cancellationToken); - Assert.IsTrue(serverInfo.UpdateInProgress); + + try + { + var serverInfoTask = adminClient.ServerInformation(cancellationToken); + var completedTask = await Task.WhenAny(serverTask, serverInfoTask); + if (completedTask == serverInfoTask) + { + var serverInfo = await serverInfoTask; + Assert.IsTrue(serverInfo.UpdateInProgress); + } + } + catch (ServiceUnavailableException) { } + catch (HttpRequestException) { } } //wait up to 3 minutes for the dl and install @@ -195,7 +216,7 @@ namespace Tgstation.Server.Tests.Live Assert.IsTrue(File.Exists(updatedAssemblyPath), "Updated assembly missing!"); var updatedAssemblyVersion = FileVersionInfo.GetVersionInfo(updatedAssemblyPath); - Assert.AreEqual(testUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver()); + Assert.AreEqual(TestUpdateVersion, Version.Parse(updatedAssemblyVersion.FileVersion).Semver()); } finally { @@ -218,7 +239,7 @@ namespace Tgstation.Server.Tests.Live [TestMethod] public async Task TestUpdateBadVersion() { - using var server = new LiveTestingServer(null, false, dumpOnMissingUpdate: false); + using var server = new LiveTestingServer(null, false); using var serverCts = new CancellationTokenSource(); var cancellationToken = serverCts.Token; var serverTask = server.Run(cancellationToken); @@ -746,8 +767,8 @@ namespace Tgstation.Server.Tests.Live Assert.AreEqual(ProcessPriorityClass.Normal, currentProcess.PriorityClass); } - const int MaximumTestMinutes = 30; - using var hardCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(MaximumTestMinutes)); + var maximumTestMinutes = LiveTestUtils.RunningInGitHubActions ? 90 : 20; + using var hardCancellationTokenSource = new CancellationTokenSource(TimeSpan.FromMinutes(maximumTestMinutes)); var hardCancellationToken = hardCancellationTokenSource.Token; ServiceCollectionExtensions.UseAdditionalLoggerProvider(); @@ -880,7 +901,17 @@ namespace Tgstation.Server.Tests.Live Assert.IsTrue(Directory.Exists(instanceClient.Metadata.Path)); - var instanceTests = FailFast(new InstanceTest(instanceClient, adminClient.Instances, GetInstanceManager(), (ushort)server.Url.Port).RunTests(cancellationToken, server.HighPriorityDreamDaemon, server.LowPriorityDeployments)); + var instanceTests = FailFast( + new InstanceTest( + instanceClient, + adminClient.Instances, + ((Host.Server)server.RealServer).Host.Services.GetRequiredService(), + GetInstanceManager(), + (ushort)server.Url.Port) + .RunTests( + cancellationToken, + server.HighPriorityDreamDaemon, + server.LowPriorityDeployments)); await Task.WhenAll(rootTest, adminTest, instancesTest, instanceTests, usersTest); @@ -1133,9 +1164,7 @@ namespace Tgstation.Server.Tests.Live url, DefaultCredentials.AdminUserName, DefaultCredentials.DefaultAdminUserPassword, - attemptLoginRefresh: false, - cancellationToken: cancellationToken) - ; + cancellationToken: cancellationToken); } catch (HttpRequestException) { diff --git a/tests/Tgstation.Server.Tests/TestDMApiConstants.cs b/tests/Tgstation.Server.Tests/TestDMApiConstants.cs index d248c6df76..fbbcf84319 100644 --- a/tests/Tgstation.Server.Tests/TestDMApiConstants.cs +++ b/tests/Tgstation.Server.Tests/TestDMApiConstants.cs @@ -12,10 +12,10 @@ namespace Tgstation.Server.Tests [TestClass] public sealed class TestDMApiConstants { - string[] definesFileLines; + static string[] definesFileLines; - [TestInitialize] - public async Task Initialize() + [ClassInitialize] + public static async Task Initialize(TestContext _) { definesFileLines = await File.ReadAllLinesAsync("../../../../../src/DMAPI/tgs/v5/_defines.dm"); } diff --git a/tests/Tgstation.Server.Tests/TestVersions.cs b/tests/Tgstation.Server.Tests/TestVersions.cs index 9977f68041..8e206913ba 100644 --- a/tests/Tgstation.Server.Tests/TestVersions.cs +++ b/tests/Tgstation.Server.Tests/TestVersions.cs @@ -20,12 +20,12 @@ namespace Tgstation.Server.Tests [TestCategory("SkipWhenLiveUnitTesting")] public sealed class TestVersions { - XNamespace xmlNamespace; + static XNamespace xmlNamespace; - XElement versionsPropertyGroup; + static XElement versionsPropertyGroup; - [TestInitialize] - public void Init() + [ClassInitialize] + public static void Init(TestContext _) { var doc = XDocument.Load("../../../../../build/Version.props"); var project = doc.Root;