mirror of https://github.com/Squidex/squidex.git
committed by
GitHub
5 changed files with 268 additions and 7 deletions
@ -0,0 +1,157 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using System.Diagnostics; |
|||
using Microsoft.Extensions.Options; |
|||
using Squidex.Assets; |
|||
using Squidex.Hosting; |
|||
|
|||
namespace Squidex.Infrastructure.Dump |
|||
{ |
|||
public sealed class Dumper : IInitializable |
|||
{ |
|||
private static readonly TimeSpan DefaultTimeout = TimeSpan.FromMinutes(30); |
|||
private readonly DumperOptions options; |
|||
private readonly IAssetStore assetStore; |
|||
private Task? scheduledGcDumpTask; |
|||
private Task? scheduledDumpTask; |
|||
private Timer? timer; |
|||
|
|||
public int Order => int.MaxValue; |
|||
|
|||
public Dumper(IOptions<DumperOptions> options, IAssetStore assetStore) |
|||
{ |
|||
this.options = options.Value; |
|||
this.assetStore = assetStore; |
|||
} |
|||
|
|||
public Task InitializeAsync( |
|||
CancellationToken ct) |
|||
{ |
|||
if (options.DumpLimit > 0 || options.GCDumpLimit > 0) |
|||
{ |
|||
timer = new Timer(CollectDump); |
|||
timer.Change(TimeSpan.Zero, TimeSpan.FromSeconds(5)); |
|||
} |
|||
|
|||
return Task.CompletedTask; |
|||
} |
|||
|
|||
public Task ReleaseAsync( |
|||
CancellationToken ct) |
|||
{ |
|||
var tasks = new List<Task>(); |
|||
|
|||
if (timer != null) |
|||
{ |
|||
tasks.Add(timer.DisposeAsync().AsTask()); |
|||
} |
|||
|
|||
if (scheduledDumpTask != null) |
|||
{ |
|||
tasks.Add(scheduledDumpTask); |
|||
} |
|||
|
|||
if (scheduledGcDumpTask != null) |
|||
{ |
|||
tasks.Add(scheduledGcDumpTask); |
|||
} |
|||
|
|||
return Task.WhenAll(tasks); |
|||
} |
|||
|
|||
private void CollectDump(object? state) |
|||
{ |
|||
try |
|||
{ |
|||
var usage = GC.GetTotalMemory(false) / (1024 * 1024); |
|||
|
|||
if (options.DumpLimit > 0 && usage > options.DumpLimit && scheduledDumpTask == null) |
|||
{ |
|||
scheduledDumpTask = CreateDumpAsync(); |
|||
} |
|||
|
|||
if (options.GCDumpLimit > 0 && usage > options.GCDumpLimit && scheduledGcDumpTask == null) |
|||
{ |
|||
scheduledGcDumpTask = CreateGCDumpAsync(); |
|||
} |
|||
} |
|||
#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body
|
|||
catch |
|||
#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body
|
|||
{ |
|||
|
|||
} |
|||
} |
|||
|
|||
public Task<bool> CreateDumpAsync( |
|||
CancellationToken ct = default) |
|||
{ |
|||
return CreateDumpAsync(options.DumpTool, "dump", ct); |
|||
} |
|||
|
|||
public Task<bool> CreateGCDumpAsync( |
|||
CancellationToken ct = default) |
|||
{ |
|||
return CreateDumpAsync(options.GcDumpTool, "gcdump", ct); |
|||
} |
|||
|
|||
private async Task<bool> CreateDumpAsync(string? tool, string extension, |
|||
CancellationToken ct = default) |
|||
{ |
|||
if (string.IsNullOrWhiteSpace(tool)) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
using var cts = new CancellationTokenSource(DefaultTimeout); |
|||
using var ctl = CancellationTokenSource.CreateLinkedTokenSource(cts.Token, ct); |
|||
|
|||
var tempPath = Path.Combine(Path.GetTempPath(), Path.GetTempFileName()); |
|||
|
|||
var writtenFile = $"{tempPath}.{extension}"; |
|||
try |
|||
{ |
|||
using var process = new Process(); |
|||
process.StartInfo.Arguments = $"collect -p {Environment.ProcessId} -o {tempPath}"; |
|||
process.StartInfo.FileName = tool; |
|||
process.StartInfo.UseShellExecute = false; |
|||
process.Start(); |
|||
|
|||
await process.WaitForExitAsync(ctl.Token); |
|||
|
|||
var isSucceess = process.ExitCode == 0; |
|||
|
|||
if (!isSucceess) |
|||
{ |
|||
return false; |
|||
} |
|||
|
|||
await using (var fs = new FileStream(writtenFile, FileMode.Open)) |
|||
{ |
|||
var name = $"diagnostics/{extension}/{DateTime.UtcNow:yyyy-MM-dd-hh-mm-ss}.{extension}"; |
|||
|
|||
await assetStore.UploadAsync(name, fs, true, ctl.Token); |
|||
} |
|||
} |
|||
finally |
|||
{ |
|||
try |
|||
{ |
|||
File.Delete(tempPath); |
|||
} |
|||
#pragma warning disable RECS0022 // A catch clause that catches System.Exception and has an empty body
|
|||
catch |
|||
#pragma warning restore RECS0022 // A catch clause that catches System.Exception and has an empty body
|
|||
{ |
|||
} |
|||
} |
|||
|
|||
return true; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,20 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
namespace Squidex.Infrastructure.Dump |
|||
{ |
|||
public sealed class DumperOptions |
|||
{ |
|||
public string? GcDumpTool { get; set; } |
|||
|
|||
public string? DumpTool { get; set; } |
|||
|
|||
public int GCDumpLimit { get; set; } |
|||
|
|||
public int DumpLimit { get; set; } |
|||
} |
|||
} |
|||
@ -0,0 +1,62 @@ |
|||
// ==========================================================================
|
|||
// Squidex Headless CMS
|
|||
// ==========================================================================
|
|||
// Copyright (c) Squidex UG (haftungsbeschraenkt)
|
|||
// All rights reserved. Licensed under the MIT license.
|
|||
// ==========================================================================
|
|||
|
|||
using Microsoft.AspNetCore.Mvc; |
|||
using Squidex.Infrastructure.Commands; |
|||
using Squidex.Infrastructure.Dump; |
|||
using Squidex.Shared; |
|||
using Squidex.Web; |
|||
|
|||
namespace Squidex.Areas.Api.Controllers.Diagnostics |
|||
{ |
|||
/// <summary>
|
|||
/// Makes a diagnostics request.
|
|||
/// </summary>
|
|||
[ApiExplorerSettings(GroupName = nameof(Diagnostics))] |
|||
public sealed class DiagnosticsController : ApiController |
|||
{ |
|||
private readonly Dumper dumper; |
|||
|
|||
public DiagnosticsController(ICommandBus commandBus, Dumper dumper) |
|||
: base(commandBus) |
|||
{ |
|||
this.dumper = dumper; |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a dump and writes it into storage..
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// 204 => Dump created successful.
|
|||
/// </returns>
|
|||
[HttpGet] |
|||
[Route("diagnostics/dump")] |
|||
[ApiPermissionOrAnonymous(Permissions.Admin)] |
|||
public async Task<IActionResult> GetDump() |
|||
{ |
|||
await dumper.CreateDumpAsync(HttpContext.RequestAborted); |
|||
|
|||
return NoContent(); |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Creates a gc dump and writes it into storage.
|
|||
/// </summary>
|
|||
/// <returns>
|
|||
/// 204 => Dump created successful.
|
|||
/// </returns>
|
|||
[HttpGet] |
|||
[Route("diagnostics/gc-dump")] |
|||
[ApiPermissionOrAnonymous(Permissions.Admin)] |
|||
public async Task<IActionResult> GetGCDump() |
|||
{ |
|||
await dumper.CreateGCDumpAsync(HttpContext.RequestAborted); |
|||
|
|||
return NoContent(); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue