Merge pull request #1677 from tgstation/FixLinuxFuckingBullshit [TGSDeploy]

v5.16.3: Fix PosixWatchdog and bad DMAPI Deployment Timeouts

Fixes #1680
This commit is contained in:
Jordan Dominion
2023-10-22 00:49:15 -04:00
committed by GitHub
37 changed files with 1123 additions and 508 deletions
+52 -52
View File
@@ -365,7 +365,7 @@ jobs:
fail-fast: false
matrix:
database-type: [ 'SqlServer', 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ]
watchdog-type: [ 'Basic', 'System' ]
watchdog-type: [ 'Basic', 'Advanced' ]
configuration: [ 'Debug', 'Release' ]
runs-on: windows-latest
steps:
@@ -382,7 +382,7 @@ jobs:
dotnet-version: ${{ env.TGS_DOTNET_VERSION }}
- name: Set TGS_TEST_DUMP_API_SPEC
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'SqlServer' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }}
run: echo "TGS_TEST_DUMP_API_SPEC=yes" >> $Env:GITHUB_ENV
- name: Set General__UseBasicWatchdog
@@ -473,7 +473,7 @@ jobs:
path: ./TestResults/
- name: Store OpenAPI Spec
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'SqlServer' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'SqlServer' }}
uses: actions/upload-artifact@v3
with:
name: openapi-spec
@@ -562,7 +562,7 @@ jobs:
fail-fast: false
matrix:
database-type: [ 'Sqlite', 'PostgresSql', 'MariaDB', 'MySql' ]
watchdog-type: [ 'Basic', 'System' ]
watchdog-type: [ 'Basic', 'Advanced' ]
configuration: [ 'Debug', 'Release' ]
runs-on: ubuntu-latest
steps:
@@ -645,7 +645,7 @@ jobs:
path: ./TestResults/
- name: Package Server Console
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'MariaDB' }}
run: |
cd src/Tgstation.Server.Host.Console
dotnet publish -c ${{ matrix.configuration }} -o ../../artifacts/Console
@@ -657,7 +657,7 @@ jobs:
build/RemoveUnsupportedRuntimes.sh artifacts/Console
- name: Package Server Update Package
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'PostgresSql' }}
run: |
cd src/Tgstation.Server.Host
dotnet publish -c ${{ matrix.configuration }}NoWindows --no-build -o ../../artifacts/ServerUpdate
@@ -666,14 +666,14 @@ jobs:
build/RemoveUnsupportedRuntimes.sh artifacts/ServerUpdate
- name: Store Server Console
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'MariaDB' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'MariaDB' }}
uses: actions/upload-artifact@v3
with:
name: ServerConsole
path: artifacts/Console/
- name: Store Server Update Package
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'System' && matrix.database-type == 'PostgresSql' }}
if: ${{ matrix.configuration == 'Release' && matrix.watchdog-type == 'Advanced' && matrix.database-type == 'PostgresSql' }}
uses: actions/upload-artifact@v3
with:
name: ServerUpdatePackage
@@ -735,100 +735,100 @@ jobs:
name: linux-unit-test-coverage-Release
path: ./code_coverage/unit_tests/linux_unit_tests_release
- name: Retrieve Linux Integration Test Coverage (Release, System, Sqlite)
- name: Retrieve Linux Integration Test Coverage (Release, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-Sqlite
name: linux-integration-test-coverage-Release-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_sqlite
- name: Retrieve Linux Integration Test Coverage (Release, System, PostgresSql)
- name: Retrieve Linux Integration Test Coverage (Release, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-PostgresSql
name: linux-integration-test-coverage-Release-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mariadb
- name: Retrieve Linux Integration Test Coverage (Release, System, MariaDB)
- name: Retrieve Linux Integration Test Coverage (Release, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-MariaDB
name: linux-integration-test-coverage-Release-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql
- name: Retrieve Linux Integration Test Coverage (Release, System, MySql)
- name: Retrieve Linux Integration Test Coverage (Release, Advanced, MySql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-MySql
name: linux-integration-test-coverage-Release-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_release_system_mysql
- name: Retrieve Linux Integration Test Coverage (Release, Basic, Sqlite)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-Sqlite
name: linux-integration-test-coverage-Release-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_sqlite
- name: Retrieve Linux Integration Test Coverage (Release, Basic, PostgresSql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-PostgresSql
name: linux-integration-test-coverage-Release-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mariadb
- name: Retrieve Linux Integration Test Coverage (Release, Basic, MariaDB)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-MariaDB
name: linux-integration-test-coverage-Release-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql
- name: Retrieve Linux Integration Test Coverage (Release, Basic, MySql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Release-System-MySql
name: linux-integration-test-coverage-Release-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_release_basic_mysql
- name: Retrieve Linux Integration Test Coverage (Debug, System, Sqlite)
- name: Retrieve Linux Integration Test Coverage (Debug, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-Sqlite
name: linux-integration-test-coverage-Debug-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_sqlite
- name: Retrieve Linux Integration Test Coverage (Debug, System, PostgresSql)
- name: Retrieve Linux Integration Test Coverage (Debug, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-PostgresSql
name: linux-integration-test-coverage-Debug-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mariadb
- name: Retrieve Linux Integration Test Coverage (Debug, System, MariaDB)
- name: Retrieve Linux Integration Test Coverage (Debug, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-MariaDB
name: linux-integration-test-coverage-Debug-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql
- name: Retrieve Linux Integration Test Coverage (Debug, System, MySql)
- name: Retrieve Linux Integration Test Coverage (Debug, Advanced, MySql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-MySql
name: linux-integration-test-coverage-Debug-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_system_mysql
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, Sqlite)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-Sqlite
name: linux-integration-test-coverage-Debug-Advanced-Sqlite
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_sqlite
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, PostgresSql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-PostgresSql
name: linux-integration-test-coverage-Debug-Advanced-PostgresSql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mariadb
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, MariaDB)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-MariaDB
name: linux-integration-test-coverage-Debug-Advanced-MariaDB
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql
- name: Retrieve Linux Integration Test Coverage (Debug, Basic, MySql)
uses: actions/download-artifact@v3
with:
name: linux-integration-test-coverage-Debug-System-MySql
name: linux-integration-test-coverage-Debug-Advanced-MySql
path: ./code_coverage/integration_tests/linux_integration_tests_debug_basic_mysql
- name: Retrieve Windows Unit Test Coverage (Release)
@@ -849,16 +849,16 @@ jobs:
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, SqlServer)
- name: Retrieve Windows Integration Test Coverage (Debug, Advanced, SqlServer)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Debug-System-SqlServer
name: windows-integration-test-coverage-Debug-Advanced-SqlServer
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlserver
- name: Retrieve Windows Integration Test Coverage (Release, System, SqlServer)
- name: Retrieve Windows Integration Test Coverage (Release, Advanced, SqlServer)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Release-System-SqlServer
name: windows-integration-test-coverage-Release-Advanced-SqlServer
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlserver
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, MariaDB)
@@ -873,16 +873,16 @@ jobs:
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)
- name: Retrieve Windows Integration Test Coverage (Debug, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Debug-System-MariaDB
name: windows-integration-test-coverage-Debug-Advanced-MariaDB
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mariadb
- name: Retrieve Windows Integration Test Coverage (Release, System, MariaDB)
- name: Retrieve Windows Integration Test Coverage (Release, Advanced, MariaDB)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Release-System-MariaDB
name: windows-integration-test-coverage-Release-Advanced-MariaDB
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mariadb
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, MySql)
@@ -897,16 +897,16 @@ jobs:
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)
- name: Retrieve Windows Integration Test Coverage (Debug, Advanced, MySql)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Debug-System-MySql
name: windows-integration-test-coverage-Debug-Advanced-MySql
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_mysql
- name: Retrieve Windows Integration Test Coverage (Release, System, MySql)
- name: Retrieve Windows Integration Test Coverage (Release, Advanced, MySql)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Release-System-MySql
name: windows-integration-test-coverage-Release-Advanced-MySql
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_mysql
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, PostgresSql)
@@ -921,16 +921,16 @@ jobs:
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)
- name: Retrieve Windows Integration Test Coverage (Debug, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Debug-System-PostgresSql
name: windows-integration-test-coverage-Debug-Advanced-PostgresSql
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_postgressql
- name: Retrieve Windows Integration Test Coverage (Release, System, PostgresSql)
- name: Retrieve Windows Integration Test Coverage (Release, Advanced, PostgresSql)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Release-System-PostgresSql
name: windows-integration-test-coverage-Release-Advanced-PostgresSql
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_postgressql
- name: Retrieve Windows Integration Test Coverage (Debug, Basic, Sqlite)
@@ -945,16 +945,16 @@ jobs:
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)
- name: Retrieve Windows Integration Test Coverage (Debug, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Debug-System-Sqlite
name: windows-integration-test-coverage-Debug-Advanced-Sqlite
path: ./code_coverage/integration_tests/windows_integration_tests_debug_system_sqlite
- name: Retrieve Windows Integration Test Coverage (Release, System, Sqlite)
- name: Retrieve Windows Integration Test Coverage (Release, Advanced, Sqlite)
uses: actions/download-artifact@v3
with:
name: windows-integration-test-coverage-Release-System-Sqlite
name: windows-integration-test-coverage-Release-Advanced-Sqlite
path: ./code_coverage/integration_tests/windows_integration_tests_release_system_sqlite
- name: Upload Coverage to CodeCov
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- Integration tests will ensure they match across the board -->
<Import Project="ControlPanelVersion.props" />
<PropertyGroup>
<TgsCoreVersion>5.16.2</TgsCoreVersion>
<TgsCoreVersion>5.16.3</TgsCoreVersion>
<TgsConfigVersion>4.7.1</TgsConfigVersion>
<TgsApiVersion>9.12.0</TgsApiVersion>
<TgsCommonLibraryVersion>6.0.1</TgsCommonLibraryVersion>
@@ -148,15 +148,18 @@ namespace Tgstation.Server.Host.Components.Deployment
cancellationToken);
}
ValueTask dmbDisposeTask;
lock (jobLockCounts)
{
nextDmbProvider?.Dispose();
dmbDisposeTask = nextDmbProvider?.DisposeAsync() ?? ValueTask.CompletedTask;
nextDmbProvider = newProvider;
// Oh god dammit
var temp = Interlocked.Exchange(ref newerDmbTcs, new TaskCompletionSource());
temp.SetResult();
}
await dmbDisposeTask;
}
/// <inheritdoc />
@@ -320,7 +323,7 @@ namespace Tgstation.Server.Host.Components.Deployment
finally
{
if (!providerSubmitted)
newProvider.Dispose();
await newProvider.DisposeAsync();
}
}
#pragma warning restore CA1506
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Models;
@@ -30,7 +31,7 @@ namespace Tgstation.Server.Host.Components.Deployment
readonly string directoryAppend;
/// <summary>
/// The <see cref="Action"/> to run when <see cref="Dispose"/> is called.
/// The <see cref="Action"/> to run when <see cref="DisposeAsync"/> is called.
/// </summary>
Action onDispose;
@@ -50,7 +51,11 @@ namespace Tgstation.Server.Host.Components.Deployment
}
/// <inheritdoc />
public void Dispose() => onDispose?.Invoke();
public ValueTask DisposeAsync()
{
onDispose?.Invoke();
return ValueTask.CompletedTask;
}
/// <inheritdoc />
public void KeepAlive() => onDispose = null;
@@ -802,7 +802,7 @@ namespace Tgstation.Server.Host.Components.Deployment
job.MinimumSecurityLevel = securityLevel; // needed for the TempDmbProvider
ApiValidationStatus validationStatus;
using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job))
await using (var provider = new TemporaryDmbProvider(ioManager.ResolvePath(job.DirectoryName.ToString()), String.Concat(job.DmeName, DmbExtension), job))
await using (var controller = await sessionControllerFactory.LaunchNew(provider, byondLock, launchParameters, true, cancellationToken))
{
var launchResult = await controller.LaunchResult.WaitAsync(cancellationToken);
@@ -0,0 +1,224 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Extensions;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Deployment
{
/// <summary>
/// A <see cref="IDmbProvider"/> that uses hard links.
/// </summary>
sealed class HardLinkDmbProvider : SwappableDmbProvider
{
/// <summary>
/// The <see cref="CancellationTokenSource"/> for <see cref="mirroringTask"/>.
/// </summary>
readonly CancellationTokenSource cancellationTokenSource;
/// <summary>
/// The <see cref="Task"/> representing the base provider mirroring operation.
/// </summary>
readonly Task<string> mirroringTask;
/// <summary>
/// The <see cref="ILogger"/> for the <see cref="HardLinkDmbProvider"/>.
/// </summary>
readonly ILogger logger;
/// <summary>
/// Initializes a new instance of the <see cref="HardLinkDmbProvider"/> class.
/// </summary>
/// <param name="baseProvider">The <see cref="IDmbProvider"/> for the <see cref="SwappableDmbProvider"/>.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="SwappableDmbProvider"/>.</param>
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="SwappableDmbProvider"/>.</param>
/// <param name="logger">The value of <see cref="logger"/>.</param>
/// <param name="generalConfiguration">The <see cref="GeneralConfiguration"/> for the <see cref="HardLinkDmbProvider"/>.</param>
public HardLinkDmbProvider(
IDmbProvider baseProvider,
IIOManager ioManager,
IFilesystemLinkFactory linkFactory,
ILogger logger,
GeneralConfiguration generalConfiguration)
: base(
baseProvider,
ioManager,
linkFactory)
{
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
cancellationTokenSource = new CancellationTokenSource();
try
{
mirroringTask = MirrorSourceDirectory(generalConfiguration.GetCopyDirectoryTaskThrottle(), cancellationTokenSource.Token);
}
catch
{
cancellationTokenSource.Dispose();
throw;
}
}
/// <inheritdoc />
public override async ValueTask DisposeAsync()
{
cancellationTokenSource.Cancel();
cancellationTokenSource.Dispose();
try
{
await mirroringTask;
}
catch (OperationCanceledException ex)
{
logger.LogDebug(ex, "Mirroring task cancelled!");
}
await base.DisposeAsync();
}
/// <inheritdoc />
public override Task FinishActivationPreparation(CancellationToken cancellationToken)
{
if (!mirroringTask.IsCompleted)
logger.LogTrace("Waiting for mirroring to complete...");
return mirroringTask.WaitAsync(cancellationToken);
}
/// <inheritdoc />
protected override async Task DoSwap(CancellationToken cancellationToken)
{
var mirroredDir = await mirroringTask.WaitAsync(cancellationToken);
var goAheadTcs = new TaskCompletionSource();
// I feel dirty...
async void DisposeOfOldDirectory()
{
var directoryMoved = false;
var disposePath = Guid.NewGuid().ToString();
try
{
await IOManager.MoveDirectory(LiveGameDirectory, disposePath, cancellationToken);
directoryMoved = true;
goAheadTcs.SetResult();
await IOManager.DeleteDirectory(disposePath, CancellationToken.None); // DCT: We're detached at this point
}
catch (DirectoryNotFoundException ex)
{
logger.LogDebug(ex, "Live directory appears to not exist");
if (!directoryMoved)
goAheadTcs.SetResult();
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to delete hard linked directory: {disposePath}", disposePath);
if (!directoryMoved)
goAheadTcs.SetException(ex);
}
}
DisposeOfOldDirectory();
await goAheadTcs.Task;
await IOManager.MoveDirectory(mirroredDir, LiveGameDirectory, cancellationToken);
}
/// <summary>
/// Mirror the <see cref="Models.CompileJob"/>.
/// </summary>
/// <param name="taskThrottle">The optional maximum number of simultaneous tasks allowed to execute.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task{TResult}"/> resulting in the full path to the mirrored directory.</returns>
async Task<string> MirrorSourceDirectory(int? taskThrottle, CancellationToken cancellationToken)
{
var stopwatch = Stopwatch.StartNew();
var mirrorGuid = Guid.NewGuid();
logger.LogDebug("Starting to mirror {sourceDir} as hard links to {mirrorGuid}...", CompileJob.DirectoryName, mirrorGuid);
if (taskThrottle.HasValue && taskThrottle < 1)
throw new ArgumentOutOfRangeException(nameof(taskThrottle), taskThrottle, "taskThrottle must be at least 1!");
var src = IOManager.ResolvePath(CompileJob.DirectoryName.ToString());
var dest = IOManager.ResolvePath(mirrorGuid.ToString());
using var semaphore = taskThrottle.HasValue ? new SemaphoreSlim(taskThrottle.Value) : null;
await Task.WhenAll(MirrorDirectoryImpl(src, dest, semaphore, cancellationToken));
stopwatch.Stop();
logger.LogDebug(
"Finished mirror of {sourceDir} to {mirrorGuid} in {seconds}s...",
CompileJob.DirectoryName,
mirrorGuid,
stopwatch.Elapsed.TotalSeconds.ToString("0.##", CultureInfo.InvariantCulture));
return dest;
}
/// <summary>
/// Recursively create tasks to create a hard link directory mirror of <paramref name="src"/> to <paramref name="dest"/>.
/// </summary>
/// <param name="src">The source directory path.</param>
/// <param name="dest">The destination directory path.</param>
/// <param name="semaphore">Optional <see cref="SemaphoreSlim"/> used to limit degree of parallelism.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="IEnumerable{T}"/> of <see cref="Task"/>s representing the running operations. The first <see cref="Task"/> returned is always the necessary call to <see cref="IIOManager.CreateDirectory(string, CancellationToken)"/>.</returns>
/// <remarks>I genuinely don't know how this will work with symlinked files. Waiting for the issue report I guess.</remarks>
IEnumerable<Task> MirrorDirectoryImpl(string src, string dest, SemaphoreSlim semaphore, CancellationToken cancellationToken)
{
var dir = new DirectoryInfo(src);
Task subdirCreationTask = null;
foreach (var subDirectory in dir.EnumerateDirectories())
{
// check if we are a symbolic link
if (!subDirectory.Attributes.HasFlag(FileAttributes.Directory) || subDirectory.Attributes.HasFlag(FileAttributes.ReparsePoint))
{
logger.LogTrace("Skipping symlink to {subdir}", subDirectory.Name);
continue;
}
var checkingSubdirCreationTask = true;
foreach (var copyTask in MirrorDirectoryImpl(subDirectory.FullName, Path.Combine(dest, subDirectory.Name), semaphore, cancellationToken))
{
if (subdirCreationTask == null)
{
subdirCreationTask = copyTask;
yield return subdirCreationTask;
}
else if (!checkingSubdirCreationTask)
yield return copyTask;
checkingSubdirCreationTask = false;
}
}
foreach (var fileInfo in dir.EnumerateFiles())
{
if (subdirCreationTask == null)
{
subdirCreationTask = IOManager.CreateDirectory(dest, cancellationToken);
yield return subdirCreationTask;
}
var sourceFile = fileInfo.FullName;
var destFile = IOManager.ConcatPath(dest, fileInfo.Name);
async Task LinkThisFile()
{
await subdirCreationTask.WaitAsync(cancellationToken);
using var lockContext = semaphore != null
? await SemaphoreSlimContext.Lock(semaphore, cancellationToken)
: null;
await LinkFactory.CreateHardLink(sourceFile, destFile, cancellationToken);
}
yield return LinkThisFile();
}
}
}
}
@@ -7,7 +7,7 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Provides absolute paths to the latest compiled .dmbs.
/// </summary>
public interface IDmbProvider : IDisposable
public interface IDmbProvider : IAsyncDisposable
{
/// <summary>
/// The file name of the .dmb.
@@ -8,23 +8,23 @@ using Tgstation.Server.Host.Models;
namespace Tgstation.Server.Host.Components.Deployment
{
/// <summary>
/// A <see cref="IDmbProvider"/> that uses symlinks.
/// A <see cref="IDmbProvider"/> that uses filesystem links to change directory structure underneath the server process.
/// </summary>
sealed class SwappableDmbProvider : IDmbProvider
abstract class SwappableDmbProvider : IDmbProvider
{
/// <summary>
/// The directory where the <see cref="baseProvider"/> is symlinked to.
/// The directory where the <see cref="BaseProvider"/> is symlinked to.
/// </summary>
public const string LiveGameDirectory = "Live";
/// <inheritdoc />
public string DmbName => baseProvider.DmbName;
public string DmbName => BaseProvider.DmbName;
/// <inheritdoc />
public string Directory => ioManager.ResolvePath(LiveGameDirectory);
public string Directory => IOManager.ResolvePath(LiveGameDirectory);
/// <inheritdoc />
public CompileJob CompileJob => baseProvider.CompileJob;
public CompileJob CompileJob => BaseProvider.CompileJob;
/// <summary>
/// If <see cref="MakeActive(CancellationToken)"/> has been run.
@@ -34,17 +34,17 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// The <see cref="IDmbProvider"/> we are swapping for.
/// </summary>
readonly IDmbProvider baseProvider;
protected IDmbProvider BaseProvider { get; }
/// <summary>
/// The <see cref="IIOManager"/> to use.
/// </summary>
readonly IIOManager ioManager;
protected IIOManager IOManager { get; }
/// <summary>
/// The <see cref="ISymlinkFactory"/> to use.
/// The <see cref="IFilesystemLinkFactory"/> to use.
/// </summary>
readonly ISymlinkFactory symlinkFactory;
protected IFilesystemLinkFactory LinkFactory { get; }
/// <summary>
/// Backing field for <see cref="Swapped"/>.
@@ -54,41 +54,47 @@ namespace Tgstation.Server.Host.Components.Deployment
/// <summary>
/// Initializes a new instance of the <see cref="SwappableDmbProvider"/> class.
/// </summary>
/// <param name="baseProvider">The value of <see cref="baseProvider"/>.</param>
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/>.</param>
public SwappableDmbProvider(IDmbProvider baseProvider, IIOManager ioManager, ISymlinkFactory symlinkFactory)
/// <param name="baseProvider">The value of <see cref="BaseProvider"/>.</param>
/// <param name="ioManager">The value of <see cref="IOManager"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="LinkFactory"/>.</param>
public SwappableDmbProvider(IDmbProvider baseProvider, IIOManager ioManager, IFilesystemLinkFactory symlinkFactory)
{
this.baseProvider = baseProvider ?? throw new ArgumentNullException(nameof(baseProvider));
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
BaseProvider = baseProvider ?? throw new ArgumentNullException(nameof(baseProvider));
IOManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
LinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
}
/// <inheritdoc />
public void Dispose() => baseProvider.Dispose();
public virtual ValueTask DisposeAsync() => BaseProvider.DisposeAsync();
/// <inheritdoc />
public void KeepAlive() => baseProvider.KeepAlive();
public void KeepAlive() => BaseProvider.KeepAlive();
/// <summary>
/// Make the <see cref="SwappableDmbProvider"/> active by replacing the live link with our <see cref="CompileJob"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
public async Task MakeActive(CancellationToken cancellationToken)
public Task MakeActive(CancellationToken cancellationToken)
{
if (Interlocked.Exchange(ref swapped, 1) != 0)
throw new InvalidOperationException("Already swapped!");
if (symlinkFactory.SymlinkedDirectoriesAreDeletedAsFiles)
await ioManager.DeleteFile(LiveGameDirectory, cancellationToken);
else
await ioManager.DeleteDirectory(LiveGameDirectory, cancellationToken);
await symlinkFactory.CreateSymbolicLink(
ioManager.ResolvePath(baseProvider.Directory),
ioManager.ResolvePath(LiveGameDirectory),
cancellationToken);
return DoSwap(cancellationToken);
}
/// <summary>
/// Should be <see langword="await"/>. before calling <see cref="MakeActive(CancellationToken)"/> to ensure the <see cref="SwappableDmbProvider"/> is ready to instantly swap. Can be called multiple times.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the preparation process.</returns>
public abstract Task FinishActivationPreparation(CancellationToken cancellationToken);
/// <summary>
/// Perform the swapping action.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected abstract Task DoSwap(CancellationToken cancellationToken);
}
}
@@ -0,0 +1,44 @@
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Host.IO;
namespace Tgstation.Server.Host.Components.Deployment
{
/// <summary>
/// A <see cref="IDmbProvider"/> that uses symlinks.
/// </summary>
sealed class SymlinkDmbProvider : SwappableDmbProvider
{
/// <summary>
/// Initializes a new instance of the <see cref="SymlinkDmbProvider"/> class.
/// </summary>
/// <param name="baseProvider">The <see cref="IDmbProvider"/> for the <see cref="SwappableDmbProvider"/>.</param>
/// <param name="ioManager">The <see cref="IIOManager"/> for the <see cref="SwappableDmbProvider"/>.</param>
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="SwappableDmbProvider"/>.</param>
public SymlinkDmbProvider(
IDmbProvider baseProvider,
IIOManager ioManager,
IFilesystemLinkFactory linkFactory)
: base(baseProvider, ioManager, linkFactory)
{
}
/// <inheritdoc />
public override Task FinishActivationPreparation(CancellationToken cancellationToken) => Task.CompletedTask;
/// <inheritdoc />
protected override async Task DoSwap(CancellationToken cancellationToken)
{
if (LinkFactory.SymlinkedDirectoriesAreDeletedAsFiles)
await IOManager.DeleteFile(LiveGameDirectory, cancellationToken);
else
await IOManager.DeleteDirectory(LiveGameDirectory, cancellationToken);
await LinkFactory.CreateSymbolicLink(
IOManager.ResolvePath(BaseProvider.Directory),
IOManager.ResolvePath(LiveGameDirectory),
cancellationToken);
}
}
}
@@ -1,4 +1,5 @@
using System;
using System.Threading.Tasks;
using Tgstation.Server.Host.Models;
@@ -32,9 +33,7 @@ namespace Tgstation.Server.Host.Components.Deployment
}
/// <inheritdoc />
public void Dispose()
{
}
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
/// <inheritdoc />
public void KeepAlive() => throw new NotSupportedException();
@@ -66,9 +66,9 @@ namespace Tgstation.Server.Host.Components
readonly ISynchronousIOManager synchronousIOManager;
/// <summary>
/// The <see cref="ISymlinkFactory"/> for the <see cref="InstanceFactory"/>.
/// The <see cref="IFilesystemLinkFactory"/> for the <see cref="InstanceFactory"/>.
/// </summary>
readonly ISymlinkFactory symlinkFactory;
readonly IFilesystemLinkFactory linkFactory;
/// <summary>
/// The <see cref="IByondInstaller"/> for the <see cref="InstanceFactory"/>.
@@ -173,7 +173,7 @@ namespace Tgstation.Server.Host.Components
/// <param name="topicClientFactory">The value of <see cref="topicClientFactory"/>.</param>
/// <param name="cryptographySuite">The value of <see cref="cryptographySuite"/>.</param>
/// <param name="synchronousIOManager">The value of <see cref="synchronousIOManager"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/>.</param>
/// <param name="linkFactory">The value of <see cref="linkFactory"/>.</param>
/// <param name="byondInstaller">The value of <see cref="byondInstaller"/>.</param>
/// <param name="chatFactory">The value of <see cref="chatFactory"/>.</param>
/// <param name="processExecutor">The value of <see cref="processExecutor"/>.</param>
@@ -199,7 +199,7 @@ namespace Tgstation.Server.Host.Components
ITopicClientFactory topicClientFactory,
ICryptographySuite cryptographySuite,
ISynchronousIOManager synchronousIOManager,
ISymlinkFactory symlinkFactory,
IFilesystemLinkFactory linkFactory,
IByondInstaller byondInstaller,
IChatManagerFactory chatFactory,
IProcessExecutor processExecutor,
@@ -225,7 +225,7 @@ namespace Tgstation.Server.Host.Components
this.topicClientFactory = topicClientFactory ?? throw new ArgumentNullException(nameof(topicClientFactory));
this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite));
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
this.byondInstaller = byondInstaller ?? throw new ArgumentNullException(nameof(byondInstaller));
this.chatFactory = chatFactory ?? throw new ArgumentNullException(nameof(chatFactory));
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
@@ -275,7 +275,7 @@ namespace Tgstation.Server.Host.Components
var configuration = new StaticFiles.Configuration(
configurationIoManager,
synchronousIOManager,
symlinkFactory,
linkFactory,
processExecutor,
postWriteHandler,
platformIdentifier,
@@ -128,7 +128,7 @@ namespace Tgstation.Server.Host.Components.Session
/// Replace the <see cref="IDmbProvider"/> in use with a given <paramref name="newProvider"/>, disposing the old one.
/// </summary>
/// <param name="newProvider">The new <see cref="IDmbProvider"/>.</param>
/// <returns>An <see cref="IDisposable"/> to be disposed once certain that the original <see cref="IDmbProvider"/> is no longer in use.</returns>
IDisposable ReplaceDmbProvider(IDmbProvider newProvider);
/// <returns>An <see cref="IAsyncDisposable"/> to be disposed once certain that the original <see cref="IDmbProvider"/> is no longer in use.</returns>
IAsyncDisposable ReplaceDmbProvider(IDmbProvider newProvider);
}
}
@@ -350,8 +350,13 @@ namespace Tgstation.Server.Host.Components.Session
await process.DisposeAsync();
byondLock.Dispose();
bridgeRegistration?.Dispose();
ReattachInformation.Dmb.Dispose();
ReattachInformation.InitialDmb?.Dispose();
var regularDmbDisposeTask = ReattachInformation.Dmb.DisposeAsync();
var initialDmb = ReattachInformation.InitialDmb;
if (initialDmb != null)
await initialDmb.DisposeAsync();
await regularDmbDisposeTask;
chatTrackingContext.Dispose();
reattachTopicCts.Dispose();
@@ -552,7 +557,7 @@ namespace Tgstation.Server.Host.Components.Session
public void Resume() => process.Resume();
/// <inheritdoc />
public IDisposable ReplaceDmbProvider(IDmbProvider dmbProvider)
public IAsyncDisposable ReplaceDmbProvider(IDmbProvider dmbProvider)
{
var oldDmb = ReattachInformation.Dmb;
ReattachInformation.Dmb = dmbProvider ?? throw new ArgumentNullException(nameof(dmbProvider));
@@ -94,9 +94,9 @@ namespace Tgstation.Server.Host.Components.StaticFiles
readonly ISynchronousIOManager synchronousIOManager;
/// <summary>
/// The <see cref="ISymlinkFactory"/> for <see cref="Configuration"/>.
/// The <see cref="IFilesystemLinkFactory"/> for <see cref="Configuration"/>.
/// </summary>
readonly ISymlinkFactory symlinkFactory;
readonly IFilesystemLinkFactory linkFactory;
/// <summary>
/// The <see cref="IProcessExecutor"/> for <see cref="Configuration"/>.
@@ -153,7 +153,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
/// </summary>
/// <param name="ioManager">The value of <see cref="ioManager"/>.</param>
/// <param name="synchronousIOManager">The value of <see cref="synchronousIOManager"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/>.</param>
/// <param name="linkFactory">The value of <see cref="linkFactory"/>.</param>
/// <param name="processExecutor">The value of <see cref="processExecutor"/>.</param>
/// <param name="postWriteHandler">The value of <see cref="postWriteHandler"/>.</param>
/// <param name="platformIdentifier">The value of <see cref="platformIdentifier"/>.</param>
@@ -164,7 +164,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
public Configuration(
IIOManager ioManager,
ISynchronousIOManager synchronousIOManager,
ISymlinkFactory symlinkFactory,
IFilesystemLinkFactory linkFactory,
IProcessExecutor processExecutor,
IPostWriteHandler postWriteHandler,
IPlatformIdentifier platformIdentifier,
@@ -175,7 +175,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
{
this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager));
this.synchronousIOManager = synchronousIOManager ?? throw new ArgumentNullException(nameof(synchronousIOManager));
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
this.linkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
this.processExecutor = processExecutor ?? throw new ArgumentNullException(nameof(processExecutor));
this.postWriteHandler = postWriteHandler ?? throw new ArgumentNullException(nameof(postWriteHandler));
this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier));
@@ -450,7 +450,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles
var fileExists = await fileExistsTask;
if (fileExists)
await ioManager.DeleteFile(destPath, cancellationToken);
await symlinkFactory.CreateSymbolicLink(ioManager.ResolvePath(file), ioManager.ResolvePath(destPath), cancellationToken);
await linkFactory.CreateSymbolicLink(ioManager.ResolvePath(file), ioManager.ResolvePath(destPath), cancellationToken);
}));
}
@@ -0,0 +1,424 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Host.Components.Chat;
using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Deployment.Remote;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Watchdog
{
/// <summary>
/// A <see cref="IWatchdog"/> that, instead of killing servers for updates, uses the wonders of filesystem links to swap out changes without killing the server process.
/// </summary>
abstract class AdvancedWatchdog : BasicWatchdog
{
/// <summary>
/// The <see cref="SwappableDmbProvider"/> for <see cref="WatchdogBase.LastLaunchParameters"/>.
/// </summary>
protected SwappableDmbProvider ActiveSwappable { get; private set; }
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="AdvancedWatchdog"/> pointing to the Game directory.
/// </summary>
protected IIOManager GameIOManager { get; }
/// <summary>
/// The <see cref="IFilesystemLinkFactory"/> for the <see cref="AdvancedWatchdog"/>.
/// </summary>
protected IFilesystemLinkFactory LinkFactory { get; }
/// <summary>
/// <see cref="List{T}"/> of <see cref="Task"/>s that are waiting to clean up old deployments.
/// </summary>
readonly List<Task> deploymentCleanupTasks;
/// <summary>
/// The active <see cref="SwappableDmbProvider"/> for <see cref="WatchdogBase.ActiveLaunchParameters"/>.
/// </summary>
SwappableDmbProvider pendingSwappable;
/// <summary>
/// The <see cref="TaskCompletionSource"/> representing the cleanup of an unused <see cref="IDmbProvider"/>.
/// </summary>
volatile TaskCompletionSource deploymentCleanupGate;
/// <summary>
/// Initializes a new instance of the <see cref="AdvancedWatchdog"/> class.
/// </summary>
/// <param name="chat">The <see cref="IChatManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="sessionControllerFactory">The <see cref="ISessionControllerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="dmbFactory">The <see cref="IDmbFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="sessionPersistor">The <see cref="ISessionPersistor"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="serverControl">The <see cref="IServerControl"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="gameIOManager">The value of <see cref="GameIOManager"/>.</param>
/// <param name="linkFactory">The value of <see cref="LinkFactory"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="initialLaunchParameters">The <see cref="DreamDaemonLaunchParameters"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="instance">The <see cref="Api.Models.Instance"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="autoStart">The autostart value for the <see cref="WatchdogBase"/>.</param>
public AdvancedWatchdog(
IChatManager chat,
ISessionControllerFactory sessionControllerFactory,
IDmbFactory dmbFactory,
ISessionPersistor sessionPersistor,
IJobManager jobManager,
IServerControl serverControl,
IAsyncDelayer asyncDelayer,
IIOManager diagnosticsIOManager,
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IIOManager gameIOManager,
IFilesystemLinkFactory linkFactory,
ILogger<AdvancedWatchdog> logger,
DreamDaemonLaunchParameters initialLaunchParameters,
Api.Models.Instance instance,
bool autoStart)
: base(
chat,
sessionControllerFactory,
dmbFactory,
sessionPersistor,
jobManager,
serverControl,
asyncDelayer,
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
logger,
initialLaunchParameters,
instance,
autoStart)
{
try
{
GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
LinkFactory = linkFactory ?? throw new ArgumentNullException(nameof(linkFactory));
deploymentCleanupTasks = new List<Task>();
}
catch
{
// Async dispose is for if we have controllers running, not the case here
var disposeTask = DisposeAsync();
Debug.Assert(disposeTask.IsCompleted, "This should always be true during construction!");
disposeTask.GetAwaiter().GetResult();
throw;
}
}
/// <inheritdoc />
protected sealed override async Task DisposeAndNullControllersImpl()
{
await base.DisposeAndNullControllersImpl();
// If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
ActiveSwappable = null;
await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask);
pendingSwappable = null;
await DrainDeploymentCleanupTasks(true);
}
/// <inheritdoc />
protected sealed override async Task<MonitorAction> HandleNormalReboot(CancellationToken cancellationToken)
{
if (pendingSwappable != null)
{
Task RunPrequel() => BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken);
var needToSwap = !pendingSwappable.Swapped;
if (needToSwap)
{
// IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING
// OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB
if (!Server.ProcessingRebootBridgeRequest)
{
// integration test logging will catch this
Logger.LogError(
"The reboot bridge request completed before the watchdog could suspend the server! This can lead to buggy DreamDaemon behaviour and should be reported! To ensure stability, we will need to hard reboot the server");
await RunPrequel();
return MonitorAction.Restart;
}
// DCT: Not necessary
if (!pendingSwappable.FinishActivationPreparation(CancellationToken.None).IsCompleted)
{
// rare pokemon
Logger.LogInformation("Deployed .dme is not ready to swap, delaying until next reboot!");
Chat.QueueWatchdogMessage("The pending deployment was not ready to be activated this reboot. It will be applied at the next one.");
return MonitorAction.Continue;
}
}
var updateTask = RunPrequel();
if (needToSwap)
await PerformDmbSwap(pendingSwappable, cancellationToken);
var currentCompileJobId = Server.ReattachInformation.Dmb.CompileJob.Id;
await DrainDeploymentCleanupTasks(false);
IAsyncDisposable lingeringDeployment;
var localDeploymentCleanupGate = new TaskCompletionSource();
async Task CleanupLingeringDeployment()
{
var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout.Value;
Logger.LogDebug(
"Holding old deployment {compileJobId} for up to {expiry} seconds...",
currentCompileJobId,
lingeringDeploymentExpirySeconds);
var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), cancellationToken);
var completedTask = await Task.WhenAny(
localDeploymentCleanupGate.Task,
timeout);
var timedOut = completedTask == timeout;
Logger.Log(
timedOut
? LogLevel.Warning
: LogLevel.Trace,
"Releasing old deployment {compileJobId}{afterTimeout}",
currentCompileJobId,
timedOut
? " due to timeout!"
: "...");
await lingeringDeployment.DisposeAsync();
}
var oldDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, localDeploymentCleanupGate);
oldDeploymentCleanupGate?.TrySetResult();
Logger.LogTrace("Replacing activeSwappable with pendingSwappable...");
lock (deploymentCleanupTasks)
{
lingeringDeployment = Server.ReplaceDmbProvider(pendingSwappable);
deploymentCleanupTasks.Add(
CleanupLingeringDeployment());
}
ActiveSwappable = pendingSwappable;
pendingSwappable = null;
await SessionPersistor.Save(Server.ReattachInformation, cancellationToken);
await updateTask;
}
else
Logger.LogTrace("Nothing to do as pendingSwappable is null.");
return MonitorAction.Continue;
}
/// <inheritdoc />
protected sealed override async Task HandleNewDmbAvailable(CancellationToken cancellationToken)
{
IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
bool canSeamlesslySwap = true;
if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion)
{
// have to do a graceful restart
Logger.LogDebug(
"Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...",
compileJobProvider.CompileJob.Id,
compileJobProvider.CompileJob.ByondVersion,
ActiveCompileJob.ByondVersion);
canSeamlesslySwap = false;
}
if (compileJobProvider.CompileJob.DmeName != ActiveCompileJob.DmeName)
{
Logger.LogDebug(
"Not swapping to new compile job {0} as it uses a different .dmb name ({1}) than what is currently active {2}. Queueing graceful restart instead...",
compileJobProvider.CompileJob.Id,
compileJobProvider.CompileJob.DmeName,
ActiveCompileJob.DmeName);
canSeamlesslySwap = false;
}
if (!canSeamlesslySwap)
{
await compileJobProvider.DisposeAsync();
await base.HandleNewDmbAvailable(cancellationToken);
return;
}
SwappableDmbProvider swappableProvider = null;
try
{
swappableProvider = CreateSwappableDmbProvider(compileJobProvider);
if (ActiveCompileJob.DMApiVersion == null)
{
Logger.LogWarning("Active compile job has no DMAPI! Commencing immediate .dmb swap. Note this behavior is known to be buggy in some DM code contexts. See https://github.com/tgstation/tgstation-server/issues/1550");
await PerformDmbSwap(swappableProvider, cancellationToken);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Exception while swapping");
IDmbProvider providerToDispose = swappableProvider ?? compileJobProvider;
await providerToDispose.DisposeAsync();
throw;
}
await (pendingSwappable?.DisposeAsync() ?? ValueTask.CompletedTask);
pendingSwappable = swappableProvider;
}
/// <inheritdoc />
protected sealed override async Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
{
if (ActiveSwappable != null)
throw new InvalidOperationException("Expected activeSwappable to be null!");
if (pendingSwappable != null)
throw new InvalidOperationException("Expected pendingSwappable to be null!");
Logger.LogTrace("Prep for server launch");
ActiveSwappable = CreateSwappableDmbProvider(dmbToUse);
try
{
await InitialLink(cancellationToken);
}
catch (Exception ex)
{
// We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here.
Logger.LogTrace(ex, "Initial link error, nulling ActiveSwappable");
ActiveSwappable = null;
throw;
}
return ActiveSwappable;
}
/// <summary>
/// Set the <see cref="ReattachInformation.InitialDmb"/> for the <see cref="BasicWatchdog.Server"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected abstract Task ApplyInitialDmb(CancellationToken cancellationToken);
/// <summary>
/// Create a <see cref="SwappableDmbProvider"/> for a given <paramref name="dmbProvider"/>.
/// </summary>
/// <param name="dmbProvider">The <see cref="IDmbProvider"/> to create a <see cref="SwappableDmbProvider"/> for.</param>
/// <returns>A new <see cref="SwappableDmbProvider"/>.</returns>
protected abstract SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider);
/// <inheritdoc />
protected override async Task SessionStartupPersist(CancellationToken cancellationToken)
{
await ApplyInitialDmb(cancellationToken);
await base.SessionStartupPersist(cancellationToken);
}
/// <inheritdoc />
protected override async Task<MonitorAction> HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken)
{
var result = await base.HandleMonitorWakeup(reason, cancellationToken);
if (reason == MonitorActivationReason.ActiveServerStartup)
await DrainDeploymentCleanupTasks(false);
return result;
}
/// <summary>
/// Create the initial link to the live game directory using <see cref="ActiveSwappable"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
async ValueTask InitialLink(CancellationToken cancellationToken)
{
await ActiveSwappable.FinishActivationPreparation(cancellationToken);
Logger.LogTrace("Linking compile job...");
await ActiveSwappable.MakeActive(cancellationToken);
}
/// <summary>
/// Suspends the <see cref="BasicWatchdog.Server"/> and calls <see cref="SwappableDmbProvider.MakeActive(CancellationToken)"/> on a <paramref name="newProvider"/>.
/// </summary>
/// <param name="newProvider">The <see cref="SwappableDmbProvider"/> to activate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationToken cancellationToken)
{
Logger.LogDebug("Swapping to compile job {id}...", newProvider.CompileJob.Id);
await newProvider.FinishActivationPreparation(cancellationToken);
var suspended = false;
var server = Server;
try
{
server.Suspend();
suspended = true;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Exception while suspending server!");
}
try
{
await newProvider.MakeActive(cancellationToken);
}
finally
{
// Let this throw hard if it fails
if (suspended)
server.Resume();
}
}
/// <summary>
/// Asynchronously drain <see cref="deploymentCleanupTasks"/>.
/// </summary>
/// <param name="blocking">If <see langword="true"/>, all <see cref="Task"/>s will be <see langword="await"/>ed. Otherwise, only <see cref="Task"/>s with <see cref="Task.IsCompleted"/> set will be <see langword="await"/>ed.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task DrainDeploymentCleanupTasks(bool blocking)
{
Logger.LogTrace("DrainDeploymentCleanupTasks...");
var localDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, null);
localDeploymentCleanupGate?.TrySetResult();
List<Task> localDeploymentCleanupTasks;
lock (deploymentCleanupTasks)
{
var totalActiveTasks = deploymentCleanupTasks.Count;
localDeploymentCleanupTasks = new List<Task>(totalActiveTasks);
for (var i = totalActiveTasks - 1; i >= 0; --i)
{
var currentTask = deploymentCleanupTasks[i];
if (!blocking && !currentTask.IsCompleted)
continue;
localDeploymentCleanupTasks.Add(currentTask);
deploymentCleanupTasks.RemoveAt(i);
}
}
return Task.WhenAll(localDeploymentCleanupTasks);
}
}
}
@@ -268,7 +268,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
// server didn't get control of this dmb
if (dmbToUse != null && !serverWasActive)
dmbToUse.Dispose();
await dmbToUse.DisposeAsync();
throw;
}
@@ -1,4 +1,5 @@
using System.Threading;
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -9,6 +10,7 @@ using Tgstation.Server.Host.Components.Deployment;
using Tgstation.Server.Host.Components.Deployment.Remote;
using Tgstation.Server.Host.Components.Events;
using Tgstation.Server.Host.Components.Session;
using Tgstation.Server.Host.Configuration;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
@@ -17,10 +19,15 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Watchdog
{
/// <summary>
/// A variant of the <see cref="WindowsWatchdog"/> that works on POSIX systems.
/// A variant of the <see cref="AdvancedWatchdog"/> that works on POSIX systems.
/// </summary>
sealed class PosixWatchdog : WindowsWatchdog
sealed class PosixWatchdog : AdvancedWatchdog
{
/// <summary>
/// The <see cref="GeneralConfiguration"/> for the <see cref="PosixWatchdog"/>.
/// </summary>
readonly GeneralConfiguration generalConfiguration;
/// <summary>
/// Initializes a new instance of the <see cref="PosixWatchdog"/> class.
/// </summary>
@@ -34,11 +41,12 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="gameIOManager">The <see cref="IIOManager"/> pointing to the game directory for the <see cref="WindowsWatchdog"/>..</param>
/// <param name="symlinkFactory">The <see cref="ISymlinkFactory"/> for the <see cref="WindowsWatchdog"/>.</param>
/// <param name="gameIOManager">The <see cref="IIOManager"/> pointing to the game directory for the <see cref="AdvancedWatchdog"/>..</param>
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="AdvancedWatchdog"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="initialLaunchParameters">The <see cref="DreamDaemonLaunchParameters"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="instance">The <see cref="Api.Models.Instance"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="generalConfiguration">The value of <see cref="GeneralConfiguration"/>.</param>
/// <param name="autoStart">The autostart value for the <see cref="WatchdogBase"/>.</param>
public PosixWatchdog(
IChatManager chat,
@@ -52,10 +60,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IIOManager gameIOManager,
ISymlinkFactory symlinkFactory,
IFilesystemLinkFactory linkFactory,
ILogger<PosixWatchdog> logger,
DreamDaemonLaunchParameters initialLaunchParameters,
Api.Models.Instance instance,
GeneralConfiguration generalConfiguration,
bool autoStart)
: base(
chat,
@@ -69,19 +78,21 @@ namespace Tgstation.Server.Host.Components.Watchdog
eventConsumer,
remoteDeploymentManagerFactory,
gameIOManager,
symlinkFactory,
linkFactory,
logger,
initialLaunchParameters,
instance,
autoStart)
{
this.generalConfiguration = generalConfiguration ?? throw new ArgumentNullException(nameof(generalConfiguration));
}
/// <inheritdoc />
protected override Task ApplyInitialDmb(CancellationToken cancellationToken)
{
// not necessary to hold initial .dmb on Linux because of based inode deletes
return Task.CompletedTask;
}
=> Task.CompletedTask; // not necessary to hold initial .dmb on Linux because of based inode deletes
/// <inheritdoc />
protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider)
=> new HardLinkDmbProvider(dmbProvider, GameIOManager, LinkFactory, Logger, generalConfiguration);
}
}
@@ -29,21 +29,21 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="symlinkFactory">The <see cref="ISymlinkFactory"/> for the <see cref="WindowsWatchdogFactory"/>.</param>
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="WindowsWatchdogFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> for <see cref="GeneralConfiguration"/> for the <see cref="WatchdogFactory"/>.</param>
public PosixWatchdogFactory(
IServerControl serverControl,
ILoggerFactory loggerFactory,
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
ISymlinkFactory symlinkFactory,
IFilesystemLinkFactory linkFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions)
: base(
serverControl,
loggerFactory,
jobManager,
asyncDelayer,
symlinkFactory,
linkFactory,
generalConfigurationOptions)
{
}
@@ -72,10 +72,11 @@ namespace Tgstation.Server.Host.Components.Watchdog
eventConsumer,
remoteDeploymentManagerFactory,
gameIOManager,
SymlinkFactory,
LinkFactory,
LoggerFactory.CreateLogger<PosixWatchdog>(),
settings,
instance,
GeneralConfiguration,
settings.AutoStart ?? throw new ArgumentNullException(nameof(settings)));
}
}
@@ -1,7 +1,4 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
@@ -20,40 +17,10 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Watchdog
{
/// <summary>
/// A <see cref="IWatchdog"/> that, instead of killing servers for updates, uses the wonders of symlinks to swap out changes without killing DreamDaemon.
/// A variant of the <see cref="AdvancedWatchdog"/> that works on Windows systems.
/// </summary>
class WindowsWatchdog : BasicWatchdog
sealed class WindowsWatchdog : AdvancedWatchdog
{
/// <summary>
/// The <see cref="SwappableDmbProvider"/> for <see cref="WatchdogBase.LastLaunchParameters"/>.
/// </summary>
protected SwappableDmbProvider ActiveSwappable { get; private set; }
/// <summary>
/// The <see cref="IIOManager"/> for the <see cref="WindowsWatchdog"/> pointing to the Game directory.
/// </summary>
protected IIOManager GameIOManager { get; }
/// <summary>
/// The <see cref="ISymlinkFactory"/> for the <see cref="WindowsWatchdog"/>.
/// </summary>
readonly ISymlinkFactory symlinkFactory;
/// <summary>
/// <see cref="List{T}"/> of <see cref="Task"/>s that are waiting to clean up old deployments.
/// </summary>
readonly List<Task> deploymentCleanupTasks;
/// <summary>
/// The active <see cref="SwappableDmbProvider"/> for <see cref="WatchdogBase.ActiveLaunchParameters"/>.
/// </summary>
SwappableDmbProvider pendingSwappable;
/// <summary>
/// The <see cref="TaskCompletionSource"/> representing the cleanup of an unused <see cref="IDmbProvider"/>.
/// </summary>
volatile TaskCompletionSource deploymentCleanupGate;
/// <summary>
/// Initializes a new instance of the <see cref="WindowsWatchdog"/> class.
/// </summary>
@@ -67,8 +34,8 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="diagnosticsIOManager">The <see cref="IIOManager"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="eventConsumer">The <see cref="IEventConsumer"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="remoteDeploymentManagerFactory">The <see cref="IRemoteDeploymentManagerFactory"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="gameIOManager">The value of <see cref="GameIOManager"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="symlinkFactory"/>.</param>
/// <param name="gameIOManager">The <see cref="IIOManager"/> pointing to the game directory for the <see cref="AdvancedWatchdog"/>..</param>
/// <param name="linkFactory">The <see cref="IFilesystemLinkFactory"/> for the <see cref="AdvancedWatchdog"/>.</param>
/// <param name="logger">The <see cref="ILogger"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="initialLaunchParameters">The <see cref="DreamDaemonLaunchParameters"/> for the <see cref="WatchdogBase"/>.</param>
/// <param name="instance">The <see cref="Api.Models.Instance"/> for the <see cref="WatchdogBase"/>.</param>
@@ -85,321 +52,39 @@ namespace Tgstation.Server.Host.Components.Watchdog
IEventConsumer eventConsumer,
IRemoteDeploymentManagerFactory remoteDeploymentManagerFactory,
IIOManager gameIOManager,
ISymlinkFactory symlinkFactory,
IFilesystemLinkFactory linkFactory,
ILogger<WindowsWatchdog> logger,
DreamDaemonLaunchParameters initialLaunchParameters,
Api.Models.Instance instance,
bool autoStart)
: base(
chat,
sessionControllerFactory,
dmbFactory,
sessionPersistor,
jobManager,
serverControl,
asyncDelayer,
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
logger,
initialLaunchParameters,
instance,
autoStart)
chat,
sessionControllerFactory,
dmbFactory,
sessionPersistor,
jobManager,
serverControl,
asyncDelayer,
diagnosticsIOManager,
eventConsumer,
remoteDeploymentManagerFactory,
gameIOManager,
linkFactory,
logger,
initialLaunchParameters,
instance,
autoStart)
{
try
{
GameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager));
this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
deploymentCleanupTasks = new List<Task>();
}
catch
{
// Async dispose is for if we have controllers running, not the case here
var disposeTask = DisposeAsync();
Debug.Assert(disposeTask.IsCompleted, "This should always be true during construction!");
disposeTask.GetAwaiter().GetResult();
throw;
}
}
/// <inheritdoc />
protected override async Task DisposeAndNullControllersImpl()
{
await base.DisposeAndNullControllersImpl();
// If we reach this point, we can guarantee PrepServerForLaunch will be called before starting again.
ActiveSwappable = null;
pendingSwappable?.Dispose();
pendingSwappable = null;
await DrainDeploymentCleanupTasks(true);
}
/// <inheritdoc />
protected override async Task<MonitorAction> HandleNormalReboot(CancellationToken cancellationToken)
{
if (pendingSwappable != null)
{
var updateTask = BeforeApplyDmb(pendingSwappable.CompileJob, cancellationToken);
if (!pendingSwappable.Swapped)
{
// IMPORTANT: THE SESSIONCONTROLLER SHOULD STILL BE PROCESSING THE BRIDGE REQUEST SO WE KNOW DD IS SLEEPING
// OTHERWISE, IT COULD RETURN TO /world/Reboot() TOO EARLY AND LOAD THE WRONG .DMB
if (!Server.ProcessingRebootBridgeRequest)
{
// integration test logging will catch this
Logger.LogError(
"The reboot bridge request completed before the watchdog could suspend the server! This can lead to buggy DreamDaemon behaviour and should be reported! To ensure stability, we will need to hard reboot the server");
await updateTask;
return MonitorAction.Restart;
}
await PerformDmbSwap(pendingSwappable, cancellationToken);
}
var currentCompileJobId = Server.ReattachInformation.Dmb.CompileJob.Id;
await DrainDeploymentCleanupTasks(false);
IDisposable lingeringDeployment;
var localDeploymentCleanupGate = new TaskCompletionSource();
async Task CleanupLingeringDeployment()
{
var lingeringDeploymentExpirySeconds = ActiveLaunchParameters.StartupTimeout.Value;
Logger.LogDebug(
"Holding old deployment {compileJobId} for up to {expiry} seconds...",
currentCompileJobId,
lingeringDeploymentExpirySeconds);
var timeout = AsyncDelayer.Delay(TimeSpan.FromSeconds(lingeringDeploymentExpirySeconds), cancellationToken);
var completedTask = await Task.WhenAny(
localDeploymentCleanupGate.Task,
timeout);
var timedOut = completedTask == timeout;
Logger.Log(
timedOut
? LogLevel.Warning
: LogLevel.Trace,
"Releasing old deployment {compileJobId}{afterTimeout}",
currentCompileJobId,
timedOut
? " due to timeout!"
: "...");
lingeringDeployment.Dispose();
}
var oldDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, localDeploymentCleanupGate);
oldDeploymentCleanupGate?.TrySetResult();
Logger.LogTrace("Replacing activeSwappable with pendingSwappable...");
lock (deploymentCleanupTasks)
{
lingeringDeployment = Server.ReplaceDmbProvider(pendingSwappable);
deploymentCleanupTasks.Add(
CleanupLingeringDeployment());
}
ActiveSwappable = pendingSwappable;
pendingSwappable = null;
await SessionPersistor.Save(Server.ReattachInformation, cancellationToken);
await updateTask;
}
else
Logger.LogTrace("Nothing to do as pendingSwappable is null.");
return MonitorAction.Continue;
}
/// <inheritdoc />
protected override async Task HandleNewDmbAvailable(CancellationToken cancellationToken)
{
IDmbProvider compileJobProvider = DmbFactory.LockNextDmb(1);
bool canSeamlesslySwap = true;
if (compileJobProvider.CompileJob.ByondVersion != ActiveCompileJob.ByondVersion)
{
// have to do a graceful restart
Logger.LogDebug(
"Not swapping to new compile job {0} as it uses a different BYOND version ({1}) than what is currently active {2}. Queueing graceful restart instead...",
compileJobProvider.CompileJob.Id,
compileJobProvider.CompileJob.ByondVersion,
ActiveCompileJob.ByondVersion);
canSeamlesslySwap = false;
}
if (compileJobProvider.CompileJob.DmeName != ActiveCompileJob.DmeName)
{
Logger.LogDebug(
"Not swapping to new compile job {0} as it uses a different .dmb name ({1}) than what is currently active {2}. Queueing graceful restart instead...",
compileJobProvider.CompileJob.Id,
compileJobProvider.CompileJob.DmeName,
ActiveCompileJob.DmeName);
canSeamlesslySwap = false;
}
if (!canSeamlesslySwap)
{
compileJobProvider.Dispose();
await base.HandleNewDmbAvailable(cancellationToken);
return;
}
SwappableDmbProvider windowsProvider = null;
try
{
windowsProvider = new SwappableDmbProvider(compileJobProvider, GameIOManager, symlinkFactory);
if (ActiveCompileJob.DMApiVersion == null)
{
Logger.LogWarning("Active compile job has no DMAPI! Commencing immediate .dmb swap. Note this behavior is known to be buggy in some DM code contexts. See https://github.com/tgstation/tgstation-server/issues/1550");
await PerformDmbSwap(windowsProvider, cancellationToken);
}
}
catch (Exception ex)
{
Logger.LogError(ex, "Exception while swapping");
IDmbProvider providerToDispose = windowsProvider ?? compileJobProvider;
providerToDispose.Dispose();
throw;
}
pendingSwappable?.Dispose();
pendingSwappable = windowsProvider;
}
/// <inheritdoc />
protected sealed override async Task<IDmbProvider> PrepServerForLaunch(IDmbProvider dmbToUse, CancellationToken cancellationToken)
{
if (ActiveSwappable != null)
throw new InvalidOperationException("Expected activeSwappable to be null!");
if (pendingSwappable != null)
throw new InvalidOperationException("Expected pendingSwappable to be null!");
Logger.LogTrace("Prep for server launch");
ActiveSwappable = new SwappableDmbProvider(dmbToUse, GameIOManager, symlinkFactory);
try
{
await InitialLink(cancellationToken);
}
catch (Exception ex)
{
// We won't worry about disposing activeSwappable here as we can't dispose dmbToUse here.
Logger.LogTrace(ex, "Initial link error, nulling ActiveSwappable");
ActiveSwappable = null;
throw;
}
return ActiveSwappable;
}
/// <summary>
/// Set the <see cref="ReattachInformation.InitialDmb"/> for the <see cref="BasicWatchdog.Server"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
protected virtual async Task ApplyInitialDmb(CancellationToken cancellationToken)
protected override async Task ApplyInitialDmb(CancellationToken cancellationToken)
{
Server.ReattachInformation.InitialDmb = await DmbFactory.FromCompileJob(Server.CompileJob, cancellationToken);
}
/// <inheritdoc />
protected override async Task SessionStartupPersist(CancellationToken cancellationToken)
{
await ApplyInitialDmb(cancellationToken);
await base.SessionStartupPersist(cancellationToken);
}
/// <inheritdoc />
protected override async Task<MonitorAction> HandleMonitorWakeup(MonitorActivationReason reason, CancellationToken cancellationToken)
{
var result = await base.HandleMonitorWakeup(reason, cancellationToken);
if (reason == MonitorActivationReason.ActiveServerStartup)
await DrainDeploymentCleanupTasks(false);
return result;
}
/// <summary>
/// Create the initial link to the live game directory using <see cref="ActiveSwappable"/>.
/// </summary>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task InitialLink(CancellationToken cancellationToken)
{
Logger.LogTrace("Symlinking compile job...");
return ActiveSwappable.MakeActive(cancellationToken);
}
/// <summary>
/// Suspends the <see cref="BasicWatchdog.Server"/> and calls <see cref="SwappableDmbProvider.MakeActive(CancellationToken)"/> on a <paramref name="newProvider"/>.
/// </summary>
/// <param name="newProvider">The <see cref="SwappableDmbProvider"/> to activate.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="ValueTask"/> representing the running operation.</returns>
async ValueTask PerformDmbSwap(SwappableDmbProvider newProvider, CancellationToken cancellationToken)
{
Logger.LogDebug("Swapping to compile job {id}...", newProvider.CompileJob.Id);
var suspended = false;
var server = Server;
try
{
server.Suspend();
suspended = true;
}
catch (Exception ex)
{
Logger.LogWarning(ex, "Exception while suspending server!");
}
try
{
await newProvider.MakeActive(cancellationToken);
}
finally
{
// Let this throw hard if it fails
if (suspended)
server.Resume();
}
}
/// <summary>
/// Asynchronously drain <see cref="deploymentCleanupTasks"/>.
/// </summary>
/// <param name="blocking">If <see langword="true"/>, all <see cref="Task"/>s will be <see langword="await"/>ed. Otherwise, only <see cref="Task"/>s with <see cref="Task.IsCompleted"/> set will be <see langword="await"/>ed.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task DrainDeploymentCleanupTasks(bool blocking)
{
Logger.LogTrace("DrainDeploymentCleanupTasks...");
var localDeploymentCleanupGate = Interlocked.Exchange(ref deploymentCleanupGate, null);
localDeploymentCleanupGate?.TrySetResult();
List<Task> localDeploymentCleanupTasks;
lock (deploymentCleanupTasks)
{
var totalActiveTasks = deploymentCleanupTasks.Count;
localDeploymentCleanupTasks = new List<Task>(totalActiveTasks);
for (var i = totalActiveTasks - 1; i >= 0; --i)
{
var currentTask = deploymentCleanupTasks[i];
if (!blocking && !currentTask.IsCompleted)
continue;
localDeploymentCleanupTasks.Add(currentTask);
deploymentCleanupTasks.RemoveAt(i);
}
}
return Task.WhenAll(localDeploymentCleanupTasks);
}
protected override SwappableDmbProvider CreateSwappableDmbProvider(IDmbProvider dmbProvider)
=> new SymlinkDmbProvider(dmbProvider, GameIOManager, LinkFactory);
}
}
@@ -18,14 +18,14 @@ using Tgstation.Server.Host.Utils;
namespace Tgstation.Server.Host.Components.Watchdog
{
/// <summary>
/// <see cref="IWatchdogFactory"/> for creating <see cref="WindowsWatchdog"/>s.
/// <see cref="IWatchdogFactory"/> for creating <see cref="AdvancedWatchdog"/>s.
/// </summary>
class WindowsWatchdogFactory : WatchdogFactory
{
/// <summary>
/// The <see cref="ISymlinkFactory"/> for the <see cref="WindowsWatchdogFactory"/>.
/// The <see cref="IFilesystemLinkFactory"/> for the <see cref="WindowsWatchdogFactory"/>.
/// </summary>
protected ISymlinkFactory SymlinkFactory { get; }
protected IFilesystemLinkFactory LinkFactory { get; }
/// <summary>
/// Initializes a new instance of the <see cref="WindowsWatchdogFactory"/> class.
@@ -34,14 +34,14 @@ namespace Tgstation.Server.Host.Components.Watchdog
/// <param name="loggerFactory">The <see cref="ILoggerFactory"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="jobManager">The <see cref="IJobManager"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="asyncDelayer">The <see cref="IAsyncDelayer"/> for the <see cref="WatchdogFactory"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="SymlinkFactory"/>.</param>
/// <param name="symlinkFactory">The value of <see cref="LinkFactory"/>.</param>
/// <param name="generalConfigurationOptions">The <see cref="IOptions{TOptions}"/> for <see cref="GeneralConfiguration"/> for the <see cref="WatchdogFactory"/>.</param>
public WindowsWatchdogFactory(
IServerControl serverControl,
ILoggerFactory loggerFactory,
IJobManager jobManager,
IAsyncDelayer asyncDelayer,
ISymlinkFactory symlinkFactory,
IFilesystemLinkFactory symlinkFactory,
IOptions<GeneralConfiguration> generalConfigurationOptions)
: base(
serverControl,
@@ -50,7 +50,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
asyncDelayer,
generalConfigurationOptions)
{
SymlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
LinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory));
}
/// <inheritdoc />
@@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
eventConsumer,
remoteDeploymentManagerFactory,
gameIOManager,
SymlinkFactory,
LinkFactory,
LoggerFactory.CreateLogger<WindowsWatchdog>(),
settings,
instance,
@@ -336,7 +336,7 @@ namespace Tgstation.Server.Host.Core
{
AddWatchdog<WindowsWatchdogFactory>(services, postSetupServices);
services.AddSingleton<ISystemIdentityFactory, WindowsSystemIdentityFactory>();
services.AddSingleton<ISymlinkFactory, WindowsSymlinkFactory>();
services.AddSingleton<IFilesystemLinkFactory, WindowsFilesystemLinkFactory>();
services.AddSingleton<IByondInstaller, WindowsByondInstaller>();
services.AddSingleton<IPostWriteHandler, WindowsPostWriteHandler>();
services.AddSingleton<IProcessFeatures, WindowsProcessFeatures>();
@@ -349,7 +349,7 @@ namespace Tgstation.Server.Host.Core
{
AddWatchdog<PosixWatchdogFactory>(services, postSetupServices);
services.AddSingleton<ISystemIdentityFactory, PosixSystemIdentityFactory>();
services.AddSingleton<ISymlinkFactory, PosixSymlinkFactory>();
services.AddSingleton<IFilesystemLinkFactory, PosixFilesystemLinkFactory>();
services.AddSingleton<IByondInstaller, PosixByondInstaller>();
services.AddSingleton<IPostWriteHandler, PosixPostWriteHandler>();
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.IO
/// <summary>
/// For creating filesystem symbolic links.
/// </summary>
interface ISymlinkFactory
interface IFilesystemLinkFactory
{
/// <summary>
/// If directory symlinks must be deleted as files would in the current environment.
@@ -22,5 +22,14 @@ namespace Tgstation.Server.Host.IO
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken);
/// <summary>
/// Creates a hard link.
/// </summary>
/// <param name="targetPath">The path to the hard target.</param>
/// <param name="linkPath">The path to the link.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken);
}
}
@@ -8,13 +8,29 @@ using Mono.Unix;
namespace Tgstation.Server.Host.IO
{
/// <summary>
/// <see cref="ISymlinkFactory"/> for posix systems.
/// <see cref="IFilesystemLinkFactory"/> for POSIX systems.
/// </summary>
sealed class PosixSymlinkFactory : ISymlinkFactory
sealed class PosixFilesystemLinkFactory : IFilesystemLinkFactory
{
/// <inheritdoc />
public bool SymlinkedDirectoriesAreDeletedAsFiles => true;
/// <inheritdoc />
public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
{
ArgumentNullException.ThrowIfNull(targetPath);
ArgumentNullException.ThrowIfNull(linkPath);
cancellationToken.ThrowIfCancellationRequested();
var fsInfo = new UnixFileInfo(targetPath);
cancellationToken.ThrowIfCancellationRequested();
fsInfo.CreateLink(linkPath);
},
cancellationToken,
DefaultIOManager.BlockingTaskCreationOptions,
TaskScheduler.Current);
/// <inheritdoc />
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
@@ -9,13 +9,17 @@ using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.IO
{
/// <summary>
/// <see cref="ISymlinkFactory"/> for windows systems.
/// <see cref="IFilesystemLinkFactory"/> for windows systems.
/// </summary>
sealed class WindowsSymlinkFactory : ISymlinkFactory
sealed class WindowsFilesystemLinkFactory : IFilesystemLinkFactory
{
/// <inheritdoc />
public bool SymlinkedDirectoriesAreDeletedAsFiles => false;
/// <inheritdoc />
public Task CreateHardLink(string targetPath, string linkPath, CancellationToken cancellationToken)
=> throw new NotSupportedException();
/// <inheritdoc />
public Task CreateSymbolicLink(string targetPath, string linkPath, CancellationToken cancellationToken) => Task.Factory.StartNew(
() =>
+9
View File
@@ -22,6 +22,15 @@
if(!length(channels))
FailTest("Expected some chat channels!")
var/test_str = "aljsdhfjahsfkjnsalkjdfhskljdackmcnvxkljhvkjsdanv,jdshlkufhklasjeFDhfjkalhdkjlfhalksfdjh"
var/res_contents = file2text('resource.txt') // we need a .rsc to be generated
if(!findtext(res_contents, test_str))
FailTest("Failed to resource? Did not find magic: [res_contents]")
if(!fexists("[DME_NAME].rsc"))
FailTest("Failed to create .rsc!")
StartAsync()
/proc/dab()
@@ -13,5 +13,8 @@
// BEGIN_INCLUDE
#include "Config.dm"
#include "../test_prelude.dm"
#ifndef DME_NAME
#define DME_NAME "long_running_test"
#endif
#include "Test.dm"
// END_INCLUDE
@@ -11,5 +11,6 @@
// END_PREFERENCES
// BEGIN_INCLUDE
#define DME_NAME "long_running_test_copy"
#include "long_running_test.dme"
// END_INCLUDE
+8
View File
@@ -0,0 +1,8 @@
if(!length(channels))
FailTest("Expected some chat channels!")
var/test_str = "aljsdhfjahsfkjnsalkjdfhskljdackmcnvxkljhvkjsdanv,jdshlkufhklasjeFDhfjkalhdkjlfhalksfdjh"
var/self_contents = file2text(file('Test.dm')) // we need a .rsc to be generated
if(!(test_str in self_contents))
@@ -47,7 +47,7 @@ namespace Tgstation.Server.Host.Components.StaticFiles.Tests
var configuration = new Configuration(
ioManager,
new SynchronousIOManager(),
Mock.Of<ISymlinkFactory>(),
Mock.Of<IFilesystemLinkFactory>(),
Mock.Of<IProcessExecutor>(),
Mock.Of<IPostWriteHandler>(),
Mock.Of<IPlatformIdentifier>(),
@@ -1,24 +1,26 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Tgstation.Server.Host.IO.Tests
{
[TestClass]
public sealed class TestSymlinkFactory
public sealed class TestFilesystemLinkFactory
{
static ISymlinkFactory symlinkFactory;
static IFilesystemLinkFactory linkFactory;
[ClassInitialize]
public static void SelectFactory(TestContext _)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
symlinkFactory = new WindowsSymlinkFactory();
linkFactory = new WindowsFilesystemLinkFactory();
else
symlinkFactory = new PosixSymlinkFactory();
linkFactory = new PosixFilesystemLinkFactory();
}
public static bool HasPermissionToMakeSymlinks()
@@ -30,6 +32,57 @@ namespace Tgstation.Server.Host.IO.Tests
return principal.IsInRole(WindowsBuiltInRole.Administrator);
}
[TestMethod]
public async Task TestHardLinkWorks()
{
string f2 = null;
var f1 = Path.GetTempFileName();
try
{
f2 = f1 + ".linked";
const string Text = "Hello world";
File.WriteAllText(f1, Text);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
await Assert.ThrowsExceptionAsync<NotSupportedException>(() => linkFactory.CreateHardLink(f1, f2, CancellationToken.None));
Assert.Inconclusive("Windows does not support hardlinks");
}
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => linkFactory.CreateHardLink(null, null, CancellationToken.None));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => linkFactory.CreateHardLink(f1, null, CancellationToken.None));
await linkFactory.CreateHardLink(f1, f2, default);
Assert.IsTrue(File.Exists(f2));
var f2Contents = File.ReadAllText(f2);
Assert.AreEqual(Text, f2Contents);
const string NewText = "asdf";
File.WriteAllText(f1, NewText);
f2Contents = File.ReadAllText(f2);
Assert.AreEqual(NewText, f2Contents);
const string NewText2 = "fdsa";
File.WriteAllText(f2, NewText2);
var f1Contents = File.ReadAllText(f1);
Assert.AreEqual(NewText2, f1Contents);
File.Delete(f1);
Assert.IsFalse(File.Exists(f1));
Assert.IsTrue(File.Exists(f2));
f2Contents = File.ReadAllText(f2);
Assert.AreEqual(NewText2, f2Contents);
}
finally
{
File.Delete(f2);
File.Delete(f1);
}
}
[TestMethod]
public async Task TestFileWorks()
{
@@ -43,10 +96,10 @@ namespace Tgstation.Server.Host.IO.Tests
f2 = f1 + ".linked";
File.WriteAllText(f1, Text);
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(null, null, default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(f1, null, default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => linkFactory.CreateSymbolicLink(null, null, default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => linkFactory.CreateSymbolicLink(f1, null, default));
await symlinkFactory.CreateSymbolicLink(f1, f2, default);
await linkFactory.CreateSymbolicLink(f1, f2, default);
Assert.IsTrue(File.Exists(f2));
var f2Contents = File.ReadAllText(f2);
@@ -76,10 +129,10 @@ namespace Tgstation.Server.Host.IO.Tests
var p1 = Path.Combine(f1, FileName);
File.WriteAllText(p1, Text);
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(null, null, default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => symlinkFactory.CreateSymbolicLink(f1, null, default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => linkFactory.CreateSymbolicLink(null, null, default));
await Assert.ThrowsExceptionAsync<ArgumentNullException>(() => linkFactory.CreateSymbolicLink(f1, null, default));
await symlinkFactory.CreateSymbolicLink(f1, f2, default);
await linkFactory.CreateSymbolicLink(f1, f2, default);
var p2 = Path.Combine(f2, FileName);
Assert.IsTrue(File.Exists(p2));
@@ -104,7 +157,7 @@ namespace Tgstation.Server.Host.IO.Tests
try
{
await symlinkFactory.CreateSymbolicLink(BadPath, BadPath, default);
await linkFactory.CreateSymbolicLink(BadPath, BadPath, default);
Assert.Fail("No exception thrown!");
}
catch { }
@@ -1,11 +1,12 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Remora.Discord.API.Objects;
using System;
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Tgstation.Server.Host.System;
namespace Tgstation.Server.Host.IO.Tests
@@ -38,6 +39,53 @@ namespace Tgstation.Server.Host.IO.Tests
}
}
[TestMethod]
public async Task TestDeleteDirectoryWithSymlinkInsideDoesntRecurse()
{
var linkFactory = (IFilesystemLinkFactory)(new PlatformIdentifier().IsWindows
? new WindowsFilesystemLinkFactory()
: new PosixFilesystemLinkFactory());
var tempPath = Path.GetTempFileName();
File.Delete(tempPath);
Directory.CreateDirectory(tempPath);
try
{
var targetDir = ioManager.ConcatPath(tempPath, "targetdir");
await ioManager.CreateDirectory(targetDir, CancellationToken.None);
var fileInTargetDir = ioManager.ConcatPath(targetDir, "test1.txt");
var expectedBytes = Encoding.UTF8.GetBytes("I want to live");
await ioManager.WriteAllBytes(fileInTargetDir, expectedBytes, CancellationToken.None);
var testDir = ioManager.ConcatPath(tempPath, "testdir");
await ioManager.CreateDirectory(testDir, CancellationToken.None);
var symlinkedFile = ioManager.ConcatPath(testDir, "test1.txt");
var symlinkedDir = ioManager.ConcatPath(testDir, "linkedDir");
await linkFactory.CreateSymbolicLink(targetDir, symlinkedDir, CancellationToken.None);
await linkFactory.CreateSymbolicLink(fileInTargetDir, symlinkedFile, CancellationToken.None);
Assert.IsTrue(await ioManager.DirectoryExists(symlinkedDir, CancellationToken.None));
Assert.IsTrue(await ioManager.FileExists(symlinkedFile, CancellationToken.None));
Assert.IsTrue(await ioManager.FileExists(ioManager.ConcatPath(symlinkedDir, "test1.txt"), CancellationToken.None));
Assert.IsTrue(await ioManager.FileExists(fileInTargetDir, CancellationToken.None));
await ioManager.DeleteDirectory(testDir, CancellationToken.None);
Assert.IsFalse(await ioManager.DirectoryExists(symlinkedDir, CancellationToken.None));
Assert.IsFalse(await ioManager.FileExists(symlinkedFile, CancellationToken.None));
Assert.IsFalse(await ioManager.FileExists(ioManager.ConcatPath(symlinkedDir, "test1.txt"), CancellationToken.None));
Assert.IsTrue(await ioManager.FileExists(fileInTargetDir, CancellationToken.None));
Assert.IsTrue(expectedBytes.SequenceEqual(await ioManager.ReadAllBytes(fileInTargetDir, CancellationToken.None)));
}
catch
{
Directory.Delete(tempPath, true);
throw;
}
}
[TestMethod]
public async Task TestFileExists()
{
@@ -12,9 +12,9 @@ namespace Tgstation.Server.Host.System.Tests
[TestClass]
public sealed class TestSymlinkFactory
{
readonly ISymlinkFactory factory = new PlatformIdentifier().IsWindows
? new WindowsSymlinkFactory()
: new PosixSymlinkFactory();
readonly IFilesystemLinkFactory factory = new PlatformIdentifier().IsWindows
? new WindowsFilesystemLinkFactory()
: new PosixFilesystemLinkFactory();
[TestMethod]
public async Task TestSymlinks()
@@ -43,6 +43,7 @@ namespace Tgstation.Server.Tests.Live.Instance
ushort ddPort,
bool highPrioDD,
bool lowPrioDeployment,
bool usingBasicWatchdog,
CancellationToken cancellationToken)
{
var byondTest = new ByondTest(instanceClient.Byond, instanceClient.Jobs, fileDownloader, instanceClient.Metadata);
@@ -67,7 +68,7 @@ namespace Tgstation.Server.Tests.Live.Instance
await byondTask;
await new WatchdogTest(
await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken), instanceClient, instanceManager, serverPort, highPrioDD, ddPort).Run(cancellationToken);
await ByondTest.GetEdgeVersion(fileDownloader, cancellationToken), instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog).Run(cancellationToken);
}
public async Task RunCompatTests(
@@ -76,6 +77,7 @@ namespace Tgstation.Server.Tests.Live.Instance
ushort dmPort,
ushort ddPort,
bool highPrioDD,
bool usingBasicWatchdog,
CancellationToken cancellationToken)
{
System.Console.WriteLine($"COMPAT TEST START: {compatVersion}");
@@ -189,7 +191,7 @@ namespace Tgstation.Server.Tests.Live.Instance
await configSetupTask;
await new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort).Run(cancellationToken);
await new WatchdogTest(compatVersion, instanceClient, instanceManager, serverPort, highPrioDD, ddPort, usingBasicWatchdog).Run(cancellationToken);
await instanceManagerClient.Update(new InstanceUpdateRequest
{
@@ -3,17 +3,22 @@
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Mono.Unix;
using Mono.Unix.Native;
using Moq;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
@@ -58,10 +63,11 @@ namespace Tgstation.Server.Tests.Live.Instance
readonly bool highPrioDD;
readonly TopicClient topicClient;
readonly Version testVersion;
readonly bool usingBasicWatchdog;
bool ranTimeoutTest = false;
public WatchdogTest(Version testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort)
public WatchdogTest(Version testVersion, IInstanceClient instanceClient, InstanceManager instanceManager, ushort serverPort, bool highPrioDD, ushort ddPort, bool usingBasicWatchdog)
: base(instanceClient.Jobs)
{
this.instanceClient = instanceClient ?? throw new ArgumentNullException(nameof(instanceClient));
@@ -70,8 +76,9 @@ namespace Tgstation.Server.Tests.Live.Instance
this.highPrioDD = highPrioDD;
this.ddPort = ddPort;
this.testVersion = testVersion ?? throw new ArgumentNullException(nameof(testVersion));
this.usingBasicWatchdog = usingBasicWatchdog;
this.topicClient = new(new SocketParameters
topicClient = new(new SocketParameters
{
SendTimeout = TimeSpan.FromSeconds(30),
ReceiveTimeout = TimeSpan.FromSeconds(30),
@@ -471,6 +478,47 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreEqual(string.Empty, daemonStatus.AdditionalParameters);
}
void TestLinuxIsntBeingFuckingCheekyAboutFilePaths(DreamDaemonResponse currentStatus, CompileJobResponse previousStatus)
{
if (new PlatformIdentifier().IsWindows || usingBasicWatchdog)
return;
Assert.IsNotNull(currentStatus.ActiveCompileJob);
Assert.IsTrue(currentStatus.ActiveCompileJob.DmeName.Contains("long_running_test"));
Assert.AreEqual(WatchdogStatus.Online, currentStatus.Status);
var procs = TestLiveServer.GetDDProcessesOnPort(currentStatus.Port.Value);
Assert.AreEqual(1, procs.Count);
var failingLinks = new List<string>();
using var proc = procs[0];
var pid = proc.Id;
var foundLivePath = false;
var allPaths = new List<string>();
foreach (var fd in Directory.EnumerateFiles($"/proc/{pid}/fd"))
{
var sb = new StringBuilder(UInt16.MaxValue);
if (Syscall.readlink(fd, sb) == -1)
throw new UnixIOException(Stdlib.GetLastError());
var path = sb.ToString();
allPaths.Add($"Path: {path}");
if (path.Contains($"Game/{previousStatus.DirectoryName}"))
failingLinks.Add($"Found fd {fd} resolving to previous absolute path game dir path: {path}");
if (path.Contains($"Game/{currentStatus.ActiveCompileJob.DirectoryName}"))
failingLinks.Add($"Found fd {fd} resolving to current absolute path game dir path: {path}");
if (path.Contains($"Game/Live"))
foundLivePath = true;
}
if (!foundLivePath)
failingLinks.Add($"Failed to find a path containing the 'Live' directory!");
Assert.IsTrue(failingLinks.Count == 0, String.Join(Environment.NewLine, failingLinks.Concat(allPaths)));
}
async Task RunHealthCheckTest(bool checkDump, CancellationToken cancellationToken)
{
System.Console.WriteLine("TEST: WATCHDOG HEALTH CHECK TEST");
@@ -901,6 +949,8 @@ namespace Tgstation.Server.Tests.Live.Instance
Assert.AreNotEqual(initialCompileJob.Id, daemonStatus.ActiveCompileJob.Id);
Assert.IsNull(daemonStatus.StagedCompileJob);
TestLinuxIsntBeingFuckingCheekyAboutFilePaths(daemonStatus, initialCompileJob);
await instanceClient.DreamDaemon.Shutdown(cancellationToken);
daemonStatus = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -63,6 +63,8 @@ namespace Tgstation.Server.Tests.Live
public bool HighPriorityDreamDaemon { get; }
public bool LowPriorityDeployments { get; }
public bool UsingBasicWatchdog { get; }
public bool RestartRequested => RealServer.RestartRequested;
readonly List<string> args;
@@ -89,6 +91,8 @@ namespace Tgstation.Server.Tests.Live
var gitHubAccessToken = Environment.GetEnvironmentVariable("TGS_TEST_GITHUB_TOKEN");
var dumpOpenAPISpecPathEnvVar = Environment.GetEnvironmentVariable("TGS_TEST_DUMP_API_SPEC");
UsingBasicWatchdog = Boolean.TryParse(Environment.GetEnvironmentVariable("General__UseBasicWatchdog"), out var result) && result;
if (String.IsNullOrEmpty(DatabaseType))
Assert.Inconclusive("No database type configured in env var TGS_TEST_DATABASE_TYPE!");
@@ -1326,8 +1326,6 @@ namespace Tgstation.Server.Tests.Live
async Task RunInstanceTests()
{
// Some earlier linux BYOND versions have a critical bug where replacing the directory in non-basic watchdogs causes the DreamDaemon cwd to change
var canRunCompatTests = new PlatformIdentifier().IsWindows;
var compatTests = FailFast(
instanceTest
.RunCompatTests(
@@ -1338,6 +1336,7 @@ namespace Tgstation.Server.Tests.Live
compatDMPort,
compatDDPort,
server.HighPriorityDreamDaemon,
server.UsingBasicWatchdog,
cancellationToken));
if (TestingUtils.RunningInGitHubActions) // they only have 2 cores, can't handle intense parallelization
@@ -1351,6 +1350,7 @@ namespace Tgstation.Server.Tests.Live
mainDDPort,
server.HighPriorityDreamDaemon,
server.LowPriorityDeployments,
server.UsingBasicWatchdog,
cancellationToken));
await compatTests;
@@ -1531,7 +1531,7 @@ namespace Tgstation.Server.Tests.Live
Assert.AreEqual(WatchdogStatus.Online, dd.Status.Value);
var compileJob = await instanceClient.DreamMaker.Compile(cancellationToken);
var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog);
await wdt.WaitForJob(compileJob, 30, false, null, cancellationToken);
dd = await instanceClient.DreamDaemon.Read(cancellationToken);
@@ -1578,7 +1578,7 @@ namespace Tgstation.Server.Tests.Live
Assert.AreEqual(WatchdogStatus.Online, currentDD.Status);
Assert.AreEqual(expectedStaged, currentDD.StagedCompileJob.Job.Id.Value);
var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort);
var wdt = new WatchdogTest(edgeByond, instanceClient, GetInstanceManager(), (ushort)server.Url.Port, server.HighPriorityDreamDaemon, mainDDPort, server.UsingBasicWatchdog);
currentDD = await wdt.TellWorldToReboot(cancellationToken);
Assert.AreEqual(expectedStaged, currentDD.ActiveCompileJob.Job.Id.Value);
Assert.IsNull(currentDD.StagedCompileJob);
+1
View File
@@ -158,6 +158,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LongRunning", "LongRunning"
tests\DMAPI\LongRunning\long_running_test.dme = tests\DMAPI\LongRunning\long_running_test.dme
tests\DMAPI\LongRunning\long_running_test_copy.dme = tests\DMAPI\LongRunning\long_running_test_copy.dme
tests\DMAPI\LongRunning\long_running_test_rooted.dme = tests\DMAPI\LongRunning\long_running_test_rooted.dme
tests\DMAPI\LongRunning\resource.txt = tests\DMAPI\LongRunning\resource.txt
tests\DMAPI\LongRunning\Test.dm = tests\DMAPI\LongRunning\Test.dm
EndProjectSection
EndProject