Merge branch 'main' into unify-pickable-columns

dhcp-server
Pat Hartl 2023-09-03 19:10:11 -05:00
commit bd2da60792
23 changed files with 439 additions and 65 deletions

View File

@ -14,7 +14,7 @@ jobs:
id: trim_tag_ref id: trim_tag_ref
with: with:
string: '${{ github.ref }}' string: '${{ github.ref }}'
pattern: 'refs/tags/' pattern: 'refs/tags/v'
replace-with: '' replace-with: ''
# Server # Server
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@ -25,9 +25,15 @@ jobs:
- name: Restore dependencies - name: Restore dependencies
run: dotnet restore run: dotnet restore
- name: Build - name: Build
run: dotnet build "./LANCommander/LANCommander.csproj" --no-restore run: dotnet build "./LANCommander/LANCommander.csproj" --no-restore /p:Version="${{ steps.trim_tag_ref.outputs.replaced }}"
- name: Publish - name: Publish
run: dotnet publish "./LANCommander/LANCommander.csproj" -c Release -o _Build --self-contained --os win -p:PublishSingleFile=true run: dotnet publish "./LANCommander/LANCommander.csproj" -c Release -o _Build --self-contained --os win -p:PublishSingleFile=true
- name: Sign Windows Binary
uses: nadeemjazmawe/Sign-action-signtool.exe@v0.1
with:
certificate: "${{ secrets.CERTIFICATE }}"
cert-password: "${{ secrets.CERTIFICATE_PASSWORD }}"
filepath: "./_Build/LANCommander.exe"
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:
@ -42,7 +48,13 @@ jobs:
- name: Restore NuGet packages - name: Restore NuGet packages
run: nuget restore LANCommander.sln run: nuget restore LANCommander.sln
- name: Build and Publish Library - name: Build and Publish Library
run: msbuild LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj /p:Configuration=Release /p:OutputPath=Build run: msbuild LANCommander.Playnite.Extension/LANCommander.PlaynitePlugin.csproj /p:Configuration=Release /p:OutputPath=Build /p:Version="${{ steps.trim_tag_ref.outputs.replaced }}"
- name: Sign Windows Binary
uses: nadeemjazmawe/Sign-action-signtool.exe@v0.1
with:
certificate: "${{ secrets.CERTIFICATE }}"
cert-password: "${{ secrets.CERTIFICATE_PASSWORD }}"
filepath: "LANCommander.Playnite.Extension/Build/LANCommander.Playnite.Extension.dll"
- name: Download Playnite Release - name: Download Playnite Release
uses: robinraju/release-downloader@v1.7 uses: robinraju/release-downloader@v1.7
with: with:
@ -51,12 +63,19 @@ jobs:
fileName: Playnite1018.zip fileName: Playnite1018.zip
- name: Extract Playnite - name: Extract Playnite
run: Expand-Archive -Path Playnite1018.zip -DestinationPath Playnite run: Expand-Archive -Path Playnite1018.zip -DestinationPath Playnite
- name: Update Manifest Versioning
uses: fjogeleit/yaml-update-action@main
with:
valueFile: "LANCommander.Playnite.Extension/Build/extension.yaml"
propertyPath: "Version"
value: "${{ steps.trim_tag_ref.outputs.replaced }}"
commitChange: false
- name: Run Playnite Toolbox - name: Run Playnite Toolbox
run: Playnite/Toolbox.exe pack LANCommander.Playnite.Extension/Build . run: Playnite/Toolbox.exe pack LANCommander.Playnite.Extension/Build .
- name: Upload Artifact - name: Upload Artifact
uses: actions/upload-artifact@v3.1.2 uses: actions/upload-artifact@v3.1.2
with: with:
name: LANCommander.PlaynitePlugin-${{ steps.trim_tag_ref.outputs.replaced }} name: LANCommander.PlaynitePlugin-v${{ steps.trim_tag_ref.outputs.replaced }}
path: LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_1_0.pext path: LANCommander.PlaynitePlugin_48e1bac7-e0a0-45d7-ba83-36f5e9e959fc_*.pext
# Release # Release

View File

@ -36,6 +36,12 @@ jobs:
run: dotnet build "./LANCommander/LANCommander.csproj" --no-restore run: dotnet build "./LANCommander/LANCommander.csproj" --no-restore
- name: Publish - name: Publish
run: dotnet publish "./LANCommander/LANCommander.csproj" -c Release -o _Build --self-contained --os win -p:PublishSingleFile=true run: dotnet publish "./LANCommander/LANCommander.csproj" -c Release -o _Build --self-contained --os win -p:PublishSingleFile=true
- name: Sign Windows Binary
uses: nadeemjazmawe/Sign-action-signtool.exe@v0.1
with:
certificate: "${{ secrets.CERTIFICATE }}"
cert-password: "${{ secrets.CERTIFICATE_PASSWORD }}"
filepath: "./_Build/LANCommander.exe"
- name: Upload Artifacts - name: Upload Artifacts
uses: actions/upload-artifact@v2 uses: actions/upload-artifact@v2
with: with:

View File

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.46" /> <PackageReference Include="HtmlAgilityPack" Version="1.11.52" />
<PackageReference Include="System.Text.Json" Version="5.0.1" /> <PackageReference Include="System.Text.Json" Version="5.0.1" />
</ItemGroup> </ItemGroup>

View File

@ -49,6 +49,34 @@ namespace LANCommander.PlaynitePlugin
PowerShellRuntime = new PowerShellRuntime(); PowerShellRuntime = new PowerShellRuntime();
GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime); GameSaveService = new GameSaveService(LANCommander, PlayniteApi, PowerShellRuntime);
api.UriHandler.RegisterSource("lancommander", args =>
{
if (args.Arguments.Length == 0)
return;
Guid gameId;
switch (args.Arguments[0].ToLower())
{
case "install":
if (args.Arguments.Length == 1)
break;
if (Guid.TryParse(args.Arguments[1], out gameId))
PlayniteApi.InstallGame(gameId);
break;
case "run":
if (args.Arguments.Length == 1)
break;
if (Guid.TryParse(args.Arguments[1], out gameId))
PlayniteApi.StartGame(gameId);
break;
}
});
} }
public override void OnApplicationStarted(OnApplicationStartedEventArgs args) public override void OnApplicationStarted(OnApplicationStartedEventArgs args)

View File

@ -7,8 +7,8 @@
ViewData["Title"] = "First Time Setup"; ViewData["Title"] = "First Time Setup";
} }
<div class="ant-row ant-row-middle ant-row-space-around" style="position: absolute; top:0; left: 0; right: 0; bottom: 0;"> <div class="ant-row ant-row-middle ant-row-space-around" style="min-height: 100vh; margin-top: -24px;">
<div class="ant-col ant-col-10"> <div class="ant-col ant-col-xs-24 ant-col-md-10">
<div style="text-align: center; margin-bottom: 24px;"> <div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" /> <img src="~/static/logo.svg" />

View File

@ -6,8 +6,8 @@
ViewData["Title"] = "Log in"; ViewData["Title"] = "Log in";
} }
<div class="ant-row ant-row-middle ant-row-space-around" style="position: absolute; top:0; left: 0; right: 0; bottom: 0;"> <div class="ant-row ant-row-middle ant-row-space-around" style="min-height: 100vh; margin-top: -24px;">
<div class="ant-col ant-col-10"> <div class="ant-col ant-col-xs-24 ant-col-md-10">
<div style="text-align: center; margin-bottom: 24px;"> <div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" /> <img src="~/static/logo.svg" />

View File

@ -5,8 +5,8 @@
ViewData["Title"] = "Register"; ViewData["Title"] = "Register";
} }
<div class="ant-row ant-row-middle ant-row-space-around" style="position: absolute; top:0; left: 0; right: 0; bottom: 0;"> <div class="ant-row ant-row-middle ant-row-space-around" style="min-height: 100vh; margin-top: -24px;">
<div class="ant-col ant-col-10"> <div class="ant-col ant-col-xs-24 ant-col-md-10">
<div style="text-align: center; margin-bottom: 24px;"> <div style="text-align: center; margin-bottom: 24px;">
<img src="~/static/logo.svg" /> <img src="~/static/logo.svg" />

View File

@ -0,0 +1,15 @@
<Header>
<div class="logo" style="background: url('/static/logo-dark.svg'); width: 143px; height: 31px; margin: 16px 24px 16px 0; float: left; background-size: contain;" />
<Menu Theme="MenuTheme.Dark" Mode="MenuMode.Horizontal">
@ChildContent
</Menu>
</Header>
<MobileMenu>
@ChildContent
</MobileMenu>
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
}

View File

@ -0,0 +1,37 @@
@inject NavigationManager NavigationManager
<div class="mobile-menu">
<Header Class="mobile-header">
<div class="logo" style="background: url('/static/logo-dark.svg'); width: 143px; height: 31px; background-size: contain;" />
<Button Icon="@IconType.Outline.Menu" Type="@ButtonType.Text" OnClick="ToggleMenu" />
</Header>
<Drawer Closable="true" Visible="@MenuDrawerOpen" Placement="@("top")" Class="menu-drawer">
<Menu Theme="MenuTheme.Dark" Mode="MenuMode.Vertical">
@ChildContent
</Menu>
</Drawer>
</div>
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
bool MenuDrawerOpen = true;
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
NavigationManager.LocationChanged += CloseMenu;
}
void ToggleMenu()
{
MenuDrawerOpen = !MenuDrawerOpen;
}
void CloseMenu(object? sender, LocationChangedEventArgs e)
{
MenuDrawerOpen = false;
StateHasChanged();
}
}

View File

@ -0,0 +1,9 @@
namespace LANCommander.Models
{
public class OrphanedFile
{
public string Path { get; set; }
public long Size { get; set; }
public DateTime CreatedOn { get; set; }
}
}

View File

@ -6,7 +6,7 @@
<PageHeader Title="Dashboard" Style="margin-bottom: 24px" /> <PageHeader Title="Dashboard" Style="margin-bottom: 24px" />
<GridRow Gutter="(16, 16)"> <GridRow Gutter="(16, 16)">
<GridCol Sm="24" Md="12"> <GridCol Xs="24" Md="12">
<Card Title="Network Download Rate"> <Card Title="Network Download Rate">
<Body> <Body>
<NetworkDownloadRate TimerHistory="60" TimerInterval="1000" /> <NetworkDownloadRate TimerHistory="60" TimerInterval="1000" />
@ -14,7 +14,7 @@
</Card> </Card>
</GridCol> </GridCol>
<GridCol Sm="24" Md="12"> <GridCol Xs="24" Md="12">
<Card Title="Network Upload Rate"> <Card Title="Network Upload Rate">
<Body> <Body>
<NetworkUploadRate TimerHistory="60" TimerInterval="1000" /> <NetworkUploadRate TimerHistory="60" TimerInterval="1000" />
@ -22,7 +22,7 @@
</Card> </Card>
</GridCol> </GridCol>
<GridCol Sm="24" Md="12"> <GridCol Xs="24" Md="12">
<Card Title="CPU Usage (%)"> <Card Title="CPU Usage (%)">
<Body> <Body>
<ProcessorUtilization TimerHistory="60" TimerInterval="1000" /> <ProcessorUtilization TimerHistory="60" TimerInterval="1000" />
@ -30,7 +30,7 @@
</Card> </Card>
</GridCol> </GridCol>
<GridCol Sm="24" Md="12"> <GridCol Xs="24" Md="12">
<Card Title="Storage Usage"> <Card Title="Storage Usage">
<Body> <Body>
<StorageUsage /> <StorageUsage />

View File

@ -30,7 +30,7 @@
<FormItem> <FormItem>
<Space Direction="DirectionVHType.Horizontal"> <Space Direction="DirectionVHType.Horizontal">
<SpaceItem> <SpaceItem>
<InputFile id="FileInput" OnChange="FileSelected" hidden /> <InputFile @ref="FileInput" id="FileInput" OnChange="FileSelected" hidden />
<Upload Name="files" FileList="FileList"> <Upload Name="files" FileList="FileList">
<label class="ant-btn" for="FileInput"> <label class="ant-btn" for="FileInput">
<Icon Type="upload" /> <Icon Type="upload" />
@ -66,6 +66,7 @@
Archive Archive; Archive Archive;
InputFile FileInput;
IBrowserFile File { get; set; } IBrowserFile File { get; set; }
List<UploadFileItem> FileList = new List<UploadFileItem>(); List<UploadFileItem> FileList = new List<UploadFileItem>();
@ -120,14 +121,24 @@
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
await Task.Delay(500); var i = 0;
if (!String.IsNullOrWhiteSpace(archive.ObjectKey) && archive.ObjectKey != Guid.Empty.ToString()) // Check every 10 seconds to see if the file input is available
await JS.InvokeVoidAsync("Uploader.Init", "FileInput", archive.ObjectKey.ToString()); while (i < 20)
else {
await JS.InvokeVoidAsync("Uploader.Init", "FileInput", ""); if (FileInput != null)
{
if (!String.IsNullOrWhiteSpace(archive.ObjectKey) && archive.ObjectKey != Guid.Empty.ToString())
await JS.InvokeVoidAsync("Uploader.Init", "FileInput", archive.ObjectKey.ToString());
else
await JS.InvokeVoidAsync("Uploader.Init", "FileInput", "");
break;
}
i++;
await Task.Delay(500);
}
} }
private async Task UploadArchiveJS() private async Task UploadArchiveJS()

View File

@ -239,7 +239,7 @@ else
{ {
Game = await GameService.Add(Game); Game = await GameService.Add(Game);
await MessageService.Success("Game added!"); NavigationManager.LocationChanged += NotifyGameAdded;
NavigationManager.NavigateTo($"/Games/{Game.Id}"); NavigationManager.NavigateTo($"/Games/{Game.Id}");
} }
@ -250,6 +250,13 @@ else
} }
} }
private void NotifyGameAdded(object? sender, LocationChangedEventArgs e)
{
NavigationManager.LocationChanged -= NotifyGameAdded;
MessageService.Success("Game added!");
}
private async Task BrowseForIcon() private async Task BrowseForIcon()
{ {
var modalOptions = new ModalOptions() var modalOptions = new ModalOptions()

View File

@ -1,6 +1,7 @@
@inject ServerService ServerService @inject ServerService ServerService
@inject ServerProcessService ServerProcessService @inject ServerProcessService ServerProcessService
@inject IMessageService MessageService @inject IMessageService MessageService
@inject INotificationService NotificationService
@implements IAsyncDisposable @implements IAsyncDisposable
<Space Size="@("large")"> <Space Size="@("large")">
@ -58,6 +59,8 @@
protected override async Task OnInitializedAsync() protected override async Task OnInitializedAsync()
{ {
Server = await ServerService.Get(ServerId); Server = await ServerService.Get(ServerId);
ServerProcessService.OnStatusUpdate += OnStatusUpdate;
} }
protected override void OnAfterRender(bool firstRender) protected override void OnAfterRender(bool firstRender)
@ -73,26 +76,32 @@
} }
} }
private void OnStatusUpdate(object sender, ServerStatusUpdateEventArgs args)
{
if (args?.Server?.Id == ServerId)
{
Status = args.Status;
StateHasChanged();
if (Status == ServerProcessStatus.Error)
{
NotificationService.Error(new NotificationConfig
{
Message = $"Error starting server {args.Server.Name}",
Description = args.Exception.Message,
}
);
}
}
}
private async Task Start() private async Task Start()
{ {
try ServerProcessService.StartServerAsync(Server);
{
Status = ServerProcessStatus.Starting;
await ServerProcessService.StartServerAsync(Server);
}
catch (Exception ex)
{
Status = ServerProcessStatus.Error;
await MessageService.Error("There was an unexpected error while trying to start the server.");
}
} }
private void Stop() private void Stop()
{ {
Status = ServerProcessStatus.Stopping;
ServerProcessService.StopServer(Server); ServerProcessService.StopServer(Server);
} }

View File

@ -196,7 +196,7 @@
{ {
Server = await ServerService.Add(Server); Server = await ServerService.Add(Server);
await MessageService.Success("Server added!"); NavigationManager.LocationChanged += NotifyServerAdded;
NavigationManager.NavigateTo($"/Servers/{Server.Id}"); NavigationManager.NavigateTo($"/Servers/{Server.Id}");
} }
@ -207,6 +207,13 @@
} }
} }
private void NotifyServerAdded(object? sender, LocationChangedEventArgs e)
{
NavigationManager.LocationChanged -= NotifyServerAdded;
MessageService.Success("Server added!");
}
private string GetIcon(Game game) private string GetIcon(Game game)
{ {
return $"/api/Games/{game?.Id}/Icon.png"; return $"/api/Games/{game?.Id}/Icon.png";

View File

@ -10,6 +10,15 @@
<PageHeader Title="Servers"> <PageHeader Title="Servers">
<PageHeaderExtra> <PageHeaderExtra>
<Space Direction="DirectionVHType.Horizontal"> <Space Direction="DirectionVHType.Horizontal">
@if (SelectedServers != null && SelectedServers.Count() > 0)
{
<SpaceItem>
<Button Type="@ButtonType.Primary" OnClick="() => StartServers()">Start</Button>
<Popconfirm OnConfirm="() => StopServers()" Title="Are you sure you want to kill these server processes?">
<Button Danger Type="@ButtonType.Primary">Stop</Button>
</Popconfirm>
</SpaceItem>
}
<SpaceItem> <SpaceItem>
<Search Placeholder="Search" @bind-Value="Search" BindOnInput DebounceMilliseconds="150" OnChange="() => LoadData()" /> <Search Placeholder="Search" @bind-Value="Search" BindOnInput DebounceMilliseconds="150" OnChange="() => LoadData()" />
</SpaceItem> </SpaceItem>
@ -20,7 +29,8 @@
</PageHeaderExtra> </PageHeaderExtra>
</PageHeader> </PageHeader>
<Table TItem="Server" DataSource="@Servers" Loading="@Loading" PageSize="25"> <Table TItem="Server" DataSource="@Servers" Loading="@Loading" PageSize="25" @bind-SelectedRows="SelectedServers">
<Selection Key="@(context.Id.ToString())" />
<PropertyColumn Property="s => s.Name" Sortable /> <PropertyColumn Property="s => s.Name" Sortable />
<PropertyColumn Property="s => s.Game"> <PropertyColumn Property="s => s.Game">
<Image Src="@GetIcon(context.Game)" Height="32" Width="32" Preview="false"></Image> <Image Src="@GetIcon(context.Game)" Height="32" Width="32" Preview="false"></Image>
@ -56,6 +66,8 @@
string Search = ""; string Search = "";
IEnumerable<Server> SelectedServers;
protected override void OnAfterRender(bool firstRender) protected override void OnAfterRender(bool firstRender)
{ {
if (firstRender) if (firstRender)
@ -102,4 +114,30 @@
{ {
return $"/api/Games/{game?.Id}/Icon.png"; return $"/api/Games/{game?.Id}/Icon.png";
} }
private async Task StartServers()
{
foreach (var server in SelectedServers)
{
try
{
var status = ServerProcessService.GetStatus(server);
if (status == ServerProcessStatus.Stopped || status == ServerProcessStatus.Error)
{
ServerProcessService.StartServerAsync(server);
}
}
catch { }
}
}
private void StopServers()
{
foreach (var server in SelectedServers)
{
if (ServerProcessService.GetStatus(server) == ServerProcessStatus.Running)
ServerProcessService.StopServer(server);
}
}
} }

View File

@ -0,0 +1,77 @@
@page "/Settings/Tools/OrphanedFiles"
@using LANCommander.Helpers;
@using LANCommander.Models;
@layout SettingsLayout
@inject ArchiveService ArchiveService;
@attribute [Authorize(Roles = "Administrator")]
<PageHeader Title="Orphaned Files" />
<div style="padding: 0 24px;">
<p>
These files exist on the server, but aren't linked in the database. Use this tool to identify and delete orphaned files.
</p>
<Table TItem="OrphanedFile" DataSource="@Orphans" Loading="@Loading" PageSize="25">
<PropertyColumn Property="f => f.Path" />
<PropertyColumn Property="f => f.Size" Sortable>
@ByteSizeLib.ByteSize.FromBytes(context.Size).ToString()
</PropertyColumn>
<PropertyColumn Property="f => f.CreatedOn" Format="MM/dd/yyyy hh:mm tt" Sortable />
<ActionColumn Title="" Style="text-align: right">
<Space Direction="DirectionVHType.Horizontal">
<SpaceItem>
<Popconfirm OnConfirm="() => Delete(context)" Title="Are you sure you want to delete this file?">
<Button Icon="@IconType.Outline.Close" Type="@ButtonType.Text" Danger />
</Popconfirm>
</SpaceItem>
</Space>
</ActionColumn>
</Table>
</div>
@code {
ICollection<OrphanedFile> Orphans = new List<OrphanedFile>();
bool Loading = true;
protected override async Task OnInitializedAsync()
{
await LoadData();
}
async Task LoadData()
{
Loading = true;
Orphans = new List<OrphanedFile>();
var archives = await ArchiveService.Get();
var archiveFiles = archives.Select(a => ArchiveService.GetArchiveFileLocation(a));
var files = Directory.GetFiles("Upload");
foreach (var file in files.Where(f => !archiveFiles.Contains(f)))
{
var fileInfo = new FileInfo(file);
Orphans.Add(new OrphanedFile
{
Path = file,
Size = fileInfo.Length,
CreatedOn = fileInfo.CreationTime
});
}
Orphans = Orphans.OrderByDescending(f => f.Size).ToList();
Loading = false;
}
async Task Delete(OrphanedFile file)
{
FileHelpers.DeleteIfExists(file.Path);
Orphans.Remove(file);
StateHasChanged();
}
}

View File

@ -27,6 +27,12 @@
<h3>Missing Archives</h3> <h3>Missing Archives</h3>
<p>List and fix all archives that are missing their backing files.</p> <p>List and fix all archives that are missing their backing files.</p>
<a href="/Settings/Tools/MissingArchives" class="ant-btn ant-btn-primary">View Missing Archives</a> <a href="/Settings/Tools/MissingArchives" class="ant-btn ant-btn-primary">View Missing Archives</a>
<Divider />
<h3>Orphaned Files</h3>
<p>Find and delete any files that don't exist in the database and may be taking up unnecessary disk space.</p>
<a href="/Settings/Tools/OrphanedFiles" class="ant-btn ant-btn-primary">View Orphaned Files</a>
</div> </div>

View File

@ -11,16 +11,20 @@ namespace LANCommander.Services
{ {
public class GameService : BaseDatabaseService<Game> public class GameService : BaseDatabaseService<Game>
{ {
public GameService(DatabaseContext dbContext, IHttpContextAccessor httpContextAccessor) : base(dbContext, httpContextAccessor) private readonly ArchiveService ArchiveService;
public GameService(DatabaseContext dbContext, IHttpContextAccessor httpContextAccessor, ArchiveService archiveService) : base(dbContext, httpContextAccessor)
{ {
ArchiveService = archiveService;
} }
public override async Task Delete(Game game) public override async Task Delete(Game game)
{ {
foreach (var archive in game.Archives.OrderByDescending(a => a.CreatedOn)) foreach (var archive in game.Archives.OrderByDescending(a => a.CreatedOn))
{ {
await ArchiveService.Delete(archive);
FileHelpers.DeleteIfExists($"Icon/{game.Id}.png".ToPath()); FileHelpers.DeleteIfExists($"Icon/{game.Id}.png".ToPath());
FileHelpers.DeleteIfExists($"Upload/{archive.ObjectKey}".ToPath());
} }
await base.Delete(game); await base.Delete(game);

View File

@ -33,6 +33,24 @@ namespace LANCommander.Services
} }
} }
public class ServerStatusUpdateEventArgs : EventArgs
{
public Server Server { get; private set; }
public ServerProcessStatus Status { get; private set; }
public Exception Exception { get; private set; }
public ServerStatusUpdateEventArgs(Server server, ServerProcessStatus status)
{
Server = server;
Status = status;
}
public ServerStatusUpdateEventArgs(Server server, ServerProcessStatus status, Exception exception) : this(server, status)
{
Exception = exception;
}
}
public class LogFileMonitor : IDisposable public class LogFileMonitor : IDisposable
{ {
private ManualResetEvent Latch; private ManualResetEvent Latch;
@ -122,6 +140,9 @@ namespace LANCommander.Services
public delegate void OnLogHandler(object sender, ServerLogEventArgs e); public delegate void OnLogHandler(object sender, ServerLogEventArgs e);
public event OnLogHandler OnLog; public event OnLogHandler OnLog;
public delegate void OnStatusUpdateHandler(object sender, ServerStatusUpdateEventArgs e);
public event OnStatusUpdateHandler OnStatusUpdate;
private IHubContext<GameServerHub> HubContext; private IHubContext<GameServerHub> HubContext;
public ServerProcessService(IHubContext<GameServerHub> hubContext) public ServerProcessService(IHubContext<GameServerHub> hubContext)
@ -157,27 +178,42 @@ namespace LANCommander.Services
}); });
} }
process.Start(); try
if (!process.StartInfo.UseShellExecute)
{ {
process.BeginErrorReadLine(); OnStatusUpdate?.Invoke(this, new ServerStatusUpdateEventArgs(server, ServerProcessStatus.Starting));
process.BeginOutputReadLine();
process.Start();
if (!process.StartInfo.UseShellExecute)
{
process.BeginErrorReadLine();
process.BeginOutputReadLine();
}
Processes[server.Id] = process;
foreach (var logFile in server.ServerConsoles.Where(sc => sc.Type == ServerConsoleType.LogFile))
{
StartMonitoringLog(logFile, server);
}
OnStatusUpdate?.Invoke(this, new ServerStatusUpdateEventArgs(server, ServerProcessStatus.Running));
await process.WaitForExitAsync();
} }
catch (Exception ex)
Processes[server.Id] = process;
foreach (var logFile in server.ServerConsoles.Where(sc => sc.Type == ServerConsoleType.LogFile))
{ {
StartMonitoringLog(logFile, server); OnStatusUpdate?.Invoke(this, new ServerStatusUpdateEventArgs(server, ServerProcessStatus.Error, ex));
}
await process.WaitForExitAsync(); Logger.Error(ex, "Could not start server process");
}
} }
public void StopServer(Server server) public void StopServer(Server server)
{ {
OnStatusUpdate?.Invoke(this, new ServerStatusUpdateEventArgs(server, ServerProcessStatus.Stopping));
if (Processes.ContainsKey(server.Id)) if (Processes.ContainsKey(server.Id))
{ {
var process = Processes[server.Id]; var process = Processes[server.Id];
@ -190,6 +226,8 @@ namespace LANCommander.Services
LogFileMonitors[server.Id].Dispose(); LogFileMonitors[server.Id].Dispose();
LogFileMonitors.Remove(server.Id); LogFileMonitors.Remove(server.Id);
} }
OnStatusUpdate?.Invoke(this, new ServerStatusUpdateEventArgs(server, ServerProcessStatus.Stopped));
} }
private void StartMonitoringLog(ServerConsole log, Server server) private void StartMonitoringLog(ServerConsole log, Server server)

View File

@ -1,18 +1,14 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
<Layout Class="layout"> <Layout Class="layout">
<Header> <MainMenu>
<div class="logo" style="background: url('/static/logo-dark.svg'); width: 143px; height: 31px; margin: 16px 24px 16px 0; float: left; background-size: contain;" /> <MenuItem RouterLink="/Dashboard">Dashboard</MenuItem>
<MenuItem RouterLink="/Games">Games</MenuItem>
<MenuItem RouterLink="/Servers">Servers</MenuItem>
<MenuItem RouterLink="/Settings">Settings</MenuItem>
</MainMenu>
<Menu Theme="MenuTheme.Dark" Mode="MenuMode.Horizontal"> <Content Style="padding: 24px; min-height: 100vh;">
<MenuItem RouterLink="/Dashboard">Dashboard</MenuItem>
<MenuItem RouterLink="/Games">Games</MenuItem>
<MenuItem RouterLink="/Servers">Servers</MenuItem>
<MenuItem RouterLink="/Settings">Settings</MenuItem>
</Menu>
</Header>
<Content Style="padding: 24px;">
@Body @Body
</Content> </Content>
</Layout> </Layout>

View File

@ -9,7 +9,11 @@
<link href="~/css/site.css" rel="stylesheet" /> <link href="~/css/site.css" rel="stylesheet" />
</head> </head>
<body> <body>
@RenderBody() <section class="layout ant-layout">
<main class="ant-layout-content" style="padding: 24px; min-height: 100vh;">
@RenderBody()
</main>
</section>
<script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/antv/g2plot/dist/g2plot.js"></script> <script src="~/lib/antv/g2plot/dist/g2plot.js"></script>

View File

@ -17,4 +17,67 @@
.uploader-progress .ant-progress-bg { .uploader-progress .ant-progress-bg {
transition: none; transition: none;
}
.menu-drawer {
margin-top: 63px;
}
.menu-drawer .ant-drawer-content {
background: #001529;
}
.menu-drawer .ant-drawer-header-no-title {
display: none;
}
.menu-drawer .ant-drawer-body {
padding: 0;
}
.menu-drawer .ant-drawer-wrapper,
.menu-drawer .ant-drawer-content {
height: auto;
}
.menu-drawer .ant-drawer-content-wrapper {
height: auto !important;
}
.mobile-header .ant-btn {
color: #fff;
position: absolute;
right: 16px;
}
@media screen and (min-width: 768px) {
.mobile-menu {
display: none;
}
}
@media screen and (max-width: 767px) {
.mobile-header {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 2000;
}
.ant-header:not(.mobile-header) {
display: none;
}
.ant-header .logo {
margin-right: 0 !important;
float: none;
}
.ant-layout-content {
margin-top: 63px;
}
} }