Add remote LANCommander server installs as viable server engine

This commit is contained in:
Pat Hartl 2026-03-08 13:49:30 -05:00
parent 8b3a63e9b5
commit de38e609a5
14 changed files with 612 additions and 38 deletions

View file

@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Server.Data.MySQL.Migrations
{
/// <inheritdoc />
public partial class AddRemoteServerEngine : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "RemoteHostId",
table: "Servers",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.AddColumn<Guid>(
name: "RemoteServerId",
table: "Servers",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RemoteHostId",
table: "Servers");
migrationBuilder.DropColumn(
name: "RemoteServerId",
table: "Servers");
}
}
}

View file

@ -1204,6 +1204,12 @@ namespace LANCommander.Server.Data.MySQL.Migrations
b.Property<int>("ProcessTerminationMethod")
.HasColumnType("int");
b.Property<Guid?>("RemoteHostId")
.HasColumnType("char(36)");
b.Property<Guid?>("RemoteServerId")
.HasColumnType("char(36)");
b.Property<Guid?>("UpdatedById")
.HasColumnType("char(36)");

View file

@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Server.Data.PostgreSQL.Migrations
{
/// <inheritdoc />
public partial class AddRemoteServerEngine : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "RemoteHostId",
table: "Servers",
type: "uuid",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "RemoteServerId",
table: "Servers",
type: "uuid",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RemoteHostId",
table: "Servers");
migrationBuilder.DropColumn(
name: "RemoteServerId",
table: "Servers");
}
}
}

View file

@ -1204,6 +1204,12 @@ namespace LANCommander.Server.Data.PostgreSQL.Migrations
b.Property<int>("ProcessTerminationMethod")
.HasColumnType("integer");
b.Property<Guid?>("RemoteHostId")
.HasColumnType("uuid");
b.Property<Guid?>("RemoteServerId")
.HasColumnType("uuid");
b.Property<Guid?>("UpdatedById")
.HasColumnType("uuid");

View file

@ -0,0 +1,39 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace LANCommander.Migrations
{
/// <inheritdoc />
public partial class AddRemoteServerEngine : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "RemoteHostId",
table: "Servers",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "RemoteServerId",
table: "Servers",
type: "TEXT",
nullable: true);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "RemoteHostId",
table: "Servers");
migrationBuilder.DropColumn(
name: "RemoteServerId",
table: "Servers");
}
}
}

View file

@ -1370,6 +1370,12 @@ namespace LANCommander.Migrations
b.Property<int>("ProcessTerminationMethod")
.HasColumnType("INTEGER");
b.Property<Guid?>("RemoteHostId")
.HasColumnType("TEXT");
b.Property<Guid?>("RemoteServerId")
.HasColumnType("TEXT");
b.Property<Guid?>("UpdatedById")
.HasColumnType("TEXT");

View file

@ -19,6 +19,8 @@ namespace LANCommander.Server.Data.Models
public string OnStopScriptPath { get; set; } = "";
public Guid? DockerHostId { get; set; }
public Guid? RemoteHostId { get; set; }
public Guid? RemoteServerId { get; set; }
[MaxLength(64)]
public string ContainerId { get; set; } = "";
public string Host { get; set; } = "";

View file

@ -72,6 +72,9 @@ public static class IServiceCollectionExtensions
services.AddSingleton<DockerServerEngine>();
services.AddSingleton<IServerEngine>(provider => provider.GetService<DockerServerEngine>());
services.AddSingleton<RemoteServerEngine>();
services.AddSingleton<IServerEngine>(provider => provider.GetService<RemoteServerEngine>());
services.AddSingleton<ScriptDebugger>();
services.AddSingleton<IScriptDebugger>(sp =>

View file

@ -0,0 +1,273 @@
using System.Net.Http.Json;
using System.Text.Json;
using LANCommander.SDK.Models;
using LANCommander.Server.Services.Abstractions;
using LANCommander.Server.Services.Enums;
using LANCommander.Server.Services.Models;
using LANCommander.Server.Settings.Enums;
using LANCommander.Server.Settings.Models;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace LANCommander.Server.Services.ServerEngines;
public class RemoteServerEngine(
ILogger<RemoteServerEngine> logger,
SettingsProvider<Settings.Settings> settingsProvider,
IServiceProvider serviceProvider) : IServerEngine
{
private record RemoteServerInfo(Guid HostId, Guid RemoteServerId);
private readonly Dictionary<Guid, RemoteServerInfo> _tracked = new();
private readonly Dictionary<Guid, HttpClient> _clients = new();
private readonly Dictionary<Guid, ServerProcessStatus> _status = new();
private Timer _pollTimer;
public event EventHandler<ServerStatusUpdateEventArgs>? OnServerStatusUpdate;
public event EventHandler<ServerLogEventArgs>? OnServerLog;
public async Task InitializeAsync()
{
foreach (var config in settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.Where(e => e.Type == ServerEngine.Remote))
{
if (!string.IsNullOrWhiteSpace(config.Address) &&
Uri.TryCreate(config.Address, UriKind.Absolute, out _))
{
_clients[config.Id] = CreateHttpClient(config);
}
}
using var scope = serviceProvider.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
var servers = await serverService.GetAsync(s => s.Engine == ServerEngine.Remote);
foreach (var server in servers)
{
if (server.RemoteHostId.HasValue && server.RemoteServerId.HasValue &&
_clients.ContainsKey(server.RemoteHostId.Value))
{
_tracked[server.Id] = new RemoteServerInfo(server.RemoteHostId.Value, server.RemoteServerId.Value);
}
}
_pollTimer = new Timer(PollStatus, null, TimeSpan.Zero, TimeSpan.FromSeconds(10));
}
public bool IsManaging(Guid serverId) => _tracked.ContainsKey(serverId);
public async Task StartAsync(Guid serverId)
{
if (!_tracked.ContainsKey(serverId))
throw new Exception("Server is not being tracked by this engine.");
var info = _tracked[serverId];
await EnsureTokenValidAsync(info.HostId);
var client = _clients[info.HostId];
await client.PostAsync($"api/Server/{info.RemoteServerId}/Start", null);
}
public async Task StopAsync(Guid serverId)
{
if (!_tracked.ContainsKey(serverId))
throw new Exception("Server is not being tracked by this engine.");
var info = _tracked[serverId];
await EnsureTokenValidAsync(info.HostId);
var client = _clients[info.HostId];
await client.PostAsync($"api/Server/{info.RemoteServerId}/Stop", null);
}
public async Task<ServerProcessStatus> GetStatusAsync(Guid serverId)
{
if (!_tracked.ContainsKey(serverId))
return ServerProcessStatus.Stopped;
var info = _tracked[serverId];
try
{
await EnsureTokenValidAsync(info.HostId);
var client = _clients[info.HostId];
var response = await client.GetAsync($"api/Server/{info.RemoteServerId}/Status");
if (!response.IsSuccessStatusCode)
return ServerProcessStatus.Stopped;
var status = await response.Content.ReadFromJsonAsync<ServerProcessStatus>();
return status;
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Could not retrieve status for remote server {ServerId}", serverId);
return ServerProcessStatus.Stopped;
}
}
public async Task<IEnumerable<SDK.Models.Server>> GetRemoteServersAsync(Guid hostId)
{
if (!_clients.TryGetValue(hostId, out var client))
return [];
try
{
await EnsureTokenValidAsync(hostId);
var servers = await client.GetFromJsonAsync<IEnumerable<SDK.Models.Server>>("api/Server/");
return servers ?? [];
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Could not retrieve servers from remote host {HostId}", hostId);
return [];
}
}
public async Task<string> AuthenticateAsync(Guid hostId, string username, string password)
{
var config = settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId);
if (config == null)
return "Remote host configuration not found.";
if (!Uri.TryCreate(config.Address, UriKind.Absolute, out _))
return "Invalid address configured for remote host.";
try
{
var tempClient = CreateHttpClient(config, includeAuth: false);
var loginModel = new { Username = username, Password = password };
var response = await tempClient.PostAsJsonAsync("api/Auth/Login", loginModel);
if (!response.IsSuccessStatusCode)
return $"Authentication failed: {response.StatusCode}";
var token = await response.Content.ReadFromJsonAsync<AuthToken>();
if (token == null)
return "Authentication failed: empty response.";
settingsProvider.Update(s =>
{
var c = s.Server.GameServers.ServerEngines.FirstOrDefault(e => e.Id == hostId);
if (c == null)
return;
c.AccessToken = token.AccessToken;
c.RefreshToken = token.RefreshToken;
c.TokenExpiration = token.Expiration;
});
_clients[hostId] = CreateHttpClient(settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId) ?? config);
return string.Empty;
}
catch (Exception ex)
{
logger?.LogError(ex, "Error authenticating with remote host {HostId}", hostId);
return ex.Message;
}
}
private async Task EnsureTokenValidAsync(Guid hostId)
{
var config = settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId);
if (config == null)
return;
if (config.TokenExpiration > DateTime.UtcNow.AddMinutes(5))
return;
if (string.IsNullOrWhiteSpace(config.RefreshToken))
return;
try
{
var tempClient = CreateHttpClient(config, includeAuth: false);
var refreshPayload = new AuthToken
{
AccessToken = config.AccessToken,
RefreshToken = config.RefreshToken,
Expiration = config.TokenExpiration
};
var response = await tempClient.PostAsJsonAsync("api/Auth/Refresh", refreshPayload);
if (!response.IsSuccessStatusCode)
return;
var token = await response.Content.ReadFromJsonAsync<AuthToken>();
if (token == null)
return;
settingsProvider.Update(s =>
{
var c = s.Server.GameServers.ServerEngines.FirstOrDefault(e => e.Id == hostId);
if (c != null)
{
c.AccessToken = token.AccessToken;
c.RefreshToken = token.RefreshToken;
c.TokenExpiration = token.Expiration;
}
});
_clients[hostId] = CreateHttpClient(settingsProvider.CurrentValue.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == hostId) ?? config);
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Could not refresh token for remote host {HostId}", hostId);
}
}
private HttpClient CreateHttpClient(ServerEngineConfiguration config, bool includeAuth = true)
{
var client = new HttpClient();
client.BaseAddress = new Uri(config.Address.TrimEnd('/') + "/");
if (includeAuth && !string.IsNullOrWhiteSpace(config.AccessToken))
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", config.AccessToken);
return client;
}
private async void PollStatus(object? state)
{
foreach (var (serverId, info) in _tracked.ToList())
{
try
{
var newStatus = await GetStatusAsync(serverId);
if (!_status.TryGetValue(serverId, out var oldStatus) || oldStatus != newStatus)
{
_status[serverId] = newStatus;
using var scope = serviceProvider.CreateScope();
var serverService = scope.ServiceProvider.GetRequiredService<ServerService>();
var server = await serverService.GetAsync(serverId);
if (server != null)
OnServerStatusUpdate?.Invoke(this, new ServerStatusUpdateEventArgs(server, newStatus));
}
}
catch (Exception ex)
{
logger?.LogWarning(ex, "Error polling status for remote server {ServerId}", serverId);
}
}
}
}

View file

@ -4,4 +4,5 @@ public enum ServerEngine
{
Local,
Docker,
Remote = 2,
}

View file

@ -8,6 +8,9 @@ public class ServerEngineConfiguration
public string Name { get; set; } = "Local";
public ServerEngine Type { get; set; } = ServerEngine.Local;
public string Address { get; set; } = String.Empty;
public string AccessToken { get; set; } = String.Empty;
public string RefreshToken { get; set; } = String.Empty;
public DateTime TokenExpiration { get; set; }
public ServerEngineConfiguration()
{

View file

@ -2,6 +2,7 @@
using LANCommander.SDK.Helpers;
using LANCommander.Server.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using System.IO.Compression;
namespace LANCommander.Server.Controllers
@ -10,15 +11,18 @@ namespace LANCommander.Server.Controllers
{
private readonly ServerService ServerService;
private readonly IMapper Mapper;
private readonly IOptions<Settings.Settings> _settings;
public ServerController(
ILogger<ServerController> logger,
SettingsProvider<Settings.Settings> settingsProvider,
IMapper mapper,
ServerService serverService) : base(logger, settingsProvider)
ServerService serverService,
IOptions<Settings.Settings> settings) : base(logger, settingsProvider)
{
ServerService = serverService;
Mapper = mapper;
_settings = settings;
}
[HttpGet("/Server/{id:guid}/{*path}")]
@ -31,6 +35,17 @@ namespace LANCommander.Server.Controllers
if (server == null)
return NotFound();
if (server.Engine == Settings.Enums.ServerEngine.Remote)
{
var config = _settings.Value.Server.GameServers.ServerEngines
.FirstOrDefault(e => e.Id == server.RemoteHostId);
if (config == null)
return NotFound();
return Redirect($"{config.Address.TrimEnd('/')}/Server/{server.RemoteServerId}/{path}");
}
if (server.HttpPaths == null || server.HttpPaths.Count == 0)
return NotFound();

View file

@ -13,6 +13,7 @@
@inject ServerService ServerService
@inject GameService GameService
@inject DockerServerEngine DockerServerEngine
@inject RemoteServerEngine RemoteServerEngine
@inject IOptions<Settings> Settings
@inject NavigationManager NavigationManager
@inject IMessageService MessageService
@ -83,7 +84,7 @@
@bind-Value="context.DockerHostId"
OnSelectedItemChanged="ChangeDockerHost"></Select>
</FormItem>
<FormItem Label="Container">
<Select
TItem="DockerContainer"
@ -95,36 +96,65 @@
@bind-Value="context.ContainerId"></Select>
</FormItem>
}
<FormItem Label="Executable Path">
<FilePicker Root="@RootPath" EntrySelectable="x => x is FileManagerFile" @bind-Value="@context.Path" OnSelected="(path) => context.WorkingDirectory = Path.GetDirectoryName(path)" />
</FormItem>
<FormItem Label="Arguments">
<Input @bind-Value="@context.Arguments" />
</FormItem>
<FormItem Label="Working Directory">
<FilePicker Root="@RootPath" Title="Choose Working Directory" OkText="Select Directory" EntrySelectable="x => x is FileManagerDirectory" @bind-Value="@context.WorkingDirectory" />
</FormItem>
@if (context.Engine == ServerEngine.Remote)
{
<FormItem Label="Remote Host">
<Select TItem="ServerEngineConfiguration"
TItemValue="Guid?"
DataSource="@Settings.Value.Server.GameServers.ServerEngines.Where(e => e.Type == ServerEngine.Remote)"
LabelName="Name"
ValueName="Id"
@bind-Value="context.RemoteHostId"
OnSelectedItemChanged="ChangeRemoteHost" />
</FormItem>
<FormItem Label="Remote Server">
<Select TItem="SDK.Models.Server"
TItemValue="Guid?"
DataSource="RemoteServers"
Disabled="!RemoteServers.Any()"
LabelName="Name"
ValueName="Id"
@bind-Value="context.RemoteServerId" />
</FormItem>
}
@if (context.Engine != ServerEngine.Remote)
{
<FormItem Label="Executable Path">
<FilePicker Root="@RootPath" EntrySelectable="x => x is FileManagerFile" @bind-Value="@context.Path" OnSelected="(path) => context.WorkingDirectory = Path.GetDirectoryName(path)" />
</FormItem>
<FormItem Label="Arguments">
<Input @bind-Value="@context.Arguments" />
</FormItem>
<FormItem Label="Working Directory">
<FilePicker Root="@RootPath" Title="Choose Working Directory" OkText="Select Directory" EntrySelectable="x => x is FileManagerDirectory" @bind-Value="@context.WorkingDirectory" />
</FormItem>
}
<FormItem Label="Host">
<Input @bind-Value="@context.Host" />
</FormItem>
<FormItem Label="Port" HasFeedback="@PortInUse" Help="@(PortInUse ? "Another server may already be bound to this port" : "")" ValidateStatus="@(PortInUse ? FormValidateStatus.Warning : FormValidateStatus.Default)">
<ServerPortInput @bind-Value="@context.Port" @bind-PortInUse="PortInUse" ServerId="context.Id" />
</FormItem>
<FormItem>
<LabelTemplate>
Use Shell Execute
<Tooltip Title="This option specifies whether you would like to run the server using the shell. Some servers may require this as they will have a UI or won't output logs to stdout">
<Icon Type="@IconType.Outline.QuestionCircle" Theme="@IconThemeType.Outline" />
</Tooltip>
</LabelTemplate>
<ChildContent>
<Switch @bind-Checked="context.UseShellExecute" />
</ChildContent>
</FormItem>
<FormItem Label="Termination Method">
<Select @bind-Value="context.ProcessTerminationMethod" TItem="ProcessTerminationMethod" TItemValue="ProcessTerminationMethod" DataSource="AllowedProcessTerminationMethods" />
</FormItem>
@if (context.Engine != ServerEngine.Remote)
{
<FormItem>
<LabelTemplate>
Use Shell Execute
<Tooltip Title="This option specifies whether you would like to run the server using the shell. Some servers may require this as they will have a UI or won't output logs to stdout">
<Icon Type="@IconType.Outline.QuestionCircle" Theme="@IconThemeType.Outline" />
</Tooltip>
</LabelTemplate>
<ChildContent>
<Switch @bind-Checked="context.UseShellExecute" />
</ChildContent>
</FormItem>
<FormItem Label="Termination Method">
<Select @bind-Value="context.ProcessTerminationMethod" TItem="ProcessTerminationMethod" TItemValue="ProcessTerminationMethod" DataSource="AllowedProcessTerminationMethods" />
</FormItem>
}
</Form>
</ChildContent>
</ServerEditView>
@ -134,6 +164,7 @@
IEnumerable<Game> Games = new List<Game>();
IEnumerable<DockerContainer> Containers = new List<DockerContainer>();
IEnumerable<SDK.Models.Server> RemoteServers = new List<SDK.Models.Server>();
Guid GameId;
@ -172,6 +203,11 @@
{
Containers = await DockerServerEngine.GetContainersAsync(hostConfiguration.Id);
}
async Task ChangeRemoteHost(ServerEngineConfiguration hostConfiguration)
{
RemoteServers = await RemoteServerEngine.GetRemoteServersAsync(hostConfiguration.Id);
}
async Task Save(Server server)
{

View file

@ -1,12 +1,14 @@
@using LANCommander.Server.Services.ServerEngines
@using LANCommander.Server.Settings.Enums
@using LANCommander.Server.Settings.Models
@inject RemoteServerEngine RemoteServerEngine
<Flex Direction="FlexDirection.Vertical" Gap="FlexGap.Large">
@if (!ServerEngines.Any())
{
<Empty Description="@("No Docker hosts have been configured")" />
<Empty Description="@("No engines have been configured")" />
}
<Collapse>
@foreach (var serverEngine in ServerEngines)
{
@ -23,9 +25,9 @@
</FormItem>
</Form>
</ChildContent>
</Panel>
</Panel>
}
@if (serverEngine.Type == ServerEngine.Docker)
{
<Panel Header="@serverEngine.Name">
@ -37,7 +39,7 @@
<FormItem Label="Name">
<Input @bind-Value="context.Name" BindOnInput />
</FormItem>
<FormItem Label="Address">
<Input @bind-Value="context.Address" />
</FormItem>
@ -45,11 +47,61 @@
</ChildContent>
</Panel>
}
@if (serverEngine.Type == ServerEngine.Remote)
{
<Panel Header="@serverEngine.Name">
<ExtraTemplate>
<Button Type="ButtonType.Text" Icon="@IconType.Outline.Close" Size="ButtonSize.Small" Danger OnClick="() => Remove(serverEngine)"/>
</ExtraTemplate>
<ChildContent>
<Form Model="serverEngine" Layout="FormLayout.Vertical">
<FormItem Label="Name">
<Input @bind-Value="context.Name" BindOnInput />
</FormItem>
<FormItem Label="Address">
<Input @bind-Value="context.Address" Placeholder="http://other-server:1337" />
</FormItem>
<FormItem Label="Status">
@if (!String.IsNullOrWhiteSpace(context.AccessToken) && context.TokenExpiration > DateTime.UtcNow)
{
<Tag Color="success">Connected</Tag>
}
else
{
<Tag Color="default">Not Connected</Tag>
}
</FormItem>
<FormItem Label="Username">
<Input @bind-Value="_remoteUsernames[serverEngine.Id]" />
</FormItem>
<FormItem Label="Password">
<InputPassword @bind-Value="_remotePasswords[serverEngine.Id]" />
</FormItem>
@if (!string.IsNullOrWhiteSpace(_remoteErrors[serverEngine.Id]))
{
<Alert Type="@AlertType.Error" Message="@_remoteErrors[serverEngine.Id]" ShowIcon />
}
<FormItem>
<Button Type="ButtonType.Primary" Loading="_remoteConnecting[serverEngine.Id]" OnClick="() => Connect(serverEngine)">Connect</Button>
</FormItem>
</Form>
</ChildContent>
</Panel>
}
}
</Collapse>
<Flex Justify="FlexJustify.Center">
<Button OnClick="Add" Type="ButtonType.Primary">Add Engine</Button>
<Flex Justify="FlexJustify.Center" Gap="FlexGap.Small">
<Button OnClick="() => Add(ServerEngine.Local)" Type="ButtonType.Default">Add Local</Button>
<Button OnClick="() => Add(ServerEngine.Docker)" Type="ButtonType.Default">Add Docker</Button>
<Button OnClick="() => Add(ServerEngine.Remote)" Type="ButtonType.Primary">Add Remote</Button>
</Flex>
</Flex>
@ -59,24 +111,76 @@
List<ServerEngineConfiguration> ServerEngines = new();
Dictionary<Guid, string> _remoteUsernames = new();
Dictionary<Guid, string> _remotePasswords = new();
Dictionary<Guid, string> _remoteErrors = new();
Dictionary<Guid, bool> _remoteConnecting = new();
protected override void OnParametersSet()
{
ServerEngines = Values.OrderBy(e => e.Type).ThenBy(e => e.Name).ToList();
foreach (var engine in ServerEngines.Where(e => e.Type == ServerEngine.Remote))
{
_remoteUsernames.TryAdd(engine.Id, string.Empty);
_remotePasswords.TryAdd(engine.Id, string.Empty);
_remoteErrors.TryAdd(engine.Id, string.Empty);
_remoteConnecting.TryAdd(engine.Id, false);
}
}
async Task Add()
async Task Add(ServerEngine type)
{
ServerEngines.Add(new ServerEngineConfiguration());
var newEngine = new ServerEngineConfiguration
{
Type = type,
Name = type.ToString()
};
ServerEngines.Add(newEngine);
if (type == ServerEngine.Remote)
{
_remoteUsernames[newEngine.Id] = string.Empty;
_remotePasswords[newEngine.Id] = string.Empty;
_remoteErrors[newEngine.Id] = string.Empty;
_remoteConnecting[newEngine.Id] = false;
}
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(ServerEngines);
}
async Task Remove(ServerEngineConfiguration serverEngine)
{
ServerEngines.Remove(serverEngine);
_remoteUsernames.Remove(serverEngine.Id);
_remotePasswords.Remove(serverEngine.Id);
_remoteErrors.Remove(serverEngine.Id);
_remoteConnecting.Remove(serverEngine.Id);
if (ValuesChanged.HasDelegate)
await ValuesChanged.InvokeAsync(ServerEngines);
}
}
async Task Connect(ServerEngineConfiguration serverEngine)
{
_remoteConnecting[serverEngine.Id] = true;
_remoteErrors[serverEngine.Id] = string.Empty;
await InvokeAsync(StateHasChanged);
var username = _remoteUsernames.GetValueOrDefault(serverEngine.Id, string.Empty);
var password = _remotePasswords.GetValueOrDefault(serverEngine.Id, string.Empty);
var error = await RemoteServerEngine.AuthenticateAsync(serverEngine.Id, username, password);
if (!string.IsNullOrWhiteSpace(error))
_remoteErrors[serverEngine.Id] = error;
_remoteConnecting[serverEngine.Id] = false;
await InvokeAsync(StateHasChanged);
}
}