diff --git a/appveyor.yml b/appveyor.yml index b5a69eab3e..88a4f72c54 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -25,7 +25,7 @@ cache: - C:\ProgramData\chocolatey\bin -> appveyor.yml - C:\ProgramData\chocolatey\lib -> appveyor.yml install: - - choco install doxygen.portable codecov graphviz.portable + - choco install doxygen.portable codecov graphviz.portable opencover.portable - nuget restore tgstation-server.sln build: project: tgstation-server.sln @@ -33,29 +33,29 @@ build: verbosity: minimal publish_nuget: true test_script: + - OpenCover.Console.exe -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Api.Tests/Tgstation.Server.Api.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Api.Tests*]*" -output:".\api_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Api.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Client.Tests/Tgstation.Server.Client.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Client.Tests*]*" -output:".\client_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Client.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Tests/Tgstation.Server.Host.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Tests*]*" -output:".\host_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Console.Tests/Tgstation.Server.Host.Console.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Console.Tests*]*" -output:".\console_coverage.xml" -oldstyle + - ps: $wc = New-Object 'System.Net.WebClient' + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Console.Tests\TestResults\results.trx)) - set path=%ProgramFiles(x86)%\Microsoft Visual Studio\2017\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow;%path% - copy "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\Community\Common7\IDE\CommonExtensions\Microsoft\TestWindow\Extensions\appveyor.*" "%ProgramFiles(x86)%\Microsoft Visual Studio\2017\TestAgent\Common7\IDE\CommonExtensions\Microsoft\TestWindow\Extensions" /y - - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Api.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Api.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Client.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Client.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Host.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Console.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Host.Console.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 - - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Service.Tests\bin\%CONFIGURATION%\Tgstation.Server.Host.Service.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 - ps: $wc = New-Object 'System.Net.WebClient' - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Host.Watchdog.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Host.Watchdog.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 + - OpenCover.Console.exe -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Host.Watchdog.Tests/Tgstation.Server.Host.Watchdog.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Host.Watchdog.Tests*]*" -output:".\watchdog_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) - - vstest.console /logger:trx;LogFileName=results.trx "tests\Tgstation.Server.Tests\bin\%CONFIGURATION%\netcoreapp2.0\Tgstation.Server.Tests.dll" /Enablecodecoverage /inIsolation /Platform:x64 + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Host.Watchdog.Tests\TestResults\results.trx)) + - OpenCover.Console.exe -register:user -target:"C:/Program Files/dotnet/dotnet.exe" -targetargs:"test -c %CONFIGURATION% --logger:trx;LogFileName=results.trx /p:DebugType=full tests/Tgstation.Server.Tests/Tgstation.Server.Tests.csproj" -filter:"+[Tgstation.Server*]* -[Tgstation.Server.Tests*]*" -output:".\server_coverage.xml" -oldstyle - ps: $wc = New-Object 'System.Net.WebClient' - - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\TestResults\results.trx)) + - ps: $wc.UploadFile("https://ci.appveyor.com/api/testresults/mstest/$($env:APPVEYOR_JOB_ID)", (Resolve-Path .\tests\Tgstation.Server.Tests\TestResults\results.trx)) after_test: - ps: build/UploadCoverage.ps1 - ps: build/BuildDox.ps1 diff --git a/build/UploadCoverage.ps1 b/build/UploadCoverage.ps1 index 3d790646cd..61a0bd1069 100644 --- a/build/UploadCoverage.ps1 +++ b/build/UploadCoverage.ps1 @@ -1,10 +1,16 @@ -$coverageFilePath = Resolve-Path -path "TestResults\*\*.coverage" - -$coverageFilePath = $coverageFilePath.ToString() +$coverageFilePaths = Get-ChildItem -Path TestResults -Filter *.coverage -Recurse -ErrorAction SilentlyContinue -Force | %{ $_.fullname } + +$coverageFilePathList = [string]$coverageFilePaths Write-Host "Running CodeCoverage.exe..." -&"C:\Program Files (x86)\Microsoft Visual Studio\2017\TestAgent\Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe" analyze /output:coverage.coveragexml "$coverageFilePath" +&"C:\Program Files (x86)\Microsoft Visual Studio\2017\TestAgent\Team Tools\Dynamic Code Coverage Tools\CodeCoverage.exe" analyze /output:service.coveragexml "$coverageFilePathList" rm -r TestResults -codecov -f coverage.coveragexml +codecov -f api_coverage.xml --flag unittests +codecov -f client_coverage.xml --flag unittests +codecov -f host_coverage.xml --flag unittests +codecov -f console_coverage.xml --flag unittests +codecov -f watchdog_coverage.xml --flag unittests +codecov -f service.coveragexml --flag unittests +codecov -f server_coverage.xml --flag integration diff --git a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs index 5f249a6330..2f9c59df69 100644 --- a/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/IRepositoryManager.cs @@ -9,6 +9,11 @@ namespace Tgstation.Server.Host.Components.Repository /// public interface IRepositoryManager : IDisposable { + /// + /// If something is holding a lock on the repository + /// + bool InUse { get; } + /// /// If a operation is in progress /// diff --git a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs index 7f3619e073..9b7ef5e95b 100644 --- a/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs +++ b/src/Tgstation.Server.Host/Components/Repository/RepositoryManager.cs @@ -11,6 +11,9 @@ namespace Tgstation.Server.Host.Components.Repository /// sealed class RepositoryManager : IRepositoryManager { + /// + public bool InUse => semaphore.CurrentCount == 0; + /// public bool CloneInProgress { get; private set; } diff --git a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs index f62bf504ba..90c7a4d218 100644 --- a/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs +++ b/src/Tgstation.Server.Host/Components/StaticFiles/Configuration.cs @@ -216,10 +216,6 @@ namespace Tgstation.Server.Host.Components.StaticFiles Path = configurationRelativePath }; } - catch (IOException e) - { - logger.LogDebug("IOException while reading {0}: {1}", path, e); - } catch (UnauthorizedAccessException) { //this happens on windows, dunno about linux @@ -308,10 +304,6 @@ namespace Tgstation.Server.Host.Components.StaticFiles Path = configurationRelativePath }; } - catch (IOException e) - { - logger.LogDebug("IOException while writing {0}: {1}", path, e); - } catch (UnauthorizedAccessException) { //this happens on windows, dunno about linux diff --git a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs index c362107fb5..34ded3ea45 100644 --- a/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs +++ b/src/Tgstation.Server.Host/Controllers/ConfigurationController.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using System; +using System.IO; using System.Net; using System.Threading; using System.Threading.Tasks; @@ -54,12 +55,23 @@ namespace Tgstation.Server.Host.Controllers { var newFile = await config.Write(model.Path, AuthenticationContext.SystemIdentity, model.Content, model.LastReadHash, cancellationToken).ConfigureAwait(false); if (newFile == null) - return Conflict(); + return Conflict(new ErrorMessage + { + Message = "" + }); newFile.Content = null; return model.LastReadHash == null ? (IActionResult)StatusCode((int)HttpStatusCode.Created, newFile) : Json(newFile); } + catch(IOException e) + { + Logger.LogInformation("IOException while updating file {0}: {1}", model.Path, e); + return Conflict(new ErrorMessage + { + Message = e.Message + }); + } catch (NotImplementedException) { return StatusCode((int)HttpStatusCode.NotImplemented); @@ -87,6 +99,14 @@ namespace Tgstation.Server.Host.Controllers return Json(result); } + catch (IOException e) + { + Logger.LogInformation("IOException while reading file {0}: {1}", filePath, e); + return Conflict(new ErrorMessage + { + Message = e.Message + }); + } catch (NotImplementedException) { return StatusCode((int)HttpStatusCode.NotImplemented); diff --git a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs index 735bc19e6d..4de3cae653 100644 --- a/src/Tgstation.Server.Host/Controllers/RepositoryController.cs +++ b/src/Tgstation.Server.Host/Controllers/RepositoryController.cs @@ -92,7 +92,7 @@ namespace Tgstation.Server.Host.Controllers return needsDbUpdate; } - static async Task PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, string lastOriginCommitSha, Action revInfoSink, CancellationToken cancellationToken) + static async Task PopulateApi(Repository model, Components.Repository.IRepository repository, IDatabaseContext databaseContext, Models.Instance instance, CancellationToken cancellationToken) { model.IsGitHub = repository.IsGitHubRepository; model.Origin = repository.Origin; @@ -100,9 +100,8 @@ namespace Tgstation.Server.Host.Controllers //rev info stuff Models.RevisionInformation revisionInfo = null; - var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, lastOriginCommitSha, x => revisionInfo = x, cancellationToken).ConfigureAwait(false); + var needsDbUpdate = await LoadRevisionInformation(repository, databaseContext, instance, null, x => revisionInfo = x, cancellationToken).ConfigureAwait(false); model.RevisionInformation = revisionInfo.ToApi(); - revInfoSink?.Invoke(revisionInfo); return needsDbUpdate; } @@ -140,13 +139,25 @@ namespace Tgstation.Server.Host.Controllers var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; if (repoManager.CloneInProgress) - return Conflict(); + return Conflict(new ErrorMessage + { + Message = "A clone operation is in progress!" + }); + + if(repoManager.InUse) + return Conflict(new ErrorMessage + { + Message = "The repo is busy!" + }); using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { if (repo != null) //clone conflict - return Conflict(); + return Conflict(new ErrorMessage + { + Message = "The repository already exists!" + }); var job = new Models.Job { @@ -164,7 +175,7 @@ namespace Tgstation.Server.Host.Controllers if (repos == null) throw new JobException("Filesystem conflict while cloning repository!"); var db = serviceProvider.GetRequiredService(); - if (await PopulateApi(api, repos, db, Instance, null, null, cancellationToken).ConfigureAwait(false)) + if (await PopulateApi(api, repos, db, Instance, cancellationToken).ConfigureAwait(false)) await db.Save(cancellationToken).ConfigureAwait(false); } }, cancellationToken).ConfigureAwait(false); @@ -220,10 +231,23 @@ namespace Tgstation.Server.Host.Controllers return StatusCode((int)HttpStatusCode.Gone); var api = currentModel.ToApi(); + var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; - using (var repo = await instanceManager.GetInstance(Instance).RepositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + if (repoManager.CloneInProgress) + return Conflict(new ErrorMessage + { + Message = "A clone operation is in progress!" + }); + + if (repoManager.InUse) + return Conflict(new ErrorMessage + { + Message = "The repo is busy!" + }); + + using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) { - if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, null, null, cancellationToken).ConfigureAwait(false)) + if (repo != null && await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false)) { //user may have fucked with the repo without telling us, do what we can await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); @@ -301,6 +325,36 @@ namespace Tgstation.Server.Host.Controllers currentModel.AccessToken = null; } + var canRead = userRights.HasFlag(RepositoryRights.Read); + + var api = canRead ? currentModel.ToApi() : new Repository(); + var repoManager = instanceManager.GetInstance(Instance).RepositoryManager; + + if (canRead) + { + if (repoManager.CloneInProgress) + return Conflict(new ErrorMessage + { + Message = "A clone operation is in progress!" + }); + + if (repoManager.InUse) + return Conflict(new ErrorMessage + { + Message = "The repo is busy!" + }); + + using (var repo = await repoManager.LoadRepository(cancellationToken).ConfigureAwait(false)) + { + if (repo == null) + return Conflict(new ErrorMessage + { + Message = "Repository could not be loaded!" + }); + await PopulateApi(api, repo, DatabaseContext, Instance, cancellationToken).ConfigureAwait(false); + } + } + //this is just db stuf so stow it away await DatabaseContext.Save(cancellationToken).ConfigureAwait(false); @@ -315,7 +369,7 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation(job, async (paramJob, serviceProvider, progressReporter, ct) => { - using (var repo = await instanceManager.GetInstance(Instance).RepositoryManager.LoadRepository(ct).ConfigureAwait(false)) + using (var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false)) { if (repo == null) throw new JobException("Repository could not be loaded!"); @@ -598,7 +652,8 @@ namespace Tgstation.Server.Host.Controllers } }, cancellationToken).ConfigureAwait(false); - return Accepted(job.ToApi()); + api.ActiveJob = job.ToApi(); + return Accepted(api); } } } diff --git a/src/Tgstation.Server.Host/Core/SemaphoreSlimContext.cs b/src/Tgstation.Server.Host/Core/SemaphoreSlimContext.cs index 9627a3e25e..d16b7ae628 100644 --- a/src/Tgstation.Server.Host/Core/SemaphoreSlimContext.cs +++ b/src/Tgstation.Server.Host/Core/SemaphoreSlimContext.cs @@ -52,11 +52,14 @@ namespace Tgstation.Server.Host.Core /// public void Dispose() { - if (disposed) - return; - lockedSemaphore.Release(); + lock (this) + { + if (disposed) + return; + disposed = true; + } GC.SuppressFinalize(this); - disposed = true; + lockedSemaphore.Release(); } } } diff --git a/tests/Tgstation.Server.Client.Tests/TestServerClientFactory.cs b/tests/Tgstation.Server.Client.Tests/TestServerClientFactory.cs new file mode 100644 index 0000000000..9b36d0a1a0 --- /dev/null +++ b/tests/Tgstation.Server.Client.Tests/TestServerClientFactory.cs @@ -0,0 +1,17 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Net.Http.Headers; + +namespace Tgstation.Server.Client.Tests +{ + [TestClass] + public sealed class TestServerClientFactory + { + [TestMethod] + public void TestConstruction() + { + Assert.ThrowsException(() => new ServerClientFactory(null)); + new ServerClientFactory(new ProductHeaderValue("Tgstation.Server.Client.Tests", GetType().Assembly.GetName().Version.ToString())); + } + } +}