Merge branch 'dev' into 979-Postgres

This commit is contained in:
Jordan Brown
2020-05-21 12:19:23 -04:00
committed by GitHub
19 changed files with 505 additions and 381 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<PropertyGroup>
<!-- This is the authorative version list -->
<!-- Integration tests will ensure they match across the board -->
<TgsCoreVersion>4.3.0</TgsCoreVersion>
<TgsCoreVersion>4.2.4</TgsCoreVersion>
<TgsApiVersion>6.4.0</TgsApiVersion>
<TgsClientVersion>6.3.0</TgsClientVersion>
<TgsDmapiVersion>5.2.0</TgsDmapiVersion>
+6 -8
View File
@@ -6,20 +6,20 @@ DOXDIR=~/tgsdox
mkdir -p $DOXDIR
if [ "$TRAVIS_PULL_REQUEST" -eq "$false" ] && [ "$TRAVIS_BRANCH" -eq "master" ]; then
if [ "$TRAVIS_PULL_REQUEST" = false ] && [ "$TRAVIS_BRANCH" = "master" ]; then
PUBLISH_DOX=true
else
PUBLISH_DOX=false
fi
if [ "$PUBLISH_DOX" = true ] ; then
GITHUB_URL="github.com/$TRAVIS_PULL_REQUEST_SLUG"
GITHUB_URL="github.com/$TRAVIS_REPO_SLUG"
echo "Cloning https://git@$GITHUB_URL..."
git clone -b gh-pages --single-branch "https://git@$GITHUB_URL" "$DOXDIR" 2> /dev/null
rm -r "$DOXDIR\*"
rm -r "$DOXDIR/*"
fi
VERSION=cat "$BUILD_FOLDER/build/Version.props" | grep -oPm1 "(?<=<TgsCoreVersion>)[^<]+"
VERSION=$(cat "build/Version.props" | grep -oPm1 "(?<=<TgsCoreVersion>)[^<]+")
echo -e "\nPROJECT_NUMBER = $VERSION\nINPUT = $BUILD_FOLDER\nOUTPUT_DIRECTORY = $DOXDIR\nPROJECT_LOGO = $BUILD_FOLDER/build/tgs.ico\nHAVE_DOT=YES" >> "$BUILD_FOLDER/docs/Doxyfile"
@@ -37,9 +37,7 @@ if [ "$PUBLISH_DOX" = true ] ; then
echo "" > .nojekyll
git add --all
git commit -m "Deploy code docs to GitHub Pages for Travis build $TRAVIS_BUILD_NUMBER" -m "Commit: $TRAVIS_COMMIT"
git push -f "https://$TGS4_TEST_GITHUB_TOKEN@$GITHUB_URL" 2>&1 | out-null
git push -f "https://$TGS4_GH_PAGES_TOKEN@$GITHUB_URL" 2>&1 | /dev/null
cd "$BUILD_FOLDER"
rm -r "$DOXDIR/.git"
rm -rf "$DOXDIR/.git"
fi
mv $DOXDIR "$BUILD_FOLDER/tgsdox"
+1 -1
View File
@@ -3,7 +3,7 @@
<!-- Navigation index tabs for HTML output -->
<navindex>
<tab type="mainpage" visible="yes" title=""/>
<tab type="user" visible="yes" title="New Features" url="@ref features" />
<tab type="user" visible="yes" title="Features" url="@ref features" />
<tab type="user" visible="yes" title="API" url="@ref api" />
<tab type="user" visible="yes" title="Architecture" url="@ref architecture" />
<tab type="namespaces" visible="yes" title="">
+86 -31
View File
@@ -1,40 +1,95 @@
/*!
@page api API
@page features Features
@section features_list New Features
@tableofcontents
TGS4 has a multitude of improvements over V3:
@section new_features New in V4
- Non proprietary API (WCF -> HTTP)
- Agnostic HTTP API: The replaces the WCF service calls used in TGS3. This helps avoid Windows vendor lock-in and get away from the SOAP API that literally no one understood (not even me). With it, it's much easier to expose TGS to the internet, all you need is a HTTPS reverse proxy in front of it. A rundown of the new API exists here: https://tgstation.github.io/tgstation-server/api.html.
- Granular Access Controls: Windows users are no longer (required to be) the basis for authentication to the server. We now have database-backed users as a login option. These use a combined Basic/JWT authentication scheme with industry standard password hashing and salting. Users are fully customizable and can be given granular access to every bit of the server via the new permissions system. From changing the BYOND version, to test merging a PR, to restarting the server, every action may now be granted or revoked on a per user basis.
- Limitation: Users can be disabled but not deleted.
- Proper Long Running Operation Support: Server actions take a long time, from a git pull to a DreamMaker compile. TGS now internally allows for them to be run in parallel with each other and provides an audit record via the database. This is an improvement over the old system where connections had to be held open for the duration of operations.
- Database Backend: TGS requires an SQL database to operate. This allows for much better concurrency and is just overall much cleaner than the old single json file storage blob per instance.
- Limitation: There is a one-to-one relationship with a TGS server and a database. **DO NOT SHARE TGS DATABASES**.
- Linux/Docker Support: TGS4 is Linux and docker compatible. (Note this does not mean that rust-g and BSQL work out of the box, they must be compiled using event scripts like PreCompile.sh).
- Limitation: TGS4 has a dependency on the native library libgit2 which is known to cause issues on Linux. The binaries distributed with TGS are kept up to date with the upstream repository, but out of the box Linux support can't be assured in every environment. Docker is guaranteed to always work, however. See the repository for the distributed binary here: https://github.com/libgit2/libgit2sharp.nativebinaries.
- Limitation: System based logins are not supported on Linux. https://github.com/tgstation/tgstation-server/issues/709
- Incredibly Detailed Logging: Various log levels exist now (Trace/Debug/Info/Warning/Error/Critical) and are sanely output to a rolling file on the host. Significant improvement over having to use the Windows event viewer with TGS3. Until such a point where bugs stop copping up I'd recommend Trace logging for the main log level.
- Historical Deployment Data: Every time code is compiled the following data is logged and stored.
- The User that initiated it.
- When it was started.
- When it finished.
- All revision information including local/remote SHAs, test merged pull requests and their SHAs.
- The BYOND version used.
- The DMAPI version in the binary
- Multiple chat bots per instance: Up to 65535 as a matter of fact (who knows why I chose that number?)!
- Automatic Chat Bot Reconnection Intervals: Set in minutes.
- Better Error State Handling: The Server and Watchdog aren't your momma's boys anymore. Every error state will be automatically resolved or reported with recommended actions.
- Watchdog Heartbeats: An interval in seconds can now be set at which TGS will send /world/Topic() packets to DreamDaemon. If four of these are missed, the server will be rebooted. No more endless @Key Holder pings in discord (and I can finally unmute the /tg/ guild)! This feature can be disabled.
- Better DMAPI: No longer requires injecting a .NET runtime .dll into the DreamDaemon process. DD -> TGS communication is now handled securely via BYOND's native /world/Export() API ("But Cyberboss, BYOND only supports GET requests." Who said anything about respecting HTTP standards when dealing with BYOND?).
- Safe/Ultrasafe Security Support: Thanks to the new DMAPI, the ultrasafe and safe security levels may be used without running into BYOND's limitations. But no one really cares...
- Self Upgrading: To upgrade TGS3 you needed to download and run the installer. This was pretty seamless, but it's now even better in V4 as the command to upgrade can be given straight to the API. At that point the server will handle downloading the update, detaching running DreamDaemon instances, restarting with the new version, and reattaching to them. Easier than ever patch delivery.
- *Gasp* TESTING: TGS4 currently has over 60% code coverage in automated unit and full stack integration tests. I aim to have that number ever increasing to prevent trivial mistakes. Big improvement over V3 which had... literally none...
Communication between client applications and TGS is now done over HTTP instead of WCF SOAP. In the past, attempts to create custom clients failed due to the complexity of the old API. The new, simple, json API should serve as a much needed improvement.
Along with these features, nearly every single V3 feature has been included and possibly improved in some fashion. This includes stuff like Windows accounts for logins, and using ACLs for static file handling. The following exceptions exist but are planned for future updates:
- Process memory/CPU diagnostic data is not generated: https://github.com/tgstation/tgstation-server/issues/611
- Process dumps may not be created: https://github.com/tgstation/tgstation-server/issues/612
- The DMAPI is required in DreamMaker code: https://github.com/tgstation/tgstation-server/issues/934
- The option to "Initialize Game Directories" is no longer present, but a variant is still performed every deployment for smoother error handling.
- Direct server announcements are no longer present but may be readded upon request.
- Granular access controls
@section features_list Comprehensive Feature List
Almost every server action a client can take is now guarded by a robust permissions system. Coupled with built in user account management, this should provide an effective way to grant limited access to servers.
- Proper long running operation support
The client no longer has to wait literal minutes for a single request. Long running operations now create @ref Tgstation.Server.Api.Models.Job objects to track their progress and errors.
- Concrete data backend
The old save file system for instances is gone. All configuration data per instance is now stored in a relational database
- Smarter repository handling
The entire repository component is now much more effective in handling complex merges, automatic updates, and tracking git objects.
- Linux support
The framework used to create the server has been changed to dotnet 3.1. Because of this, we can now support linux hosting somewhat effectively. Note that seamless .dmb updates are still a Windows only feature for the time being.
- Detailed logging
We now actually have log files to reference which will greatly help with debugging.
- OPTIONAL Windows FS permission integration
No longer a requirement as in V3. While Windows logins are still supported for authentication and remote file manipulation (on Windows), remote file manipulation can now be done via the server's executing user.
tgstation-server is a BYOND server managment suite. It includes all the following features
- Standalone server with OpenAPI 3.0 defined HTTP REST API
- Secure user authentication, management, and permissions system
- Support for using Windows accounts as logins
- Long running operation support via the jobs subsystem
- Logging system with configurable levels of verbosity
- Able to self update with user input
- Instances: Managing multiple sets of the below features seperately on the same machine
- Git repository managment
- Cloning
- Checking out SHAs and references
- Local Merging
- Hard resetting to references
- Merging GitHub pull requests, optionally commenting on them
- Optionally pushing commits for GitHub visibility
- Can be deleted and changed to a different origin
- BYOND version management
- Downloading and installing a given BYOND version
- Includes DirectX installation on Windows and proper handling of POSIX shared libraries
- Supporting any amount of installed versions
- Cleaning the BYOND cache
- Code Deployment
- Compiles git code with DreamMaker and the active BYOND version
- Validates interop with the compiled binary
- Can be scheduled to pull the repository and build on a given interval
- The Watchdog, a DreamDaemon uptime monitor
- Automatically restarts the server when crashed until told to stop
- "Heartbeat" system for checking DreamDaemon isn't hung
- Automatic application of new deployments on /world/Reboot
- Automatic startup at server boot time
- Automatically handles Windows prompts related to trusted mode
- TGS restarts/updates do not interrupt the DreamDaemon process
- Chat System
- Support multiple chat bots per instance. Current providers:
- Discord
- IRC
- Bots come out of the box with some basic commands like showing the server revision
- Support different chat channel types & tags for use in DM code
- Configuration system
- Allows specifying files/directories to be permanently symlinked with active game code
- Support for scripts that run on certain events. e.g. Before/after compiling
- Allows including arbitrary .dm code in compilations
- ACL support with Windows based logins
- DMAPI, DM code interop
- Provides metadata such as the current TGS version, git revision, active pull requests, security level
- Provides API for responding to TGS events (compile, update, etc)
- Provides API for sending chat bot messages
- Provides API for implementing chat bot commands in DM code
- Gives the ability to restart the DreamDaemon process from DM code
- Functions with all 3 DreamDaemon security levels
- Allows specifying the .dmb's minimum required security level
*/
@@ -534,7 +534,7 @@ namespace Tgstation.Server.Host.Components.Deployment
#pragma warning disable CA1506
public async Task DeploymentProcess(
Models.Job job,
IDatabaseContext databaseContext,
IDatabaseContextFactory databaseContextFactory,
Action<int> progressReporter,
CancellationToken cancellationToken)
{
@@ -542,44 +542,41 @@ namespace Tgstation.Server.Host.Components.Deployment
if (job == null)
throw new ArgumentNullException(nameof(job));
#pragma warning restore IDE0016 // Use 'throw' expression
if (databaseContext == null)
throw new ArgumentNullException(nameof(databaseContext));
if (databaseContextFactory == null)
throw new ArgumentNullException(nameof(databaseContextFactory));
if (progressReporter == null)
throw new ArgumentNullException(nameof(progressReporter));
var averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false);
var ddSettings = await databaseContext
.DreamDaemonSettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
var dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
Models.RepositorySettings repositorySettings = null;
string repoOwner = null;
string repoName = null;
Models.CompileJob compileJob;
Models.RevisionInformation revInfo;
using (var repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false))
{
if (repo == null)
throw new JobException(ErrorCode.RepoMissing);
if (repo.IsGitHubRepository)
TimeSpan? averageSpan = null;
Models.RepositorySettings repositorySettings = null;
Models.DreamDaemonSettings ddSettings = null;
DreamMakerSettings dreamMakerSettings = null;
IRepository repo = null;
Models.CompileJob compileJob = null;
Models.RevisionInformation revInfo = null;
await databaseContextFactory.UseContext(
async databaseContext =>
{
repoOwner = repo.GitHubOwner;
repoName = repo.GitHubRepoName;
averageSpan = await CalculateExpectedDeploymentTime(databaseContext, cancellationToken).ConfigureAwait(false);
ddSettings = await databaseContext
.DreamDaemonSettings
.Where(x => x.InstanceId == metadata.Id)
.Select(x => new Models.DreamDaemonSettings
{
StartupTimeout = x.StartupTimeout,
})
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (ddSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamDaemonSettings);
dreamMakerSettings = await databaseContext.DreamMakerSettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(cancellationToken).ConfigureAwait(false);
if (dreamMakerSettings == default)
throw new JobException(ErrorCode.InstanceMissingDreamMakerSettings);
repositorySettings = await databaseContext
.RepositorySettings
.Where(x => x.InstanceId == metadata.Id)
@@ -594,34 +591,55 @@ namespace Tgstation.Server.Host.Components.Deployment
.ConfigureAwait(false);
if (repositorySettings == default)
throw new JobException(ErrorCode.InstanceMissingRepositorySettings);
}
var repoSha = repo.Head;
revInfo = await databaseContext
.RevisionInformations
.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ThenInclude(x => x.MergedBy)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new Models.RevisionInformation
repo = await repositoryManager.LoadRepository(cancellationToken).ConfigureAwait(false);
try
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
if (repo == null)
throw new JobException(ErrorCode.RepoMissing);
if (repo.IsGitHubRepository)
{
Id = metadata.Id
repoOwner = repo.GitHubOwner;
repoName = repo.GitHubRepoName;
}
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
}
var repoSha = repo.Head;
revInfo = await databaseContext
.RevisionInformations
.Where(x => x.CommitSha == repoSha && x.Instance.Id == metadata.Id)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ThenInclude(x => x.MergedBy)
.FirstOrDefaultAsync(cancellationToken)
.ConfigureAwait(false);
if (revInfo == default)
{
revInfo = new Models.RevisionInformation
{
CommitSha = repoSha,
OriginCommitSha = repoSha,
Instance = new Models.Instance
{
Id = metadata.Id
}
};
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, repoSha);
databaseContext.Instances.Attach(revInfo.Instance);
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
}
catch
{
repo.Dispose();
throw;
}
})
.ConfigureAwait(false);
using (repo)
compileJob = await Compile(
revInfo,
dreamMakerSettings,
@@ -632,33 +650,46 @@ namespace Tgstation.Server.Host.Components.Deployment
cancellationToken)
.ConfigureAwait(false);
try
{
compileJob.Job = job;
databaseContext.CompileJobs.Add(compileJob);
await PostDeploymentComments(compileJob, repositorySettings, repoOwner, repoName).ConfigureAwait(false);
// The difficulty with compile jobs is they have a two part commit
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
try
try
{
await databaseContextFactory.UseContext(
async databaseContext =>
{
await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
}
catch
{
// So we need to un-commit the compile job if the above throws
databaseContext.CompileJobs.Remove(compileJob);
await databaseContext.Save(default).ConfigureAwait(false);
throw;
}
}
catch (Exception ex)
{
await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false);
throw;
}
compileJob.Job = new Models.Job
{
Id = job.Id
};
compileJob.RevisionInformation = new Models.RevisionInformation
{
Id = revInfo.Id
};
databaseContext.RevisionInformations.Attach(compileJob.RevisionInformation);
databaseContext.Jobs.Attach(compileJob.Job);
databaseContext.CompileJobs.Add(compileJob);
await PostDeploymentComments(compileJob, repositorySettings, repoOwner, repoName).ConfigureAwait(false);
// The difficulty with compile jobs is they have a two part commit
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
try
{
await compileJobConsumer.LoadCompileJob(compileJob, cancellationToken).ConfigureAwait(false);
}
catch
{
// So we need to un-commit the compile job if the above throws
databaseContext.CompileJobs.Remove(compileJob);
await databaseContext.Save(default).ConfigureAwait(false);
throw;
}
})
.ConfigureAwait(false);
}
catch (Exception ex)
{
await CleanupFailedCompile(compileJob, ex is OperationCanceledException, default).ConfigureAwait(false);
throw;
}
await eventConsumer.HandleEvent(EventType.DeploymentComplete, null, cancellationToken).ConfigureAwait(false);
@@ -15,13 +15,13 @@ namespace Tgstation.Server.Host.Components.Deployment
/// Create and a compile job and insert it into the database. Meant to be called by a <see cref="Jobs.IJobManager"/>.
/// </summary>
/// <param name="job">The running <see cref="Job"/>.</param>
/// <param name="databaseContext">The <see cref="IDatabaseContext"/> for the operation.</param>
/// <param name="databaseContextFactory">The <see cref="IDatabaseContextFactory"/> for the operation.</param>
/// <param name="progressReporter">The <see cref="Action{T1}"/> to report compilation progress.</param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation.</param>
/// <returns>A <see cref="Task"/> representing the running operation.</returns>
Task DeploymentProcess(
Job job,
IDatabaseContext databaseContext,
IDatabaseContextFactory databaseContextFactory,
Action<int> progressReporter,
CancellationToken cancellationToken);
}
+127 -119
View File
@@ -179,148 +179,156 @@ namespace Tgstation.Server.Host.Components
};
string deploySha = null;
await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContext, progressReporter, jobCancellationToken) =>
await jobManager.RegisterOperation(repositoryUpdateJob, async (paramJob, databaseContextFactory, progressReporter, jobCancellationToken) =>
{
var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
// assume 5 steps with synchronize
const int ProgressSections = 7;
const int ProgressStep = 100 / ProgressSections;
string repoHead = null;
const int NumSteps = 3;
var doneSteps = 0;
Action<int> NextProgressReporter()
{
var tmpDoneSteps = doneSteps;
++doneSteps;
return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps);
}
using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false);
if (repo == null)
{
logger.LogTrace("Aborting repo update, no repository!");
return;
}
var startSha = repo.Head;
if (!repo.Tracking)
{
logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
deploySha = startSha;
return;
}
var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
// the main point of auto update is to pull the remote
await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
RevisionInformation currentRevInfo = null;
bool hasDbChanges = false;
Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
.Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
.Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
.FirstOrDefaultAsync(cancellationToken);
async Task UpdateRevInfo(string currentHead, bool onOrigin)
{
if (currentRevInfo == null)
currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
if (currentRevInfo == default)
await databaseContextFactory.UseContext(
async databaseContext =>
{
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
onOrigin = true;
}
var repositorySettingsTask = databaseContext.RepositorySettings.Where(x => x.InstanceId == metadata.Id).FirstAsync(jobCancellationToken);
var attachedInstance = new Models.Instance
{
Id = metadata.Id
};
var oldRevInfo = currentRevInfo;
currentRevInfo = new RevisionInformation
{
CommitSha = currentHead,
OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
Instance = attachedInstance
};
if (!onOrigin)
currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
const int NumSteps = 3;
var doneSteps = 0;
databaseContext.Instances.Attach(attachedInstance);
databaseContext.RevisionInformations.Add(currentRevInfo);
hasDbChanges = true;
}
Action<int> NextProgressReporter()
{
var tmpDoneSteps = doneSteps;
++doneSteps;
return progress => progressReporter((progress + (100 * tmpDoneSteps)) / NumSteps);
}
// take appropriate auto update actions
bool shouldSyncTracked;
if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
{
logger.LogTrace("Preserving test merges...");
using var repo = await RepositoryManager.LoadRepository(jobCancellationToken).ConfigureAwait(false);
if (repo == null)
{
logger.LogTrace("Aborting repo update, no repository!");
return;
}
var currentRevInfoTask = LoadRevInfo();
var startSha = repo.Head;
if (!repo.Tracking)
{
logger.LogTrace("Aborting repo update, active ref not tracking any remote branch!");
deploySha = startSha;
return;
}
var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
var repositorySettings = await repositorySettingsTask.ConfigureAwait(false);
if (!result.HasValue)
throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
// the main point of auto update is to pull the remote
await repo.FetchOrigin(repositorySettings.AccessUser, repositorySettings.AccessToken, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
RevisionInformation currentRevInfo = null;
bool hasDbChanges = false;
var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
Task<RevisionInformation> LoadRevInfo() => databaseContext.RevisionInformations
.Where(x => x.CommitSha == startSha && x.Instance.Id == metadata.Id)
.Include(x => x.ActiveTestMerges).ThenInclude(x => x.TestMerge)
.FirstOrDefaultAsync(cancellationToken);
var currentHead = repo.Head;
if (currentHead != startSha)
{
await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
shouldSyncTracked = stillOnOrigin;
}
else
shouldSyncTracked = false;
}
else
{
logger.LogTrace("Not preserving test merges...");
await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
async Task UpdateRevInfo(string currentHead, bool onOrigin)
{
if (currentRevInfo == null)
currentRevInfo = await LoadRevInfo().ConfigureAwait(false);
var currentHead = repo.Head;
if (currentRevInfo == default)
{
logger.LogWarning(Repository.Repository.OriginTrackingErrorTemplate, currentHead);
onOrigin = true;
}
currentRevInfo = await databaseContext.RevisionInformations
.Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
.FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
var attachedInstance = new Models.Instance
{
Id = metadata.Id
};
var oldRevInfo = currentRevInfo;
currentRevInfo = new RevisionInformation
{
CommitSha = currentHead,
OriginCommitSha = onOrigin ? currentHead : oldRevInfo.OriginCommitSha,
Instance = attachedInstance
};
if (!onOrigin)
currentRevInfo.ActiveTestMerges = new List<RevInfoTestMerge>(oldRevInfo.ActiveTestMerges);
if (currentHead != startSha && currentRevInfo != default)
await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
databaseContext.Instances.Attach(attachedInstance);
databaseContext.RevisionInformations.Add(currentRevInfo);
hasDbChanges = true;
}
shouldSyncTracked = true;
}
// take appropriate auto update actions
bool shouldSyncTracked;
if (repositorySettings.AutoUpdatesKeepTestMerges.Value)
{
logger.LogTrace("Preserving test merges...");
// synch if necessary
if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
{
var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
var currentHead = repo.Head;
if (currentHead != currentRevInfo.CommitSha)
await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
}
var currentRevInfoTask = LoadRevInfo();
if (hasDbChanges)
try
{
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
catch
{
await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
throw;
}
var result = await repo.MergeOrigin(repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
if (!result.HasValue)
throw new JobException(Api.Models.ErrorCode.InstanceUpdateTestMergeConflict);
currentRevInfo = await currentRevInfoTask.ConfigureAwait(false);
var lastRevInfoWasOriginCommit = currentRevInfo == default || currentRevInfo.CommitSha == currentRevInfo.OriginCommitSha;
var stillOnOrigin = result.Value && lastRevInfoWasOriginCommit;
var currentHead = repo.Head;
if (currentHead != startSha)
{
await UpdateRevInfo(currentHead, stillOnOrigin).ConfigureAwait(false);
shouldSyncTracked = stillOnOrigin;
}
else
shouldSyncTracked = false;
}
else
{
logger.LogTrace("Not preserving test merges...");
await repo.ResetToOrigin(NextProgressReporter(), jobCancellationToken).ConfigureAwait(false);
var currentHead = repo.Head;
currentRevInfo = await databaseContext.RevisionInformations
.Where(x => x.CommitSha == currentHead && x.Instance.Id == metadata.Id)
.FirstOrDefaultAsync(jobCancellationToken).ConfigureAwait(false);
if (currentHead != startSha && currentRevInfo != default)
await UpdateRevInfo(currentHead, true).ConfigureAwait(false);
shouldSyncTracked = true;
}
// synch if necessary
if (repositorySettings.AutoUpdatesSynchronize.Value && startSha != repo.Head)
{
var pushedOrigin = await repo.Sychronize(repositorySettings.AccessUser, repositorySettings.AccessToken, repositorySettings.CommitterName, repositorySettings.CommitterEmail, NextProgressReporter(), shouldSyncTracked, jobCancellationToken).ConfigureAwait(false);
var currentHead = repo.Head;
if (currentHead != currentRevInfo.CommitSha)
await UpdateRevInfo(currentHead, pushedOrigin).ConfigureAwait(false);
}
repoHead = repo.Head;
if (hasDbChanges)
try
{
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
}
catch
{
await repo.ResetToSha(startSha, progressReporter, default).ConfigureAwait(false);
throw;
}
})
.ConfigureAwait(false);
progressReporter(5 * ProgressStep);
deploySha = repo.Head;
deploySha = repoHead;
}, cancellationToken).ConfigureAwait(false);
await jobManager.WaitForJobCompletion(repositoryUpdateJob, user, cancellationToken, default).ConfigureAwait(false);
@@ -211,8 +211,7 @@ namespace Tgstation.Server.Host.Components
repoIoManager,
eventConsumer,
loggerFactory.CreateLogger<Repository.Repository>(),
loggerFactory.CreateLogger<RepositoryManager>(),
metadata.RepositorySettings);
loggerFactory.CreateLogger<RepositoryManager>());
try
{
var byond = new ByondManager(byondIOManager, byondInstaller, eventConsumer, loggerFactory.CreateLogger<ByondManager>());
@@ -3,9 +3,10 @@ using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;
using Tgstation.Server.Api.Models.Internal;
using Tgstation.Server.Api.Models;
using Tgstation.Server.Host.Core;
using Tgstation.Server.Host.IO;
using Tgstation.Server.Host.Jobs;
namespace Tgstation.Server.Host.Components.Repository
{
@@ -48,11 +49,6 @@ namespace Tgstation.Server.Host.Components.Repository
/// </summary>
readonly ILogger<RepositoryManager> logger;
/// <summary>
/// The <see cref="RepositorySettings"/> for the <see cref="RepositoryManager"/>
/// </summary>
readonly RepositorySettings repositorySettings;
/// <summary>
/// Used for controlling single access to the <see cref="IRepository"/>
/// </summary>
@@ -67,15 +63,13 @@ namespace Tgstation.Server.Host.Components.Repository
/// <param name="eventConsumer">The value of <see cref="eventConsumer"/></param>
/// <param name="repositoryLogger">The value of <see cref="repositoryLogger"/></param>
/// <param name="logger">The value of <see cref="logger"/></param>
/// <param name="repositorySettings">The value of <see cref="repositorySettings"/></param>
public RepositoryManager(
ILibGit2RepositoryFactory repositoryFactory,
ILibGit2Commands commands,
IIOManager ioManager,
IEventConsumer eventConsumer,
ILogger<Repository> repositoryLogger,
ILogger<RepositoryManager> logger,
RepositorySettings repositorySettings)
ILogger<RepositoryManager> logger)
{
this.repositoryFactory = repositoryFactory ?? throw new ArgumentNullException(nameof(repositoryFactory));
this.commands = commands ?? throw new ArgumentNullException(nameof(commands));
@@ -83,7 +77,6 @@ namespace Tgstation.Server.Host.Components.Repository
this.eventConsumer = eventConsumer ?? throw new ArgumentNullException(nameof(eventConsumer));
this.repositoryLogger = repositoryLogger ?? throw new ArgumentNullException(nameof(repositoryLogger));
this.logger = logger ?? throw new ArgumentNullException(nameof(logger));
this.repositorySettings = repositorySettings ?? throw new ArgumentNullException(nameof(repositorySettings));
semaphore = new SemaphoreSlim(1);
}
@@ -106,7 +99,7 @@ namespace Tgstation.Server.Host.Components.Repository
lock (semaphore)
{
if (CloneInProgress)
throw new InvalidOperationException("The repository is already being cloned!");
throw new JobException(ErrorCode.RepoCloning);
CloneInProgress = true;
}
@@ -179,33 +172,33 @@ namespace Tgstation.Server.Host.Components.Repository
logger.LogTrace("Begin LoadRepository...");
lock (semaphore)
if (CloneInProgress)
throw new InvalidOperationException("The repository is being cloned!");
using (var context = await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false))
try
{
var libGitRepo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false);
throw new JobException(ErrorCode.RepoCloning);
using var context = await SemaphoreSlimContext.Lock(semaphore, cancellationToken).ConfigureAwait(false);
try
{
var libGitRepo = await repositoryFactory.CreateFromPath(ioManager.ResolvePath(), cancellationToken).ConfigureAwait(false);
if (libGitRepo == null)
return null;
return new Repository(
libGitRepo,
commands,
ioManager,
eventConsumer,
repositoryFactory,
repositoryLogger, () =>
{
logger.LogTrace("Releasing semaphore due to Repository disposal...");
semaphore.Release();
});
}
catch (RepositoryNotFoundException e)
{
logger.LogDebug("Repository not found!");
logger.LogTrace("Exception: {0}", e);
if (libGitRepo == null)
return null;
}
return new Repository(
libGitRepo,
commands,
ioManager,
eventConsumer,
repositoryFactory,
repositoryLogger, () =>
{
logger.LogTrace("Releasing semaphore due to Repository disposal...");
semaphore.Release();
});
}
catch (RepositoryNotFoundException e)
{
logger.LogDebug("Repository not found!");
logger.LogTrace("Exception: {0}", e);
return null;
}
}
/// <inheritdoc />
@@ -865,7 +865,7 @@ namespace Tgstation.Server.Host.Components.Watchdog
CancelRight = (ulong)DreamDaemonRights.Shutdown,
CancelRightsType = RightsType.DreamDaemon
};
await jobManager.RegisterOperation(job, async (j, databaseContext, progressFunction, ct) =>
await jobManager.RegisterOperation(job, async (j, databaseContextFactory, progressFunction, ct) =>
{
using (await SemaphoreSlimContext.Lock(Semaphore, ct).ConfigureAwait(false))
await LaunchImplNoLock(true, true, reattachInfo, ct).ConfigureAwait(false);
@@ -120,7 +120,7 @@ namespace Tgstation.Server.Host.Controllers
CancelRight = (ulong)ByondRights.CancelInstall,
Instance = Instance
};
await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => byondManager.ChangeVersion(installingVersion, ct), cancellationToken).ConfigureAwait(false);
result.InstallJob = job.ToApi();
}
@@ -77,7 +77,7 @@ namespace Tgstation.Server.Host.Controllers
Instance = Instance,
StartedBy = AuthenticationContext.User
};
await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, innerCt) => instance.Watchdog.Launch(innerCt), cancellationToken).ConfigureAwait(false);
return Accepted(job.ToApi());
}
@@ -274,7 +274,7 @@ namespace Tgstation.Server.Host.Controllers
var watchdog = instanceManager.GetInstance(Instance).Watchdog;
await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => watchdog.Restart(false, ct), cancellationToken).ConfigureAwait(false);
return Accepted(job.ToApi());
}
}
@@ -505,7 +505,7 @@ namespace Tgstation.Server.Host.Controllers
StartedBy = AuthenticationContext.User
};
await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressHandler, ct) => instanceManager.MoveInstance(originalModel, rawPath, ct), cancellationToken).ConfigureAwait(false);
api.MoveJob = job.ToApi();
}
@@ -82,7 +82,11 @@ namespace Tgstation.Server.Host.Controllers
// If the DB doesn't have it, check the local set
if (revisionInfo == default)
revisionInfo = databaseContext.RevisionInformations.Local.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id).FirstOrDefault();
revisionInfo = databaseContext
.RevisionInformations
.Local
.Where(x => x.CommitSha == repoSha && x.Instance.Id == instance.Id)
.FirstOrDefault();
var needsDbUpdate = revisionInfo == default;
if (needsDbUpdate)
@@ -193,7 +197,7 @@ namespace Tgstation.Server.Host.Controllers
Instance = Instance
};
var api = currentModel.ToApi();
await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) =>
await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
{
using var repos = await repoManager.CloneRepository(new Uri(origin), cloneBranch, currentModel.AccessUser, currentModel.AccessToken, progressReporter, ct).ConfigureAwait(false);
if (repos == null)
@@ -202,9 +206,14 @@ namespace Tgstation.Server.Host.Controllers
{
Id = Instance.Id
};
databaseContext.Instances.Attach(instance);
if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
await databaseContext.Save(ct).ConfigureAwait(false);
await databaseContextFactory.UseContext(
async databaseContext =>
{
databaseContext.Instances.Attach(instance);
if (await PopulateApi(api, repos, databaseContext, instance, ct).ConfigureAwait(false))
await databaseContext.Save(ct).ConfigureAwait(false);
})
.ConfigureAwait(false);
}, cancellationToken).ConfigureAwait(false);
api.Origin = model.Origin;
@@ -246,7 +255,7 @@ namespace Tgstation.Server.Host.Controllers
Instance = Instance
};
var api = currentModel.ToApi();
await jobManager.RegisterOperation(job, (paramJob, databaseContext, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
await jobManager.RegisterOperation(job, (paramJob, databaseContextFactory, progressReporter, ct) => instanceManager.GetInstance(Instance).RepositoryManager.DeleteRepository(cancellationToken), cancellationToken).ConfigureAwait(false);
api.ActiveJob = job.ToApi();
return Accepted(api);
}
@@ -435,7 +444,8 @@ namespace Tgstation.Server.Host.Controllers
CancelRight = (ulong)RepositoryRights.CancelPendingChanges,
};
await jobManager.RegisterOperation(job, async (paramJob, databaseContext, progressReporter, ct) =>
// Time to access git, do it in a job
await jobManager.RegisterOperation(job, async (paramJob, databaseContextFactory, progressReporter, ct) =>
{
using var repo = await repoManager.LoadRepository(ct).ConfigureAwait(false);
if (repo == null)
@@ -475,15 +485,57 @@ namespace Tgstation.Server.Host.Controllers
{
Id = Instance.Id
};
databaseContext.Instances.Attach(attachedInstance);
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
Task CallLoadRevInfo(Models.TestMerge testMergeToAdd = null, string lastOriginCommitSha = null) => databaseContextFactory
.UseContext(
async databaseContext =>
{
databaseContext.Instances.Attach(attachedInstance);
var needsUpdate = await LoadRevisionInformation(
repo,
databaseContext,
attachedInstance,
lastOriginCommitSha,
x => lastRevisionInfo = x,
ct)
.ConfigureAwait(false);
if (testMergeToAdd != null)
{
// rev info may have already loaded the user
var mergedBy = databaseContext.Users.Local.FirstOrDefault(x => x.Id == AuthenticationContext.User.Id);
if (mergedBy == default)
{
mergedBy = new Models.User
{
Id = AuthenticationContext.User.Id
};
databaseContext.Users.Attach(mergedBy);
}
testMergeToAdd.MergedBy = mergedBy;
lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
{
TestMerge = testMergeToAdd
});
lastRevisionInfo.PrimaryTestMerge = testMergeToAdd;
databaseContext.Users.Attach(testMergeToAdd.MergedBy);
}
if (needsUpdate)
await databaseContext.Save(cancellationToken).ConfigureAwait(false);
});
await CallLoadRevInfo().ConfigureAwait(false);
// apply new rev info, tracking applied test merges
async Task UpdateRevInfo()
async Task UpdateRevInfo(Models.TestMerge testMergeToAdd = null)
{
var last = lastRevisionInfo;
await LoadRevisionInformation(repo, databaseContext, attachedInstance, last.OriginCommitSha, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
await CallLoadRevInfo(testMergeToAdd, last.OriginCommitSha).ConfigureAwait(false);
lastRevisionInfo.ActiveTestMerges.AddRange(last.ActiveTestMerges);
}
@@ -531,7 +583,7 @@ namespace Tgstation.Server.Host.Controllers
throw new JobException(ErrorCode.RepoSwappedShaOrReference);
await repo.CheckoutObject(committish, NextProgressReporter(), ct).ConfigureAwait(false);
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin
await CallLoadRevInfo().ConfigureAwait(false); // we've either seen origin before or what we're checking out is on origin
}
else
NextProgressReporter()(100);
@@ -542,7 +594,7 @@ namespace Tgstation.Server.Host.Controllers
throw new JobException(ErrorCode.RepoReferenceNotTracking);
await repo.ResetToOrigin(NextProgressReporter(), ct).ConfigureAwait(false);
await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), true, ct).ConfigureAwait(false);
await LoadRevisionInformation(repo, databaseContext, attachedInstance, null, x => lastRevisionInfo = x, ct).ConfigureAwait(false);
await CallLoadRevInfo().ConfigureAwait(false);
// repo head is on origin so force this
// will update the db if necessary
@@ -559,10 +611,10 @@ namespace Tgstation.Server.Host.Controllers
I.PullRequestRevision = null;
var gitHubClient = currentModel.AccessToken != null
? gitHubClientFactory.CreateClient(currentModel.AccessToken)
: (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
? gitHubClientFactory.CreateClient()
: gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
? gitHubClientFactory.CreateClient(currentModel.AccessToken)
: (String.IsNullOrEmpty(generalConfiguration.GitHubAccessToken)
? gitHubClientFactory.CreateClient()
: gitHubClientFactory.CreateClient(generalConfiguration.GitHubAccessToken));
var repoOwner = repo.GitHubOwner;
var repoName = repo.GitHubRepoName;
@@ -600,14 +652,20 @@ namespace Tgstation.Server.Host.Controllers
if (!cantSearch)
{
var dbPull = await databaseContext.RevisionInformations
.Where(x => x.Instance.Id == Instance.Id
&& x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
&& x.ActiveTestMerges.Count <= model.NewTestMerges.Count
&& x.ActiveTestMerges.Count > 0)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ToListAsync(cancellationToken).ConfigureAwait(false);
List<Models.RevisionInformation> dbPull = null;
await databaseContextFactory.UseContext(
async databaseContext =>
dbPull = await databaseContext.RevisionInformations
.Where(x => x.Instance.Id == Instance.Id
&& x.OriginCommitSha == lastRevisionInfo.OriginCommitSha
&& x.ActiveTestMerges.Count <= model.NewTestMerges.Count
&& x.ActiveTestMerges.Count > 0)
.Include(x => x.ActiveTestMerges)
.ThenInclude(x => x.TestMerge)
.ToListAsync(cancellationToken)
.ConfigureAwait(false))
.ConfigureAwait(false);
// split here cause this bit has to be done locally
revInfoWereLookingFor = dbPull
@@ -619,7 +677,7 @@ namespace Tgstation.Server.Host.Controllers
&& (y.Comment?.Trim().ToUpperInvariant() == z.Comment?.Trim().ToUpperInvariant() || z.Comment == null))))
.FirstOrDefault();
if (revInfoWereLookingFor == null && model.NewTestMerges.Count > 1)
if (revInfoWereLookingFor == default && model.NewTestMerges.Count > 1)
{
// okay try to add at least SOME prs we've seen before
var search = model.NewTestMerges.ToList();
@@ -669,20 +727,6 @@ namespace Tgstation.Server.Host.Controllers
if (needToApplyRemainingPrs)
{
// an invocation of LoadRevisionInformation could have already loaded this user
var contextUser = databaseContext.Users.Local.Where(x => x.Id == AuthenticationContext.User.Id).FirstOrDefault();
if (contextUser == default)
{
// No reason to call the DB, just attach it
contextUser = new Models.User
{
Id = AuthenticationContext.User.Id
};
databaseContext.Users.Attach(contextUser);
}
else
Logger.LogTrace("Skipping attaching the user to the database context as it is already loaded!");
foreach (var I in model.NewTestMerges)
{
Octokit.PullRequest pr = null;
@@ -740,8 +784,7 @@ namespace Tgstation.Server.Host.Controllers
++doneSteps;
var revInfoUpdateTask = UpdateRevInfo();
// MergedBy will be set later
var tm = new Models.TestMerge
{
Author = pr?.User.Login ?? errorMessage,
@@ -750,18 +793,11 @@ namespace Tgstation.Server.Host.Controllers
TitleAtMerge = pr?.Title ?? errorMessage ?? String.Empty,
Comment = I.Comment,
Number = I.Number,
MergedBy = contextUser,
PullRequestRevision = I.PullRequestRevision,
Url = pr?.HtmlUrl ?? errorMessage
};
await revInfoUpdateTask.ConfigureAwait(false);
lastRevisionInfo.PrimaryTestMerge = tm;
lastRevisionInfo.ActiveTestMerges.Add(new RevInfoTestMerge
{
TestMerge = tm
});
await UpdateRevInfo(tm).ConfigureAwait(false);
}
}
}
@@ -772,8 +808,6 @@ namespace Tgstation.Server.Host.Controllers
await repo.Sychronize(currentModel.AccessUser, currentModel.AccessToken, currentModel.CommitterName, currentModel.CommitterEmail, NextProgressReporter(), false, ct).ConfigureAwait(false);
await UpdateRevInfo().ConfigureAwait(false);
}
await databaseContext.Save(ct).ConfigureAwait(false);
}
catch
{
@@ -19,8 +19,8 @@ namespace Tgstation.Server.Host.Core
{
if (semaphore == null)
throw new ArgumentNullException(nameof(semaphore));
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
cancellationToken.ThrowIfCancellationRequested();
await semaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
return new SemaphoreSlimContext(semaphore);
}
@@ -6,7 +6,7 @@ namespace Tgstation.Server.Host.Database
/// <summary>
/// Factory for scoping usage of <see cref="IDatabaseContext"/>s. Meant for use by <see cref="Components"/>
/// </summary>
interface IDatabaseContextFactory
public interface IDatabaseContextFactory
{
/// <summary>
/// Run an <paramref name="operation"/> in the scope of an <see cref="IDatabaseContext"/>
@@ -23,10 +23,10 @@ namespace Tgstation.Server.Host.Jobs
/// Registers a given <see cref="Job"/> and begins running it
/// </summary>
/// <param name="job">The <see cref="Job"/></param>
/// <param name="operation">The operation to run taking the started <see cref="Job"/>, a <see cref="IDatabaseContext"/>, progress reporter <see cref="Action{T1}"/> and a <see cref="CancellationToken"/></param>
/// <param name="operation">The operation to run taking the started <see cref="Job"/>, a <see cref="IDatabaseContextFactory"/>, progress reporter <see cref="Action{T1}"/> and a <see cref="CancellationToken"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing a running operation</returns>
Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken);
Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken);
/// <summary>
/// Wait for a given <paramref name="job"/> to complete
+44 -38
View File
@@ -75,50 +75,56 @@ namespace Tgstation.Server.Host.Jobs
/// <param name="operation">The operation for the <paramref name="job"/></param>
/// <param name="cancellationToken">The <see cref="CancellationToken"/> for the operation</param>
/// <returns>A <see cref="Task"/> representing the running operation</returns>
async Task RunJob(Job job, Func<Job, IDatabaseContext, CancellationToken, Task> operation, CancellationToken cancellationToken)
async Task RunJob(Job job, Func<Job, IDatabaseContextFactory, CancellationToken, Task> operation, CancellationToken cancellationToken)
{
try
{
void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
try
{
var oldJob = job;
job = new Job { Id = oldJob.Id };
await operation(job, databaseContextFactory, cancellationToken).ConfigureAwait(false);
logger.LogDebug("Job {0} completed!", job.Id);
}
catch (OperationCanceledException)
{
logger.LogDebug("Job {0} cancelled!", job.Id);
job.Cancelled = true;
}
catch (JobException e)
{
job.ErrorCode = e.ErrorCode;
job.ExceptionDetails = e.Message;
LogRegularException();
if (e.InnerException != null)
logger.LogDebug(
"Inner exception for job {0}: {1}",
job.Id,
e.InnerException is JobException
? e.InnerException.Message
: e.InnerException.ToString());
}
catch (Exception e)
{
job.ExceptionDetails = e.ToString();
LogRegularException();
}
await databaseContextFactory.UseContext(async databaseContext =>
{
void LogRegularException() => logger.LogDebug("Job {0} exited with error! Exception: {1}", job.Id, job.ExceptionDetails);
try
var attachedJob = new Job
{
var oldJob = job;
job = new Job { Id = oldJob.Id };
databaseContext.Jobs.Attach(job);
Id = job.Id
};
await operation(job, databaseContext, cancellationToken).ConfigureAwait(false);
logger.LogDebug("Job {0} completed!", job.Id);
}
catch (OperationCanceledException)
{
logger.LogDebug("Job {0} cancelled!", job.Id);
job.Cancelled = true;
}
catch (JobException e)
{
job.ErrorCode = e.ErrorCode;
job.ExceptionDetails = e.Message;
LogRegularException();
if (e.InnerException != null)
logger.LogDebug(
"Inner exception for job {0}: {1}",
job.Id,
e.InnerException is JobException
? e.InnerException.Message
: e.InnerException.ToString());
}
catch (Exception e)
{
job.ExceptionDetails = e.ToString();
LogRegularException();
}
finally
{
job.StoppedAt = DateTimeOffset.Now;
}
databaseContext.Jobs.Attach(attachedJob);
attachedJob.StoppedAt = DateTimeOffset.Now;
attachedJob.ExceptionDetails = job.ExceptionDetails;
attachedJob.ErrorCode = job.ErrorCode;
attachedJob.Cancelled = job.Cancelled;
await databaseContext.Save(default).ConfigureAwait(false);
}).ConfigureAwait(false);
@@ -135,7 +141,7 @@ namespace Tgstation.Server.Host.Jobs
}
/// <inheritdoc />
public Task RegisterOperation(Job job, Func<Job, IDatabaseContext, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
public Task RegisterOperation(Job job, Func<Job, IDatabaseContextFactory, Action<int>, CancellationToken, Task> operation, CancellationToken cancellationToken) => databaseContextFactory.UseContext(async databaseContext =>
{
if (job == null)
throw new ArgumentNullException(nameof(job));
@@ -402,7 +402,7 @@ namespace Tgstation.Server.Host.Setup
databaseName = await ValidateNonExistantSqliteDBName(databaseName, cancellationToken).ConfigureAwait(false);
}
else
dbExists = await PromptYesNo("Does this database already exist? (y/n): ", cancellationToken).ConfigureAwait(false);
dbExists = await PromptYesNo("Does this database already exist? If not, we will attempt to CREATE it. (y/n): ", cancellationToken).ConfigureAwait(false);
}
if (String.IsNullOrWhiteSpace(databaseName))