Skip to content

Commit

Permalink
feat: Interactive logs command (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
Fleny113 committed Sep 29, 2024
1 parent c183535 commit 5e0ae3a
Show file tree
Hide file tree
Showing 16 changed files with 808 additions and 142 deletions.
15 changes: 6 additions & 9 deletions Hexus.Daemon/Configuration/EnvironmentHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ public static class EnvironmentHelper
public static readonly bool IsDevelopment = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") == "Development";
public static readonly string Home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);

// XDG directories based on the XDG basedir spec, we use there folder on Windows too.
// XDG directories based on the XDG basedir spec, we use these folders on Windows too.
//
// XDG_RUNTIME_DIR does not have a default we can point to due to the requirement this folder has (being owned by the user and being the only with Read Write Execute so 0o700)
// This mean that we need to default to a directory in the temp, on Windows we instead use the XDG_STATE_HOME
Expand All @@ -17,19 +17,19 @@ public static class EnvironmentHelper
private static readonly string HexusStateDirectory = $"{XdgState}/hexus";
private static readonly string HexusRuntimeDirectory = XdgRuntime ?? CreateRuntimeDirectory();

public static readonly string LogFile = NormalizePath(IsDevelopment ? $"{HexusStateDirectory}/daemon.dev.log" : $"{HexusStateDirectory}/daemon.log");
public static readonly string ApplicationLogsDirectory = NormalizePath($"{HexusStateDirectory}/applications");
public static readonly string LogFile = Path.GetFullPath(IsDevelopment ? $"{HexusStateDirectory}/daemon.dev.log" : $"{HexusStateDirectory}/daemon.log");
public static readonly string ApplicationLogsDirectory = Path.GetFullPath($"{HexusStateDirectory}/applications");

public static readonly string ConfigurationFile = NormalizePath(IsDevelopment ? $"{XdgConfig}/hexus.dev.yaml" : $"{XdgConfig}/hexus.yaml");
public static readonly string SocketFile = NormalizePath(IsDevelopment ? $"{HexusRuntimeDirectory}/hexus.dev.sock" : $"{HexusRuntimeDirectory}/hexus.sock");
public static readonly string ConfigurationFile = Path.GetFullPath(IsDevelopment ? $"{XdgConfig}/hexus.dev.yaml" : $"{XdgConfig}/hexus.yaml");
public static readonly string SocketFile = Path.GetFullPath(IsDevelopment ? $"{HexusRuntimeDirectory}/hexus.dev.sock" : $"{HexusRuntimeDirectory}/hexus.sock");

public static void EnsureDirectoriesExistence()
{
// We don't want to create the runtime directory if it doesn't exist
// The check is performed on the env itself to prevent erroring if we are falling back to something else (the XDG_STATE_HOME on Windows and /tmp/hexus-runtime on Linux)
if (XdgRuntime is not null && !Directory.Exists(XdgRuntime))
{
throw new InvalidOperationException("The directory XDG_RUNTIME_DIR does not exist.");
throw new InvalidOperationException("The directory $XDG_RUNTIME_DIR does not exist.");
}

Directory.CreateDirectory(XdgConfig);
Expand All @@ -38,9 +38,6 @@ public static void EnsureDirectoriesExistence()
Directory.CreateDirectory(ApplicationLogsDirectory);
}

// Used to convert to the correct slashes ("/" and "\") based on the platform
public static string NormalizePath(string path) => Path.GetFullPath(path);

private static string CreateRuntimeDirectory()
{
// For Windows we just put the runtime files in the XDG_STATE_HOME directory as we don't have many other solutions available
Expand Down
11 changes: 1 addition & 10 deletions Hexus.Daemon/Contracts/ApplicationLog.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,3 @@
namespace Hexus.Daemon.Contracts;

public record ApplicationLog(DateTimeOffset Date, LogType LogType, string Text)
{
public bool IsLogDateInRange(DateTimeOffset? before = null, DateTimeOffset? after = null)
{
if (before is { } b && Date > b) return false;
if (after is { } a && Date < a) return false;

return true;
}
}
public record ApplicationLog(DateTimeOffset Date, LogType LogType, string Text);
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ public static Results<NoContent, NotFound, ValidationProblem> Handle(
// FIlle the rest of the request with the data from the application to edit
request = new EditApplicationRequest(
Name: request.Name ?? application.Name,
Executable: EnvironmentHelper.NormalizePath(request.Executable ?? application.Executable),
Executable: Path.GetFullPath(request.Executable ?? application.Executable),
Arguments: request.Arguments ?? application.Arguments,
Note: request.Note ?? application.Note ?? "",
WorkingDirectory: EnvironmentHelper.NormalizePath(request.WorkingDirectory ?? application.WorkingDirectory),
WorkingDirectory: Path.GetFullPath(request.WorkingDirectory ?? application.WorkingDirectory),
NewEnvironmentVariables: request.NewEnvironmentVariables ?? [],
RemoveEnvironmentVariables: request.RemoveEnvironmentVariables ?? [],
IsReloadingEnvironmentVariables: request.IsReloadingEnvironmentVariables ?? false
Expand Down
31 changes: 26 additions & 5 deletions Hexus.Daemon/Endpoints/Applications/GetLogsEndpoint.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
using EndpointMapper;
using Hexus.Daemon.Configuration;
using Hexus.Daemon.Contracts;
using Hexus.Daemon.Services;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.Net;
using System.Text.Json;

namespace Hexus.Daemon.Endpoints.Applications;

internal sealed class GetLogsEndpoint : IEndpoint
{
[HttpMap(HttpMapMethod.Get, "/{name}/logs")]
public static Results<Ok<IAsyncEnumerable<ApplicationLog>>, NotFound> Handle(
public static async Task<NotFound?> Handle(
[FromServices] HexusConfiguration configuration,
[FromServices] ProcessLogsService processLogsService,
HttpContext context,
[FromServices] IOptions<JsonOptions> jsonOptions,
[FromRoute] string name,
[FromQuery] DateTimeOffset? before = null,
[FromQuery] DateTimeOffset? after = null,
CancellationToken ct = default)
{
if (!configuration.Applications.TryGetValue(name, out var application))
Expand All @@ -24,8 +27,26 @@ public static Results<Ok<IAsyncEnumerable<ApplicationLog>>, NotFound> Handle(
// When the aspnet or the hexus cancellation token get cancelled it cancels this as well
var combinedCtSource = CancellationTokenSource.CreateLinkedTokenSource(ct, HexusLifecycle.DaemonStoppingToken);

var logs = processLogsService.GetLogs(application, before, after, combinedCtSource.Token);
var logs = processLogsService.GetLogs(application, before, combinedCtSource.Token);

return TypedResults.Ok(logs);
context.Response.StatusCode = (int)HttpStatusCode.OK;
context.Response.Headers.ContentType = "application/json; charset=utf-8";

// We need to manually write the response body as ASP.NET won't send the header using TypedResults.Ok() or anything similar

await context.Response.WriteAsync("[", ct);
await context.Response.Body.FlushAsync(ct);

await foreach (var item in logs)
{
await context.Response.WriteAsync(JsonSerializer.Serialize(item, jsonOptions.Value.JsonSerializerOptions), cancellationToken: ct);
await context.Response.WriteAsync(",", ct);
await context.Response.Body.FlushAsync(ct);
}

await context.Response.WriteAsync("]", ct);
await context.Response.Body.FlushAsync(ct);

return null;
}
}
8 changes: 4 additions & 4 deletions Hexus.Daemon/Extensions/MapperExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ public static HexusApplication MapToApplication(this NewApplicationRequest reque
return new()
{
Name = request.Name,
Executable = EnvironmentHelper.NormalizePath(request.Executable),
Executable = Path.GetFullPath(request.Executable),
Arguments = request.Arguments,
WorkingDirectory = EnvironmentHelper.NormalizePath(request.WorkingDirectory ?? EnvironmentHelper.Home),
WorkingDirectory = Path.GetFullPath(request.WorkingDirectory ?? EnvironmentHelper.Home),
Note = request.Note,
EnvironmentVariables = request.EnvironmentVariables ?? [],
};
Expand All @@ -24,9 +24,9 @@ public static ApplicationResponse MapToResponse(this HexusApplication applicatio
{
return new(
Name: application.Name,
Executable: EnvironmentHelper.NormalizePath(application.Executable),
Executable: Path.GetFullPath(application.Executable),
Arguments: application.Arguments,
WorkingDirectory: EnvironmentHelper.NormalizePath(application.WorkingDirectory),
WorkingDirectory: Path.GetFullPath(application.WorkingDirectory),
Note: application.Note,
EnvironmentVariables: application.EnvironmentVariables,
Status: application.Status,
Expand Down
16 changes: 6 additions & 10 deletions Hexus.Daemon/Extensions/ProcessExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,15 @@ internal static class ProcessExtensions
public static double GetProcessCpuUsage(this Process process, ProcessStatisticsService.CpuStatistics cpuStatistics)
{
var currentTime = DateTimeOffset.UtcNow;
var timeDifference = currentTime - cpuStatistics.LastGetProcessCpuUsageInvocation;
var deltaTime = currentTime - cpuStatistics.LastTime;

// In a situation like this we are provably going to give an unreasonable number, it's better to just say 0% than 100%
if (timeDifference < TimeSpan.FromMilliseconds(100))
return 0.00;
var totalProcessTime = process.TotalProcessorTime;
var deltaProcessTime = totalProcessTime - cpuStatistics.LastTotalProcessorTime;

var currentTotalProcessorTime = process.TotalProcessorTime;
var processorTimeDifference = currentTotalProcessorTime - cpuStatistics.LastTotalProcessorTime;
var cpuUsage = deltaProcessTime / Environment.ProcessorCount / deltaTime;

var cpuUsage = processorTimeDifference / Environment.ProcessorCount / timeDifference;

cpuStatistics.LastTotalProcessorTime = currentTotalProcessorTime;
cpuStatistics.LastGetProcessCpuUsageInvocation = currentTime;
cpuStatistics.LastTotalProcessorTime = totalProcessTime;
cpuStatistics.LastTime = currentTime;

return cpuUsage * 100;
}
Expand Down
16 changes: 6 additions & 10 deletions Hexus.Daemon/Services/ProcessLogsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,25 @@ internal partial class ProcessLogsService(ILogger<ProcessLogsService> logger)

internal void ProcessApplicationLog(HexusApplication application, LogType logType, string message)
{
if (!_logChannels.TryGetValue(application.Name, out var channels))
{
LogUnableToGetLogController(logger, application.Name);
return;
}

if (logType != LogType.SYSTEM)
{
LogApplicationOutput(logger, application.Name, message);
}

var applicationLog = new ApplicationLog(DateTimeOffset.UtcNow, logType, message);

channels.ForEach(channel => channel.Writer.TryWrite(applicationLog));
if (_logChannels.TryGetValue(application.Name, out var channels))
{
channels.ForEach(channel => channel.Writer.TryWrite(applicationLog));
}

using var logFile = File.Open($"{EnvironmentHelper.ApplicationLogsDirectory}/{application.Name}.log", FileMode.Append, FileAccess.Write, FileShare.Read);
using var log = new StreamWriter(logFile, Encoding.UTF8);

log.WriteLine($"[{applicationLog.Date.DateTime:O},{applicationLog.LogType}] {applicationLog.Text}");
}

public async IAsyncEnumerable<ApplicationLog> GetLogs(HexusApplication application, DateTimeOffset? before,
DateTimeOffset? after, [EnumeratorCancellation] CancellationToken ct)
public async IAsyncEnumerable<ApplicationLog> GetLogs(HexusApplication application, DateTimeOffset? before, [EnumeratorCancellation] CancellationToken ct)
{
if (!_logChannels.TryGetValue(application.Name, out var channels))
{
Expand All @@ -52,7 +48,7 @@ public async IAsyncEnumerable<ApplicationLog> GetLogs(HexusApplication applicati
{
await foreach (var log in channel.Reader.ReadAllAsync(ct))
{
if (!log.IsLogDateInRange(before, after)) continue;
if (before.HasValue && log.Date > before.Value) yield break;

yield return log;
}
Expand Down
4 changes: 2 additions & 2 deletions Hexus.Daemon/Services/ProcessStatisticsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ private static IEnumerable<double> GetApplicationCpuUsage(ApplicationCpuStatisti
var stats = statistics.ProcessCpuStatistics.GetOrCreate(process.Id, _ => new CpuStatistics
{
LastTotalProcessorTime = TimeSpan.Zero,
LastGetProcessCpuUsageInvocation = DateTimeOffset.UtcNow,
LastTime = DateTimeOffset.UtcNow,
});

yield return process.GetProcessCpuUsage(stats);
Expand Down Expand Up @@ -147,7 +147,7 @@ private record ApplicationCpuStatistics
internal record CpuStatistics
{
public TimeSpan LastTotalProcessorTime { get; set; }
public DateTimeOffset LastGetProcessCpuUsageInvocation { get; set; }
public DateTimeOffset LastTime { get; set; }
}
}

Expand Down
4 changes: 2 additions & 2 deletions Hexus/Commands/Applications/EditCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,11 @@ private static async Task Handler(InvocationContext context)
}

if (newWorkingDirectory is not null)
newWorkingDirectory = EnvironmentHelper.NormalizePath(newWorkingDirectory);
newWorkingDirectory = Path.GetFullPath(newWorkingDirectory);

if (newExecutable is not null)
newExecutable = Path.IsPathFullyQualified(newExecutable)
? EnvironmentHelper.NormalizePath(newExecutable)
? Path.GetFullPath(newExecutable)
: PathHelper.ResolveExecutable(newExecutable);

if (reloadEnv)
Expand Down
Loading

0 comments on commit 5e0ae3a

Please sign in to comment.