Implement autostart functionality, ZIP packaging, and related installation scripts; update documentation and tests

This commit is contained in:
2026-09-09 18:27:32 +03:00
parent 49d10a5242
commit 369c0347a7
18 changed files with 368 additions and 13 deletions
+5
View File
@@ -11,6 +11,7 @@ internal sealed class MdnsPublisher(string fingerprint, int port) : IDisposable
private ServiceProfile? profile;
private string addressesKey = "";
private DateTimeOffset nextAnnouncement;
private DateTimeOffset lastRefresh;
public string Status { get; private set; } = "mDNS: ожидание локальной сети.";
public static ServiceProfile CreateProfile(string fingerprint, int port, IPAddress[] addresses)
@@ -28,6 +29,10 @@ internal sealed class MdnsPublisher(string fingerprint, int port) : IDisposable
{
try
{
// Rebind after a long pause (sleep/resume), even if Windows kept the same IP.
var now = DateTimeOffset.UtcNow;
if (lastRefresh != default && (now - lastRefresh > TimeSpan.FromSeconds(20) || now < lastRefresh)) Stop();
lastRefresh = now;
var key = string.Join(",", addresses.Select(a => a.ToString()));
if (discovery is null || key != addressesKey)
{
+19 -4
View File
@@ -60,7 +60,10 @@ internal sealed class RemoteServer : IAsyncDisposable
});
app = builder.Build();
app.UseRateLimiter();
app.UseWebSockets(new WebSocketOptions { KeepAliveInterval = TimeSpan.FromSeconds(20) });
app.UseWebSockets(new WebSocketOptions
{
KeepAliveInterval = TimeSpan.FromSeconds(20), KeepAliveTimeout = TimeSpan.FromSeconds(10)
});
app.Use(async (context, next) =>
{
context.Response.Headers.CacheControl = "no-store";
@@ -116,7 +119,7 @@ internal sealed class RemoteServer : IAsyncDisposable
public async Task StartAsync(CancellationToken cancellationToken = default)
{
media.Changed += RequestRefresh;
Volatile.Write(ref latest, await media.ReadAsync(cancellationToken));
Volatile.Write(ref latest, await ReadSafeAsync(cancellationToken));
await app.StartAsync(cancellationToken);
polling = PollAsync(stop.Token);
}
@@ -134,7 +137,7 @@ internal sealed class RemoteServer : IAsyncDisposable
// Coalesce bursts of Windows events without accumulating work.
await Task.Delay(25, cancellationToken);
while (refresh.Reader.TryRead(out _)) { }
var state = await media.ReadAsync(cancellationToken);
var state = await ReadSafeAsync(cancellationToken);
Volatile.Write(ref latest, state);
Published?.Invoke();
}
@@ -142,6 +145,16 @@ internal sealed class RemoteServer : IAsyncDisposable
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { }
}
private async Task<MediaState> ReadSafeAsync(CancellationToken cancellationToken)
{
using var timeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
timeout.CancelAfter(TimeSpan.FromSeconds(5));
try { return await media.ReadAsync(timeout.Token); }
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; }
catch (OperationCanceledException) { return new MediaState { MediaError = "Плеер не ответил вовремя. Повторяем подключение." }; }
catch (Exception ex) { return new MediaState { MediaError = $"Плеер временно недоступен: {ex.Message}" }; }
}
// WSS is a state stream; commands use HTTPS so each has its own response.
private async Task StreamAsync(HttpContext context)
{
@@ -195,7 +208,9 @@ internal sealed class RemoteServer : IAsyncDisposable
while (pending.Reader.TryRead(out _)) { }
if (!pairing.Authorize(token)) return;
var bytes = JsonSerializer.SerializeToUtf8Bytes(new { type = "state", state = Latest }, JsonOptions);
await socket.SendAsync(bytes.AsMemory(), WebSocketMessageType.Text, true, cancellationToken);
using var sendTimeout = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
sendTimeout.CancelAfter(TimeSpan.FromSeconds(5));
await socket.SendAsync(bytes.AsMemory(), WebSocketMessageType.Text, true, sendTimeout.Token);
}
}
finally { Published -= Signal; pairing.TrustChanged -= Signal; }