Skip to content

Commit

Permalink
Refactor to avoid internals in HexusApplication
Browse files Browse the repository at this point in the history
  • Loading branch information
Fleny113 committed May 12, 2024
1 parent 854c908 commit ee0f30f
Show file tree
Hide file tree
Showing 23 changed files with 571 additions and 421 deletions.
4 changes: 2 additions & 2 deletions Hexus.Daemon/AppJsonSerializerContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
namespace Hexus.Daemon;

[JsonSerializable(typeof(HttpValidationProblemDetails))]
[JsonSerializable(typeof(HexusApplicationResponse))]
[JsonSerializable(typeof(IEnumerable<HexusApplicationResponse>))]
[JsonSerializable(typeof(ApplicationResponse))]
[JsonSerializable(typeof(IEnumerable<ApplicationResponse>))]
[JsonSerializable(typeof(IAsyncEnumerable<ApplicationLog>))]
[JsonSerializable(typeof(NewApplicationRequest))]
[JsonSerializable(typeof(EditApplicationRequest))]
Expand Down
26 changes: 1 addition & 25 deletions Hexus.Daemon/Configuration/HexusApplication.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
using Hexus.Daemon.Contracts;
using System.ComponentModel;
using System.Diagnostics;
using System.Threading.Channels;
using YamlDotNet.Serialization;
using System.ComponentModel;

namespace Hexus.Daemon.Configuration;

Expand All @@ -17,24 +13,4 @@ public sealed record HexusApplication
[DefaultValue("")] public string Note { get; set; } = "";

public Dictionary<string, string> EnvironmentVariables { get; set; } = [];

#region Internal proprieties

[YamlIgnore] internal Process? Process { get; set; }

// Logs
[YamlIgnore] internal SemaphoreSlim LogSemaphore { get; } = new(initialCount: 1, maxCount: 1);
[YamlIgnore] internal List<Channel<ApplicationLog>> LogChannels { get; } = [];

// Performance tracking
[YamlIgnore] internal Dictionary<int, CpuStats> CpuStatsMap { get; } = [];
[YamlIgnore] internal double LastCpuUsage { get; set; }

internal record CpuStats
{
public TimeSpan LastTotalProcessorTime { get; set; }
public DateTimeOffset LastGetProcessCpuUsageInvocation { get; set; }
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

namespace Hexus.Daemon.Contracts.Responses;

public sealed record HexusApplicationResponse(
public sealed record ApplicationResponse(
string Name,
string Executable,
string Arguments,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@ internal sealed class DeleteApplicationEndpoint : IEndpoint
public static Results<NoContent, NotFound> Handle(
[FromServices] HexusConfigurationManager configManager,
[FromServices] ProcessManagerService processManager,
[FromServices] LogService logService,
[FromRoute] string name,
[FromQuery] bool forceStop = false)
{
if (!configManager.Configuration.Applications.ContainsKey(name))
if (!configManager.Configuration.Applications.TryGetValue(name, out var application))
return TypedResults.NotFound();

processManager.StopApplication(name, forceStop);
processManager.StopApplication(application, forceStop);
logService.DeleteApplication(application);

File.Delete($"{EnvironmentHelper.LogsDirectory}/{name}.log");
configManager.Configuration.Applications.Remove(name);
configManager.Configuration.Applications.Remove(name, out _);
configManager.SaveConfiguration();

return TypedResults.NoContent();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ public static Results<NoContent, NotFound, ValidationProblem> Handle(
[FromRoute] string name,
[FromBody] EditApplicationRequest request,
[FromServices] IValidator<EditApplicationRequest> validator,
[FromServices] ProcessManagerService processManagerService,
[FromServices] HexusConfigurationManager configurationManager)
{
if (!configurationManager.Configuration.Applications.TryGetValue(name, out var application))
return TypedResults.NotFound();

if (ProcessManagerService.IsApplicationRunning(application))
if (processManagerService.IsApplicationRunning(application, out _))
return TypedResults.ValidationProblem(ErrorResponses.ApplicationRunningWhileEditing);

if (request.Name is not null && configurationManager.Configuration.Applications.TryGetValue(request.Name, out _))
Expand Down
8 changes: 5 additions & 3 deletions Hexus.Daemon/Endpoints/Applications/GetApplicationEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Hexus.Daemon.Configuration;
using Hexus.Daemon.Contracts.Responses;
using Hexus.Daemon.Extensions;
using Hexus.Daemon.Services;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -10,13 +11,14 @@ namespace Hexus.Daemon.Endpoints.Applications;
internal sealed class GetApplicationEndpoint : IEndpoint
{
[HttpMap(HttpMapMethod.Get, "/{name}")]
public static Results<Ok<HexusApplicationResponse>, NotFound> Handle(
public static Results<Ok<ApplicationResponse>, NotFound> Handle(
[FromRoute] string name,
[FromServices] HexusConfiguration configuration)
[FromServices] HexusConfiguration configuration,
[FromServices] ProcessStatisticsService processStatisticsService)
{
if (!configuration.Applications.TryGetValue(name, out var application))
return TypedResults.NotFound();

return TypedResults.Ok(application.MapToResponse());
return TypedResults.Ok(application.MapToResponse(processStatisticsService.GetApplicationStats(application)));
}
}
190 changes: 4 additions & 186 deletions Hexus.Daemon/Endpoints/Applications/GetLogsEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@
using Hexus.Daemon.Services;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Channels;

namespace Hexus.Daemon.Endpoints.Applications;

internal partial class GetLogsEndpoint : IEndpoint
internal sealed class GetLogsEndpoint : IEndpoint
{
[HttpMap(HttpMapMethod.Get, "/{name}/logs")]
public static Results<Ok<IAsyncEnumerable<ApplicationLog>>, NotFound> Handle(
[FromServices] HexusConfiguration configuration,
[FromServices] ILogger<GetLogsEndpoint> logger,
[FromServices] LogService logService,
[FromRoute] string name,
[FromQuery] int lines = 100,
[FromQuery] bool noStreaming = false,
Expand All @@ -33,186 +29,8 @@ public static Results<Ok<IAsyncEnumerable<ApplicationLog>>, NotFound> Handle(
// If the before is in the past we can disable steaming
if (before is not null && before < DateTimeOffset.UtcNow) noStreaming = true;

return TypedResults.Ok(GetLogs(application, logger, lines, noStreaming, before, after, combinedCtSource.Token));
}

private static async IAsyncEnumerable<ApplicationLog> GetLogs(
HexusApplication application,
ILogger<GetLogsEndpoint> logger,
int lines,
bool noStreaming,
DateTimeOffset? before,
DateTimeOffset? after,
[EnumeratorCancellation] CancellationToken ct)
{
Channel<ApplicationLog>? channel = null;

if (!noStreaming)
{
channel = Channel.CreateUnbounded<ApplicationLog>();
application.LogChannels.Add(channel);
}

try
{
var logs = GetLogs(application, logger, lines, before, after);

foreach (var log in logs.Reverse())
{
if (!IsLogDateInRange(log.Date, before, after)) continue;
var logs = logService.GetLogs(application, lines, !noStreaming, before, after, combinedCtSource.Token);

yield return log;
}

if (noStreaming || channel is null)
yield break;

await foreach (var log in channel.Reader.ReadAllAsync(ct))
{
if (!IsLogDateInRange(log.Date, before, after)) continue;

yield return log;
}
}
finally
{
if (channel is not null)
{
channel.Writer.Complete();
application.LogChannels.Remove(channel);
}
}
return TypedResults.Ok(logs);
}

private static IEnumerable<ApplicationLog> GetLogs(HexusApplication application, ILogger<GetLogsEndpoint> logger, int lines, DateTimeOffset? before = null, DateTimeOffset? after = null)
{
application.LogSemaphore.Wait();

try
{
using var stream = File.OpenRead($"{EnvironmentHelper.LogsDirectory}/{application.Name}.log");
using var reader = new StreamReader(stream, Encoding.UTF8);

if (stream.Length <= 2)
yield break;

var lineFound = 0;

// Go to the end of the file.
stream.Position = stream.Length - 1;

// If the last character is a LF we can skip it as it isn't a log line.
if (stream.ReadByte() == '\n')
stream.Position -= 2;

while (lineFound < lines)
{
// We are in a line, so we go back until we find the start of this line.
if (stream.Position != 0 && stream.ReadByte() != '\n')
{
if (stream.Position >= 2)
{
stream.Position -= 2;
continue;
}

break;
}

var positionBeforeRead = stream.Position;

reader.DiscardBufferedData();
var line = reader.ReadLine();

stream.Position = positionBeforeRead;

if (string.IsNullOrEmpty(line))
{
if (stream.Position >= 2)
stream.Position -= 2;

continue;
}

var logDateString = line[1..34];
if (!TryLogTimeFormat(logDateString, out var logDate))
{
LogFailedDateTimeParsing(logger, application.Name, logDateString);

if (stream.Position >= 2)
{
stream.Position -= 2;
continue;
}

break;
}

if (!IsLogDateInRange(logDate, before, after))
{
if (stream.Position >= 2)
{
stream.Position -= 2;
continue;
}

break;
}

lineFound++;

var logTypeString = line[35..41];
var logText = line[43..];

if (!LogType.TryParse(logTypeString.AsSpan(), out var logType))
{
LogFailedTypeParsing(logger, application.Name, logTypeString);

if (stream.Position >= 2)
{
stream.Position -= 2;
continue;
}

break;
}

yield return new ApplicationLog(logDate, logType, logText);

if (stream.Position >= 2)
{
stream.Position -= 2;
continue;
}

break;
}
}
finally
{
application.LogSemaphore.Release();
}
}

private static bool TryLogTimeFormat(ReadOnlySpan<char> logDate, out DateTimeOffset dateTimeOffset)
{
return DateTimeOffset.TryParseExact(logDate, "O", null, DateTimeStyles.AssumeUniversal, out dateTimeOffset);
}

private static bool IsLogDateInRange(DateTimeOffset time, DateTimeOffset? before = null, DateTimeOffset? after = null)
{
if (before is not null && time > before.Value)
return false;

if (after is not null && time < after.Value)
return false;

return true;
}

[LoggerMessage(LogLevel.Warning, "There was an error parsing the log file for application {Name}: Couldn't parse \"{LogDate}\" as a DateTime. Skipping log line.")]
private static partial void LogFailedDateTimeParsing(ILogger<GetLogsEndpoint> logger, string name, string logDate);

[LoggerMessage(LogLevel.Warning, "There was an error parsing the log file for application {Name}: Couldn't parse \"{LogType}\" as a LogType. Skipping log line.")]
private static partial void LogFailedTypeParsing(ILogger<GetLogsEndpoint> logger, string name, string logType);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Hexus.Daemon.Configuration;
using Hexus.Daemon.Contracts.Responses;
using Hexus.Daemon.Extensions;
using Hexus.Daemon.Services;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;

Expand All @@ -10,6 +11,10 @@ namespace Hexus.Daemon.Endpoints.Applications;
internal sealed class ListApplicationsEndpoint : IEndpoint
{
[HttpMap(HttpMapMethod.Get, "/list")]
public static Ok<IEnumerable<HexusApplicationResponse>> Handle([FromServices] HexusConfiguration config)
=> TypedResults.Ok(config.Applications.MapToResponse());
public static Ok<IEnumerable<ApplicationResponse>> Handle(
[FromServices] HexusConfiguration config,
[FromServices] ProcessStatisticsService processStatisticsService)
{
return TypedResults.Ok(config.Applications.Values.MapToResponse(processStatisticsService.GetApplicationStats));
}
}
14 changes: 11 additions & 3 deletions Hexus.Daemon/Endpoints/Applications/NewApplicationEndpoint.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ namespace Hexus.Daemon.Endpoints.Applications;
internal sealed class NewApplicationEndpoint : IEndpoint
{
[HttpMap(HttpMapMethod.Post, "/new")]
public static Results<Ok<HexusApplicationResponse>, ValidationProblem, StatusCodeHttpResult> Handle(
public static Results<Ok<ApplicationResponse>, ValidationProblem, StatusCodeHttpResult> Handle(
[FromBody] NewApplicationRequest request,
[FromServices] IValidator<NewApplicationRequest> validator,
[FromServices] HexusConfigurationManager configManager,
[FromServices] ProcessManagerService processManager)
[FromServices] ProcessManagerService processManager,
[FromServices] ProcessStatisticsService processStatisticsService)
{
// Fill some defaults that are not compile time constants, so they require to be filled in here.
request = request with
Expand All @@ -36,12 +37,19 @@ public static Results<Ok<HexusApplicationResponse>, ValidationProblem, StatusCod
if (configManager.Configuration.Applications.TryGetValue(application.Name, out _))
return TypedResults.ValidationProblem(ErrorResponses.ApplicationAlreadyExists);

processStatisticsService.TrackApplicationUsages(application);

if (!processManager.StartApplication(application))
{
processStatisticsService.StopTrackingApplicationUsage(application);
return TypedResults.StatusCode((int)HttpStatusCode.InternalServerError);
}

configManager.Configuration.Applications.Add(application.Name, application);
configManager.SaveConfiguration();

return TypedResults.Ok(application.MapToResponse());
var stats = processStatisticsService.GetApplicationStats(application);

return TypedResults.Ok(application.MapToResponse(stats));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public static Results<NoContent, NotFound, StatusCodeHttpResult> Handle(
if (!configuration.Applications.TryGetValue(name, out var application))
return TypedResults.NotFound();

processManager.StopApplication(application.Name, forceStop);
processManager.StopApplication(application, forceStop);

if (!processManager.StartApplication(application))
return TypedResults.StatusCode((int)HttpStatusCode.InternalServerError);
Expand Down
Loading

0 comments on commit ee0f30f

Please sign in to comment.