Fixed logs not being written when the bot view is not active
All checks were successful
EllieBotDevs/EllieHub/pipeline/head This commit looks good

This commit is contained in:
Toastie 2025-01-07 18:55:57 +13:00
parent 2a8cd606c5
commit 6fc3eba498
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
6 changed files with 57 additions and 42 deletions

View file

@ -15,35 +15,35 @@ public static partial class AppStatics
/// Defines the default location where the updater configuration and bot instances are stored. /// Defines the default location where the updater configuration and bot instances are stored.
/// </summary> /// </summary>
#if DEBUG #if DEBUG
public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieHubDebug"); public static string AppDefaultConfigDirectoryUri { get; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "EllieHubDebug");
#else #else
public static string AppDefaultConfigDirectoryUri { get; } = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EllieHub"); public static string AppDefaultConfigDirectoryUri { get; } = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "EllieHub");
#endif #endif
/// <summary> /// <summary>
/// Defines the default location where the bot instances are stored. /// Defines the default location where the bot instances are stored.
/// </summary> /// </summary>
public static string AppDefaultBotDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Bots"); public static string AppDefaultBotDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Bots");
/// <summary> /// <summary>
/// Defines the default location where the backups of bot instances are stored. /// Defines the default location where the backups of bot instances are stored.
/// </summary> /// </summary>
public static string AppDefaultBotBackupDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Backups"); public static string AppDefaultBotBackupDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Backups");
/// <summary> /// <summary>
/// Defines the default location where the logs of bot instances are stored. /// Defines the default location where the logs of bot instances are stored.
/// </summary> /// </summary>
public static string AppDefaultLogDirectoryUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Logs"); public static string AppDefaultLogDirectoryUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Logs");
/// <summary> /// <summary>
/// Defines the location of the application's configuration file. /// Defines the location of the application's configuration file.
/// </summary> /// </summary>
public static string AppConfigUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "config.json"); public static string AppConfigUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "config.json");
/// <summary> /// <summary>
/// Defines the location of the application's dependencies. /// Defines the location of the application's dependencies.
/// </summary> /// </summary>
public static string AppDepsUri { get; } = Path.Combine(AppDefaultConfigDirectoryUri, "Dependencies"); public static string AppDepsUri { get; } = Path.Join(AppDefaultConfigDirectoryUri, "Dependencies");
/// <summary> /// <summary>
/// Defines a transparent color brush. /// Defines a transparent color brush.
@ -56,11 +56,11 @@ public static partial class AppStatics
public static FilePickerOpenOptions ImageFilePickerOptions { get; } = new() public static FilePickerOpenOptions ImageFilePickerOptions { get; } = new()
{ {
AllowMultiple = false, AllowMultiple = false,
FileTypeFilter = new FilePickerFileType[] FileTypeFilter =
{ [
new("Image") { Patterns = new[] { "*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp" } }, new("Image") { Patterns = ["*.png", "*.jpg", "*.jpeg", "*.gif", "*.webp"]},
new("All") { Patterns = new[] { "*.*" } } new("All") { Patterns = ["*.*"]}
} ]
}; };
/// <summary> /// <summary>

View file

@ -11,29 +11,29 @@ namespace EllieHub.Common;
internal static class Utilities internal static class Utilities
{ {
/// <summary> /// <summary>
/// Loads an image embeded with this application. /// Loads an image embedded with this application.
/// </summary> /// </summary>
/// <param name="uri">An uri that starts with "avares://"</param> /// <param name="uri">An uri that starts with "avares://"</param>
/// <remarks>Valid uris must start with "avares://".</remarks> /// <remarks>Valid uris must start with "avares://".</remarks>
/// <returns>The embeded image or the default bot avatar placeholder.</returns> /// <returns>The embedded image or the default bot avatar placeholder.</returns>
/// <exception cref="FileNotFoundException">Occurs when the embeded resource does not exist.</exception> /// <exception cref="FileNotFoundException">Occurs when the embedded resource does not exist.</exception>
public static SKBitmap LoadEmbededImage(string? uri = default) public static SKBitmap LoadEmbeddedImage(string? uri = default)
{ {
return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://", StringComparison.Ordinal)) return (string.IsNullOrWhiteSpace(uri) || !uri.StartsWith("avares://", StringComparison.Ordinal))
? SKBitmap.Decode(AssetLoader.Open(new Uri(AppConstants.BotAvatarUri))) ? SKBitmap.Decode(AssetLoader.Open(new(AppConstants.BotAvatarUri)))
: SKBitmap.Decode(AssetLoader.Open(new Uri(uri))); : SKBitmap.Decode(AssetLoader.Open(new(uri)));
} }
/// <summary> /// <summary>
/// Loads the image at the specified location or the bot avatar placeholder if it was not found. /// Loads the image at the specified location or the bot avatar placeholder if it was not found.
/// </summary> /// </summary>
/// <param name="uri">The absolute path to the image file or <see langword="null"/> to get the avatar placeholder.</param> /// <param name="imagePath">The absolute path to the image file or <see langword="null"/> to get the avatar placeholder.</param>
/// <remarks>This fallsback to <see cref="LoadEmbededImage(string?)"/> if <paramref name="uri"/> doesn't point to a valid image file.</remarks> /// <remarks>This fallsback to <see cref="LoadEmbeddedImage(string?)"/> if <paramref name="imagePath"/> doesn't point to a valid image file.</remarks>
/// <returns>The requested image or the default bot avatar placeholder.</returns> /// <returns>The requested image or the default bot avatar placeholder.</returns>
public static SKBitmap LoadLocalImage(string? uri = default) public static SKBitmap LoadLocalImage(string? imagePath)
{ {
return (File.Exists(uri)) return (File.Exists(imagePath))
? SKBitmap.Decode(uri) ? SKBitmap.Decode(imagePath)
: LoadEmbededImage(uri); : LoadEmbeddedImage(imagePath);
} }
} }

View file

@ -10,7 +10,7 @@ namespace EllieHub.Features.AppConfig.Services.Mocks;
internal sealed class MockAppConfigManager : IAppConfigManager internal sealed class MockAppConfigManager : IAppConfigManager
{ {
/// <inheritdoc/> /// <inheritdoc/>
public ReadOnlyAppSettings AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Combine(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } }); public ReadOnlyAppSettings AppConfig { get; } = new(new() { BotEntries = new() { [Guid.Empty] = new("MockBot", Path.Join(AppStatics.AppDefaultBotDirectoryUri, "MockBot"), 0) } });
/// <inheritdoc/> /// <inheritdoc/>
public ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default) public ValueTask<BotEntry> CreateBotEntryAsync(CancellationToken cToken = default)

View file

@ -15,14 +15,21 @@ public sealed class BotExitEventArgs : EventArgs
/// </summary> /// </summary>
public int ExitCode { get; } public int ExitCode { get; }
/// <summary>
/// The exit message.
/// </summary>
public string Message { get; }
/// <summary> /// <summary>
/// Creates the event arguments when a bot process exits. /// Creates the event arguments when a bot process exits.
/// </summary> /// </summary>
/// <param name="botId">The bot's Id.</param> /// <param name="botId">The bot's Id.</param>
/// <param name="exitCode">The exit code.</param> /// <param name="exitCode">The exit code.</param>
public BotExitEventArgs(Guid botId, int exitCode) /// <param name="message">The message for the bot process that just exited.</param>
public BotExitEventArgs(Guid botId, int exitCode, string message)
{ {
Id = botId; Id = botId;
ExitCode = exitCode; ExitCode = exitCode;
Message = message;
} }
} }

View file

@ -10,8 +10,9 @@ namespace EllieHub.Features.BotConfig.Services;
/// </summary> /// </summary>
public sealed class EllieOrchestrator : IBotOrchestrator public sealed class EllieOrchestrator : IBotOrchestrator
{ {
private readonly Dictionary<Guid, Process> _runningBots = new();
private readonly ReadOnlyAppSettings _appConfig; private readonly ReadOnlyAppSettings _appConfig;
private readonly ILogWriter _logWriter;
private readonly Dictionary<Guid, Process> _runningBots = new();
private readonly string _fileName = OperatingSystem.IsWindows() ? "EllieBot.exe" : "EllieBot"; private readonly string _fileName = OperatingSystem.IsWindows() ? "EllieBot.exe" : "EllieBot";
/// <inheritdoc/> /// <inheritdoc/>
@ -27,8 +28,12 @@ public sealed class EllieOrchestrator : IBotOrchestrator
/// Creates an object that coordinates multiple running processes of EllieBot. /// Creates an object that coordinates multiple running processes of EllieBot.
/// </summary> /// </summary>
/// <param name="appConfig">The application settings.</param> /// <param name="appConfig">The application settings.</param>
public EllieOrchestrator(ReadOnlyAppSettings appConfig) /// <param name="logWriter">The service that writes bot logs to disk.</param>
=> _appConfig = appConfig; public EllieOrchestrator(ReadOnlyAppSettings appConfig, ILogWriter logWriter)
{
_appConfig = appConfig;
_logWriter = logWriter;
}
/// <inheritdoc/> /// <inheritdoc/>
public bool IsBotRunning(Guid botId) public bool IsBotRunning(Guid botId)
@ -39,12 +44,12 @@ public sealed class EllieOrchestrator : IBotOrchestrator
{ {
if (_runningBots.ContainsKey(botId) if (_runningBots.ContainsKey(botId)
|| !_appConfig.BotEntries.TryGetValue(botId, out var botEntry) || !_appConfig.BotEntries.TryGetValue(botId, out var botEntry)
|| !File.Exists(Path.Combine(botEntry.InstanceDirectoryUri, _fileName))) || !File.Exists(Path.Join(botEntry.InstanceDirectoryUri, _fileName)))
return false; return false;
var botProcess = Process.Start(new ProcessStartInfo() var botProcess = Process.Start(new ProcessStartInfo()
{ {
FileName = Path.Combine(botEntry.InstanceDirectoryUri, _fileName), FileName = Path.Join(botEntry.InstanceDirectoryUri, _fileName),
WorkingDirectory = botEntry.InstanceDirectoryUri, WorkingDirectory = botEntry.InstanceDirectoryUri,
UseShellExecute = false, UseShellExecute = false,
CreateNoWindow = true, CreateNoWindow = true,
@ -80,6 +85,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
{ {
var amount = _runningBots.Count; var amount = _runningBots.Count;
// ReSharper disable once EmptyGeneralCatchClause
foreach (var process in _runningBots.Values) foreach (var process in _runningBots.Values)
try { process.Kill(true); } catch { } try { process.Kill(true); } catch { }
@ -95,7 +101,12 @@ public sealed class EllieOrchestrator : IBotOrchestrator
private void OnExit(object? sender, EventArgs eventArgs) private void OnExit(object? sender, EventArgs eventArgs)
{ {
var (id, process) = _runningBots.First(x => x.Value.Equals(sender)); var (id, process) = _runningBots.First(x => x.Value.Equals(sender));
OnBotExit?.Invoke(this, new(id, process.ExitCode)); var message = Environment.NewLine
+ $"{_appConfig.BotEntries[id].Name} stopped. Status code: {process.ExitCode}"
+ Environment.NewLine;
_logWriter.TryAdd(id, message);
OnBotExit?.Invoke(this, new(id, process.ExitCode, message));
_runningBots.Remove(id); _runningBots.Remove(id);
process.CancelOutputRead(); process.CancelOutputRead();
@ -116,6 +127,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
var (id, _) = _runningBots.First(x => x.Value.Equals(sender)); var (id, _) = _runningBots.First(x => x.Value.Equals(sender));
var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data); var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data);
_logWriter.TryAdd(id, eventArgs.Data);
OnStdout?.Invoke(this, newEventArgs); OnStdout?.Invoke(this, newEventArgs);
} }
@ -132,6 +144,7 @@ public sealed class EllieOrchestrator : IBotOrchestrator
var (id, _) = _runningBots.First(x => x.Value.Equals(sender)); var (id, _) = _runningBots.First(x => x.Value.Equals(sender));
var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data); var newEventArgs = new ProcessStdWriteEventArgs(id, eventArgs.Data);
_logWriter.TryAdd(id, eventArgs.Data);
OnStderr?.Invoke(this, newEventArgs); OnStderr?.Invoke(this, newEventArgs);
} }
} }

View file

@ -103,7 +103,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
{ {
var sanitizedValue = value.ReplaceLineEndings(string.Empty); var sanitizedValue = value.ReplaceLineEndings(string.Empty);
DirectoryHint = $"Select the absolute path to the bot directory. For example: {Path.Combine(_appConfigManager.AppConfig.BotsDirectoryUri, sanitizedValue)}"; DirectoryHint = $"Select the absolute path to the bot directory. For example: {Path.Join(_appConfigManager.AppConfig.BotsDirectoryUri, sanitizedValue)}";
this.RaiseAndSetIfChanged(ref _botName, sanitizedValue); this.RaiseAndSetIfChanged(ref _botName, sanitizedValue);
} }
} }
@ -180,7 +180,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
if (IsBotRunning) if (IsBotRunning)
EnableButtons(true, false); EnableButtons(true, false);
else else
EnableButtons(!File.Exists(Path.Combine(botEntry.InstanceDirectoryUri, Resolver.FileName)), true); EnableButtons(!File.Exists(Path.Join(botEntry.InstanceDirectoryUri, Resolver.FileName)), true);
// Dispose when the view is deactivated // Dispose when the view is deactivated
this.WhenActivated(disposables => Disposable.Create(Dispose).DisposeWith(disposables)); this.WhenActivated(disposables => Disposable.Create(Dispose).DisposeWith(disposables));
@ -245,7 +245,7 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
{ {
_ = LoadUpdateBarAsync(Resolver, UpdateBar); _ = LoadUpdateBarAsync(Resolver, UpdateBar);
if (!wereButtonsUnlocked && File.Exists(Path.Combine(BotDirectoryUriBar.CurrentUri, Resolver.FileName))) if (!wereButtonsUnlocked && File.Exists(Path.Join(BotDirectoryUriBar.CurrentUri, Resolver.FileName)))
EnableButtons(false, true); EnableButtons(false, true);
else else
EnableButtons(!wereButtonsUnlocked, true); EnableButtons(!wereButtonsUnlocked, true);
@ -466,8 +466,6 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
if (eventArgs.Id != Resolver.Id) if (eventArgs.Id != Resolver.Id)
return; return;
_logWriter.TryAdd(eventArgs.Id, eventArgs.Output);
FakeConsole.Content = FakeConsole.Content.Length > 100_000 FakeConsole.Content = FakeConsole.Content.Length > 100_000
? FakeConsole.Content[FakeConsole.Content.IndexOf(Environment.NewLine, 60_000, StringComparison.Ordinal)..] + eventArgs.Output + Environment.NewLine ? FakeConsole.Content[FakeConsole.Content.IndexOf(Environment.NewLine, 60_000, StringComparison.Ordinal)..] + eventArgs.Output + Environment.NewLine
: FakeConsole.Content + eventArgs.Output + Environment.NewLine; : FakeConsole.Content + eventArgs.Output + Environment.NewLine;
@ -483,14 +481,11 @@ public class BotConfigViewModel : ViewModelBase<BotConfigView>, IDisposable
if (eventArgs.Id != Resolver.Id) if (eventArgs.Id != Resolver.Id)
return; return;
var message = Environment.NewLine + ActualBotName + " stopped." + Environment.NewLine; FakeConsole.Content += eventArgs.Message;
_logWriter.TryAdd(Resolver.Id, message);
FakeConsole.Content += message;
} }
/// <summary> /// <summary>
/// Reenables the buttons when the bot instance associated with this view-model exits. /// Re-enables the buttons when the bot instance associated with this view-model exits.
/// </summary> /// </summary>
/// <param name="botOrchestrator">The bot orchestrator.</param> /// <param name="botOrchestrator">The bot orchestrator.</param>
/// <param name="eventArgs">The event arguments.</param> /// <param name="eventArgs">The event arguments.</param>