From f84a659d47732c28ec54848a9121196349ec67bf Mon Sep 17 00:00:00 2001 From: Marco Kirchner Date: Sun, 5 Dec 2021 14:50:06 +0100 Subject: [PATCH] improve exception handling when starting instances --- .github/workflows/release.yaml | 44 +++++++++---- CHANGELOG.md | 7 ++ GiganticEmu.Agent/Pages/Error.cshtml | 28 ++++---- GiganticEmu.Agent/Pages/Error.cshtml.cs | 15 ++++- GiganticEmu.Agent/Pages/Shared/_Layout.cshtml | 11 +++- GiganticEmu.Agent/ServerManager.cs | 65 ++++++++++++++----- 6 files changed, 124 insertions(+), 46 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index c5264b2..bc4e0b8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -23,14 +23,33 @@ jobs: - name: Setup msvc uses: ilammy/msvc-dev-cmd@v1 + - name: Build + run: pwsh dist.ps1 + + - uses: actions/upload-artifact@v2 + with: + name: dist + path: dist + + release: + runs-on: ubuntu-latest + needs: [build] + if: startsWith(github.ref, 'refs/tags/v') + + steps: + - uses: actions/checkout@v2 + - run: git fetch --prune --unshallow --tags + + - uses: actions/download-artifact@v2 + with: + name: dist + path: dist + - name: Install GitVersion uses: gittools/actions/gitversion/setup@v0.9.7 with: versionSpec: '5.x' - - name: Build - run: pwsh dist.ps1 - - name: Set Version id: version uses: gittools/actions/gitversion/execute@v0.9.7 @@ -76,7 +95,6 @@ jobs: - name: Create Release uses: ncipollo/release-action@v1 - if: ${{ steps.version.outputs.commitsSinceVersionSource == '0' }} with: allowUpdates: true name: v${{ steps.version.outputs.informationalVersion }} @@ -88,15 +106,13 @@ jobs: prerelease: ${{ steps.version.outputs.preReleaseLabel != '' }} - name: Create Discord release notification - uses: rjstone/discord-webhook-notify@v1.0.4 - if: ${{ steps.version.outputs.commitsSinceVersionSource == '0' }} + uses: tsickert/discord-webhook@v4.0.0 with: - webhookUrl: ${{ secrets.DISCORD_WEBHOOK }} - severity: info - username: GiganticEmu - color: "#1a7d93" - avatarUrl: https://cdn.jsdelivr.net/gh/BigBoot/GiganticEmu@master/GiganticEmu.Agent/icon.png - description: "[Release v${{ steps.version.outputs.informationalVersion }}](https://github.com/BigBoot/GiganticEmu/releases/tag/v${{ steps.version.outputs.informationalVersion }})" - details: ${{ steps.changelog.outputs.description }} - footer: Release Notification + webhook-url: ${{ secrets.DISCORD_WEBHOOK }} + username: GiganticEmu Release Notifications + avatar-url: https://cdn.jsdelivr.net/gh/BigBoot/GiganticEmu@master/GiganticEmu.Agent/icon.png + embed-title: "Version v${{ steps.version.outputs.informationalVersion }}" + embed-description: "**Changes:**\n${{ steps.changelog.outputs.description }}\n\n[Show more](https://github.com/BigBoot/GiganticEmu/releases/tag/v${{ steps.version.outputs.informationalVersion }})" + embed-color: 1736083 + embed-thumbnail-url: https://cdn.jsdelivr.net/gh/BigBoot/GiganticEmu@master/GiganticEmu.Agent/icon.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d9478a..62baf7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [2.0.5] - 2021-12-05 +### Added +- Error page with Exception details to Gigantic.Agent + +### Changed +- Made launching instances more resilient to errors + ## [2.0.4] - 2021-12-04 ### Fixed diff --git a/GiganticEmu.Agent/Pages/Error.cshtml b/GiganticEmu.Agent/Pages/Error.cshtml index 6f92b95..caa697c 100644 --- a/GiganticEmu.Agent/Pages/Error.cshtml +++ b/GiganticEmu.Agent/Pages/Error.cshtml @@ -7,20 +7,24 @@

Error.

An error occurred while processing your request.

-@if (Model.ShowRequestId) +@if (!string.IsNullOrEmpty(Model.ErrorMessage)) {

- Request ID: @Model.RequestId + Error Message: @Model.ErrorMessage +

+} + +@if (!string.IsNullOrEmpty(Model.ErrorMessage)) +{ +

+ Stacktrace: +

} -

Development Mode

-

- Swapping to the Development environment displays detailed information about the error that occurred. -

-

- The Development environment shouldn't be enabled for deployed applications. - It can result in displaying sensitive information from exceptions to end users. - For local debugging, enable the Development environment by setting the ASPNETCORE_ENVIRONMENT environment variable to Development - and restarting the app. -

+@if (!string.IsNullOrEmpty(Model.RequestId)) +{ +

+ Request ID: @Model.RequestId +

+} diff --git a/GiganticEmu.Agent/Pages/Error.cshtml.cs b/GiganticEmu.Agent/Pages/Error.cshtml.cs index 1a8e776..75a8edb 100644 --- a/GiganticEmu.Agent/Pages/Error.cshtml.cs +++ b/GiganticEmu.Agent/Pages/Error.cshtml.cs @@ -1,4 +1,5 @@ using System.Diagnostics; +using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.Extensions.Logging; @@ -11,7 +12,9 @@ public class ErrorModel : PageModel { public string? RequestId { get; set; } - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + public string? ErrorMessage { get; set; } + + public string? StackTrace { get; set; } private readonly ILogger _logger; @@ -23,6 +26,16 @@ public ErrorModel(ILogger logger) public void OnGet() { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier; + + var exceptionHandlerPathFeature = HttpContext.Features.Get(); + + ErrorMessage = exceptionHandlerPathFeature?.Error?.Message; + StackTrace = exceptionHandlerPathFeature?.Error?.StackTrace; + } + + public void OnPost() + { + OnGet(); } } } \ No newline at end of file diff --git a/GiganticEmu.Agent/Pages/Shared/_Layout.cshtml b/GiganticEmu.Agent/Pages/Shared/_Layout.cshtml index 8f6a6da..303b527 100644 --- a/GiganticEmu.Agent/Pages/Shared/_Layout.cshtml +++ b/GiganticEmu.Agent/Pages/Shared/_Layout.cshtml @@ -84,14 +84,21 @@
  • - © + © BigBoot
  • diff --git a/GiganticEmu.Agent/ServerManager.cs b/GiganticEmu.Agent/ServerManager.cs index 0d4d9c8..5ed161a 100644 --- a/GiganticEmu.Agent/ServerManager.cs +++ b/GiganticEmu.Agent/ServerManager.cs @@ -14,7 +14,7 @@ namespace GiganticEmu.Agent; public class ServerManager { - private record Instance(Process Process, string AdminPassword); + private record Instance(Process Process, string AdminPassword, string DefaultGameIniPath, int Port); private static Regex RE_CREATURE = new Regex(@"DefaultMinionLoadout\[(\d+)]=""\w*""", RegexOptions.Compiled | RegexOptions.Multiline); private static Regex RE_MAX_PLAYERS = new Regex(@"MaxPlayers=\d*", RegexOptions.Compiled | RegexOptions.Multiline); @@ -30,7 +30,7 @@ private record Instance(Process Process, string AdminPassword); private readonly string _binaryPath; private readonly string _configPath; - public int RunningInstances { get => _instances.Count; } + public int RunningInstances { get => _configuration.MaxInstances - _freePorts.Count; } public ServerManager(ILogger logger, IOptions configuration) { @@ -51,8 +51,33 @@ public async Task StartInstance(string map, int? maxPlayers = null, (string throw new NoInstanceAvailableException(); } - var process = new Process(); + Instance instance; + try + { + instance = await StartInstance(port, map, maxPlayers, creatures, useLobby); + } + catch (Exception) + { + _instances.Remove(port); + _freePorts.Enqueue(port, port); + + throw; + } + + if (instance != null) + { + _ = Task + .Run(async () => RunInstance(instance)) + .LogExceptions(_logger); + } + + return port; + } + + private async Task StartInstance(int port, string map, int? maxPlayers, (string, string, string)? creatures, bool useLobby) + { + var process = new Process(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -143,24 +168,30 @@ public async Task StartInstance(string map, int? maxPlayers = null, (string } process.Start(); - _instances.Add(port, new Instance(process, adminPassword)); ChildProcessTracker.AddProcess(process); - _ = Task.Run(async () => - { - await process.WaitForExitAsync(); - _instances.Remove(port); - _freePorts.Enqueue(port, port); - File.Delete(defGameIniPath); - }).LogExceptions(_logger); + var instance = new Instance(process, adminPassword, defGameIniPath, port); + _instances.Add(port, instance); + return instance; + } - _ = Task.Run(async () => + private async Task RunInstance(Instance instance) + { + try { - await Task.Delay(TimeSpan.FromHours(1)); - process.Kill(); - }).LogExceptions(_logger); - - return port; + var timeout = Task.Delay(TimeSpan.FromHours(1)); + if (await Task.WhenAny(instance.Process.WaitForExitAsync(), timeout) == timeout) + { + instance.Process.Kill(); + await instance.Process.WaitForExitAsync(); + } + } + finally + { + _instances.Remove(instance.Port); + _freePorts.Enqueue(instance.Port, instance.Port); + File.Delete(instance.DefaultGameIniPath); + } } public async Task KillInstance(int port)