From 0e4f78a5c247c4c410c78ee3ee160a1ab2879966 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 11 Jun 2020 16:13:06 -0400 Subject: [PATCH 01/25] Fix cross instance chat bot leak --- src/Tgstation.Server.Host/Controllers/ChatController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Controllers/ChatController.cs b/src/Tgstation.Server.Host/Controllers/ChatController.cs index 153406d210..619d4c3620 100644 --- a/src/Tgstation.Server.Host/Controllers/ChatController.cs +++ b/src/Tgstation.Server.Host/Controllers/ChatController.cs @@ -197,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers { var query = DatabaseContext.ChatBots .AsQueryable() - .Where(x => x.Id == id) + .Where(x => x.Id == id && x.InstanceId == Instance.Id) .Include(x => x.Channels); var results = await query.FirstOrDefaultAsync(cancellationToken).ConfigureAwait(false); From 750fe3c2613aeedc66e7eceaa82c307b7987415c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 11 Jun 2020 18:14:01 -0400 Subject: [PATCH 02/25] Fix potential shutdown deadlock - Prevent deadlock related to chat bot automatic reconnection --- .../Components/Chat/Providers/DiscordProvider.cs | 2 +- .../Components/Chat/Providers/IProvider.cs | 2 +- .../Components/Chat/Providers/IrcProvider.cs | 2 +- .../Components/Chat/Providers/Provider.cs | 13 ++++++++++++- 4 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index ba23173db1..b2b5c7c86c 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -177,7 +177,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } /// - public override async Task Disconnect(CancellationToken cancellationToken) + protected override async Task DisconnectImpl(CancellationToken cancellationToken) { Logger.LogTrace("Disconnecting..."); if (!Connected) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs index c06c22844f..cca006f663 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IProvider.cs @@ -37,7 +37,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Task Connect(CancellationToken cancellationToken); /// - /// Gracefully disconnects the provider. Implies a call to + /// Gracefully disconnects the provider. Permanently stops the reconnection timer. /// /// The for the operation /// A representing the running operation diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs index 67e85e152a..014a5d9d63 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/IrcProvider.cs @@ -346,7 +346,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current); /// - public override async Task Disconnect(CancellationToken cancellationToken) + protected override async Task DisconnectImpl(CancellationToken cancellationToken) { if (!Connected) return; diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs index 9f686bedca..070db5a8ef 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/Provider.cs @@ -87,8 +87,19 @@ namespace Tgstation.Server.Host.Components.Chat.Providers /// public abstract Task Connect(CancellationToken cancellationToken); + /// + /// Gracefully disconnects the provider. + /// + /// The for the operation. + /// A representing the running operation. + protected abstract Task DisconnectImpl(CancellationToken cancellationToken); + /// - public abstract Task Disconnect(CancellationToken cancellationToken); + public async Task Disconnect(CancellationToken cancellationToken) + { + await StopReconnectionTimer().ConfigureAwait(false); + await DisconnectImpl(cancellationToken).ConfigureAwait(false); + } /// public abstract Task> MapChannels(IEnumerable channels, CancellationToken cancellationToken); From 9b9ecd622388f183869669e53f14173d1eea969f Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Thu, 11 Jun 2020 18:15:36 -0400 Subject: [PATCH 03/25] Version bump to 4.3.3 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index 2a24e56613..4ed6782372 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,7 +2,7 @@ - 4.3.2 + 4.3.3 6.6.0 7.2.0 5.2.2 From ccf5e4a8d4afd8bcc9f07fb76370a345a481a354 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 15:02:09 -0400 Subject: [PATCH 04/25] Add a message for users who have embeds disabled. --- .../Components/Chat/Providers/DiscordProvider.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index b2b5c7c86c..48e3c2ebba 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -350,7 +350,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers } var message = await channel.SendMessageAsync( - String.Empty, + "DM: Deployment in Progress...", false, builder.Build(), new RequestOptions @@ -361,7 +361,8 @@ namespace Tgstation.Server.Host.Components.Chat.Providers return async (errorMessage, dreamMakerOutput) => { - builder.Footer.Text = errorMessage == null ? "Succeeded" : "Failed"; + var completionString = errorMessage == null ? "Succeeded" : "Failed"; + builder.Footer.Text = completionString; builder.Color = errorMessage == null ? Color.Green : Color.Red; builder.Timestamp = DateTimeOffset.Now; builder.Description = errorMessage == null @@ -394,7 +395,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers try { await channel.SendMessageAsync( - String.Empty, + $"DM: Deployment {completionString}!", false, builder.Build()) .ConfigureAwait(false); From 01826087e67cfc4342ff764d252185fc7e1b7aab Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 15:02:56 -0400 Subject: [PATCH 05/25] Don't show ETA in deploy embeds if there isn't one --- .../Components/Chat/Providers/DiscordProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs index 48e3c2ebba..2a82485356 100644 --- a/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs +++ b/src/Tgstation.Server.Host/Components/Chat/Providers/DiscordProvider.cs @@ -337,7 +337,7 @@ namespace Tgstation.Server.Host.Components.Chat.Providers Title = "Code Deployment", Footer = new EmbedFooterBuilder { - Text = "In progress... ETA" + Text = $"In progress...{(estimatedCompletionTime.HasValue ? " ETA" : String.Empty)}" }, Timestamp = estimatedCompletionTime }; From 541c0effb0a563b944bbbc7c8d2d1afd714bd3a1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 15:04:36 -0400 Subject: [PATCH 06/25] Version bump to 4.3.4 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index 4ed6782372..2101f58ad7 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,7 +2,7 @@ - 4.3.3 + 4.3.4 6.6.0 7.2.0 5.2.2 From c895c3c15508e83d0c60daf44d12c7371018555b Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 16:32:25 -0400 Subject: [PATCH 07/25] Enable database context pooling --- .../Components/InstanceManager.cs | 12 +- src/Tgstation.Server.Host/Core/Application.cs | 16 ++- .../Database/DatabaseContext.cs | 103 ++++++------------ .../Database/DatabaseSeeder.cs | 99 +++++++++++++++-- .../Design/MySqlDesignTimeDbContextFactory.cs | 33 ------ .../PostgresSqlDesignTimeDbContextFactory.cs | 31 ------ .../SqlServerDesignTimeDbContextFactory.cs | 33 ------ .../SqliteDesignTimeDbContextFactory.cs | 19 +--- .../Database/IDatabaseContext.cs | 26 +++-- .../Database/IDatabaseSeeder.cs | 30 ++--- .../Database/MySqlDatabaseContext.cs | 46 ++++---- .../Database/PostgresSqlDatabaseContext.cs | 42 ++++--- .../Database/SqlServerDatabaseContext.cs | 31 +++--- .../Database/SqliteDatabaseContext.cs | 42 +++---- 14 files changed, 260 insertions(+), 303 deletions(-) delete mode 100644 src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs delete mode 100644 src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs delete mode 100644 src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs diff --git a/src/Tgstation.Server.Host/Components/InstanceManager.cs b/src/Tgstation.Server.Host/Components/InstanceManager.cs index e0045ccbc9..d76ab47826 100644 --- a/src/Tgstation.Server.Host/Components/InstanceManager.cs +++ b/src/Tgstation.Server.Host/Components/InstanceManager.cs @@ -67,6 +67,11 @@ namespace Tgstation.Server.Host.Components /// readonly IAsyncDelayer asyncDelayer; + /// + /// The for the + /// + readonly IDatabaseSeeder databaseSeeder; + /// /// The for the /// @@ -113,6 +118,7 @@ namespace Tgstation.Server.Host.Components /// The value of /// The value of . /// The value of . + /// The value of . /// The containing the value of . /// The value of public InstanceManager( @@ -124,6 +130,7 @@ namespace Tgstation.Server.Host.Components IServerControl serverControl, ISystemIdentityFactory systemIdentityFactory, IAsyncDelayer asyncDelayer, + IDatabaseSeeder databaseSeeder, IOptions generalConfigurationOptions, ILogger logger) { @@ -135,6 +142,7 @@ namespace Tgstation.Server.Host.Components this.serverControl = serverControl ?? throw new ArgumentNullException(nameof(serverControl)); this.systemIdentityFactory = systemIdentityFactory ?? throw new ArgumentNullException(nameof(systemIdentityFactory)); this.asyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); generalConfiguration = generalConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(generalConfigurationOptions)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); @@ -274,7 +282,7 @@ namespace Tgstation.Server.Host.Components { CheckSystemCompatibility(); var factoryStartup = instanceFactory.StartAsync(cancellationToken); - await databaseContext.Initialize(cancellationToken).ConfigureAwait(false); + await databaseSeeder.Initialize(databaseContext, cancellationToken).ConfigureAwait(false); await jobManager.StartAsync(cancellationToken).ConfigureAwait(false); var dbInstances = databaseContext .Instances @@ -323,7 +331,7 @@ namespace Tgstation.Server.Host.Components // downgrade the db if necessary if (downgradeVersion != null) - await databaseContextFactory.UseContext(db => db.SchemaDowngradeForServerVersion(downgradeVersion, cancellationToken)).ConfigureAwait(false); + await databaseContextFactory.UseContext(db => databaseSeeder.Downgrade(db, downgradeVersion, cancellationToken)).ConfigureAwait(false); } /// diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 8bc5133bc8..1883a4bf71 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -18,6 +18,7 @@ using Serilog.Formatting.Display; using System; using System.Globalization; using System.IdentityModel.Tokens.Jwt; +using System.Reflection; using System.Threading.Tasks; using Tgstation.Server.Api; using Tgstation.Server.Api.Models; @@ -207,10 +208,23 @@ namespace Tgstation.Server.Host.Core void AddTypedContext() where TContext : DatabaseContext { - services.AddDbContext(builder => + // HACK HACK HACK HACK HACK + const string ConfigureMethodName = nameof(SqlServerDatabaseContext.ConfigureWith); + var configureFunction = typeof(TContext).GetMethod( + nameof(SqlServerDatabaseContext.ConfigureWith), + BindingFlags.Public | BindingFlags.Static); + + if (configureFunction == null) + throw new InvalidOperationException($"Context type {typeof(TContext).FullName} missing static {ConfigureMethodName} function!"); + + services.AddDbContextPool((serviceProvider, builder) => { if (hostingEnvironment.IsDevelopment()) builder.EnableSensitiveDataLogging(); + + var databaseConfigOptions = serviceProvider.GetRequiredService>(); + var databaseConfig = databaseConfigOptions.Value ?? throw new InvalidOperationException("DatabaseConfiguration missing!"); + configureFunction.Invoke(null, new object[] { builder, databaseConfig }); }); services.AddScoped(x => x.GetRequiredService()); } diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index b75baf3e09..25243ea7e4 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -3,7 +3,6 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Globalization; using System.Linq; @@ -19,11 +18,8 @@ namespace Tgstation.Server.Host.Database /// Backend abstract implementation of /// #pragma warning disable CA1506 // TODO: Decomplexify - abstract class DatabaseContext : DbContext, IDatabaseContext + public abstract class DatabaseContext : DbContext, IDatabaseContext { - /// - public DatabaseType DatabaseType => DatabaseConfiguration.DatabaseType; - /// /// The s in the . /// @@ -99,16 +95,6 @@ namespace Tgstation.Server.Host.Database /// public DbSet RevInfoTestMerges { get; set; } - /// - /// The for the - /// - protected ILogger Logger { get; } - - /// - /// The for the - /// - protected DatabaseConfiguration DatabaseConfiguration { get; } - /// /// The for the / foreign key. /// @@ -153,11 +139,6 @@ namespace Tgstation.Server.Host.Database /// IDatabaseCollection IDatabaseContext.WatchdogReattachInformations => watchdogReattachInformationsCollection; - /// - /// The for the - /// - readonly IDatabaseSeeder databaseSeeder; - /// /// Backing field for . /// @@ -227,15 +208,8 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the . - /// The containing the value of - /// The value of - /// The value of - public DatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfigurationOptions, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions) + public DatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { - DatabaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); - this.databaseSeeder = databaseSeeder ?? throw new ArgumentNullException(nameof(databaseSeeder)); - Logger = logger ?? throw new ArgumentNullException(nameof(logger)); - usersCollection = new DatabaseCollection(Users); instancesCollection = new DatabaseCollection(Instances); instanceUsersCollection = new DatabaseCollection(InstanceUsers); @@ -254,8 +228,9 @@ namespace Tgstation.Server.Host.Database /// protected override void OnModelCreating(ModelBuilder modelBuilder) { - // Setup our more complex database relations - Logger.LogTrace("Building entity framework context..."); + if (modelBuilder == null) + throw new ArgumentNullException(nameof(modelBuilder)); + base.OnModelCreating(modelBuilder); var userModel = modelBuilder.Entity(); @@ -306,52 +281,41 @@ namespace Tgstation.Server.Host.Database } /// - public async Task Initialize(CancellationToken cancellationToken) + public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken); + + /// + public Task Drop(CancellationToken cancellationToken) => Database.EnsureCreatedAsync(cancellationToken); + + /// + public async Task Migrate(ILogger logger, CancellationToken cancellationToken) { - ValidateDatabaseType(); - - if (DatabaseConfiguration.DropDatabase) - { - Logger.LogCritical("DropDatabase configuration option set! Dropping any existing database..."); - await Database.EnsureDeletedAsync(cancellationToken).ConfigureAwait(false); - } - + if (logger == null) + throw new ArgumentNullException(nameof(logger)); var migrations = await Database.GetAppliedMigrationsAsync(cancellationToken).ConfigureAwait(false); var wasEmpty = !migrations.Any(); if (wasEmpty || (await Database.GetPendingMigrationsAsync(cancellationToken).ConfigureAwait(false)).Any()) { - Logger.LogInformation("Migrating database..."); + logger.LogInformation("Migrating database..."); await Database.MigrateAsync(cancellationToken).ConfigureAwait(false); } else - Logger.LogDebug("No migrations to apply."); + logger.LogDebug("No migrations to apply"); wasEmpty |= (await Users.AsQueryable().CountAsync(cancellationToken).ConfigureAwait(false)) == 0; - if (wasEmpty) - { - Logger.LogInformation("Seeding database..."); - await databaseSeeder.SeedDatabase(this, cancellationToken).ConfigureAwait(false); - } - else - { - if (DatabaseConfiguration.ResetAdminPassword) - { - Logger.LogWarning("Enabling and resetting admin password due to configuration!"); - await databaseSeeder.ResetAdminPassword(this, cancellationToken).ConfigureAwait(false); - } - - await databaseSeeder.SanitizeDatabase(this, cancellationToken).ConfigureAwait(false); - } + return wasEmpty; } /// - public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken); - - /// - public async Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken) + public async Task SchemaDowngradeForServerVersion( + ILogger logger, + Version version, + DatabaseType currentDatabaseType, + CancellationToken cancellationToken) { + if(logger == null) + throw new ArgumentNullException(nameof(logger)); if (version == null) throw new ArgumentNullException(nameof(version)); if (version < new Version(4, 0)) @@ -360,23 +324,23 @@ namespace Tgstation.Server.Host.Database // Update this with new migrations as they are made string targetMigration = null; - if (DatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0)) + if (currentDatabaseType == DatabaseType.PostgresSql && version < new Version(4, 3, 0)) throw new NotSupportedException("Cannot migrate below version 4.3.0 with PostgresSql!"); if (version < new Version(4, 1, 0)) throw new NotSupportedException("Cannot migrate below version 4.1.0!"); if (version < new Version(4, 2, 0)) - targetMigration = DatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete); + targetMigration = currentDatabaseType == DatabaseType.Sqlite ? nameof(SLRebuild) : nameof(MSFixCascadingDelete); if (targetMigration == null) { - Logger.LogDebug("No down migration required."); + logger.LogDebug("No down migration required."); return; } string migrationSubstitution; - switch (DatabaseType) + switch (currentDatabaseType) { case DatabaseType.SqlServer: // already setup @@ -393,7 +357,7 @@ namespace Tgstation.Server.Host.Database migrationSubstitution = "PG{0}"; break; default: - throw new InvalidOperationException($"Invalid DatabaseType: {DatabaseType}"); + throw new InvalidOperationException($"Invalid DatabaseType: {currentDatabaseType}"); } if (migrationSubstitution != null) @@ -403,20 +367,15 @@ namespace Tgstation.Server.Host.Database var dbServiceProvider = ((IInfrastructure)Database).Instance; var migrator = dbServiceProvider.GetRequiredService(); - Logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration); + logger.LogInformation("Migrating down to version {0}. Target: {1}", version, targetMigration); try { await migrator.MigrateAsync(targetMigration, cancellationToken).ConfigureAwait(false); } catch (Exception e) { - Logger.LogCritical("Failed to migrate! Exception: {0}", e); + logger.LogCritical("Failed to migrate! Exception: {0}", e); } } - - /// - /// Ensure the is correct for the . - /// - protected abstract void ValidateDatabaseType(); } } diff --git a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs index e7173b0067..4bbc995197 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseSeeder.cs @@ -1,9 +1,12 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using System; using System.Linq; using System.Threading; using System.Threading.Tasks; using Tgstation.Server.Api.Rights; +using Tgstation.Server.Host.Configuration; using Tgstation.Server.Host.Models; using Tgstation.Server.Host.Security; using Tgstation.Server.Host.System; @@ -23,15 +26,41 @@ namespace Tgstation.Server.Host.Database /// readonly IPlatformIdentifier platformIdentifier; + /// + /// The used for s. + /// + readonly ILogger databaseLogger; + + /// + /// The for the . + /// + readonly ILogger logger; + + /// + /// The for the . + /// + readonly DatabaseConfiguration databaseConfiguration; + /// /// Construct a /// /// The value of /// The value of . - public DatabaseSeeder(ICryptographySuite cryptographySuite, IPlatformIdentifier platformIdentifier) + /// The containing the value of . + /// The value of + /// The value of . + public DatabaseSeeder( + ICryptographySuite cryptographySuite, + IPlatformIdentifier platformIdentifier, + IOptions databaseConfigurationOptions, + ILogger databaseLogger, + ILogger logger) { this.cryptographySuite = cryptographySuite ?? throw new ArgumentNullException(nameof(cryptographySuite)); this.platformIdentifier = platformIdentifier ?? throw new ArgumentNullException(nameof(platformIdentifier)); + databaseConfiguration = databaseConfigurationOptions?.Value ?? throw new ArgumentNullException(nameof(databaseConfigurationOptions)); + this.databaseLogger = databaseLogger ?? throw new ArgumentNullException(nameof(databaseLogger)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } /// @@ -53,15 +82,25 @@ namespace Tgstation.Server.Host.Database databaseContext.Users.Add(admin); } - /// - public async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) + /// + /// Initially seed a given + /// + /// The to seed + /// The for the operation + /// A representing the running operation + async Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) { SeedAdminUser(databaseContext); await databaseContext.Save(cancellationToken).ConfigureAwait(false); } - /// - public async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) + /// + /// Correct invalid database data caused by previous versions. + /// + /// The to sanitize. + /// The for the operation. + /// A representing the running operation. + async Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken) { var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false); if (admin != null) @@ -88,8 +127,13 @@ namespace Tgstation.Server.Host.Database await databaseContext.Save(cancellationToken).ConfigureAwait(false); } - /// - public async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken) + /// + /// Changes the admin password in back to it's default and enables the account + /// + /// The to reset the admin password for + /// The for the operation + /// A representing the running operation + async Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken) { var admin = await GetAdminUser(databaseContext, cancellationToken).ConfigureAwait(false); if (admin != null) @@ -120,5 +164,46 @@ namespace Tgstation.Server.Host.Database return admin; } + + /// + public async Task Initialize(IDatabaseContext databaseContext, CancellationToken cancellationToken) + { + if (databaseContext == null) + throw new ArgumentNullException(nameof(databaseContext)); + + if (databaseConfiguration.DropDatabase) + { + logger.LogCritical("DropDatabase configuration option set! Dropping any existing database..."); + await databaseContext.Drop(cancellationToken).ConfigureAwait(false); + } + + var wasEmpty = await databaseContext.Migrate(databaseLogger, cancellationToken).ConfigureAwait(false); + if (wasEmpty) + { + logger.LogInformation("Seeding database..."); + await SeedDatabase(databaseContext, cancellationToken).ConfigureAwait(false); + } + else + { + if (databaseConfiguration.ResetAdminPassword) + { + logger.LogWarning("Enabling and resetting admin password due to configuration!"); + await ResetAdminPassword(databaseContext, cancellationToken).ConfigureAwait(false); + } + + await SanitizeDatabase(databaseContext, cancellationToken).ConfigureAwait(false); + } + } + + /// + public Task Downgrade(IDatabaseContext databaseContext, Version downgradeVersion, CancellationToken cancellationToken) + { + if (databaseContext == null) + throw new ArgumentNullException(nameof(databaseContext)); + if (downgradeVersion == null) + throw new ArgumentNullException(nameof(downgradeVersion)); + + return databaseContext.SchemaDowngradeForServerVersion(databaseLogger, downgradeVersion, databaseConfiguration.DatabaseType, cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs deleted file mode 100644 index 5b39d9e63d..0000000000 --- a/src/Tgstation.Server.Host/Database/Design/MySqlDesignTimeDbContextFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Logging; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; - -namespace Tgstation.Server.Host.Database.Design -{ - /// - /// for creating s. - /// - sealed class MySqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory - { - /// - public MySqlDatabaseContext CreateDbContext(string[] args) - { - using var loggerFactory = new LoggerFactory(); - return new MySqlDatabaseContext( - new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( - DatabaseType.MariaDB, - "Server=127.0.0.1;User Id=root;Password=fake;Database=TGS_Design"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs deleted file mode 100644 index 2af7d7c939..0000000000 --- a/src/Tgstation.Server.Host/Database/Design/PostgresSqlDesignTimeDbContextFactory.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Logging; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; - -namespace Tgstation.Server.Host.Database.Design -{ - /// - sealed class PostgresSqlDesignTimeDbContextFactory : IDesignTimeDbContextFactory - { - /// - public PostgresSqlDatabaseContext CreateDbContext(string[] args) - { - using var loggerFactory = new LoggerFactory(); - return new PostgresSqlDatabaseContext( - new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( - DatabaseType.PostgresSql, - "Application Name=tgstation-server;Host=127.0.0.1;Password=qCkWimNgLfWwpr7TnUHs;Username=postgres;Database=TGS_Design"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs deleted file mode 100644 index 427d84ba8a..0000000000 --- a/src/Tgstation.Server.Host/Database/Design/SqlServerDesignTimeDbContextFactory.cs +++ /dev/null @@ -1,33 +0,0 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Design; -using Microsoft.Extensions.Logging; -using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; - -namespace Tgstation.Server.Host.Database.Design -{ - /// - /// for creating s. - /// - sealed class SqlServerDesignTimeDbContextFactory : IDesignTimeDbContextFactory - { - /// - public SqlServerDatabaseContext CreateDbContext(string[] args) - { - using var loggerFactory = new LoggerFactory(); - return new SqlServerDatabaseContext( - new DbContextOptions(), - DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( - DatabaseType.SqlServer, - "Data Source=fake;Initial Catalog=TGS_Design;Integrated Security=True;Application Name=tgstation-server"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); - } - } -} diff --git a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs index 91de1284f2..330cbc39f3 100644 --- a/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs +++ b/src/Tgstation.Server.Host/Database/Design/SqliteDesignTimeDbContextFactory.cs @@ -1,11 +1,7 @@ -using Microsoft.AspNetCore.Identity; -using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using Microsoft.Extensions.Logging; using Tgstation.Server.Host.Configuration; -using Tgstation.Server.Host.Models; -using Tgstation.Server.Host.Security; -using Tgstation.Server.Host.System; namespace Tgstation.Server.Host.Database.Design { @@ -18,16 +14,13 @@ namespace Tgstation.Server.Host.Database.Design public SqliteDatabaseContext CreateDbContext(string[] args) { using var loggerFactory = new LoggerFactory(); - return new SqliteDatabaseContext( - new DbContextOptions(), + var config = DesignTimeDbContextFactoryHelpers.GetDatabaseConfiguration( DatabaseType.Sqlite, - "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"), - new DatabaseSeeder( - new CryptographySuite( - new PasswordHasher()), - new PlatformIdentifier()), - loggerFactory.CreateLogger()); + "Data Source=tgs_design.sqlite3;Mode=ReadWriteCreate"); + SqliteDatabaseContext.DesignTime = config.Value.DesignTime; + return new SqliteDatabaseContext( + new DbContextOptions()); } } } diff --git a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs index 90c472d5b7..36ce44583b 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseContext.cs @@ -1,4 +1,5 @@ using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; using System; using System.Threading; using System.Threading.Tasks; @@ -12,11 +13,6 @@ namespace Tgstation.Server.Host.Database /// public interface IDatabaseContext { - /// - /// The . - /// - DatabaseType DatabaseType { get; } - /// /// The s in the /// @@ -89,19 +85,33 @@ namespace Tgstation.Server.Host.Database /// A representing the running operation Task Save(CancellationToken cancellationToken); + /// + /// Attempts to delete all tables and drop the database in use. + /// + /// The for the operation. + /// A representing the running operation. + Task Drop(CancellationToken cancellationToken); + /// /// Creates and migrates the /// + /// The to use. /// The for the operation - /// A representing the running operation - Task Initialize(CancellationToken cancellationToken); + /// A resulting in if the database should be seeded, otherwise. + Task Migrate(ILogger logger, CancellationToken cancellationToken); /// /// Attempt to downgrade the schema to the migration used for a given server /// + /// The to use. /// The tgstation-server that the schema should downgrade for + /// The in use. /// The for the operation /// A representing the running operation - Task SchemaDowngradeForServerVersion(Version version, CancellationToken cancellationToken); + Task SchemaDowngradeForServerVersion( + ILogger logger, + Version version, + DatabaseType currentDatabaseType, + CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs b/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs index 7ff210979a..8e218c5e61 100644 --- a/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs +++ b/src/Tgstation.Server.Host/Database/IDatabaseSeeder.cs @@ -1,35 +1,29 @@ -using System.Threading; +using System; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.Database { /// - /// For initially seeding a database + /// For initially setting up a database. /// interface IDatabaseSeeder { /// - /// Initially seed a given + /// Setup up a given . /// - /// The to seed - /// The for the operation - /// A representing the running operation - Task SeedDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken); - - /// - /// Correct invalid database data caused by previous versions. - /// - /// The to sanitize. + /// The to setup. /// The for the operation. /// A representing the running operation. - Task SanitizeDatabase(IDatabaseContext databaseContext, CancellationToken cancellationToken); + Task Initialize(IDatabaseContext databaseContext, CancellationToken cancellationToken); /// - /// Changes the admin password in back to it's default and enables the account + /// Migrate a given down. /// - /// The to reset the admin password for - /// The for the operation - /// A representing the running operation - Task ResetAdminPassword(IDatabaseContext databaseContext, CancellationToken cancellationToken); + /// The to downgrade. + /// The migration to downgrade the to. + /// The for the operation. + /// A representing the running operation. + Task Downgrade(IDatabaseContext databaseContext, Version downgradeVersion, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs index 78e936352c..46bd93309c 100644 --- a/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/MySqlDatabaseContext.cs @@ -1,7 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using MySql.Data.MySqlClient; using Pomelo.EntityFrameworkCore.MySql.Infrastructure; using System; using Tgstation.Server.Host.Configuration; @@ -20,42 +17,37 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the - public MySqlDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + public MySqlDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - var stringDeconstructor = new MySqlConnectionStringBuilder - { - ConnectionString = DatabaseConfiguration.ConnectionString - }; - if (stringDeconstructor.Server == "localhost") - Logger.LogWarning("MariaDB/MySQL server address is set to 'localhost'! If there are connection issues, try setting it to '127.0.0.1'!"); + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); + + if (databaseConfiguration.DatabaseType != DatabaseType.MariaDB && databaseConfiguration.DatabaseType != DatabaseType.MySql) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(MySqlDatabaseContext)}!"); + options.UseMySql( - DatabaseConfiguration.ConnectionString, + databaseConfiguration.ConnectionString, mySqlOptions => { mySqlOptions.EnableRetryOnFailure(); - if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion)) + if (!String.IsNullOrEmpty(databaseConfiguration.ServerVersion)) mySqlOptions.ServerVersion( - Version.Parse(DatabaseConfiguration.ServerVersion), - DatabaseConfiguration.DatabaseType == DatabaseType.MariaDB + Version.Parse(databaseConfiguration.ServerVersion), + databaseConfiguration.DatabaseType == DatabaseType.MariaDB ? ServerType.MariaDb : ServerType.MySql); }); } - - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.MariaDB && DatabaseType != DatabaseType.MySql) - throw new InvalidOperationException("Invalid DatabaseType for MySqlDatabaseContext!"); - } } } diff --git a/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs index 6514bc5333..090ec770ca 100644 --- a/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/PostgresSqlDatabaseContext.cs @@ -1,6 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using Tgstation.Server.Host.Configuration; @@ -18,36 +16,34 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the public PostgresSqlDatabaseContext( - DbContextOptions dbContextOptions, - IOptions databaseConfiguration, - IDatabaseSeeder databaseSeeder, - ILogger logger) - : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + DbContextOptions dbContextOptions) + : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - options.UseNpgsql(DatabaseConfiguration.ConnectionString, options => + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); + + if (databaseConfiguration.DatabaseType != DatabaseType.PostgresSql) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(PostgresSqlDatabaseContext)}!"); + + options.UseNpgsql(databaseConfiguration.ConnectionString, options => { options.EnableRetryOnFailure(); - if (!String.IsNullOrEmpty(DatabaseConfiguration.ServerVersion)) + if (!String.IsNullOrEmpty(databaseConfiguration.ServerVersion)) options.SetPostgresVersion( - Version.Parse(DatabaseConfiguration.ServerVersion)); + Version.Parse(databaseConfiguration.ServerVersion)); }); } - - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.PostgresSql) - throw new InvalidOperationException("Invalid DatabaseType for PostgresSqlDatabaseContext!"); - } } } diff --git a/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs index 42c6aedc4e..3b5eb77b83 100644 --- a/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqlServerDatabaseContext.cs @@ -1,6 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using Tgstation.Server.Host.Configuration; @@ -15,24 +13,25 @@ namespace Tgstation.Server.Host.Database /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the - public SqlServerDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + public SqlServerDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - options.UseSqlServer(DatabaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure()); - } + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.SqlServer) - throw new InvalidOperationException("Invalid DatabaseType for SqlServerDatabaseContext!"); + if (databaseConfiguration.DatabaseType != DatabaseType.SqlServer) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqlServerDatabaseContext)}!"); + + options.UseSqlServer(databaseConfiguration.ConnectionString, x => x.EnableRetryOnFailure()); } } } diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index 4d9fecf844..db2f500e28 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -1,7 +1,5 @@ using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; using System; using System.Linq; using Tgstation.Server.Host.Configuration; @@ -9,25 +7,38 @@ using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database { /// - /// for MySQL + /// for SQLite. /// sealed class SqliteDatabaseContext : DatabaseContext { + /// + /// Static property to receive the configured value of . + /// + public static bool DesignTime { get; set; } + /// /// Construct a /// /// The for the - /// The of for the - /// The for the - /// The for the - public SqliteDatabaseContext(DbContextOptions dbContextOptions, IOptions databaseConfiguration, IDatabaseSeeder databaseSeeder, ILogger logger) : base(dbContextOptions, databaseConfiguration, databaseSeeder, logger) + public SqliteDatabaseContext(DbContextOptions dbContextOptions) : base(dbContextOptions) { } - /// - protected override void OnConfiguring(DbContextOptionsBuilder options) + /// + /// Configure the . + /// + /// The to configure. + /// The . + public static void ConfigureWith(DbContextOptionsBuilder options, DatabaseConfiguration databaseConfiguration) { - base.OnConfiguring(options); - options.UseSqlite(DatabaseConfiguration.ConnectionString); + if (options == null) + throw new ArgumentNullException(nameof(options)); + if (databaseConfiguration == null) + throw new ArgumentNullException(nameof(databaseConfiguration)); + + if (databaseConfiguration.DatabaseType != DatabaseType.Sqlite) + throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqliteDatabaseContext)}!"); + + options.UseSqlite(databaseConfiguration.ConnectionString); } /// @@ -43,7 +54,7 @@ namespace Tgstation.Server.Host.Database // use the DateTimeOffsetToBinaryConverter // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 // This only supports millisecond precision, but should be sufficient for most use cases. - if (!DatabaseConfiguration.DesignTime) + if (DesignTime) foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var properties = entityType @@ -57,12 +68,5 @@ namespace Tgstation.Server.Host.Database .HasConversion(new DateTimeOffsetToBinaryConverter()); } } - - /// - protected override void ValidateDatabaseType() - { - if (DatabaseType != DatabaseType.Sqlite) - throw new InvalidOperationException("Invalid DatabaseType for SqliteDatabaseContext!"); - } } } From 45ff551a31f5cb098e1a726e573cae29f8dc8cc0 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 16:33:19 -0400 Subject: [PATCH 08/25] Version bump to 4.4.0 --- build/Version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/Version.props b/build/Version.props index 12ef300ad6..e68888800a 100644 --- a/build/Version.props +++ b/build/Version.props @@ -2,7 +2,7 @@ - 4.3.2 + 4.4.0 7.0.0 7.3.0 5.2.2 From c333f47ec9f5b63324f6fa733194935e904b5a44 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 16:57:07 -0400 Subject: [PATCH 09/25] Correct call to drop database --- src/Tgstation.Server.Host/Database/DatabaseContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Database/DatabaseContext.cs b/src/Tgstation.Server.Host/Database/DatabaseContext.cs index 25243ea7e4..17cfaa3323 100644 --- a/src/Tgstation.Server.Host/Database/DatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/DatabaseContext.cs @@ -284,7 +284,7 @@ namespace Tgstation.Server.Host.Database public Task Save(CancellationToken cancellationToken) => SaveChangesAsync(cancellationToken); /// - public Task Drop(CancellationToken cancellationToken) => Database.EnsureCreatedAsync(cancellationToken); + public Task Drop(CancellationToken cancellationToken) => Database.EnsureDeletedAsync(cancellationToken); /// public async Task Migrate(ILogger logger, CancellationToken cancellationToken) From 8bbc8cf5d8a29a2fd9e56d1c6a6e7e16fd32d7c9 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 17:04:56 -0400 Subject: [PATCH 10/25] Always explicitly set SqliteDatabaseContext.DesignTime --- src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index db2f500e28..502e05eded 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -38,6 +38,7 @@ namespace Tgstation.Server.Host.Database if (databaseConfiguration.DatabaseType != DatabaseType.Sqlite) throw new InvalidOperationException($"Invalid DatabaseType for {nameof(SqliteDatabaseContext)}!"); + DesignTime = databaseConfiguration.DesignTime; options.UseSqlite(databaseConfiguration.ConnectionString); } From 413ef4c32ff0ab858a7b8da5f98d280801da279a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Fri, 12 Jun 2020 18:06:15 -0400 Subject: [PATCH 11/25] Implement minidumps on Windows --- .../Models/DreamDaemon.cs | 5 +++ .../Rights/DreamDaemonRights.cs | 5 +++ src/Tgstation.Server.Api/Routes.cs | 5 +++ .../Session/DeadSessionController.cs | 3 ++ .../Components/Session/SessionController.cs | 3 ++ .../Components/Watchdog/IWatchdog.cs | 7 ++++ .../Components/Watchdog/WatchdogBase.cs | 23 +++++++++++++ .../Controllers/DreamDaemonController.cs | 32 +++++++++++++++++++ .../Extensions/DateTimeOffsetExtensions.cs | 19 +++++++++++ src/Tgstation.Server.Host/NativeMethods.cs | 24 +++++++++++++- .../System/IProcessBase.cs | 9 ++++++ .../System/IProcessFeatures.cs | 11 ++++++- .../System/PosixProcessFeatures.cs | 6 ++++ src/Tgstation.Server.Host/System/Process.cs | 10 ++++++ .../System/WindowsProcessFeatures.cs | 24 ++++++++++++++ 15 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs diff --git a/src/Tgstation.Server.Api/Models/DreamDaemon.cs b/src/Tgstation.Server.Api/Models/DreamDaemon.cs index 38e29363e0..8f2545af4f 100644 --- a/src/Tgstation.Server.Api/Models/DreamDaemon.cs +++ b/src/Tgstation.Server.Api/Models/DreamDaemon.cs @@ -48,5 +48,10 @@ namespace Tgstation.Server.Api.Models /// If the server is undergoing a soft shutdown /// public bool? SoftShutdown { get; set; } + + /// + /// If a dump of the active DreamDaemon executable should be created. + /// + public bool? CreateDump { get; set; } } } diff --git a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs index ed341547cd..fd4c53f641 100644 --- a/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs +++ b/src/Tgstation.Server.Api/Rights/DreamDaemonRights.cs @@ -77,5 +77,10 @@ namespace Tgstation.Server.Api.Rights /// User can change /// SetHeartbeatInterval = 4096, + + /// + /// User can create DreamDaemon process dumps. + /// + CreateDump = 8192, } } diff --git a/src/Tgstation.Server.Api/Routes.cs b/src/Tgstation.Server.Api/Routes.cs index 6a2da6c8f0..2bb3f4e603 100644 --- a/src/Tgstation.Server.Api/Routes.cs +++ b/src/Tgstation.Server.Api/Routes.cs @@ -43,6 +43,11 @@ namespace Tgstation.Server.Api /// public const string DreamDaemon = Root + nameof(Models.DreamDaemon); + /// + /// For accessing DD diagnostics + /// + public const string Diagnostics = DreamDaemon + "/Diagnostics"; + /// /// The controller /// diff --git a/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs b/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs index 2f5ff7f5c4..a425b5aac3 100644 --- a/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/DeadSessionController.cs @@ -123,5 +123,8 @@ namespace Tgstation.Server.Host.Components.Session /// public Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken) => Task.CompletedTask; + + /// + public Task CreateDump(string outputFile, CancellationToken cancellationToken) => throw new NotSupportedException(); } } diff --git a/src/Tgstation.Server.Host/Components/Session/SessionController.cs b/src/Tgstation.Server.Host/Components/Session/SessionController.cs index dca20a3a14..47cbac6bd8 100644 --- a/src/Tgstation.Server.Host/Components/Session/SessionController.cs +++ b/src/Tgstation.Server.Host/Components/Session/SessionController.cs @@ -696,5 +696,8 @@ namespace Tgstation.Server.Host.Components.Session new TopicParameters( new ChatUpdate(newChannels)), cancellationToken); + + /// + public Task CreateDump(string outputFile, CancellationToken cancellationToken) => process.CreateDump(outputFile, cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs index 6c40b1bd97..bc2fdbd8a7 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdog.cs @@ -80,5 +80,12 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the operation /// A representing the running operation Task ResetRebootState(CancellationToken cancellationToken); + + /// + /// Attempt to create a process dump for DreamDaemon. + /// + /// The for the operation. + /// A representing the running operation. + Task CreateDump(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index 6f2b0341d1..af48e18841 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -18,6 +18,7 @@ using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; using Tgstation.Server.Host.Extensions; +using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Watchdog @@ -114,6 +115,11 @@ namespace Tgstation.Server.Host.Components.Watchdog /// readonly IRestartRegistration restartRegistration; + /// + /// The pointing to the Diagnostics directory. + /// + readonly IIOManager diagnosticsIOManager; + /// /// used for . /// @@ -919,5 +925,22 @@ namespace Tgstation.Server.Host.Components.Watchdog /// public abstract Task InstanceRenamed(string newInstanceName, CancellationToken cancellationToken); + + /// + public async Task CreateDump(CancellationToken cancellationToken) + { + var session = GetActiveController(); + + const string DumpDirectory = "ProcessDumps"; + await diagnosticsIOManager.CreateDirectory(DumpDirectory, cancellationToken).ConfigureAwait(false); + + var dumpFileName = diagnosticsIOManager.ResolvePath( + diagnosticsIOManager.ConcatPath( + DumpDirectory, + $"DreamDaemon-{DateTimeOffset.Now.ToFileStamp()}.dmp")); + + Logger.LogInformation("Dumping session to {0}...", dumpFileName); + await session.CreateDump(dumpFileName, cancellationToken).ConfigureAwait(false); + } } } diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 707f1d9309..5ce0fdfb2c 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -290,5 +290,37 @@ namespace Tgstation.Server.Host.Controllers await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false); return Accepted(job.ToApi()); } + + /// + /// Creates a to generate a DreamDaemon process dump. + /// + /// The for the operation + /// A resulting in the of the request + /// Dump started successfully. + [HttpPost(Routes.Diagnostics)] + [TgsAuthorize(DreamDaemonRights.CreateDump)] + [ProducesResponseType(typeof(Api.Models.Job), 202)] + public async Task CreateDump(CancellationToken cancellationToken) + { + var job = new Models.Job + { + Instance = Instance, + CancelRightsType = RightsType.DreamDaemon, + CancelRight = (ulong)DreamDaemonRights.CreateDump, + StartedBy = AuthenticationContext.User, + Description = "Create DreamDaemon Process Dump" + }; + + var watchdog = instanceManager.GetInstance(Instance).Watchdog; + + if (!watchdog.Running) + return Conflict(new ErrorMessage(ErrorCode.WatchdogNotRunning)); + + await jobManager.RegisterOperation( + job, + (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.CreateDump(ct), cancellationToken) + .ConfigureAwait(false); + return Accepted(job.ToApi()); + } } } diff --git a/src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs b/src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs new file mode 100644 index 0000000000..f3f36d3b23 --- /dev/null +++ b/src/Tgstation.Server.Host/Extensions/DateTimeOffsetExtensions.cs @@ -0,0 +1,19 @@ +using System; +using System.Globalization; + +namespace Tgstation.Server.Host.Extensions +{ + /// + /// Extension methods for the . + /// + static class DateTimeOffsetExtensions + { + /// + /// Convert a given into a that can be used to stamp file creation times. + /// + /// The to convert. + /// as a file stamp . + public static string ToFileStamp(this DateTimeOffset dateTimeOffset) + => dateTimeOffset.ToString("yyyyMMddhhmmss", CultureInfo.InvariantCulture); + } +} diff --git a/src/Tgstation.Server.Host/NativeMethods.cs b/src/Tgstation.Server.Host/NativeMethods.cs index afc7211071..bdace24b63 100644 --- a/src/Tgstation.Server.Host/NativeMethods.cs +++ b/src/Tgstation.Server.Host/NativeMethods.cs @@ -5,7 +5,7 @@ using System.Text; namespace Tgstation.Server.Host { /// - /// Native Windows methods used by the code + /// Native Windows methods used by the code. /// #pragma warning disable SA1600 #pragma warning disable SA1602 @@ -32,6 +32,15 @@ namespace Tgstation.Server.Host SuspendResume = 0x0002, } + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/ne-minidumpapiset-minidump_type + /// + [Flags] + public enum MiniDumpType : uint + { + Normal = 0x00000000 + } + /// /// See https://docs.microsoft.com/en-us/windows/desktop/api/winuser/nf-winuser-getwindowthreadprocessid /// @@ -102,5 +111,18 @@ namespace Tgstation.Server.Host /// [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern uint ResumeThread(IntPtr hThread); + + /// + /// See https://docs.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump + /// + [DllImport("dbghelp.dll", SetLastError = true, CharSet = CharSet.Unicode)] + public static extern bool MiniDumpWriteDump( + IntPtr hProcess, + uint processId, + SafeHandle hFile, + MiniDumpType dumpType, + IntPtr expParam, + IntPtr userStreamParam, + IntPtr callbackParam); } } diff --git a/src/Tgstation.Server.Host/System/IProcessBase.cs b/src/Tgstation.Server.Host/System/IProcessBase.cs index a9013d567a..c861f8ae5f 100644 --- a/src/Tgstation.Server.Host/System/IProcessBase.cs +++ b/src/Tgstation.Server.Host/System/IProcessBase.cs @@ -1,4 +1,5 @@ using System; +using System.Threading; using System.Threading.Tasks; namespace Tgstation.Server.Host.System @@ -27,5 +28,13 @@ namespace Tgstation.Server.Host.System /// Resumes the process. /// void Resume(); + + /// + /// Create a dump file of the process. + /// + /// The full path to the output file. + /// The for the operation. + /// A representing the running operation. + Task CreateDump(string outputFile, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/System/IProcessFeatures.cs b/src/Tgstation.Server.Host/System/IProcessFeatures.cs index c56d03f5d0..b45d0831e0 100644 --- a/src/Tgstation.Server.Host/System/IProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/IProcessFeatures.cs @@ -17,7 +17,7 @@ namespace Tgstation.Server.Host.System Task GetExecutingUsername(global::System.Diagnostics.Process process, CancellationToken cancellationToken); /// - /// Suspend a given . + /// Suspend a given . /// /// The to suspend. void SuspendProcess(global::System.Diagnostics.Process process); @@ -27,5 +27,14 @@ namespace Tgstation.Server.Host.System /// /// The to susperesumend. void ResumeProcess(global::System.Diagnostics.Process process); + + /// + /// Create a dump file for a given . + /// + /// The to dump. + /// The full path to the output file. + /// The for the operation. + /// A representing the running operation. + Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 751bd47eff..452ce62af0 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -93,5 +93,11 @@ namespace Tgstation.Server.Host.System .FirstOrDefault(x => !String.IsNullOrWhiteSpace(x)) ?? "UNPARSABLE"; } + + /// + public Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } } } diff --git a/src/Tgstation.Server.Host/System/Process.cs b/src/Tgstation.Server.Host/System/Process.cs index 044f5fb34c..5f2c3cf5a6 100644 --- a/src/Tgstation.Server.Host/System/Process.cs +++ b/src/Tgstation.Server.Host/System/Process.cs @@ -165,5 +165,15 @@ namespace Tgstation.Server.Host.System logger.LogTrace("PID {0} Username: {1}", Id, result); return result; } + + /// + public Task CreateDump(string outputFile, CancellationToken cancellationToken) + { + if (outputFile == null) + throw new ArgumentNullException(nameof(outputFile)); + + logger.LogTrace("Dumping PID {0} to {1}...", Id, outputFile); + return processFeatures.CreateDump(handle, outputFile, cancellationToken); + } } } diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index 4ea3a49a74..a34bb41687 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -2,6 +2,7 @@ using Microsoft.Extensions.Logging; using System; using System.Diagnostics; +using System.IO; using System.Linq; using System.Management; using System.Threading; @@ -110,5 +111,28 @@ namespace Tgstation.Server.Host.System return Task.FromResult("NO OWNER"); } + + /// + public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + { + await Task.Factory.StartNew( + () => + { + using var fileStream = new FileStream(outputFile, FileMode.CreateNew); + if (!NativeMethods.MiniDumpWriteDump( + process.Handle, + (uint)process.Id, + fileStream.SafeFileHandle, + NativeMethods.MiniDumpType.Normal, + IntPtr.Zero, + IntPtr.Zero, + IntPtr.Zero)) + throw new Win32Exception(); + }, + cancellationToken, + TaskCreationOptions.LongRunning, + TaskScheduler.Current) + .ConfigureAwait(false); + } } } From d5b2bcf400341de4e8013c7d074dfaa0fb9d5858 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 13:02:03 -0400 Subject: [PATCH 12/25] Fixes bad documentation comment --- src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index 4d9fecf844..229a30a8dc 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -9,7 +9,7 @@ using Tgstation.Server.Host.Configuration; namespace Tgstation.Server.Host.Database { /// - /// for MySQL + /// for Sqlite. /// sealed class SqliteDatabaseContext : DatabaseContext { From 161a0fd67106d5e9f169e666f413be7970479343 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 13:22:14 -0400 Subject: [PATCH 13/25] Use gcore to create dumps on Linux --- README.md | 1 + build/Dockerfile | 3 +- src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 +++ src/Tgstation.Server.Host/Core/Application.cs | 4 ++ .../System/PosixProcessFeatures.cs | 41 +++++++++++++++++-- 5 files changed, 51 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 16bf6e737d..ecf0d06e30 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ The following dependencies are required to run tgstation-server on Linux alongsi - libc6-i386 - libstdc++6:i386 - libssl1.0.0 +- gdb (for using gcore to create core dumps) - gcc-multilib (Only on 64-bit systems) Note that tgstation-server has only ever been tested on Linux via it's [docker environment](build/Dockerfile#L22). If you are having trouble with something in a native installation, or figure out a required workaround, please contact project maintainers so this documentation may be better updated. diff --git a/build/Dockerfile b/build/Dockerfile index aa852c81e0..fa4e18131f 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -58,7 +58,8 @@ FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-bionic #needed for byond RUN apt-get update \ && apt-get install -y \ - gcc-multilib \ + gcc-multilib \ + gdb \ && rm -rf /var/lib/apt/lists/* EXPOSE 5000 diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index 1347fafed7..c7eb8b199c 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -519,5 +519,11 @@ namespace Tgstation.Server.Api.Models /// [Description("Cannot cancel the job as it is no longer running.")] JobStopped, + + /// + /// Missing GCore executable. + /// + [Description("Attempted to create a process dump but /usr/bin/gcore could not be located!")] + MissingGCore, } } \ No newline at end of file diff --git a/src/Tgstation.Server.Host/Core/Application.cs b/src/Tgstation.Server.Host/Core/Application.cs index 8bc5133bc8..5e96354cfb 100644 --- a/src/Tgstation.Server.Host/Core/Application.cs +++ b/src/Tgstation.Server.Host/Core/Application.cs @@ -273,7 +273,11 @@ namespace Tgstation.Server.Host.Core services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + + // PosixProcessFeatures also needs a IProcessExecutor for gcore + services.AddSingleton(x => new Lazy(() => x.GetRequiredService(), true)); services.AddSingleton(); } diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 452ce62af0..233d2daa4c 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -7,13 +7,20 @@ using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; +using Tgstation.Server.Api.Models; using Tgstation.Server.Host.IO; +using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.System { /// sealed class PosixProcessFeatures : IProcessFeatures { + /// + /// loaded . + /// + readonly Lazy lazyLoadedProcessExecutor; + /// /// The for the . /// @@ -27,10 +34,12 @@ namespace Tgstation.Server.Host.System /// /// Initializes a new instance of the . /// + /// The value of . /// The value of . /// The value of . - public PosixProcessFeatures(IIOManager ioManager, ILogger logger) + public PosixProcessFeatures(Lazy lazyLoadedProcessExecutor, IIOManager ioManager, ILogger logger) { + this.lazyLoadedProcessExecutor = lazyLoadedProcessExecutor ?? throw new ArgumentNullException(nameof(lazyLoadedProcessExecutor)); this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } @@ -95,9 +104,35 @@ namespace Tgstation.Server.Host.System } /// - public Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) { - throw new NotImplementedException(); + const string GCorePath = "/usr/bin/gcore"; + if (!await ioManager.FileExists(GCorePath, cancellationToken).ConfigureAwait(false)) + throw new JobException(ErrorCode.MissingGCore); + + string output; + int exitCode; + using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess( + GCorePath, + Environment.CurrentDirectory, + $"-o {outputFile} {process.Id}", + true, + true)) + { + using (cancellationToken.Register(() => gcoreProc.Terminate())) + exitCode = await gcoreProc.Lifetime.ConfigureAwait(false); + + output = gcoreProc.GetCombinedOutput(); + logger.LogDebug("gcore output:{0}{1}", Environment.NewLine, output); + } + + cancellationToken.ThrowIfCancellationRequested(); + + if (exitCode != 0) + throw new JobException( + ErrorCode.GCoreFailure, + new JobException( + $"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}")); } } } From b40b077b0a22b7196e288c790b3aa7eeeed6728a Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 13:22:37 -0400 Subject: [PATCH 14/25] Actually give WatchdogBase the Diagnostics IIOManager --- .../Components/InstanceFactory.cs | 2 ++ .../Components/Watchdog/BasicWatchdog.cs | 4 ++++ .../Watchdog/ExperimentalWatchdog.cs | 4 ++++ .../Components/Watchdog/IWatchdogFactory.cs | 6 ++++-- .../Components/Watchdog/WatchdogBase.cs | 3 +++ .../Components/Watchdog/WatchdogFactory.cs | 21 +++++++++++++++---- .../Components/Watchdog/WindowsWatchdog.cs | 17 ++++++++------- .../Watchdog/WindowsWatchdogFactory.cs | 6 ++++-- 8 files changed, 48 insertions(+), 15 deletions(-) diff --git a/src/Tgstation.Server.Host/Components/InstanceFactory.cs b/src/Tgstation.Server.Host/Components/InstanceFactory.cs index 844aa5c264..15876d33cd 100644 --- a/src/Tgstation.Server.Host/Components/InstanceFactory.cs +++ b/src/Tgstation.Server.Host/Components/InstanceFactory.cs @@ -202,6 +202,7 @@ namespace Tgstation.Server.Host.Components var repoIoManager = new ResolvingIOManager(instanceIoManager, "Repository"); var byondIOManager = new ResolvingIOManager(instanceIoManager, "Byond"); var gameIoManager = new ResolvingIOManager(instanceIoManager, "Game"); + var diagnosticsIOManager = new ResolvingIOManager(instanceIoManager, "Diagnostics"); var configurationIoManager = new ResolvingIOManager(instanceIoManager, "Configuration"); var configuration = new StaticFiles.Configuration(configurationIoManager, synchronousIOManager, symlinkFactory, processExecutor, postWriteHandler, platformIdentifier, loggerFactory.CreateLogger()); @@ -248,6 +249,7 @@ namespace Tgstation.Server.Host.Components reattachInfoHandler, sessionControllerFactory, gameIoManager, + diagnosticsIOManager, metadata.CloneMetadata(), metadata.DreamDaemonSettings); eventConsumer.SetWatchdog(watchdog); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs index 96fbbbdf30..73d7a74d22 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/BasicWatchdog.cs @@ -10,6 +10,7 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Watchdog @@ -49,6 +50,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . + /// The for the . /// The for the . /// The for the . /// The for the . @@ -62,6 +64,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, + IIOManager diagnosticsIOManager, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, @@ -75,6 +78,7 @@ namespace Tgstation.Server.Host.Components.Watchdog jobManager, serverControl, asyncDelayer, + diagnosticsIOManager, logger, initialLaunchParameters, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs index a399db4e8d..0addffef36 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/ExperimentalWatchdog.cs @@ -11,6 +11,7 @@ using Tgstation.Server.Host.Components.Deployment; using Tgstation.Server.Host.Components.Session; using Tgstation.Server.Host.Core; using Tgstation.Server.Host.Database; +using Tgstation.Server.Host.IO; using Tgstation.Server.Host.Jobs; namespace Tgstation.Server.Host.Components.Watchdog @@ -60,6 +61,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . + /// The for the . /// The for the . /// The for the . /// The for the . @@ -73,6 +75,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, + IIOManager diagnosticsIOManager, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, bool autoStart) @@ -85,6 +88,7 @@ namespace Tgstation.Server.Host.Components.Watchdog jobManager, serverControl, asyncDelayer, + diagnosticsIOManager, logger, initialLaunchParameters, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs index 4734c17f85..63f30ec362 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/IWatchdogFactory.cs @@ -18,7 +18,8 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the with /// The for the /// The for the - /// The for the . + /// The pointing to the Game directory for the . + /// The pointing to the Diagnostics directory for the . /// The for the /// The initial for the /// A new @@ -27,7 +28,8 @@ namespace Tgstation.Server.Host.Components.Watchdog IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, ISessionControllerFactory sessionControllerFactory, - IIOManager ioManager, + IIOManager gameIOManager, + IIOManager diagnosticsIOManager, Api.Models.Instance instance, DreamDaemonSettings settings); } diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs index af48e18841..d8e6139afa 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogBase.cs @@ -176,6 +176,7 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The value of /// The to populate with /// The value of . + /// The value of . /// The value of /// The initial value of . May be modified /// The value of @@ -189,6 +190,7 @@ namespace Tgstation.Server.Host.Components.Watchdog IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, + IIOManager diagnosticsIOManager, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, Api.Models.Instance instance, @@ -201,6 +203,7 @@ namespace Tgstation.Server.Host.Components.Watchdog this.databaseContextFactory = databaseContextFactory ?? throw new ArgumentNullException(nameof(databaseContextFactory)); this.jobManager = jobManager ?? throw new ArgumentNullException(nameof(jobManager)); AsyncDelayer = asyncDelayer ?? throw new ArgumentNullException(nameof(asyncDelayer)); + this.diagnosticsIOManager = diagnosticsIOManager ?? throw new ArgumentNullException(nameof(diagnosticsIOManager)); Logger = logger ?? throw new ArgumentNullException(nameof(logger)); ActiveLaunchParameters = initialLaunchParameters ?? throw new ArgumentNullException(nameof(initialLaunchParameters)); this.instance = instance ?? throw new ArgumentNullException(nameof(instance)); diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs index 7f5f08e88e..b7fcaaa3bd 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WatchdogFactory.cs @@ -77,7 +77,8 @@ namespace Tgstation.Server.Host.Components.Watchdog IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, ISessionControllerFactory sessionControllerFactory, - IIOManager ioManager, + IIOManager gameIOManager, + IIOManager diagnosticsIOManager, Api.Models.Instance instance, DreamDaemonSettings settings) { @@ -91,12 +92,21 @@ namespace Tgstation.Server.Host.Components.Watchdog JobManager, ServerControl, AsyncDelayer, + diagnosticsIOManager, LoggerFactory.CreateLogger(), settings, instance, settings.AutoStart.Value); - return CreateNonExperimentalWatchdog(chat, dmbFactory, reattachInfoHandler, sessionControllerFactory, ioManager, instance, settings); + return CreateNonExperimentalWatchdog( + chat, + dmbFactory, + reattachInfoHandler, + sessionControllerFactory, + gameIOManager, + diagnosticsIOManager, + instance, + settings); } /// @@ -106,7 +116,8 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the with /// The for the /// The for the - /// The for the . + /// The pointing to the Game directory for the . + /// The pointing to the Diagnostics directory for the . /// The for the /// The initial for the /// A new @@ -115,7 +126,8 @@ namespace Tgstation.Server.Host.Components.Watchdog IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, ISessionControllerFactory sessionControllerFactory, - IIOManager ioManager, + IIOManager gameIOManager, + IIOManager diagnosticsIOManager, Api.Models.Instance instance, DreamDaemonSettings settings) => new BasicWatchdog( @@ -127,6 +139,7 @@ namespace Tgstation.Server.Host.Components.Watchdog JobManager, ServerControl, AsyncDelayer, + diagnosticsIOManager, LoggerFactory.CreateLogger(), settings, instance, diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs index 473b3f36cc..206995886c 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdog.cs @@ -19,9 +19,9 @@ namespace Tgstation.Server.Host.Components.Watchdog sealed class WindowsWatchdog : BasicWatchdog { /// - /// The for the . + /// The for the pointing to the Game directory. /// - readonly IIOManager ioManager; + readonly IIOManager gameIOManager; /// /// The for the . @@ -54,7 +54,8 @@ namespace Tgstation.Server.Host.Components.Watchdog /// The for the . /// The for the . /// The for the . - /// The value of . + /// The for the . + /// The value of . /// The value of . /// The for the . /// The for the . @@ -69,7 +70,8 @@ namespace Tgstation.Server.Host.Components.Watchdog IJobManager jobManager, IServerControl serverControl, IAsyncDelayer asyncDelayer, - IIOManager ioManager, + IIOManager diagnosticsIOManager, + IIOManager gameIOManager, ISymlinkFactory symlinkFactory, ILogger logger, DreamDaemonLaunchParameters initialLaunchParameters, @@ -83,6 +85,7 @@ namespace Tgstation.Server.Host.Components.Watchdog jobManager, serverControl, asyncDelayer, + diagnosticsIOManager, logger, initialLaunchParameters, instance, @@ -90,7 +93,7 @@ namespace Tgstation.Server.Host.Components.Watchdog { try { - this.ioManager = ioManager ?? throw new ArgumentNullException(nameof(ioManager)); + this.gameIOManager = gameIOManager ?? throw new ArgumentNullException(nameof(gameIOManager)); this.symlinkFactory = symlinkFactory ?? throw new ArgumentNullException(nameof(symlinkFactory)); } catch @@ -151,7 +154,7 @@ namespace Tgstation.Server.Host.Components.Watchdog bool suspended = false; try { - windowsProvider = new WindowsSwappableDmbProvider(compileJobProvider, ioManager, symlinkFactory); + windowsProvider = new WindowsSwappableDmbProvider(compileJobProvider, gameIOManager, symlinkFactory); Logger.LogDebug("Swapping to compile job {0}...", windowsProvider.CompileJob.Id); try @@ -195,7 +198,7 @@ namespace Tgstation.Server.Host.Components.Watchdog // Add another lock to the startup DMB because it'll be used throughout the lifetime of the watchdog startupDmbProvider = await DmbFactory.FromCompileJob(dmbToUse.CompileJob, cancellationToken).ConfigureAwait(false); - activeSwappable = pendingSwappable ?? new WindowsSwappableDmbProvider(dmbToUse, ioManager, symlinkFactory); + activeSwappable = pendingSwappable ?? new WindowsSwappableDmbProvider(dmbToUse, gameIOManager, symlinkFactory); pendingSwappable = null; try diff --git a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs index 2d8273386d..d8c4314280 100644 --- a/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs +++ b/src/Tgstation.Server.Host/Components/Watchdog/WindowsWatchdogFactory.cs @@ -58,7 +58,8 @@ namespace Tgstation.Server.Host.Components.Watchdog IDmbFactory dmbFactory, IReattachInfoHandler reattachInfoHandler, ISessionControllerFactory sessionControllerFactory, - IIOManager ioManager, + IIOManager gameIOManager, + IIOManager diagnosticsIOManager, Api.Models.Instance instance, DreamDaemonSettings settings) => new WindowsWatchdog( @@ -70,7 +71,8 @@ namespace Tgstation.Server.Host.Components.Watchdog JobManager, ServerControl, AsyncDelayer, - ioManager, + diagnosticsIOManager, + gameIOManager, symlinkFactory, LoggerFactory.CreateLogger(), settings, From 979d8c928fd7235c8331b699c08c3dc4c17db0c1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 13:22:50 -0400 Subject: [PATCH 15/25] Add client function for creating dumps --- src/Tgstation.Server.Client/Components/DreamDaemonClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs index 6c2d5869d2..566c8058fe 100644 --- a/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs +++ b/src/Tgstation.Server.Client/Components/DreamDaemonClient.cs @@ -44,5 +44,8 @@ namespace Tgstation.Server.Client.Components /// public Task Update(DreamDaemon dreamDaemon, CancellationToken cancellationToken) => apiClient.Update(Routes.DreamDaemon, dreamDaemon ?? throw new ArgumentNullException(nameof(dreamDaemon)), instance.Id, cancellationToken); + + /// + public Task CreateDump(CancellationToken cancellationToken) => apiClient.Patch(Routes.Diagnostics, instance.Id, cancellationToken); } } \ No newline at end of file From 6c187a059034e514a65c965bb5189434d9df9358 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 14:05:28 -0400 Subject: [PATCH 16/25] Fixes --- src/Tgstation.Server.Api/Models/ErrorCode.cs | 6 ++++++ .../Components/IDreamDaemonClient.cs | 7 +++++++ .../Controllers/DreamDaemonController.cs | 2 +- src/Tgstation.Server.Host/NativeMethods.cs | 6 +++++- .../System/WindowsProcessFeatures.cs | 15 ++++++++------- 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index c7eb8b199c..b9cedb7554 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -525,5 +525,11 @@ namespace Tgstation.Server.Api.Models /// [Description("Attempted to create a process dump but /usr/bin/gcore could not be located!")] MissingGCore, + + /// + /// Non-zero gcore exit code. + /// + [Description("Could not create process as gcore exited with a non-zero exit code!")] + GCoreFailure, } } \ No newline at end of file diff --git a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs index 94fd0b5dbe..f5b78966bb 100644 --- a/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs +++ b/src/Tgstation.Server.Client/Components/IDreamDaemonClient.cs @@ -44,5 +44,12 @@ namespace Tgstation.Server.Client.Components /// The for the operation /// A resulting in the information Task Update(DreamDaemon dreamDaemon, CancellationToken cancellationToken); + + /// + /// Start a job to create a process dump of the active DreamDaemon executable. + /// + /// The for the operation. + /// A resulting in the of the running operation. + Task CreateDump(CancellationToken cancellationToken); } } diff --git a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs index 5ce0fdfb2c..1e03b0163d 100644 --- a/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs +++ b/src/Tgstation.Server.Host/Controllers/DreamDaemonController.cs @@ -297,7 +297,7 @@ namespace Tgstation.Server.Host.Controllers /// The for the operation /// A resulting in the of the request /// Dump started successfully. - [HttpPost(Routes.Diagnostics)] + [HttpPatch(Routes.Diagnostics)] [TgsAuthorize(DreamDaemonRights.CreateDump)] [ProducesResponseType(typeof(Api.Models.Job), 202)] public async Task CreateDump(CancellationToken cancellationToken) diff --git a/src/Tgstation.Server.Host/NativeMethods.cs b/src/Tgstation.Server.Host/NativeMethods.cs index bdace24b63..90fe18ec03 100644 --- a/src/Tgstation.Server.Host/NativeMethods.cs +++ b/src/Tgstation.Server.Host/NativeMethods.cs @@ -38,7 +38,11 @@ namespace Tgstation.Server.Host [Flags] public enum MiniDumpType : uint { - Normal = 0x00000000 + WithDataSegs = 0x00000001, + WithFullMemory = 0x00000002, + WithHandleData = 0x00000004, + WithUnloadedModules = 0x00000020, + WithThreadInfo = 0x00001000, } /// diff --git a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs index a34bb41687..339e847e75 100644 --- a/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/WindowsProcessFeatures.cs @@ -113,9 +113,8 @@ namespace Tgstation.Server.Host.System } /// - public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) - { - await Task.Factory.StartNew( + public Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) + => Task.Factory.StartNew( () => { using var fileStream = new FileStream(outputFile, FileMode.CreateNew); @@ -123,7 +122,11 @@ namespace Tgstation.Server.Host.System process.Handle, (uint)process.Id, fileStream.SafeFileHandle, - NativeMethods.MiniDumpType.Normal, + NativeMethods.MiniDumpType.WithDataSegs + | NativeMethods.MiniDumpType.WithFullMemory + | NativeMethods.MiniDumpType.WithHandleData + | NativeMethods.MiniDumpType.WithThreadInfo + | NativeMethods.MiniDumpType.WithUnloadedModules, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero)) @@ -131,8 +134,6 @@ namespace Tgstation.Server.Host.System }, cancellationToken, TaskCreationOptions.LongRunning, - TaskScheduler.Current) - .ConfigureAwait(false); - } + TaskScheduler.Current); } } From a1510153846afa9da1d7d9ffe6b75da4fa25aa6d Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 14:15:34 -0400 Subject: [PATCH 17/25] Add integration test --- .../Instance/WatchdogTest.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs index 9955bc1af4..4823cebd9d 100644 --- a/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs +++ b/tests/Tgstation.Server.Tests/Instance/WatchdogTest.cs @@ -43,6 +43,9 @@ namespace Tgstation.Server.Tests.Instance SoftRestart = true }, cancellationToken), ErrorCode.DreamDaemonDoubleSoft); + await ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.CreateDump(cancellationToken), ErrorCode.WatchdogNotRunning); + await ApiAssert.ThrowsException(() => instanceClient.DreamDaemon.Restart(cancellationToken), ErrorCode.WatchdogNotRunning); + await RunBasicTest(cancellationToken); // await RunLongRunningTestThenUpdate(cancellationToken); @@ -53,6 +56,15 @@ namespace Tgstation.Server.Tests.Instance await RunHeartbeatTest(cancellationToken); await StartAndLeaveRunning(cancellationToken); + + var dumpJob = await instanceClient.DreamDaemon.CreateDump(cancellationToken); + await WaitForJob(dumpJob, 3000, false, cancellationToken); + + var dumpFiles = Directory.GetFiles(Path.Combine( + instanceClient.Metadata.Path, "Diagnostics", "ProcessDumps"), "*.dmp"); + Assert.AreEqual(1, dumpFiles.Length); + File.Delete(dumpFiles.Single()); + global::System.Console.WriteLine("TEST: END WATCHDOG TESTS"); } @@ -103,12 +115,14 @@ namespace Tgstation.Server.Tests.Instance Assert.Inconclusive($"Incorrect number of DD processes: {ddProcs.Count}"); using var ddProc = ddProcs.Single(); - using var ourProcessHandler = new ProcessExecutor( + IProcessExecutor executor = null; + executor = new ProcessExecutor( new PlatformIdentifier().IsWindows ? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of>()) - : new PosixProcessFeatures(Mock.Of(), Mock.Of>()), + : new PosixProcessFeatures(new Lazy(() => executor), Mock.Of(), Mock.Of>()), Mock.Of>(), - LoggerFactory.Create(x => { })) + LoggerFactory.Create(x => { })); + using var ourProcessHandler = executor .GetProcess(ddProc.Id); // Ensure it's responding to heartbeats From c7bc6caf37063ed3c437d1cea650a1818c862f05 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 14:20:57 -0400 Subject: [PATCH 18/25] Fix the build --- tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs index 065813a50c..cfa6173cce 100644 --- a/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs +++ b/tests/Tgstation.Server.Host.Tests/System/TestProcessFeatures.cs @@ -21,7 +21,7 @@ namespace Tgstation.Server.Host.System.Tests { features = new PlatformIdentifier().IsWindows ? (IProcessFeatures)new WindowsProcessFeatures(Mock.Of>()) - : new PosixProcessFeatures(new DefaultIOManager(), Mock.Of>()); + : new PosixProcessFeatures(new Lazy(() => null), new DefaultIOManager(), Mock.Of>()); } [TestMethod] From a11928cc9fc9e205fceb193744f2e5dd8a912d64 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 15:02:19 -0400 Subject: [PATCH 19/25] Add GDB to travis integration tests --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index d5196bdd03..685ce06e27 100644 --- a/.travis.yml +++ b/.travis.yml @@ -32,6 +32,7 @@ jobs: packages: - libc6-i386 - libstdc++6:i386 + - gdb - env: - DoxGeneration=false - DockerBuild=false @@ -51,6 +52,7 @@ jobs: packages: - libc6-i386 - libstdc++6:i386 + - gdb - env: - DoxGeneration=false - DockerBuild=false @@ -74,6 +76,7 @@ jobs: - postgresql-12 - libc6-i386 - libstdc++6:i386 + - gdb - env: - DoxGeneration=false - DockerBuild=false From 0e842914282e0a8541acabde318e38130a3606e1 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 15:10:45 -0400 Subject: [PATCH 20/25] Handle gcore's prefixing of the output file --- .../System/PosixProcessFeatures.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index 233d2daa4c..adab81beda 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -106,10 +106,16 @@ namespace Tgstation.Server.Host.System /// public async Task CreateDump(global::System.Diagnostics.Process process, string outputFile, CancellationToken cancellationToken) { + if (process == null) + throw new ArgumentNullException(nameof(process)); + if (outputFile == null) + throw new ArgumentNullException(nameof(outputFile)); + const string GCorePath = "/usr/bin/gcore"; if (!await ioManager.FileExists(GCorePath, cancellationToken).ConfigureAwait(false)) throw new JobException(ErrorCode.MissingGCore); + var pid = process.Id; string output; int exitCode; using (var gcoreProc = lazyLoadedProcessExecutor.Value.LaunchProcess( @@ -126,13 +132,15 @@ namespace Tgstation.Server.Host.System logger.LogDebug("gcore output:{0}{1}", Environment.NewLine, output); } - cancellationToken.ThrowIfCancellationRequested(); - if (exitCode != 0) throw new JobException( ErrorCode.GCoreFailure, new JobException( $"Exit Code: {exitCode}{Environment.NewLine}Output:{Environment.NewLine}{output}")); + + // gcore outputs name.pid so remove the pid part + var generatedGCoreFile = $"{outputFile}.{pid}"; + await ioManager.MoveFile(generatedGCoreFile, outputFile, cancellationToken).ConfigureAwait(false); } } } From cc360d1ce9ea2a292495dde2c6878522f82d81d4 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 15:11:19 -0400 Subject: [PATCH 21/25] Fix noShellExecute documentation --- src/Tgstation.Server.Host/System/IProcessExecutor.cs | 2 +- src/Tgstation.Server.Host/System/PosixProcessFeatures.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/System/IProcessExecutor.cs b/src/Tgstation.Server.Host/System/IProcessExecutor.cs index 3dea91680b..e7f0d98388 100644 --- a/src/Tgstation.Server.Host/System/IProcessExecutor.cs +++ b/src/Tgstation.Server.Host/System/IProcessExecutor.cs @@ -13,7 +13,7 @@ /// The arguments for the /// If standard output should be read /// If standard error should be read - /// If shell execute should not be used. Ignored if or are set + /// If shell execute should not be used. Must be set if or are set. /// A new IProcess LaunchProcess(string fileName, string workingDirectory, string arguments = null, bool readOutput = false, bool readError = false, bool noShellExecute = false); diff --git a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs index adab81beda..f808e588de 100644 --- a/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs +++ b/src/Tgstation.Server.Host/System/PosixProcessFeatures.cs @@ -123,6 +123,7 @@ namespace Tgstation.Server.Host.System Environment.CurrentDirectory, $"-o {outputFile} {process.Id}", true, + true, true)) { using (cancellationToken.Register(() => gcoreProc.Terminate())) From d8985e1bb719451c7287cbfc100d13885813a602 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 16:37:09 -0400 Subject: [PATCH 22/25] Fix ErrorCode description --- src/Tgstation.Server.Api/Models/ErrorCode.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Api/Models/ErrorCode.cs b/src/Tgstation.Server.Api/Models/ErrorCode.cs index b9cedb7554..b6f3bd1e5b 100644 --- a/src/Tgstation.Server.Api/Models/ErrorCode.cs +++ b/src/Tgstation.Server.Api/Models/ErrorCode.cs @@ -529,7 +529,7 @@ namespace Tgstation.Server.Api.Models /// /// Non-zero gcore exit code. /// - [Description("Could not create process as gcore exited with a non-zero exit code!")] + [Description("Could not create dump as gcore exited with a non-zero exit code!")] GCoreFailure, } } \ No newline at end of file From 7d9ee9ee216763a4fb1c0e1fad86086b2f30122c Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 16:37:21 -0400 Subject: [PATCH 23/25] TRRRRAAAAAAAAAAAAAAAVVVVVVVVVVVIIIIIIIIIIISSSSSS!! --- build/integration_test.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build/integration_test.sh b/build/integration_test.sh index 8492301359..33342223ac 100755 --- a/build/integration_test.sh +++ b/build/integration_test.sh @@ -1,6 +1,9 @@ #!/bin/bash set -e +# Needed so gcore can work +echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope + export TGS4_TEST_DISCORD_CHANNEL=493119635319947269 export TGS4_TEST_IRC_CHANNEL=\#botbus export TGS4_TEST_TEMP_DIRECTORY=~/tgs4_test From a67de3de47a199b633668dd43428db09b94f7686 Mon Sep 17 00:00:00 2001 From: Jordan Brown Date: Sat, 13 Jun 2020 19:53:33 -0400 Subject: [PATCH 24/25] Fix bad conditional --- src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs index 502e05eded..f23aedca95 100644 --- a/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs +++ b/src/Tgstation.Server.Host/Database/SqliteDatabaseContext.cs @@ -55,7 +55,7 @@ namespace Tgstation.Server.Host.Database // use the DateTimeOffsetToBinaryConverter // Based on: https://github.com/aspnet/EntityFrameworkCore/issues/10784#issuecomment-415769754 // This only supports millisecond precision, but should be sufficient for most use cases. - if (DesignTime) + if (!DesignTime) foreach (var entityType in modelBuilder.Model.GetEntityTypes()) { var properties = entityType