mirror of
https://github.com/chylex/Discord-History-Tracker.git
synced 2024-11-25 14:42:44 +01:00
Compare commits
2 Commits
2875f5943b
...
031d521402
Author | SHA1 | Date | |
---|---|---|---|
031d521402 | |||
0131f8cb50 |
@ -21,6 +21,7 @@
|
|||||||
<PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
|
<PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
|
||||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.6" Condition=" '$(Configuration)' == 'Debug' " />
|
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.6" Condition=" '$(Configuration)' == 'Debug' " />
|
||||||
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
|
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.0.6" />
|
||||||
|
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.6" />
|
||||||
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
|
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.6" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Threading;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Server;
|
using DHT.Desktop.Server;
|
||||||
using DHT.Server;
|
using DHT.Server;
|
||||||
using DHT.Server.Service;
|
using DHT.Server.Service;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Controls;
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
||||||
|
private static readonly Log Log = Log.ForType<ServerConfigurationPanelModel>();
|
||||||
|
|
||||||
private string inputPort;
|
private string inputPort;
|
||||||
|
|
||||||
public string InputPort {
|
public string InputPort {
|
||||||
@ -29,7 +34,7 @@ sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool HasMadeChanges => ServerManager.Port.ToString() != InputPort || ServerManager.Token != InputToken;
|
public bool HasMadeChanges => ServerConfiguration.Port.ToString() != InputPort || ServerConfiguration.Token != InputToken;
|
||||||
|
|
||||||
private bool isToggleServerButtonEnabled = true;
|
private bool isToggleServerButtonEnabled = true;
|
||||||
|
|
||||||
@ -38,59 +43,69 @@ sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
|||||||
set => Change(ref isToggleServerButtonEnabled, value);
|
set => Change(ref isToggleServerButtonEnabled, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToggleServerButtonText => serverManager.IsRunning ? "Stop Server" : "Start Server";
|
public string ToggleServerButtonText => server.IsRunning ? "Stop Server" : "Start Server";
|
||||||
|
|
||||||
public event EventHandler<StatusBarModel.Status>? ServerStatusChanged;
|
|
||||||
|
|
||||||
private readonly Window window;
|
private readonly Window window;
|
||||||
private readonly ServerManager serverManager;
|
private readonly ServerManager server;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public ServerConfigurationPanelModel() : this(null!, new ServerManager(State.Dummy)) {}
|
public ServerConfigurationPanelModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public ServerConfigurationPanelModel(Window window, ServerManager serverManager) {
|
public ServerConfigurationPanelModel(Window window, State state) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.serverManager = serverManager;
|
this.server = state.Server;
|
||||||
this.inputPort = ServerManager.Port.ToString();
|
this.inputPort = ServerConfiguration.Port.ToString();
|
||||||
this.inputToken = ServerManager.Token;
|
this.inputToken = ServerConfiguration.Token;
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
server.StatusChanged += OnServerStatusChanged;
|
||||||
ServerLauncher.ServerStatusChanged += ServerLauncherOnServerStatusChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
ServerLauncher.ServerStatusChanged -= ServerLauncherOnServerStatusChanged;
|
server.StatusChanged -= OnServerStatusChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ServerLauncherOnServerStatusChanged(object? sender, EventArgs e) {
|
private void OnServerStatusChanged(object? sender, ServerManager.Status e) {
|
||||||
ServerStatusChanged?.Invoke(this, serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped);
|
Dispatcher.UIThread.InvokeAsync(UpdateServerStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateServerStatus() {
|
||||||
OnPropertyChanged(nameof(ToggleServerButtonText));
|
OnPropertyChanged(nameof(ToggleServerButtonText));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartServer() {
|
||||||
|
IsToggleServerButtonEnabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await server.Start(ServerConfiguration.Port, ServerConfiguration.Token);
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error(e);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateServerStatus();
|
||||||
IsToggleServerButtonEnabled = true;
|
IsToggleServerButtonEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void BeforeServerStart() {
|
private async Task StopServer() {
|
||||||
IsToggleServerButtonEnabled = false;
|
IsToggleServerButtonEnabled = false;
|
||||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Starting);
|
|
||||||
|
try {
|
||||||
|
await server.Stop();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.Error(e);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StartServer() {
|
UpdateServerStatus();
|
||||||
BeforeServerStart();
|
IsToggleServerButtonEnabled = true;
|
||||||
serverManager.Launch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void StopServer() {
|
public async Task OnClickToggleServerButton() {
|
||||||
IsToggleServerButtonEnabled = false;
|
if (server.IsRunning) {
|
||||||
ServerStatusChanged?.Invoke(this, StatusBarModel.Status.Stopping);
|
await StopServer();
|
||||||
serverManager.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void OnClickToggleServerButton() {
|
|
||||||
if (serverManager.IsRunning) {
|
|
||||||
StopServer();
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
StartServer();
|
await StartServer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,19 +113,21 @@ sealed class ServerConfigurationPanelModel : BaseModel, IDisposable {
|
|||||||
InputToken = ServerUtils.GenerateRandomToken(20);
|
InputToken = ServerUtils.GenerateRandomToken(20);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async void OnClickApplyChanges() {
|
public async Task OnClickApplyChanges() {
|
||||||
if (!ushort.TryParse(InputPort, out ushort port)) {
|
if (!ushort.TryParse(InputPort, out ushort port)) {
|
||||||
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
await Dialog.ShowOk(window, "Invalid Port", "Port must be a number between 0 and 65535.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
BeforeServerStart();
|
ServerConfiguration.Port = port;
|
||||||
serverManager.Relaunch(port, InputToken);
|
ServerConfiguration.Token = inputToken;
|
||||||
|
await StartServer();
|
||||||
|
|
||||||
OnPropertyChanged(nameof(HasMadeChanges));
|
OnPropertyChanged(nameof(HasMadeChanges));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnClickCancelChanges() {
|
public void OnClickCancelChanges() {
|
||||||
InputPort = ServerManager.Port.ToString();
|
InputPort = ServerConfiguration.Port.ToString();
|
||||||
InputToken = ServerManager.Token;
|
InputToken = ServerConfiguration.Token;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -40,7 +40,7 @@
|
|||||||
<StackPanel Orientation="Horizontal" Margin="6 3">
|
<StackPanel Orientation="Horizontal" Margin="6 3">
|
||||||
<StackPanel Orientation="Vertical" Width="65">
|
<StackPanel Orientation="Vertical" Width="65">
|
||||||
<TextBlock Classes="label">Status</TextBlock>
|
<TextBlock Classes="label">Status</TextBlock>
|
||||||
<TextBlock FontSize="12" Margin="0 3 0 0" Text="{Binding StatusText}" />
|
<TextBlock FontSize="12" Margin="0 3 0 0" Text="{Binding ServerStatusText}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<Rectangle />
|
<Rectangle />
|
||||||
<StackPanel Orientation="Vertical">
|
<StackPanel Orientation="Vertical">
|
||||||
|
@ -1,45 +1,46 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using DHT.Server;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
|
using DHT.Server.Service;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Controls;
|
namespace DHT.Desktop.Main.Controls;
|
||||||
|
|
||||||
sealed class StatusBarModel : BaseModel {
|
sealed class StatusBarModel : BaseModel, IDisposable {
|
||||||
public DatabaseStatistics DatabaseStatistics { get; }
|
public DatabaseStatistics DatabaseStatistics { get; }
|
||||||
|
|
||||||
private Status status = Status.Stopped;
|
private ServerManager.Status serverStatus;
|
||||||
|
|
||||||
public Status CurrentStatus {
|
public string ServerStatusText => serverStatus switch {
|
||||||
get => status;
|
ServerManager.Status.Starting => "STARTING",
|
||||||
set {
|
ServerManager.Status.Started => "READY",
|
||||||
status = value;
|
ServerManager.Status.Stopping => "STOPPING",
|
||||||
OnPropertyChanged(nameof(StatusText));
|
ServerManager.Status.Stopped => "STOPPED",
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public string StatusText {
|
|
||||||
get {
|
|
||||||
return CurrentStatus switch {
|
|
||||||
Status.Starting => "STARTING",
|
|
||||||
Status.Ready => "READY",
|
|
||||||
Status.Stopping => "STOPPING",
|
|
||||||
Status.Stopped => "STOPPED",
|
|
||||||
_ => ""
|
_ => ""
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
private readonly State state;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public StatusBarModel() : this(new DatabaseStatistics()) {}
|
public StatusBarModel() : this(State.Dummy) {}
|
||||||
|
|
||||||
public StatusBarModel(DatabaseStatistics databaseStatistics) {
|
public StatusBarModel(State state) {
|
||||||
this.DatabaseStatistics = databaseStatistics;
|
this.state = state;
|
||||||
|
this.DatabaseStatistics = state.Db.Statistics;
|
||||||
|
|
||||||
|
state.Server.StatusChanged += OnServerStatusChanged;
|
||||||
|
serverStatus = state.Server.IsRunning ? ServerManager.Status.Started : ServerManager.Status.Stopped;
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum Status {
|
public void Dispose() {
|
||||||
Starting,
|
state.Server.StatusChanged += OnServerStatusChanged;
|
||||||
Ready,
|
}
|
||||||
Stopping,
|
|
||||||
Stopped
|
private void OnServerStatusChanged(object? sender, ServerManager.Status e) {
|
||||||
|
Dispatcher.UIThread.InvokeAsync(() => {
|
||||||
|
serverStatus = e;
|
||||||
|
OnPropertyChanged(nameof(ServerStatusText));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ using DHT.Desktop.Dialogs.Message;
|
|||||||
using DHT.Desktop.Main.Screens;
|
using DHT.Desktop.Main.Screens;
|
||||||
using DHT.Desktop.Server;
|
using DHT.Desktop.Server;
|
||||||
using DHT.Server;
|
using DHT.Server;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main;
|
namespace DHT.Desktop.Main;
|
||||||
@ -15,6 +16,8 @@ namespace DHT.Desktop.Main;
|
|||||||
sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
||||||
private const string DefaultTitle = "Discord History Tracker";
|
private const string DefaultTitle = "Discord History Tracker";
|
||||||
|
|
||||||
|
private static readonly Log Log = Log.ForType<MainWindowModel>();
|
||||||
|
|
||||||
public string Title { get; private set; } = DefaultTitle;
|
public string Title { get; private set; } = DefaultTitle;
|
||||||
|
|
||||||
public UserControl CurrentScreen { get; private set; }
|
public UserControl CurrentScreen { get; private set; }
|
||||||
@ -63,11 +66,11 @@ sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServerPort != null) {
|
if (args.ServerPort != null) {
|
||||||
ServerManager.Port = args.ServerPort.Value;
|
ServerConfiguration.Port = args.ServerPort.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args.ServerToken != null) {
|
if (args.ServerToken != null) {
|
||||||
ServerManager.Token = args.ServerToken;
|
ServerConfiguration.Token = args.ServerToken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,15 +85,23 @@ sealed class MainWindowModel : BaseModel, IAsyncDisposable {
|
|||||||
await state.DisposeAsync();
|
await state.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
state = welcomeScreenModel.Db == null ? null : new State(welcomeScreenModel.Db);
|
if (welcomeScreenModel.Db == null) {
|
||||||
|
state = null;
|
||||||
if (state == null) {
|
|
||||||
Title = DefaultTitle;
|
Title = DefaultTitle;
|
||||||
mainContentScreenModel = null;
|
mainContentScreenModel = null;
|
||||||
mainContentScreen = null;
|
mainContentScreen = null;
|
||||||
CurrentScreen = welcomeScreen;
|
CurrentScreen = welcomeScreen;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
state = new State(welcomeScreenModel.Db);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await state.Server.Start(ServerConfiguration.Port, ServerConfiguration.Token);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
Log.Error(ex);
|
||||||
|
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
Title = Path.GetFileName(state.Db.Path) + " - " + DefaultTitle;
|
Title = Path.GetFileName(state.Db.Path) + " - " + DefaultTitle;
|
||||||
mainContentScreenModel = new MainContentScreenModel(window, state);
|
mainContentScreenModel = new MainContentScreenModel(window, state);
|
||||||
await mainContentScreenModel.Initialize();
|
await mainContentScreenModel.Initialize();
|
||||||
|
@ -2,7 +2,6 @@ using System;
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
using DHT.Desktop.Dialogs.Message;
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
using DHT.Desktop.Server;
|
|
||||||
using DHT.Server;
|
using DHT.Server;
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
|
|
||||||
@ -15,17 +14,13 @@ sealed class AdvancedPageModel : BaseModel, IDisposable {
|
|||||||
private readonly State state;
|
private readonly State state;
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public AdvancedPageModel() : this(null!, State.Dummy, new ServerManager(State.Dummy)) {}
|
public AdvancedPageModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public AdvancedPageModel(Window window, State state, ServerManager serverManager) {
|
public AdvancedPageModel(Window window, State state) {
|
||||||
this.window = window;
|
this.window = window;
|
||||||
this.state = state;
|
this.state = state;
|
||||||
|
|
||||||
ServerConfigurationModel = new ServerConfigurationPanelModel(window, serverManager);
|
ServerConfigurationModel = new ServerConfigurationPanelModel(window, state);
|
||||||
}
|
|
||||||
|
|
||||||
public void Initialize() {
|
|
||||||
ServerConfigurationModel.Initialize();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.Threading;
|
using System.Reactive.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Threading;
|
using Avalonia.ReactiveUI;
|
||||||
using DHT.Desktop.Common;
|
using DHT.Desktop.Common;
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
using DHT.Server;
|
using DHT.Server;
|
||||||
@ -11,7 +11,6 @@ using DHT.Server.Data;
|
|||||||
using DHT.Server.Data.Aggregations;
|
using DHT.Server.Data.Aggregations;
|
||||||
using DHT.Server.Data.Filters;
|
using DHT.Server.Data.Filters;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Download;
|
|
||||||
using DHT.Utils.Models;
|
using DHT.Utils.Models;
|
||||||
using DHT.Utils.Tasks;
|
using DHT.Utils.Tasks;
|
||||||
|
|
||||||
@ -61,6 +60,7 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
private readonly State state;
|
private readonly State state;
|
||||||
private readonly AsyncValueComputer<DownloadStatusStatistics>.Single downloadStatisticsComputer;
|
private readonly AsyncValueComputer<DownloadStatusStatistics>.Single downloadStatisticsComputer;
|
||||||
|
|
||||||
|
private IDisposable? finishedItemsSubscription;
|
||||||
private int doneItemsCount;
|
private int doneItemsCount;
|
||||||
private int initialFinishedCount;
|
private int initialFinishedCount;
|
||||||
private int? totalItemsToDownloadCount;
|
private int? totalItemsToDownloadCount;
|
||||||
@ -76,12 +76,11 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
downloadStatisticsComputer.Recompute();
|
downloadStatisticsComputer.Recompute();
|
||||||
|
|
||||||
state.Db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
state.Db.Statistics.PropertyChanged += OnDbStatisticsChanged;
|
||||||
state.Downloader.OnItemFinished += DownloaderOnOnItemFinished;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
state.Db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
state.Db.Statistics.PropertyChanged -= OnDbStatisticsChanged;
|
||||||
state.Downloader.OnItemFinished -= DownloaderOnOnItemFinished;
|
finishedItemsSubscription?.Dispose();
|
||||||
FilterModel.Dispose();
|
FilterModel.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,19 +138,22 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
OnPropertyChanged(nameof(DownloadProgress));
|
OnPropertyChanged(nameof(DownloadProgress));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DownloaderOnOnItemFinished(object? sender, DownloadItem e) {
|
private void OnItemsFinished(int finishedItemCount) {
|
||||||
Interlocked.Increment(ref doneItemsCount);
|
doneItemsCount += finishedItemCount;
|
||||||
|
UpdateDownloadMessage();
|
||||||
Dispatcher.UIThread.Invoke(UpdateDownloadMessage);
|
|
||||||
downloadStatisticsComputer.Recompute();
|
downloadStatisticsComputer.Recompute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task OnClickToggleDownload() {
|
public async Task OnClickToggleDownload() {
|
||||||
if (IsDownloading) {
|
|
||||||
IsToggleDownloadButtonEnabled = false;
|
IsToggleDownloadButtonEnabled = false;
|
||||||
|
|
||||||
|
if (IsDownloading) {
|
||||||
await state.Downloader.Stop();
|
await state.Downloader.Stop();
|
||||||
|
|
||||||
|
finishedItemsSubscription?.Dispose();
|
||||||
|
finishedItemsSubscription = null;
|
||||||
|
|
||||||
downloadStatisticsComputer.Recompute();
|
downloadStatisticsComputer.Recompute();
|
||||||
IsToggleDownloadButtonEnabled = true;
|
|
||||||
|
|
||||||
state.Db.RemoveDownloadItems(EnqueuedItemFilter, FilterRemovalMode.RemoveMatching);
|
state.Db.RemoveDownloadItems(EnqueuedItemFilter, FilterRemovalMode.RemoveMatching);
|
||||||
|
|
||||||
@ -161,13 +163,22 @@ sealed class AttachmentsPageModel : BaseModel, IDisposable {
|
|||||||
UpdateDownloadMessage();
|
UpdateDownloadMessage();
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
var finishedItems = await state.Downloader.Start();
|
||||||
|
|
||||||
initialFinishedCount = statisticsDownloaded.Items + statisticsFailed.Items;
|
initialFinishedCount = statisticsDownloaded.Items + statisticsFailed.Items;
|
||||||
await state.Downloader.Start();
|
finishedItemsSubscription = finishedItems.Select(static _ => true)
|
||||||
|
.Buffer(TimeSpan.FromMilliseconds(100))
|
||||||
|
.Select(static items => items.Count)
|
||||||
|
.Where(static items => items > 0)
|
||||||
|
.ObserveOn(AvaloniaScheduler.Instance)
|
||||||
|
.Subscribe(OnItemsFinished);
|
||||||
|
|
||||||
EnqueueDownloadItems();
|
EnqueueDownloadItems();
|
||||||
}
|
}
|
||||||
|
|
||||||
OnPropertyChanged(nameof(ToggleDownloadButtonText));
|
OnPropertyChanged(nameof(ToggleDownloadButtonText));
|
||||||
OnPropertyChanged(nameof(IsDownloading));
|
OnPropertyChanged(nameof(IsDownloading));
|
||||||
|
IsToggleDownloadButtonEnabled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void OnClickRetryFailedDownloads() {
|
public void OnClickRetryFailedDownloads() {
|
||||||
|
@ -54,7 +54,7 @@ sealed class TrackingPageModel : BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> OnClickCopyTrackingScript() {
|
public async Task<bool> OnClickCopyTrackingScript() {
|
||||||
string url = $"http://127.0.0.1:{ServerManager.Port}/get-tracking-script?token={HttpUtility.UrlEncode(ServerManager.Token)}";
|
string url = $"http://127.0.0.1:{ServerConfiguration.Port}/get-tracking-script?token={HttpUtility.UrlEncode(ServerConfiguration.Token)}";
|
||||||
string script = (await Resources.ReadTextAsync("tracker-loader.js")).Trim().Replace("{url}", url);
|
string script = (await Resources.ReadTextAsync("tracker-loader.js")).Trim().Replace("{url}", url);
|
||||||
|
|
||||||
var clipboard = window.Clipboard;
|
var clipboard = window.Clipboard;
|
||||||
|
@ -110,7 +110,7 @@ sealed class ViewerPageModel : BaseModel, IDisposable {
|
|||||||
TemporaryFiles.Add(fullPath);
|
TemporaryFiles.Add(fullPath);
|
||||||
|
|
||||||
Directory.CreateDirectory(rootPath);
|
Directory.CreateDirectory(rootPath);
|
||||||
await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerManager.Port, ServerManager.Token));
|
await WriteViewerFile(fullPath, new LiveViewerExportStrategy(ServerConfiguration.Port, ServerConfiguration.Token));
|
||||||
|
|
||||||
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
|
Process.Start(new ProcessStartInfo(fullPath) { UseShellExecute = true });
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,9 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using DHT.Desktop.Dialogs.Message;
|
|
||||||
using DHT.Desktop.Main.Controls;
|
using DHT.Desktop.Main.Controls;
|
||||||
using DHT.Desktop.Main.Pages;
|
using DHT.Desktop.Main.Pages;
|
||||||
using DHT.Desktop.Server;
|
|
||||||
using DHT.Server;
|
using DHT.Server;
|
||||||
using DHT.Server.Service;
|
|
||||||
using DHT.Utils.Logging;
|
using DHT.Utils.Logging;
|
||||||
|
|
||||||
namespace DHT.Desktop.Main.Screens;
|
namespace DHT.Desktop.Main.Screens;
|
||||||
@ -49,18 +46,10 @@ sealed class MainContentScreenModel : IDisposable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly Window window;
|
|
||||||
private readonly ServerManager serverManager;
|
|
||||||
|
|
||||||
[Obsolete("Designer")]
|
[Obsolete("Designer")]
|
||||||
public MainContentScreenModel() : this(null!, State.Dummy) {}
|
public MainContentScreenModel() : this(null!, State.Dummy) {}
|
||||||
|
|
||||||
public MainContentScreenModel(Window window, State state) {
|
public MainContentScreenModel(Window window, State state) {
|
||||||
this.window = window;
|
|
||||||
this.serverManager = new ServerManager(state);
|
|
||||||
|
|
||||||
ServerLauncher.ServerManagementExceptionCaught += ServerLauncherOnServerManagementExceptionCaught;
|
|
||||||
|
|
||||||
DatabasePageModel = new DatabasePageModel(window, state);
|
DatabasePageModel = new DatabasePageModel(window, state);
|
||||||
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
DatabasePage = new DatabasePage { DataContext = DatabasePageModel };
|
||||||
|
|
||||||
@ -73,7 +62,7 @@ sealed class MainContentScreenModel : IDisposable {
|
|||||||
ViewerPageModel = new ViewerPageModel(window, state);
|
ViewerPageModel = new ViewerPageModel(window, state);
|
||||||
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
ViewerPage = new ViewerPage { DataContext = ViewerPageModel };
|
||||||
|
|
||||||
AdvancedPageModel = new AdvancedPageModel(window, state, serverManager);
|
AdvancedPageModel = new AdvancedPageModel(window, state);
|
||||||
AdvancedPage = new AdvancedPage { DataContext = AdvancedPageModel };
|
AdvancedPage = new AdvancedPage { DataContext = AdvancedPageModel };
|
||||||
|
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
@ -83,37 +72,17 @@ sealed class MainContentScreenModel : IDisposable {
|
|||||||
DebugPage = null;
|
DebugPage = null;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
StatusBarModel = new StatusBarModel(state.Db.Statistics);
|
StatusBarModel = new StatusBarModel(state);
|
||||||
|
|
||||||
AdvancedPageModel.ServerConfigurationModel.ServerStatusChanged += OnServerStatusChanged;
|
|
||||||
DatabaseClosed += OnDatabaseClosed;
|
|
||||||
|
|
||||||
StatusBarModel.CurrentStatus = serverManager.IsRunning ? StatusBarModel.Status.Ready : StatusBarModel.Status.Stopped;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Initialize() {
|
public async Task Initialize() {
|
||||||
await TrackingPageModel.Initialize();
|
await TrackingPageModel.Initialize();
|
||||||
AdvancedPageModel.Initialize();
|
|
||||||
serverManager.Launch();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
ServerLauncher.ServerManagementExceptionCaught -= ServerLauncherOnServerManagementExceptionCaught;
|
|
||||||
AttachmentsPageModel.Dispose();
|
AttachmentsPageModel.Dispose();
|
||||||
ViewerPageModel.Dispose();
|
ViewerPageModel.Dispose();
|
||||||
serverManager.Dispose();
|
AdvancedPageModel.Dispose();
|
||||||
}
|
StatusBarModel.Dispose();
|
||||||
|
|
||||||
private void OnServerStatusChanged(object? sender, StatusBarModel.Status e) {
|
|
||||||
StatusBarModel.CurrentStatus = e;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnDatabaseClosed(object? sender, EventArgs e) {
|
|
||||||
serverManager.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void ServerLauncherOnServerManagementExceptionCaught(object? sender, Exception ex) {
|
|
||||||
Log.Error(ex);
|
|
||||||
await Dialog.ShowOk(window, "Internal Server Error", ex.Message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8
app/Desktop/Server/ServerConfiguration.cs
Normal file
8
app/Desktop/Server/ServerConfiguration.cs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
using DHT.Server.Service;
|
||||||
|
|
||||||
|
namespace DHT.Desktop.Server;
|
||||||
|
|
||||||
|
static class ServerConfiguration {
|
||||||
|
public static ushort Port { get; set; } = ServerUtils.FindAvailablePort(50000, 60000);
|
||||||
|
public static string Token { get; set; } = ServerUtils.GenerateRandomToken(20);
|
||||||
|
}
|
@ -1,50 +0,0 @@
|
|||||||
using System;
|
|
||||||
using DHT.Server;
|
|
||||||
using DHT.Server.Service;
|
|
||||||
|
|
||||||
namespace DHT.Desktop.Server;
|
|
||||||
|
|
||||||
sealed class ServerManager : IDisposable {
|
|
||||||
public static ushort Port { get; set; } = ServerUtils.FindAvailablePort(50000, 60000);
|
|
||||||
public static string Token { get; set; } = ServerUtils.GenerateRandomToken(20);
|
|
||||||
|
|
||||||
private static ServerManager? instance;
|
|
||||||
|
|
||||||
public bool IsRunning => ServerLauncher.IsRunning;
|
|
||||||
|
|
||||||
private readonly State state;
|
|
||||||
|
|
||||||
public ServerManager(State state) {
|
|
||||||
if (state != State.Dummy) {
|
|
||||||
if (instance != null) {
|
|
||||||
throw new InvalidOperationException("Only one instance of ServerManager can exist at the same time!");
|
|
||||||
}
|
|
||||||
|
|
||||||
instance = this;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.state = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Launch() {
|
|
||||||
ServerLauncher.Relaunch(Port, Token, state.Db);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Relaunch(ushort port, string token) {
|
|
||||||
Port = port;
|
|
||||||
Token = token;
|
|
||||||
Launch();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Stop() {
|
|
||||||
ServerLauncher.Stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() {
|
|
||||||
Stop();
|
|
||||||
|
|
||||||
if (instance == this) {
|
|
||||||
instance = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -9,8 +9,6 @@ public sealed class Downloader {
|
|||||||
private DownloaderTask? current;
|
private DownloaderTask? current;
|
||||||
public bool IsDownloading => current != null;
|
public bool IsDownloading => current != null;
|
||||||
|
|
||||||
public event EventHandler<DownloadItem>? OnItemFinished;
|
|
||||||
|
|
||||||
private readonly IDatabaseFile db;
|
private readonly IDatabaseFile db;
|
||||||
private readonly SemaphoreSlim semaphore = new (1, 1);
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
@ -18,13 +16,11 @@ public sealed class Downloader {
|
|||||||
this.db = db;
|
this.db = db;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task Start() {
|
public async Task<IObservable<DownloadItem>> Start() {
|
||||||
await semaphore.WaitAsync();
|
await semaphore.WaitAsync();
|
||||||
try {
|
try {
|
||||||
if (current == null) {
|
current ??= new DownloaderTask(db);
|
||||||
current = new DownloaderTask(db);
|
return current.FinishedItems;
|
||||||
current.OnItemFinished += DelegateOnItemFinished;
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
semaphore.Release();
|
semaphore.Release();
|
||||||
}
|
}
|
||||||
@ -34,16 +30,11 @@ public sealed class Downloader {
|
|||||||
await semaphore.WaitAsync();
|
await semaphore.WaitAsync();
|
||||||
try {
|
try {
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
await current.Stop();
|
await current.DisposeAsync();
|
||||||
current.OnItemFinished -= DelegateOnItemFinished;
|
|
||||||
current = null;
|
current = null;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
semaphore.Release();
|
semaphore.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void DelegateOnItemFinished(object? sender, DownloadItem e) {
|
|
||||||
OnItemFinished?.Invoke(this, e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,25 +1,23 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Reactive.Subjects;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Channels;
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Utils.Logging;
|
using DHT.Utils.Logging;
|
||||||
using DHT.Utils.Models;
|
|
||||||
using DHT.Utils.Tasks;
|
using DHT.Utils.Tasks;
|
||||||
|
|
||||||
namespace DHT.Server.Download;
|
namespace DHT.Server.Download;
|
||||||
|
|
||||||
sealed class DownloaderTask : BaseModel {
|
sealed class DownloaderTask : IAsyncDisposable {
|
||||||
private static readonly Log Log = Log.ForType<DownloaderTask>();
|
private static readonly Log Log = Log.ForType<DownloaderTask>();
|
||||||
|
|
||||||
private const int DownloadTasks = 4;
|
private const int DownloadTasks = 4;
|
||||||
private const int QueueSize = 25;
|
private const int QueueSize = 25;
|
||||||
private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
private const string UserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36";
|
||||||
|
|
||||||
internal event EventHandler<DownloadItem>? OnItemFinished;
|
|
||||||
|
|
||||||
private readonly Channel<DownloadItem> downloadQueue = Channel.CreateBounded<DownloadItem>(new BoundedChannelOptions(QueueSize) {
|
private readonly Channel<DownloadItem> downloadQueue = Channel.CreateBounded<DownloadItem>(new BoundedChannelOptions(QueueSize) {
|
||||||
SingleReader = false,
|
SingleReader = false,
|
||||||
SingleWriter = true,
|
SingleWriter = true,
|
||||||
@ -31,12 +29,16 @@ sealed class DownloaderTask : BaseModel {
|
|||||||
private readonly CancellationToken cancellationToken;
|
private readonly CancellationToken cancellationToken;
|
||||||
|
|
||||||
private readonly IDatabaseFile db;
|
private readonly IDatabaseFile db;
|
||||||
|
private readonly Subject<DownloadItem> finishedItemPublisher = new ();
|
||||||
|
|
||||||
private readonly Task queueWriterTask;
|
private readonly Task queueWriterTask;
|
||||||
private readonly Task[] downloadTasks;
|
private readonly Task[] downloadTasks;
|
||||||
|
|
||||||
|
public IObservable<DownloadItem> FinishedItems => finishedItemPublisher;
|
||||||
|
|
||||||
internal DownloaderTask(IDatabaseFile db) {
|
internal DownloaderTask(IDatabaseFile db) {
|
||||||
this.cancellationToken = cancellationTokenSource.Token;
|
|
||||||
this.db = db;
|
this.db = db;
|
||||||
|
this.cancellationToken = cancellationTokenSource.Token;
|
||||||
this.queueWriterTask = Task.Run(RunQueueWriterTask);
|
this.queueWriterTask = Task.Run(RunQueueWriterTask);
|
||||||
this.downloadTasks = Enumerable.Range(1, DownloadTasks).Select(taskIndex => Task.Run(() => RunDownloadTask(taskIndex))).ToArray();
|
this.downloadTasks = Enumerable.Range(1, DownloadTasks).Select(taskIndex => Task.Run(() => RunDownloadTask(taskIndex))).ToArray();
|
||||||
}
|
}
|
||||||
@ -79,7 +81,7 @@ sealed class DownloaderTask : BaseModel {
|
|||||||
log.Error(e);
|
log.Error(e);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
OnItemFinished?.Invoke(this, item);
|
finishedItemPublisher.OnNext(item);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.Error("Caught exception in event handler: " + e);
|
log.Error("Caught exception in event handler: " + e);
|
||||||
}
|
}
|
||||||
@ -87,7 +89,7 @@ sealed class DownloaderTask : BaseModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task Stop() {
|
public async ValueTask DisposeAsync() {
|
||||||
try {
|
try {
|
||||||
await cancellationTokenSource.CancelAsync();
|
await cancellationTokenSource.CancelAsync();
|
||||||
} catch (Exception) {
|
} catch (Exception) {
|
||||||
@ -102,6 +104,7 @@ sealed class DownloaderTask : BaseModel {
|
|||||||
await Task.WhenAll(downloadTasks).WaitIgnoringCancellation();
|
await Task.WhenAll(downloadTasks).WaitIgnoringCancellation();
|
||||||
} finally {
|
} finally {
|
||||||
cancellationTokenSource.Dispose();
|
cancellationTokenSource.Dispose();
|
||||||
|
finishedItemPublisher.OnCompleted();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
|
||||||
|
<PackageReference Include="System.Reactive" Version="6.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
@ -1,126 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics.CodeAnalysis;
|
|
||||||
using System.Threading;
|
|
||||||
using DHT.Server.Database;
|
|
||||||
using DHT.Utils.Logging;
|
|
||||||
using Microsoft.AspNetCore.Hosting;
|
|
||||||
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
|
|
||||||
namespace DHT.Server.Service;
|
|
||||||
|
|
||||||
public static class ServerLauncher {
|
|
||||||
private static readonly Log Log = Log.ForType(typeof(ServerLauncher));
|
|
||||||
|
|
||||||
private static IWebHost? Server { get; set; } = null;
|
|
||||||
|
|
||||||
public static bool IsRunning { get; private set; }
|
|
||||||
public static event EventHandler? ServerStatusChanged;
|
|
||||||
public static event EventHandler<Exception>? ServerManagementExceptionCaught;
|
|
||||||
|
|
||||||
private static Thread? ManagementThread { get; set; } = null;
|
|
||||||
private static readonly Mutex ManagementThreadLock = new();
|
|
||||||
private static readonly BlockingCollection<IMessage> Messages = new(new ConcurrentQueue<IMessage>());
|
|
||||||
|
|
||||||
private static void EnqueueMessage(IMessage message) {
|
|
||||||
ManagementThreadLock.WaitOne();
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (ManagementThread == null) {
|
|
||||||
ManagementThread = new Thread(RunManagementThread) {
|
|
||||||
Name = "DHT server management thread",
|
|
||||||
IsBackground = true
|
|
||||||
};
|
|
||||||
ManagementThread.Start();
|
|
||||||
}
|
|
||||||
|
|
||||||
Messages.Add(message);
|
|
||||||
} finally {
|
|
||||||
ManagementThreadLock.ReleaseMutex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SuppressMessage("ReSharper", "FunctionNeverReturns")]
|
|
||||||
private static void RunManagementThread() {
|
|
||||||
foreach (IMessage message in Messages.GetConsumingEnumerable()) {
|
|
||||||
try {
|
|
||||||
switch (message) {
|
|
||||||
case IMessage.StartServer start:
|
|
||||||
StopServerFromManagementThread();
|
|
||||||
StartServerFromManagementThread(start.Port, start.Token, start.Db);
|
|
||||||
break;
|
|
||||||
case IMessage.StopServer:
|
|
||||||
StopServerFromManagementThread();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
ServerManagementExceptionCaught?.Invoke(null, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void StartServerFromManagementThread(ushort port, string token, IDatabaseFile db) {
|
|
||||||
Log.Info("Starting server on port " + port + "...");
|
|
||||||
|
|
||||||
void AddServices(IServiceCollection services) {
|
|
||||||
services.AddSingleton(typeof(IDatabaseFile), db);
|
|
||||||
services.AddSingleton(typeof(ServerParameters), new ServerParameters(port, token));
|
|
||||||
}
|
|
||||||
|
|
||||||
void SetKestrelOptions(KestrelServerOptions options) {
|
|
||||||
options.Limits.MaxRequestBodySize = null;
|
|
||||||
options.Limits.MinResponseDataRate = null;
|
|
||||||
options.ListenLocalhost(port, static listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
|
|
||||||
}
|
|
||||||
|
|
||||||
Server = new WebHostBuilder()
|
|
||||||
.ConfigureServices(AddServices)
|
|
||||||
.UseKestrel(SetKestrelOptions)
|
|
||||||
.UseStartup<Startup>()
|
|
||||||
.Build();
|
|
||||||
|
|
||||||
Server.Start();
|
|
||||||
|
|
||||||
Log.Info("Server started");
|
|
||||||
IsRunning = true;
|
|
||||||
ServerStatusChanged?.Invoke(null, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void StopServerFromManagementThread() {
|
|
||||||
if (Server != null) {
|
|
||||||
Log.Info("Stopping server...");
|
|
||||||
Server.StopAsync().Wait();
|
|
||||||
Server.Dispose();
|
|
||||||
Server = null;
|
|
||||||
|
|
||||||
Log.Info("Server stopped");
|
|
||||||
IsRunning = false;
|
|
||||||
ServerStatusChanged?.Invoke(null, EventArgs.Empty);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Relaunch(ushort port, string token, IDatabaseFile db) {
|
|
||||||
EnqueueMessage(new IMessage.StartServer(port, token, db));
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void Stop() {
|
|
||||||
EnqueueMessage(new IMessage.StopServer());
|
|
||||||
}
|
|
||||||
|
|
||||||
private interface IMessage {
|
|
||||||
public sealed class StartServer : IMessage {
|
|
||||||
public ushort Port { get; }
|
|
||||||
public string Token { get; }
|
|
||||||
public IDatabaseFile Db { get; }
|
|
||||||
|
|
||||||
public StartServer(ushort port, string token, IDatabaseFile db) {
|
|
||||||
this.Port = port;
|
|
||||||
this.Token = token;
|
|
||||||
this.Db = db;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public sealed class StopServer : IMessage {}
|
|
||||||
}
|
|
||||||
}
|
|
107
app/Server/Service/ServerManager.cs
Normal file
107
app/Server/Service/ServerManager.cs
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using DHT.Server.Database;
|
||||||
|
using DHT.Utils.Logging;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Server.Kestrel.Core;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
namespace DHT.Server.Service;
|
||||||
|
|
||||||
|
public sealed class ServerManager {
|
||||||
|
private static readonly Log Log = Log.ForType(typeof(ServerManager));
|
||||||
|
|
||||||
|
private IWebHost? server;
|
||||||
|
public bool IsRunning => server != null;
|
||||||
|
|
||||||
|
public event EventHandler<Status>? StatusChanged;
|
||||||
|
|
||||||
|
public enum Status {
|
||||||
|
Starting,
|
||||||
|
Started,
|
||||||
|
Stopping,
|
||||||
|
Stopped
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly IDatabaseFile db;
|
||||||
|
private readonly SemaphoreSlim semaphore = new (1, 1);
|
||||||
|
|
||||||
|
internal ServerManager(IDatabaseFile db) {
|
||||||
|
this.db = db;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Start(ushort port, string token) {
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
await StartInternal(port, token);
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Stop() {
|
||||||
|
await semaphore.WaitAsync();
|
||||||
|
try {
|
||||||
|
await StopInternal();
|
||||||
|
} finally {
|
||||||
|
semaphore.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StartInternal(ushort port, string token) {
|
||||||
|
await StopInternal();
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Starting);
|
||||||
|
|
||||||
|
void AddServices(IServiceCollection services) {
|
||||||
|
services.AddSingleton(typeof(IDatabaseFile), db);
|
||||||
|
services.AddSingleton(typeof(ServerParameters), new ServerParameters(port, token));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SetKestrelOptions(KestrelServerOptions options) {
|
||||||
|
options.Limits.MaxRequestBodySize = null;
|
||||||
|
options.Limits.MinResponseDataRate = null;
|
||||||
|
options.ListenLocalhost(port, static listenOptions => listenOptions.Protocols = HttpProtocols.Http1);
|
||||||
|
}
|
||||||
|
|
||||||
|
var newServer = new WebHostBuilder()
|
||||||
|
.ConfigureServices(AddServices)
|
||||||
|
.UseKestrel(SetKestrelOptions)
|
||||||
|
.UseStartup<Startup>()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
Log.Info("Starting server on port " + port + "...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
await newServer.StartAsync();
|
||||||
|
} catch (Exception) {
|
||||||
|
Log.Error("Server could not start");
|
||||||
|
StatusChanged?.Invoke(this, Status.Stopped);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Info("Server started");
|
||||||
|
|
||||||
|
server = newServer;
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Started);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task StopInternal() {
|
||||||
|
if (server == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Stopping);
|
||||||
|
|
||||||
|
Log.Info("Stopping server...");
|
||||||
|
await server.StopAsync();
|
||||||
|
Log.Info("Server stopped");
|
||||||
|
|
||||||
|
server.Dispose();
|
||||||
|
server = null;
|
||||||
|
|
||||||
|
StatusChanged?.Invoke(this, Status.Stopped);
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using DHT.Server.Database;
|
using DHT.Server.Database;
|
||||||
using DHT.Server.Download;
|
using DHT.Server.Download;
|
||||||
|
using DHT.Server.Service;
|
||||||
|
|
||||||
namespace DHT.Server;
|
namespace DHT.Server;
|
||||||
|
|
||||||
@ -10,14 +11,17 @@ public sealed class State : IAsyncDisposable {
|
|||||||
|
|
||||||
public IDatabaseFile Db { get; }
|
public IDatabaseFile Db { get; }
|
||||||
public Downloader Downloader { get; }
|
public Downloader Downloader { get; }
|
||||||
|
public ServerManager Server { get; }
|
||||||
|
|
||||||
public State(IDatabaseFile db) {
|
public State(IDatabaseFile db) {
|
||||||
Db = db;
|
Db = db;
|
||||||
Downloader = new Downloader(db);
|
Downloader = new Downloader(db);
|
||||||
|
Server = new ServerManager(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync() {
|
public async ValueTask DisposeAsync() {
|
||||||
await Downloader.Stop();
|
await Downloader.Stop();
|
||||||
|
await Server.Stop();
|
||||||
Db.Dispose();
|
Db.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user