Доработано управление жизненным циклом
This commit is contained in:
@@ -5,12 +5,13 @@ using BotPages.Core.Context;
|
||||
using BotPages.Core.Logging;
|
||||
using BotPages.Core.Routing;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
|
||||
/// <summary>
|
||||
/// Основное приложение BotPages.
|
||||
/// Управляет маршрутизацией, командами, middleware и страницами.
|
||||
/// </summary>
|
||||
public sealed class BotPagesApp
|
||||
public sealed class BotPagesApp : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly IMessengerAdapterFactory _adapterFactory;
|
||||
private readonly List<IPageMiddleware> _middlewares = new();
|
||||
@@ -19,12 +20,21 @@ public sealed class BotPagesApp
|
||||
private readonly IStateStorage _state;
|
||||
private readonly ILogger _logger;
|
||||
private readonly NavigationService _navigation;
|
||||
private CancellationTokenSource? _internalCts;
|
||||
private bool _isRunning;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Серсвис логирования.
|
||||
/// </summary>
|
||||
public ILogger Logger => _logger;
|
||||
|
||||
/// <summary>
|
||||
/// ТОкен отмены приложения. Отменяется при вызове Stop() или при отмене внешнего токена, переданного в RunAsync.
|
||||
/// </summary>
|
||||
public CancellationToken CancellationToken => _internalCts?.Token ?? CancellationToken.None;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Создать приложение BotPages.
|
||||
/// </summary>
|
||||
@@ -177,14 +187,14 @@ public sealed class BotPagesApp
|
||||
/// <summary>
|
||||
/// Обработать входящее обновление.
|
||||
/// </summary>
|
||||
public async Task HandleUpdateAsync(UpdateContext update, CancellationToken ct)
|
||||
public async Task HandleUpdateAsync(UpdateContext update)
|
||||
{
|
||||
var ctx = await CreatePageContextAsync(update, ct);
|
||||
var ctx = await CreatePageContextAsync(update);
|
||||
|
||||
// Команды выше событий страниц
|
||||
if (update.Kind == UpdateKind.Text && update.Text is not null && update.Text.StartsWith("/"))
|
||||
{
|
||||
if (_commands.TryDispatch(ctx, update.Text, ct, out var dispatched) && dispatched is not null)
|
||||
if (_commands.TryDispatch(ctx, update.Text, this.CancellationToken, out var dispatched) && dispatched is not null)
|
||||
{
|
||||
await dispatched;
|
||||
return;
|
||||
@@ -194,7 +204,7 @@ public sealed class BotPagesApp
|
||||
// Конвейер middleware
|
||||
var pipeline = BuildPipeline(ctx, async () =>
|
||||
{
|
||||
await DispatchToPageAsync(ctx, update, ct);
|
||||
await DispatchToPageAsync(ctx, update);
|
||||
});
|
||||
|
||||
await pipeline();
|
||||
@@ -203,10 +213,8 @@ public sealed class BotPagesApp
|
||||
/// <summary>
|
||||
/// Создать контекст страницы для текущего обновления.
|
||||
/// </summary>
|
||||
private async Task<PageContext> CreatePageContextAsync(UpdateContext update, CancellationToken ct)
|
||||
private async Task<PageContext> CreatePageContextAsync(UpdateContext update)
|
||||
{
|
||||
_currentCt = ct;
|
||||
|
||||
var sessionKey = CompositeSessionKey.FromUpdate(update);
|
||||
|
||||
var ctx = new PageContext
|
||||
@@ -228,43 +236,40 @@ public sealed class BotPagesApp
|
||||
foreach (var mw in _middlewares.AsEnumerable().Reverse())
|
||||
{
|
||||
var prev = next;
|
||||
next = () => mw.InvokeAsync(ctx, prev, _currentCt);
|
||||
next = () => mw.InvokeAsync(ctx, prev, this.CancellationToken);
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
// Технические поля для конвейера
|
||||
private CancellationToken _currentCt;
|
||||
|
||||
/// <summary>
|
||||
/// Отправить обновление на текущую страницу.
|
||||
/// </summary>
|
||||
private async Task DispatchToPageAsync(PageContext ctx, UpdateContext update, CancellationToken ct)
|
||||
private async Task DispatchToPageAsync(PageContext ctx, UpdateContext update)
|
||||
{
|
||||
var page = ResolveCurrentPage(ctx);
|
||||
|
||||
if (page is null)
|
||||
{
|
||||
await ctx.Navigation.GoToHomeAsync(ctx, ct);
|
||||
await ctx.Navigation.GoToHomeAsync(ctx, this.CancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
try
|
||||
{
|
||||
await page.OnUpdate(ctx, update, ct);
|
||||
await page.OnUpdate(ctx, update, this.CancellationToken);
|
||||
|
||||
if (update.Kind.HasFlag(UpdateKind.Text) && update.Text is not null) await page.OnText(ctx, update.Text, ct);
|
||||
if (update.Kind.HasFlag(UpdateKind.Button) && update.Text is not null) await page.OnButton(ctx, update.Text, ct);
|
||||
if (update.Kind.HasFlag(UpdateKind.File) && update.Files.Count > 0) await page.OnFile(ctx, update.Files, ct);
|
||||
if (update.Kind.HasFlag(UpdateKind.Pin) && update.PinInfo is not null) await page.OnPin(ctx, update.PinInfo, ct);
|
||||
if (update.Kind.HasFlag(UpdateKind.Delete) && update.DeleteInfo is not null) await page.OnDelete(ctx, update.DeleteInfo, ct);
|
||||
if (update.Kind.HasFlag(UpdateKind.Text) && update.Text is not null) await page.OnText(ctx, update.Text, this.CancellationToken);
|
||||
if (update.Kind.HasFlag(UpdateKind.Button) && update.Text is not null) await page.OnButton(ctx, update.Text, this.CancellationToken);
|
||||
if (update.Kind.HasFlag(UpdateKind.File) && update.Files.Count > 0) await page.OnFile(ctx, update.Files, this.CancellationToken);
|
||||
if (update.Kind.HasFlag(UpdateKind.Pin) && update.PinInfo is not null) await page.OnPin(ctx, update.PinInfo, this.CancellationToken);
|
||||
if (update.Kind.HasFlag(UpdateKind.Delete) && update.DeleteInfo is not null) await page.OnDelete(ctx, update.DeleteInfo, this.CancellationToken);
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Critical, "Unhandled page error.", ex);
|
||||
await page.OnError(ctx, ex, ct);
|
||||
await page.OnError(ctx, ex, this.CancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,16 +279,166 @@ public sealed class BotPagesApp
|
||||
private Page? ResolveCurrentPage(PageContext ctx)
|
||||
=> _navigation.ResolveCurrentPage(ctx);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Запустить приложение и синхронно ожидать его завершения.
|
||||
/// </summary>
|
||||
/// <param name="ct">Токен отмены для остановки приложения извне</param>
|
||||
public void Run(CancellationToken ct = default)
|
||||
{
|
||||
RunAsync(ct).Wait();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Сборка и запуск приложения.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken"></param>
|
||||
/// <returns></returns>
|
||||
public async Task Build(CancellationToken cancellationToken)
|
||||
public async Task RunAsync() => await RunAsync(new CancellationTokenSource().Token);
|
||||
|
||||
/// <summary>
|
||||
/// Сборка и запуск приложения.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public async Task RunAsync(CancellationToken ct = default)
|
||||
{
|
||||
if (_isRunning)
|
||||
throw new InvalidOperationException("Application is already running.");
|
||||
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(nameof(BotPagesApp));
|
||||
|
||||
_internalCts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.Log(LogLevel.Info, "Starting BotPages application...");
|
||||
|
||||
var adapterTasks = new List<Task>();
|
||||
foreach (var adapter in _adapterFactory.AllAdapters)
|
||||
{
|
||||
await adapter.StartAdapterAsync(update => HandleUpdateAsync(update, cancellationToken), _commands.Commands, cancellationToken);
|
||||
var adapterTask = adapter.StartAdapterAsync(HandleUpdateAsync, _commands.Commands, CancellationToken);
|
||||
adapterTasks.Add(adapterTask);
|
||||
}
|
||||
|
||||
await Task.WhenAll(adapterTasks);
|
||||
|
||||
_isRunning = true;
|
||||
_logger.Log(LogLevel.Info, "BotPages application started successfully.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_internalCts?.Dispose();
|
||||
_internalCts = null;
|
||||
_logger.Log(LogLevel.Critical, "Failed to start BotPages application.", ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
_isRunning = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Остановить работу приложения.
|
||||
/// </summary>
|
||||
public void Stop()
|
||||
{
|
||||
if (!_isRunning || _disposed)
|
||||
return;
|
||||
|
||||
_logger.Log(LogLevel.Info, "Stopping BotPages application...");
|
||||
_internalCts?.Cancel();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Синхронно ожидать завершения работы приложения.
|
||||
/// </summary>
|
||||
/// <exception cref="InvalidOperationException">Если приложение не запущено</exception>
|
||||
public void Wait()
|
||||
{
|
||||
if (!_isRunning)
|
||||
throw new InvalidOperationException("Application is not running.");
|
||||
|
||||
try
|
||||
{
|
||||
// Ждем, пока токен не будет отменен
|
||||
WaitHandle.WaitAny([_internalCts.Token.WaitHandle]);
|
||||
}
|
||||
catch (AggregateException ex) when (ex.InnerException is OperationCanceledException)
|
||||
{
|
||||
// Игнорируем отмену операции
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Асинхронно ожидать завершения работы приложения.
|
||||
/// Ожидает, пока не будет вызван <see cref="Stop"/> или внешний <see cref="CancellationTokenSource"/> не будет отменен.
|
||||
/// </summary>
|
||||
public async Task WaitAsync()
|
||||
{
|
||||
if (!_isRunning || _internalCts == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
var tcs = new TaskCompletionSource<bool>();
|
||||
|
||||
using (_internalCts.Token.Register(() => tcs.TrySetResult(true)))
|
||||
{
|
||||
await tcs.Task;
|
||||
}
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
// Токен уже освобожден
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
DisposeAsync().AsTask().Wait();
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_disposed)
|
||||
return;
|
||||
|
||||
_disposed = true;
|
||||
|
||||
// Останавливаем приложение, если оно запущено
|
||||
if (_isRunning)
|
||||
{
|
||||
Stop();
|
||||
}
|
||||
|
||||
_internalCts?.Dispose();
|
||||
|
||||
foreach (var adapter in _adapterFactory.AllAdapters)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (adapter is IAsyncDisposable asyncDisposable)
|
||||
{
|
||||
await asyncDisposable.DisposeAsync();
|
||||
}
|
||||
else if (adapter is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Log(LogLevel.Warn, $"Error disposing adapter {adapter.Id}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -402,6 +402,7 @@ public sealed class TelegramAdapter : IMessengerAdapterSetup
|
||||
{
|
||||
MessageFormat.Html => ParseMode.Html,
|
||||
MessageFormat.Markdown => ParseMode.MarkdownV2,
|
||||
MessageFormat.Plain => ParseMode.None,
|
||||
_ => ParseMode.None,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -198,15 +198,21 @@ public static class TelegramUpdateMapper
|
||||
|
||||
private static Func<CancellationToken, Task<Stream>> GetStreamAsync(TelegramBotClient client, string fileId)
|
||||
{
|
||||
Func<CancellationToken, Task<Stream>> getStreamAsync = async _ =>
|
||||
return async ct =>
|
||||
{
|
||||
var file = await client.GetFile(fileId);
|
||||
try
|
||||
{
|
||||
var file = await client.GetFile(fileId, ct);
|
||||
var stream = new MemoryStream();
|
||||
await client.DownloadFile(file, stream);
|
||||
await client.DownloadFile(file.FilePath!, stream, ct);
|
||||
stream.Position = 0;
|
||||
return stream;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Stream.Null;
|
||||
throw;
|
||||
}
|
||||
};
|
||||
|
||||
return getStreamAsync;
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ namespace Demo
|
||||
|
||||
var logger = new ConsoleLogger();
|
||||
var state = new InMemoryStateStorage();
|
||||
using var cts = new CancellationTokenSource();
|
||||
|
||||
// Можно использовать команды для открытия страниц с роутингом
|
||||
// /open Welcome
|
||||
@@ -48,19 +47,20 @@ namespace Demo
|
||||
.AutoMapRoute()
|
||||
.AddMiddleware(new ErrorHandlingMiddleware(logger))
|
||||
.AddMiddleware(new LoggingMiddleware(logger))
|
||||
.AddTelegramAdapter(token, "Telegram")
|
||||
.Build(cts.Token);
|
||||
.AddTelegramAdapter(token, "Telegram");
|
||||
|
||||
await app.RunAsync();
|
||||
|
||||
logger.Log(LogLevel.Info, "Bot is running. Press Ctrl+C to stop.");
|
||||
|
||||
Console.WriteLine("Bot is running. Press Ctrl+C to stop.");
|
||||
Console.CancelKeyPress += (sender, e) =>
|
||||
{
|
||||
Console.WriteLine("Cancel key pressed");
|
||||
cts.Cancel();
|
||||
app.Stop();
|
||||
e.Cancel = true;
|
||||
};
|
||||
|
||||
app.Wait();
|
||||
await app.WaitAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user