Add server log file monitoring

This commit is contained in:
Pat Hartl 2023-08-15 20:15:53 -05:00
parent 4acd8cdc5c
commit 8055b3ae69
10 changed files with 1777 additions and 1 deletions

View file

@ -98,6 +98,12 @@ namespace LANCommander.Data
.WithMany(g => g.Servers)
.IsRequired(false)
.OnDelete(DeleteBehavior.NoAction);
builder.Entity<Server>()
.HasMany<ServerLog>()
.WithOne(sl => sl.Server)
.IsRequired(true)
.OnDelete(DeleteBehavior.Cascade);
}
public DbSet<Game>? Games { get; set; }

View file

@ -22,5 +22,7 @@ namespace LANCommander.Data.Models
[ForeignKey(nameof(GameId))]
[InverseProperty("Servers")]
public virtual Game? Game { get; set; }
public virtual ICollection<ServerLog>? ServerLogs { get; set; }
}
}

View file

@ -0,0 +1,17 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Text.Json.Serialization;
namespace LANCommander.Data.Models
{
public class ServerLog : BaseModel
{
public string Name { get; set; } = "";
public string Path { get; set; } = "";
public Guid? ServerId { get; set; }
[JsonIgnore]
[ForeignKey(nameof(ServerId))]
[InverseProperty("ServerLogs")]
public virtual Server? Server { get; set; }
}
}

View file

@ -1,10 +1,23 @@
using Microsoft.AspNetCore.SignalR;
using LANCommander.Services;
using Microsoft.AspNetCore.SignalR;
using NLog;
namespace LANCommander.Hubs
{
public class GameServerHub : Hub
{
readonly ServerProcessService ServerProcessService;
public GameServerHub(ServerProcessService serverProcessService) {
ServerProcessService = serverProcessService;
ServerProcessService.OnLog += ServerProcessService_OnLog;
}
private void ServerProcessService_OnLog(object sender, ServerLogEventArgs e)
{
Clients.All.SendAsync("Log", e.Log.ServerId, e.Line);
}
public void Log(Guid serverId, string message)
{
Clients.All.SendAsync("Log", serverId, message);

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,82 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddServerLogModel : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "ServerLog",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
Name = table.Column<string>(type: "TEXT", nullable: false),
Path = table.Column<string>(type: "TEXT", nullable: false),
ServerId = table.Column<Guid>(type: "TEXT", nullable: false),
ServerId1 = table.Column<Guid>(type: "TEXT", nullable: true),
CreatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
CreatedById = table.Column<Guid>(type: "TEXT", nullable: true),
UpdatedOn = table.Column<DateTime>(type: "TEXT", nullable: false),
UpdatedById = table.Column<Guid>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_ServerLog", x => x.Id);
table.ForeignKey(
name: "FK_ServerLog_AspNetUsers_CreatedById",
column: x => x.CreatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_ServerLog_AspNetUsers_UpdatedById",
column: x => x.UpdatedById,
principalTable: "AspNetUsers",
principalColumn: "Id");
table.ForeignKey(
name: "FK_ServerLog_Servers_ServerId",
column: x => x.ServerId,
principalTable: "Servers",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
table.ForeignKey(
name: "FK_ServerLog_Servers_ServerId1",
column: x => x.ServerId1,
principalTable: "Servers",
principalColumn: "Id");
});
migrationBuilder.CreateIndex(
name: "IX_ServerLog_CreatedById",
table: "ServerLog",
column: "CreatedById");
migrationBuilder.CreateIndex(
name: "IX_ServerLog_ServerId",
table: "ServerLog",
column: "ServerId");
migrationBuilder.CreateIndex(
name: "IX_ServerLog_ServerId1",
table: "ServerLog",
column: "ServerId1");
migrationBuilder.CreateIndex(
name: "IX_ServerLog_UpdatedById",
table: "ServerLog",
column: "UpdatedById");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "ServerLog");
}
}
}

View file

@ -683,6 +683,52 @@ namespace LANCommander.Migrations
b.ToTable("Servers");
});
modelBuilder.Entity("LANCommander.Data.Models.ServerLog", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("TEXT");
b.Property<Guid?>("CreatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("CreatedOn")
.HasColumnType("TEXT");
b.Property<string>("Name")
.IsRequired()
.HasColumnType("TEXT");
b.Property<string>("Path")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("ServerId")
.IsRequired()
.HasColumnType("TEXT");
b.Property<Guid?>("ServerId1")
.HasColumnType("TEXT");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");
b.Property<DateTime>("UpdatedOn")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("CreatedById");
b.HasIndex("ServerId");
b.HasIndex("ServerId1");
b.HasIndex("UpdatedById");
b.ToTable("ServerLog");
});
modelBuilder.Entity("LANCommander.Data.Models.Tag", b =>
{
b.Property<Guid>("Id")
@ -1236,6 +1282,33 @@ namespace LANCommander.Migrations
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.ServerLog", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
.WithMany()
.HasForeignKey("CreatedById");
b.HasOne("LANCommander.Data.Models.Server", "Server")
.WithMany()
.HasForeignKey("ServerId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.HasOne("LANCommander.Data.Models.Server", null)
.WithMany("ServerLogs")
.HasForeignKey("ServerId1");
b.HasOne("LANCommander.Data.Models.User", "UpdatedBy")
.WithMany()
.HasForeignKey("UpdatedById");
b.Navigation("CreatedBy");
b.Navigation("Server");
b.Navigation("UpdatedBy");
});
modelBuilder.Entity("LANCommander.Data.Models.Tag", b =>
{
b.HasOne("LANCommander.Data.Models.User", "CreatedBy")
@ -1326,6 +1399,11 @@ namespace LANCommander.Migrations
b.Navigation("Servers");
});
modelBuilder.Entity("LANCommander.Data.Models.Server", b =>
{
b.Navigation("ServerLogs");
});
modelBuilder.Entity("LANCommander.Data.Models.User", b =>
{
b.Navigation("GameSaves");

View file

@ -0,0 +1,74 @@
@using LANCommander.Data.Enums
@using LANCommander.Data.Models
@using LANCommander.Extensions;
<Space Direction="DirectionVHType.Vertical" Size="@("large")" Style="width: 100%">
<SpaceItem>
<Table TItem="ServerLog" DataSource="@Value" HidePagination="true">
<PropertyColumn Property="m => m.Name">
<Input Type="text" @bind-Value="context.Name" />
</PropertyColumn>
<PropertyColumn Property="m => m.Path">
<Input Type="text" @bind-Value="context.Path" />
</PropertyColumn>
<ActionColumn>
<Space Style="display: flex; justify-content: end">
<SpaceItem>
<Button OnClick="() => RemoveLog(context)" Type="@ButtonType.Text" Danger Icon="@IconType.Outline.Close" />
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</SpaceItem>
<SpaceItem>
<GridRow Justify="end">
<GridCol>
<Button OnClick="AddLog" Type="@ButtonType.Primary">Add Log</Button>
</GridCol>
</GridRow>
</SpaceItem>
</Space>
@code {
[Parameter] public ICollection<ServerLog> Value { get; set; }
[Parameter] public EventCallback<ICollection<ServerLog>> ValueChanged { get; set; }
[Parameter] public Guid ServerId { get; set; }
protected override async Task OnInitializedAsync()
{
if (Value == null)
Value = new List<ServerLog>();
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(Value);
StateHasChanged();
}
private async Task AddLog()
{
if (Value == null)
Value = new List<ServerLog>();
Value.Add(new ServerLog
{
ServerId = ServerId
});
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(Value);
StateHasChanged();
}
private async Task RemoveLog(ServerLog log)
{
Value.Remove(log);
if (ValueChanged.HasDelegate)
await ValueChanged.InvokeAsync(Value);
StateHasChanged();
}
}

View file

@ -126,6 +126,11 @@
</AntDesign.Input>
</FormItem>
}
<FormItem>
<ServerLogEditor @bind-Value="Server.ServerLogs" ServerId="Id" />
</FormItem>
<FormItem>
<Button Type="@ButtonType.Primary" OnClick="Save" Icon="@IconType.Fill.Save">Save</Button>
</FormItem>

View file

@ -1,4 +1,6 @@
using LANCommander.Data.Models;
using LANCommander.Hubs;
using Microsoft.AspNetCore.SignalR;
using NLog;
using System.Diagnostics;
@ -12,11 +14,33 @@ namespace LANCommander.Services
Error
}
public class ServerLogEventArgs : EventArgs
{
public string Line { get; private set; }
public ServerLog Log { get; private set; }
public ServerLogEventArgs(string line, ServerLog log)
{
Line = line;
Log = log;
}
}
public class ServerProcessService : BaseService
{
public Dictionary<Guid, Process> Processes = new Dictionary<Guid, Process>();
public Dictionary<Guid, int> Threads { get; set; } = new Dictionary<Guid, int>();
public delegate void OnLogHandler(object sender, ServerLogEventArgs e);
public event OnLogHandler OnLog;
private IHubContext<GameServerHub> HubContext;
public ServerProcessService(IHubContext<GameServerHub> hubContext)
{
HubContext = hubContext;
}
public async Task StartServerAsync(Server server)
{
var process = new Process();
@ -53,6 +77,11 @@ namespace LANCommander.Services
Processes[server.Id] = process;
foreach (var log in server.ServerLogs)
{
MonitorLog(log, server);
}
await process.WaitForExitAsync();
}
@ -67,6 +96,59 @@ namespace LANCommander.Services
}
}
private void MonitorLog(ServerLog log, Server server)
{
var logPath = Path.Combine(server.WorkingDirectory, log.Path);
if (File.Exists(logPath))
{
var lockMe = new object();
using (var latch = new ManualResetEvent(true))
using (var fs = new FileStream(logPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var fsw = new FileSystemWatcher(Path.GetDirectoryName(logPath)))
{
fsw.Changed += (s, e) =>
{
lock (lockMe)
{
if (e.FullPath != logPath)
return;
latch.Set();
}
};
using (var sr = new StreamReader(fs))
{
while (true)
{
Thread.Sleep(100);
latch.WaitOne();
lock(lockMe)
{
String line;
while ((line = sr.ReadLine()) != null)
{
HubContext.Clients.All.SendAsync("Log", log.ServerId, line);
//OnLog?.Invoke(this, new ServerLogEventArgs(line, log));
}
latch.Set();
}
}
}
}
}
}
private void Fsw_Changed(object sender, FileSystemEventArgs e)
{
throw new NotImplementedException();
}
public ServerProcessStatus GetStatus(Server server)
{
Process process = null;