mirror of
https://github.com/MindWorkAI/AI-Studio.git
synced 2025-04-28 21:59:48 +00:00
Refactored logging into a unified logging
This commit is contained in:
parent
200b87f47a
commit
82f7329a39
@ -7,7 +7,7 @@ using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Agents;
|
||||
|
||||
public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
|
||||
public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : IAgent
|
||||
{
|
||||
protected SettingsManager SettingsManager { get; init; } = settingsManager;
|
||||
|
||||
@ -15,6 +15,8 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
|
||||
|
||||
protected ThreadSafeRandom RNG { get; init; } = rng;
|
||||
|
||||
protected ILogger<AgentBase> Logger { get; init; } = logger;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the type or category of this agent.
|
||||
/// </summary>
|
||||
@ -104,6 +106,6 @@ public abstract class AgentBase(SettingsManager settingsManager, IJSRuntime jsRu
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
|
||||
await aiText.CreateFromProviderAsync(providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, providerSettings.Model, thread);
|
||||
}
|
||||
}
|
@ -4,7 +4,7 @@ using AIStudio.Tools;
|
||||
|
||||
namespace AIStudio.Agents;
|
||||
|
||||
public sealed class AgentTextContentCleaner(SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(settingsManager, jsRuntime, rng)
|
||||
public sealed class AgentTextContentCleaner(ILogger<AgentBase> logger, SettingsManager settingsManager, IJSRuntime jsRuntime, ThreadSafeRandom rng) : AgentBase(logger, settingsManager, jsRuntime, rng)
|
||||
{
|
||||
private static readonly ContentBlock EMPTY_BLOCK = new()
|
||||
{
|
||||
|
@ -10,7 +10,7 @@ namespace AIStudio.Assistants;
|
||||
public abstract partial class AssistantBase : ComponentBase
|
||||
{
|
||||
[Inject]
|
||||
protected SettingsManager SettingsManager { get; set; } = null!;
|
||||
protected SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected IJSRuntime JsRuntime { get; init; } = null!;
|
||||
@ -27,6 +27,9 @@ public abstract partial class AssistantBase : ComponentBase
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected ILogger<AssistantBase> Logger { get; init; } = null!;
|
||||
|
||||
internal const string AFTER_RESULT_DIV_ID = "afterAssistantResult";
|
||||
internal const string RESULT_DIV_ID = "assistantResult";
|
||||
|
||||
@ -151,7 +154,7 @@ public abstract partial class AssistantBase : ComponentBase
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
||||
|
||||
this.isProcessing = false;
|
||||
this.StateHasChanged();
|
||||
|
@ -24,6 +24,9 @@ public partial class Workspaces : ComponentBase
|
||||
[Inject]
|
||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<Workspaces> Logger { get; init; } = null!;
|
||||
|
||||
[Parameter]
|
||||
public ChatThread? CurrentChatThread { get; set; }
|
||||
|
||||
@ -309,7 +312,7 @@ public partial class Workspaces : ComponentBase
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
this.Logger.LogError($"Failed to load chat from '{chatPath}': {e.Message}");
|
||||
}
|
||||
|
||||
return null;
|
||||
|
@ -19,7 +19,7 @@ namespace AIStudio.Pages;
|
||||
public partial class Chat : MSGComponentBase, IAsyncDisposable
|
||||
{
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; set; } = null!;
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime JsRuntime { get; init; } = null!;
|
||||
@ -28,7 +28,10 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
|
||||
private ThreadSafeRandom RNG { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public IDialogService DialogService { get; set; } = null!;
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<Chat> Logger { get; init; } = null!;
|
||||
|
||||
private InnerScrolling scrollingArea = null!;
|
||||
|
||||
@ -189,7 +192,7 @@ public partial class Chat : MSGComponentBase, IAsyncDisposable
|
||||
// Use the selected provider to get the AI response.
|
||||
// By awaiting this line, we wait for the entire
|
||||
// content to be streamed.
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
||||
await aiText.CreateFromProviderAsync(this.providerSettings.CreateProvider(this.Logger), this.JsRuntime, this.SettingsManager, this.providerSettings.Model, this.chatThread);
|
||||
|
||||
// Save the chat:
|
||||
if (this.SettingsManager.ConfigurationData.Workspace.StorageBehavior is WorkspaceStorageBehavior.STORE_CHATS_AUTOMATICALLY)
|
||||
|
@ -14,16 +14,19 @@ namespace AIStudio.Pages;
|
||||
public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
{
|
||||
[Inject]
|
||||
public SettingsManager SettingsManager { get; init; } = null!;
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public IDialogService DialogService { get; init; } = null!;
|
||||
private IDialogService DialogService { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime JsRuntime { get; init; } = null!;
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
protected MessageBus MessageBus { get; init; } = null!;
|
||||
private MessageBus MessageBus { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<Settings> Logger { get; init; } = null!;
|
||||
|
||||
private readonly List<ConfigurationSelectData<string>> availableProviders = new();
|
||||
|
||||
@ -111,7 +114,7 @@ public partial class Settings : ComponentBase, IMessageBusReceiver, IDisposable
|
||||
if (dialogResult is null || dialogResult.Canceled)
|
||||
return;
|
||||
|
||||
var providerInstance = provider.CreateProvider();
|
||||
var providerInstance = provider.CreateProvider(this.Logger);
|
||||
var deleteSecretResponse = await this.SettingsManager.DeleteAPIKey(this.JsRuntime, providerInstance);
|
||||
if(deleteSecretResponse.Success)
|
||||
{
|
||||
|
@ -4,6 +4,8 @@ using AIStudio.Settings;
|
||||
using AIStudio.Tools;
|
||||
using AIStudio.Tools.Services;
|
||||
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
|
||||
using MudBlazor.Services;
|
||||
|
||||
#if !DEBUG
|
||||
@ -35,6 +37,18 @@ if(string.IsNullOrWhiteSpace(secretKey))
|
||||
}
|
||||
|
||||
var builder = WebApplication.CreateBuilder();
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Logging.SetMinimumLevel(LogLevel.Debug);
|
||||
builder.Logging.AddFilter("Microsoft", LogLevel.Information);
|
||||
builder.Logging.AddFilter("Microsoft.AspNetCore.Hosting.Diagnostics", LogLevel.Warning);
|
||||
builder.Logging.AddFilter("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogLevel.Warning);
|
||||
builder.Logging.AddFilter("Microsoft.AspNetCore.StaticFiles", LogLevel.Warning);
|
||||
builder.Logging.AddFilter("MudBlazor", LogLevel.Information);
|
||||
builder.Logging.AddConsole(options =>
|
||||
{
|
||||
options.FormatterName = TerminalLogger.FORMATTER_NAME;
|
||||
}).AddConsoleFormatter<TerminalLogger, ConsoleFormatterOptions>();
|
||||
|
||||
builder.Services.AddMudServices(config =>
|
||||
{
|
||||
config.SnackbarConfiguration.PositionClass = Defaults.Classes.Position.BottomLeft;
|
||||
@ -101,5 +115,8 @@ app.MapRazorComponents<App>()
|
||||
|
||||
var serverTask = app.RunAsync();
|
||||
|
||||
var rustLogger = app.Services.GetRequiredService<ILogger<Rust>>();
|
||||
rust.SetLogger(rustLogger);
|
||||
|
||||
await rust.AppIsReady();
|
||||
await serverTask;
|
@ -8,7 +8,7 @@ using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Anthropic;
|
||||
|
||||
public sealed class ProviderAnthropic() : BaseProvider("https://api.anthropic.com/v1/"), IProvider
|
||||
public sealed class ProviderAnthropic(ILogger logger) : BaseProvider("https://api.anthropic.com/v1/", logger), IProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
|
@ -10,12 +10,20 @@ public abstract class BaseProvider
|
||||
/// </summary>
|
||||
protected readonly HttpClient httpClient = new();
|
||||
|
||||
/// <summary>
|
||||
/// The logger to use.
|
||||
/// </summary>
|
||||
protected readonly ILogger logger;
|
||||
|
||||
/// <summary>
|
||||
/// Constructor for the base provider.
|
||||
/// </summary>
|
||||
/// <param name="url">The base URL for the provider.</param>
|
||||
protected BaseProvider(string url)
|
||||
/// <param name="loggerService">The logger service to use.</param>
|
||||
protected BaseProvider(string url, ILogger loggerService)
|
||||
{
|
||||
this.logger = loggerService;
|
||||
|
||||
// Set the base URL:
|
||||
this.httpClient.BaseAddress = new(url);
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Fireworks;
|
||||
|
||||
public class ProviderFireworks() : BaseProvider("https://api.fireworks.ai/inference/v1/"), IProvider
|
||||
public class ProviderFireworks(ILogger logger) : BaseProvider("https://api.fireworks.ai/inference/v1/", logger), IProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
|
@ -9,7 +9,7 @@ using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.Mistral;
|
||||
|
||||
public sealed class ProviderMistral() : BaseProvider("https://api.mistral.ai/v1/"), IProvider
|
||||
public sealed class ProviderMistral(ILogger logger) : BaseProvider("https://api.mistral.ai/v1/", logger), IProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
|
@ -11,7 +11,7 @@ namespace AIStudio.Provider.OpenAI;
|
||||
/// <summary>
|
||||
/// The OpenAI provider.
|
||||
/// </summary>
|
||||
public sealed class ProviderOpenAI() : BaseProvider("https://api.openai.com/v1/"), IProvider
|
||||
public sealed class ProviderOpenAI(ILogger logger) : BaseProvider("https://api.openai.com/v1/", logger), IProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
|
@ -1,9 +1,3 @@
|
||||
using AIStudio.Provider.Anthropic;
|
||||
using AIStudio.Provider.Fireworks;
|
||||
using AIStudio.Provider.Mistral;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
/// <summary>
|
||||
@ -21,58 +15,3 @@ public enum Providers
|
||||
|
||||
SELF_HOSTED = 4,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for the provider enum.
|
||||
/// </summary>
|
||||
public static class ExtensionsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the human-readable name of the provider.
|
||||
/// </summary>
|
||||
/// <param name="provider">The provider.</param>
|
||||
/// <returns>The human-readable name of the provider.</returns>
|
||||
public static string ToName(this Providers provider) => provider switch
|
||||
{
|
||||
Providers.NONE => "No provider selected",
|
||||
|
||||
Providers.OPEN_AI => "OpenAI",
|
||||
Providers.ANTHROPIC => "Anthropic",
|
||||
Providers.MISTRAL => "Mistral",
|
||||
|
||||
Providers.FIREWORKS => "Fireworks.ai",
|
||||
|
||||
Providers.SELF_HOSTED => "Self-hosted",
|
||||
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new provider instance based on the provider value.
|
||||
/// </summary>
|
||||
/// <param name="providerSettings">The provider settings.</param>
|
||||
/// <returns>The provider instance.</returns>
|
||||
public static IProvider CreateProvider(this Settings.Provider providerSettings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return providerSettings.UsedProvider switch
|
||||
{
|
||||
Providers.OPEN_AI => new ProviderOpenAI { InstanceName = providerSettings.InstanceName },
|
||||
Providers.ANTHROPIC => new ProviderAnthropic { InstanceName = providerSettings.InstanceName },
|
||||
Providers.MISTRAL => new ProviderMistral { InstanceName = providerSettings.InstanceName },
|
||||
|
||||
Providers.FIREWORKS => new ProviderFireworks { InstanceName = providerSettings.InstanceName },
|
||||
|
||||
Providers.SELF_HOSTED => new ProviderSelfHosted(providerSettings) { InstanceName = providerSettings.InstanceName },
|
||||
|
||||
_ => new NoProvider(),
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to create provider: {e.Message}");
|
||||
return new NoProvider();
|
||||
}
|
||||
}
|
||||
}
|
60
app/MindWork AI Studio/Provider/ProvidersExtensions.cs
Normal file
60
app/MindWork AI Studio/Provider/ProvidersExtensions.cs
Normal file
@ -0,0 +1,60 @@
|
||||
using AIStudio.Provider.Anthropic;
|
||||
using AIStudio.Provider.Fireworks;
|
||||
using AIStudio.Provider.Mistral;
|
||||
using AIStudio.Provider.OpenAI;
|
||||
using AIStudio.Provider.SelfHosted;
|
||||
|
||||
namespace AIStudio.Provider;
|
||||
|
||||
public static class ProvidersExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Returns the human-readable name of the provider.
|
||||
/// </summary>
|
||||
/// <param name="provider">The provider.</param>
|
||||
/// <returns>The human-readable name of the provider.</returns>
|
||||
public static string ToName(this Providers provider) => provider switch
|
||||
{
|
||||
Providers.NONE => "No provider selected",
|
||||
|
||||
Providers.OPEN_AI => "OpenAI",
|
||||
Providers.ANTHROPIC => "Anthropic",
|
||||
Providers.MISTRAL => "Mistral",
|
||||
|
||||
Providers.FIREWORKS => "Fireworks.ai",
|
||||
|
||||
Providers.SELF_HOSTED => "Self-hosted",
|
||||
|
||||
_ => "Unknown",
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new provider instance based on the provider value.
|
||||
/// </summary>
|
||||
/// <param name="providerSettings">The provider settings.</param>
|
||||
/// <param name="logger">The logger to use.</param>
|
||||
/// <returns>The provider instance.</returns>
|
||||
public static IProvider CreateProvider(this Settings.Provider providerSettings, ILogger logger)
|
||||
{
|
||||
try
|
||||
{
|
||||
return providerSettings.UsedProvider switch
|
||||
{
|
||||
Providers.OPEN_AI => new ProviderOpenAI(logger) { InstanceName = providerSettings.InstanceName },
|
||||
Providers.ANTHROPIC => new ProviderAnthropic(logger) { InstanceName = providerSettings.InstanceName },
|
||||
Providers.MISTRAL => new ProviderMistral(logger) { InstanceName = providerSettings.InstanceName },
|
||||
|
||||
Providers.FIREWORKS => new ProviderFireworks(logger) { InstanceName = providerSettings.InstanceName },
|
||||
|
||||
Providers.SELF_HOSTED => new ProviderSelfHosted(logger, providerSettings) { InstanceName = providerSettings.InstanceName },
|
||||
|
||||
_ => new NoProvider(),
|
||||
};
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
logger.LogError($"Failed to create provider: {e.Message}");
|
||||
return new NoProvider();
|
||||
}
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ using AIStudio.Settings;
|
||||
|
||||
namespace AIStudio.Provider.SelfHosted;
|
||||
|
||||
public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}"), IProvider
|
||||
public sealed class ProviderSelfHosted(ILogger logger, Settings.Provider provider) : BaseProvider($"{provider.Hostname}{provider.Host.BaseURL()}", logger), IProvider
|
||||
{
|
||||
private static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
|
||||
{
|
||||
@ -162,7 +162,7 @@ public sealed class ProviderSelfHosted(Settings.Provider provider) : BaseProvide
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
Console.WriteLine($"Failed to load text models from self-hosted provider: {e.Message}");
|
||||
this.logger.LogError($"Failed to load text models from self-hosted provider: {e.Message}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -71,10 +71,13 @@ public partial class ProviderDialog : ComponentBase
|
||||
public bool IsEditing { get; init; }
|
||||
|
||||
[Inject]
|
||||
private SettingsManager SettingsManager { get; set; } = null!;
|
||||
private SettingsManager SettingsManager { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private IJSRuntime JsRuntime { get; set; } = null!;
|
||||
private IJSRuntime JsRuntime { get; init; } = null!;
|
||||
|
||||
[Inject]
|
||||
private ILogger<ProviderDialog> Logger { get; init; } = null!;
|
||||
|
||||
private static readonly Dictionary<string, object?> SPELLCHECK_ATTRIBUTES = new();
|
||||
|
||||
@ -133,7 +136,7 @@ public partial class ProviderDialog : ComponentBase
|
||||
}
|
||||
|
||||
var loadedProviderSettings = this.CreateProviderSettings();
|
||||
var provider = loadedProviderSettings.CreateProvider();
|
||||
var provider = loadedProviderSettings.CreateProvider(this.Logger);
|
||||
if(provider is NoProvider)
|
||||
{
|
||||
await base.OnInitializedAsync();
|
||||
@ -187,7 +190,7 @@ public partial class ProviderDialog : ComponentBase
|
||||
if (addedProviderSettings.UsedProvider != Providers.SELF_HOSTED)
|
||||
{
|
||||
// We need to instantiate the provider to store the API key:
|
||||
var provider = addedProviderSettings.CreateProvider();
|
||||
var provider = addedProviderSettings.CreateProvider(this.Logger);
|
||||
|
||||
// Store the API key in the OS secure storage:
|
||||
var storeResponse = await this.SettingsManager.SetAPIKey(this.JsRuntime, provider, this.dataAPIKey);
|
||||
@ -318,7 +321,7 @@ public partial class ProviderDialog : ComponentBase
|
||||
private async Task ReloadModels()
|
||||
{
|
||||
var currentProviderSettings = this.CreateProviderSettings();
|
||||
var provider = currentProviderSettings.CreateProvider();
|
||||
var provider = currentProviderSettings.CreateProvider(this.Logger);
|
||||
if(provider is NoProvider)
|
||||
return;
|
||||
|
||||
|
@ -11,7 +11,7 @@ namespace AIStudio.Settings;
|
||||
/// <summary>
|
||||
/// The settings manager.
|
||||
/// </summary>
|
||||
public sealed class SettingsManager
|
||||
public sealed class SettingsManager(ILogger<SettingsManager> logger)
|
||||
{
|
||||
private const string SETTINGS_FILENAME = "settings.json";
|
||||
|
||||
@ -21,6 +21,8 @@ public sealed class SettingsManager
|
||||
Converters = { new JsonStringEnumConverter() },
|
||||
};
|
||||
|
||||
private ILogger<SettingsManager> logger = logger;
|
||||
|
||||
/// <summary>
|
||||
/// The directory where the configuration files are stored.
|
||||
/// </summary>
|
||||
@ -102,11 +104,17 @@ public sealed class SettingsManager
|
||||
public async Task LoadSettings()
|
||||
{
|
||||
if(!this.IsSetUp)
|
||||
{
|
||||
this.logger.LogWarning("Cannot load settings, because the configuration is not set up yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
||||
if(!File.Exists(settingsPath))
|
||||
{
|
||||
this.logger.LogWarning("Cannot load settings, because the settings file does not exist.");
|
||||
return;
|
||||
}
|
||||
|
||||
// We read the `"Version": "V3"` line to determine the version of the settings file:
|
||||
await foreach (var line in File.ReadLinesAsync(settingsPath))
|
||||
@ -123,16 +131,16 @@ public sealed class SettingsManager
|
||||
Enum.TryParse(settingsVersionText, out Version settingsVersion);
|
||||
if(settingsVersion is Version.UNKNOWN)
|
||||
{
|
||||
Console.WriteLine("Error: Unknown version of the settings file.");
|
||||
this.logger.LogError("Unknown version of the settings file found.");
|
||||
this.ConfigurationData = new();
|
||||
return;
|
||||
}
|
||||
|
||||
this.ConfigurationData = SettingsMigrations.Migrate(settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
|
||||
this.ConfigurationData = SettingsMigrations.Migrate(this.logger, settingsVersion, await File.ReadAllTextAsync(settingsPath), JSON_OPTIONS);
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine("Error: Failed to read the version of the settings file.");
|
||||
this.logger.LogError("Failed to read the version of the settings file.");
|
||||
this.ConfigurationData = new();
|
||||
}
|
||||
|
||||
@ -142,14 +150,22 @@ public sealed class SettingsManager
|
||||
public async Task StoreSettings()
|
||||
{
|
||||
if(!this.IsSetUp)
|
||||
{
|
||||
this.logger.LogWarning("Cannot store settings, because the configuration is not set up yet.");
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsPath = Path.Combine(ConfigDirectory!, SETTINGS_FILENAME);
|
||||
if(!Directory.Exists(ConfigDirectory))
|
||||
{
|
||||
this.logger.LogInformation("Creating the configuration directory.");
|
||||
Directory.CreateDirectory(ConfigDirectory!);
|
||||
}
|
||||
|
||||
var settingsJson = JsonSerializer.Serialize(this.ConfigurationData, JSON_OPTIONS);
|
||||
await File.WriteAllTextAsync(settingsPath, settingsJson);
|
||||
|
||||
this.logger.LogInformation("Stored the settings to the file system.");
|
||||
}
|
||||
|
||||
public void InjectSpellchecking(Dictionary<string, object?> attributes) => attributes["spellcheck"] = this.ConfigurationData.App.EnableSpellchecking ? "true" : "false";
|
||||
|
@ -9,7 +9,7 @@ namespace AIStudio.Settings;
|
||||
|
||||
public static class SettingsMigrations
|
||||
{
|
||||
public static Data Migrate(Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
|
||||
public static Data Migrate(ILogger<SettingsManager> logger, Version previousVersion, string configData, JsonSerializerOptions jsonOptions)
|
||||
{
|
||||
switch (previousVersion)
|
||||
{
|
||||
@ -17,41 +17,41 @@ public static class SettingsMigrations
|
||||
var configV1 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
||||
if (configV1 is null)
|
||||
{
|
||||
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
|
||||
logger.LogError("Failed to parse the v1 configuration. Using default values.");
|
||||
return new();
|
||||
}
|
||||
|
||||
configV1 = MigrateV1ToV2(configV1);
|
||||
configV1 = MigrateV2ToV3(configV1);
|
||||
return MigrateV3ToV4(configV1);
|
||||
configV1 = MigrateV1ToV2(logger, configV1);
|
||||
configV1 = MigrateV2ToV3(logger, configV1);
|
||||
return MigrateV3ToV4(logger, configV1);
|
||||
|
||||
case Version.V2:
|
||||
var configV2 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
||||
if (configV2 is null)
|
||||
{
|
||||
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
|
||||
logger.LogError("Failed to parse the v2 configuration. Using default values.");
|
||||
return new();
|
||||
}
|
||||
|
||||
configV2 = MigrateV2ToV3(configV2);
|
||||
return MigrateV3ToV4(configV2);
|
||||
configV2 = MigrateV2ToV3(logger, configV2);
|
||||
return MigrateV3ToV4(logger, configV2);
|
||||
|
||||
case Version.V3:
|
||||
var configV3 = JsonSerializer.Deserialize<DataV1V3>(configData, jsonOptions);
|
||||
if (configV3 is null)
|
||||
{
|
||||
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
|
||||
logger.LogError("Failed to parse the v3 configuration. Using default values.");
|
||||
return new();
|
||||
}
|
||||
|
||||
return MigrateV3ToV4(configV3);
|
||||
return MigrateV3ToV4(logger, configV3);
|
||||
|
||||
default:
|
||||
Console.WriteLine("No configuration migration needed.");
|
||||
logger.LogInformation("No configuration migration is needed.");
|
||||
var configV4 = JsonSerializer.Deserialize<Data>(configData, jsonOptions);
|
||||
if (configV4 is null)
|
||||
{
|
||||
Console.WriteLine("Error: failed to parse the configuration. Using default values.");
|
||||
logger.LogError("Failed to parse the v4 configuration. Using default values.");
|
||||
return new();
|
||||
}
|
||||
|
||||
@ -59,14 +59,14 @@ public static class SettingsMigrations
|
||||
}
|
||||
}
|
||||
|
||||
private static DataV1V3 MigrateV1ToV2(DataV1V3 previousData)
|
||||
private static DataV1V3 MigrateV1ToV2(ILogger<SettingsManager> logger, DataV1V3 previousData)
|
||||
{
|
||||
//
|
||||
// Summary:
|
||||
// In v1 we had no self-hosted providers. Thus, we had no hostnames.
|
||||
//
|
||||
|
||||
Console.WriteLine("Migrating from v1 to v2...");
|
||||
logger.LogInformation("Migrating from v1 to v2...");
|
||||
return new()
|
||||
{
|
||||
Version = Version.V2,
|
||||
@ -81,14 +81,14 @@ public static class SettingsMigrations
|
||||
};
|
||||
}
|
||||
|
||||
private static DataV1V3 MigrateV2ToV3(DataV1V3 previousData)
|
||||
private static DataV1V3 MigrateV2ToV3(ILogger<SettingsManager> logger, DataV1V3 previousData)
|
||||
{
|
||||
//
|
||||
// Summary:
|
||||
// In v2, self-hosted providers had no host (LM Studio, llama.cpp, ollama, etc.)
|
||||
//
|
||||
|
||||
Console.WriteLine("Migrating from v2 to v3...");
|
||||
logger.LogInformation("Migrating from v2 to v3...");
|
||||
return new()
|
||||
{
|
||||
Version = Version.V3,
|
||||
@ -110,14 +110,14 @@ public static class SettingsMigrations
|
||||
};
|
||||
}
|
||||
|
||||
private static Data MigrateV3ToV4(DataV1V3 previousConfig)
|
||||
private static Data MigrateV3ToV4(ILogger<SettingsManager> logger, DataV1V3 previousConfig)
|
||||
{
|
||||
//
|
||||
// Summary:
|
||||
// We grouped the settings into different categories.
|
||||
//
|
||||
|
||||
Console.WriteLine("Migrating from v3 to v4...");
|
||||
logger.LogInformation("Migrating from v3 to v4...");
|
||||
return new()
|
||||
{
|
||||
Version = Version.V4,
|
||||
|
@ -10,6 +10,13 @@ public sealed class Rust(string apiPort) : IDisposable
|
||||
BaseAddress = new Uri($"http://127.0.0.1:{apiPort}"),
|
||||
};
|
||||
|
||||
private ILogger<Rust>? logger;
|
||||
|
||||
public void SetLogger(ILogger<Rust> logService)
|
||||
{
|
||||
this.logger = logService;
|
||||
}
|
||||
|
||||
public async Task<int> GetAppPort()
|
||||
{
|
||||
Console.WriteLine("Trying to get app port from Rust runtime...");
|
||||
@ -36,14 +43,14 @@ public sealed class Rust(string apiPort) : IDisposable
|
||||
var response = await initialHttp.GetAsync(url);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($" Try {tris}/{MAX_TRIES}");
|
||||
Console.WriteLine($"Try {tris}/{MAX_TRIES} to get the app port from Rust runtime");
|
||||
await Task.Delay(wait4Try);
|
||||
continue;
|
||||
}
|
||||
|
||||
var appPortContent = await response.Content.ReadAsStringAsync();
|
||||
var appPort = int.Parse(appPortContent);
|
||||
Console.WriteLine($" Received app port from Rust runtime: '{appPort}'");
|
||||
Console.WriteLine($"Received app port from Rust runtime: '{appPort}'");
|
||||
return appPort;
|
||||
}
|
||||
|
||||
@ -54,11 +61,11 @@ public sealed class Rust(string apiPort) : IDisposable
|
||||
public async Task AppIsReady()
|
||||
{
|
||||
const string URL = "/system/dotnet/ready";
|
||||
Console.WriteLine($"Notifying Rust runtime that the app is ready.");
|
||||
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
|
||||
var response = await this.http.PostAsync(URL, new StringContent(string.Empty));
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
|
||||
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,7 @@ public sealed class MarkdownClipboardService(Rust rust, IJSRuntime jsRuntime, IS
|
||||
private Rust Rust { get; } = rust;
|
||||
|
||||
/// <summary>
|
||||
/// Gets called when the user wants to copy the markdown to the clipboard.
|
||||
/// Gets called when the user wants to copy the Markdown to the clipboard.
|
||||
/// </summary>
|
||||
/// <param name="text">The Markdown text to copy.</param>
|
||||
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(this.JsRuntime, this.Snackbar, text);
|
||||
|
@ -3,11 +3,13 @@ using AIStudio.Settings.DataModel;
|
||||
|
||||
namespace AIStudio.Tools.Services;
|
||||
|
||||
public class TemporaryChatService(SettingsManager settingsManager) : BackgroundService
|
||||
public class TemporaryChatService(ILogger<TemporaryChatService> logger, SettingsManager settingsManager) : BackgroundService
|
||||
{
|
||||
private static readonly TimeSpan CHECK_INTERVAL = TimeSpan.FromDays(1);
|
||||
private static bool IS_INITIALIZED;
|
||||
|
||||
private readonly ILogger<TemporaryChatService> logger = logger;
|
||||
|
||||
#region Overrides of BackgroundService
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
@ -15,10 +17,12 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
|
||||
while (!stoppingToken.IsCancellationRequested && !IS_INITIALIZED)
|
||||
await Task.Delay(TimeSpan.FromSeconds(3), stoppingToken);
|
||||
|
||||
this.logger.LogInformation("The temporary chat maintenance service was initialized.");
|
||||
|
||||
await settingsManager.LoadSettings();
|
||||
if(settingsManager.ConfigurationData.Workspace.StorageTemporaryMaintenancePolicy is WorkspaceStorageTemporaryMaintenancePolicy.NO_AUTOMATIC_MAINTENANCE)
|
||||
{
|
||||
Console.WriteLine("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service.");
|
||||
this.logger.LogWarning("Automatic maintenance of temporary chat storage is disabled. Exiting maintenance service.");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -34,9 +38,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
|
||||
|
||||
private Task StartMaintenance()
|
||||
{
|
||||
this.logger.LogInformation("Starting maintenance of temporary chat storage.");
|
||||
var temporaryDirectories = Path.Join(SettingsManager.DataDirectory, "tempChats");
|
||||
if(!Directory.Exists(temporaryDirectories))
|
||||
{
|
||||
this.logger.LogWarning("Temporary chat storage directory does not exist. End maintenance.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
foreach (var tempChatDirPath in Directory.EnumerateDirectories(temporaryDirectories))
|
||||
{
|
||||
@ -59,9 +67,13 @@ public class TemporaryChatService(SettingsManager settingsManager) : BackgroundS
|
||||
};
|
||||
|
||||
if(deleteChat)
|
||||
{
|
||||
this.logger.LogInformation($"Deleting temporary chat storage directory '{tempChatDirPath}' due to maintenance policy.");
|
||||
Directory.Delete(tempChatDirPath, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.LogInformation("Finished maintenance of temporary chat storage.");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
23
app/MindWork AI Studio/Tools/TerminalLogger.cs
Normal file
23
app/MindWork AI Studio/Tools/TerminalLogger.cs
Normal file
@ -0,0 +1,23 @@
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Logging.Console;
|
||||
|
||||
namespace AIStudio.Tools;
|
||||
|
||||
public sealed class TerminalLogger() : ConsoleFormatter(FORMATTER_NAME)
|
||||
{
|
||||
public const string FORMATTER_NAME = "AI Studio Terminal Logger";
|
||||
|
||||
public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter)
|
||||
{
|
||||
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
|
||||
var timestamp = DateTimeOffset.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||
var logLevel = logEntry.LogLevel.ToString();
|
||||
var category = logEntry.Category;
|
||||
|
||||
textWriter.Write($"=> {timestamp} [{logLevel}] {category}: {message}");
|
||||
if (logEntry.Exception is not null)
|
||||
textWriter.Write($" Exception was = {logEntry.Exception}");
|
||||
|
||||
textWriter.WriteLine();
|
||||
}
|
||||
}
|
@ -17,7 +17,7 @@ keyring = { version = "3.2", features = ["apple-native", "windows-native", "sync
|
||||
arboard = "3.4.0"
|
||||
tokio = { version = "1.39", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
flexi_logger = "0.28"
|
||||
log = "0.4"
|
||||
log = { version = "0.4", features = ["kv"] }
|
||||
once_cell = "1.19.0"
|
||||
rocket = { version = "0.5", default-features = false, features = ["json"] }
|
||||
rand = "0.8"
|
||||
|
@ -4,9 +4,11 @@
|
||||
extern crate rocket;
|
||||
extern crate core;
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::iter::once;
|
||||
use std::net::TcpListener;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::fmt::Write;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use arboard::Clipboard;
|
||||
@ -15,9 +17,12 @@ use serde::Serialize;
|
||||
use tauri::{Manager, Url, Window};
|
||||
use tauri::api::process::{Command, CommandChild, CommandEvent};
|
||||
use tokio::time;
|
||||
use flexi_logger::{AdaptiveFormat, Logger};
|
||||
use flexi_logger::{DeferredNow, Duplicate, FileSpec, Logger};
|
||||
use flexi_logger::writers::FileLogWriter;
|
||||
use keyring::error::Error::NoEntry;
|
||||
use log::{debug, error, info, warn};
|
||||
use log::{debug, error, info, kv, warn};
|
||||
use log::kv::{Key, Value, VisitSource};
|
||||
use rand::{RngCore, SeedableRng};
|
||||
use rocket::figment::Figment;
|
||||
use rocket::{get, post, routes};
|
||||
use rocket::config::Shutdown;
|
||||
@ -65,16 +70,36 @@ async fn main() {
|
||||
let tauri_version = metadata_lines.next().unwrap();
|
||||
let app_commit_hash = metadata_lines.next().unwrap();
|
||||
|
||||
// Set the log level according to the environment:
|
||||
// In debug mode, the log level is set to debug, in release mode to info.
|
||||
let log_level = match is_dev() {
|
||||
true => "debug",
|
||||
false => "info",
|
||||
//
|
||||
// Configure the logger:
|
||||
//
|
||||
let mut log_config = String::new();
|
||||
|
||||
// Set the log level depending on the environment:
|
||||
match is_dev() {
|
||||
true => log_config.push_str("debug, "),
|
||||
false => log_config.push_str("info, "),
|
||||
};
|
||||
|
||||
Logger::try_with_str(log_level).expect("Cannot create logging")
|
||||
.log_to_stdout()
|
||||
.adaptive_format_for_stdout(AdaptiveFormat::Detailed)
|
||||
// Set the log level for the Rocket library:
|
||||
log_config.push_str("rocket=info, ");
|
||||
|
||||
// Set the log level for the Rocket server:
|
||||
log_config.push_str("rocket::server=warn, ");
|
||||
|
||||
// Set the log level for the Reqwest library:
|
||||
log_config.push_str("reqwest::async_impl::client=info");
|
||||
|
||||
let logger = Logger::try_with_str(log_config).expect("Cannot create logging")
|
||||
.log_to_file(FileSpec::default()
|
||||
.basename("AI Studio Events")
|
||||
.suppress_timestamp()
|
||||
.suffix("log"))
|
||||
.duplicate_to_stdout(Duplicate::All)
|
||||
.use_utc()
|
||||
.format_for_files(file_logger_format)
|
||||
.format_for_stderr(terminal_colored_logger_format)
|
||||
.format_for_stdout(terminal_colored_logger_format)
|
||||
.start().expect("Cannot start logging");
|
||||
|
||||
info!("Starting MindWork AI Studio:");
|
||||
@ -94,6 +119,8 @@ async fn main() {
|
||||
|
||||
let api_port = *API_SERVER_PORT;
|
||||
info!("Try to start the API server on 'http://localhost:{api_port}'...");
|
||||
|
||||
// Configure the runtime API server:
|
||||
let figment = Figment::from(rocket::Config::release_default())
|
||||
|
||||
// We use the next available port which was determined before:
|
||||
@ -112,6 +139,9 @@ async fn main() {
|
||||
.merge(("workers", 3))
|
||||
.merge(("max_blocking", 12))
|
||||
|
||||
// No colors and emojis in the log output:
|
||||
.merge(("cli_colors", false))
|
||||
|
||||
// Set the shutdown configuration:
|
||||
.merge(("shutdown", Shutdown {
|
||||
|
||||
@ -125,6 +155,7 @@ async fn main() {
|
||||
..Shutdown::default()
|
||||
}));
|
||||
|
||||
//
|
||||
// Start the runtime API server in a separate thread. This is necessary
|
||||
// because the server is blocking, and we need to run the Tauri app in
|
||||
// parallel:
|
||||
@ -134,6 +165,7 @@ async fn main() {
|
||||
.mount("/", routes![dotnet_port, dotnet_ready])
|
||||
.ignite().await.unwrap()
|
||||
.launch().await.unwrap();
|
||||
});
|
||||
|
||||
//
|
||||
// Generate a secret key for the AES encryption for the IPC channel:
|
||||
@ -208,56 +240,33 @@ async fn main() {
|
||||
// Log the output of the .NET server:
|
||||
while let Some(CommandEvent::Stdout(line)) = rx.recv().await {
|
||||
|
||||
_ if line_cleared.contains("fail") || line_cleared.contains("error") || line_cleared.contains("exception") => _ = sender.send(ServerEvent::Error(line)).await,
|
||||
_ if line_cleared.contains("warn") => _ = sender.send(ServerEvent::Warning(line)).await,
|
||||
_ if line_cleared.contains("404") => _ = sender.send(ServerEvent::NotFound(line)).await,
|
||||
_ => (),
|
||||
// Remove newline characters from the end:
|
||||
let line = line.trim_end();
|
||||
|
||||
// Starts the line with '=>'?
|
||||
if line.starts_with("=>") {
|
||||
// Yes. This means that the line is a log message from the .NET server.
|
||||
// The format is: '<YYYY-MM-dd HH:mm:ss.fff> [<log level>] <source>: <message>'.
|
||||
// We try to parse this line and log it with the correct log level:
|
||||
let line = line.trim_start_matches("=>").trim();
|
||||
let parts = line.split_once(": ").unwrap();
|
||||
let left_part = parts.0.trim();
|
||||
let message = parts.1.trim();
|
||||
let parts = left_part.split_once("] ").unwrap();
|
||||
let level = parts.0.split_once("[").unwrap().1.trim();
|
||||
let source = parts.1.trim();
|
||||
match level {
|
||||
"Trace" => debug!(Source = ".NET Server", Comp = source; "{message}"),
|
||||
"Debug" => debug!(Source = ".NET Server", Comp = source; "{message}"),
|
||||
"Information" => info!(Source = ".NET Server", Comp = source; "{message}"),
|
||||
"Warning" => warn!(Source = ".NET Server", Comp = source; "{message}"),
|
||||
"Error" => error!(Source = ".NET Server", Comp = source; "{message}"),
|
||||
"Critical" => error!(Source = ".NET Server", Comp = source; "{message}"),
|
||||
|
||||
_ => error!(Source = ".NET Server", Comp = source; "{message} (unknown log level '{level}')"),
|
||||
}
|
||||
}
|
||||
|
||||
let sending_stop_result = sender.send(ServerEvent::Stopped).await;
|
||||
match sending_stop_result {
|
||||
Ok(_) => (),
|
||||
Err(e) => error!("Was not able to send the server stop message: {e}."),
|
||||
}
|
||||
});
|
||||
} else {
|
||||
warn!("Running in development mode, no .NET server will be started.");
|
||||
}
|
||||
|
||||
// TODO: Migrate logging to runtime API server:
|
||||
let server_receive_clone = DOTNET_SERVER.clone();
|
||||
|
||||
// Create a thread to handle server events:
|
||||
tauri::async_runtime::spawn(async move {
|
||||
info!("Start listening for server events...");
|
||||
loop {
|
||||
match receiver.recv().await {
|
||||
Some(ServerEvent::Started) => {
|
||||
info!("The .NET server was started.");
|
||||
},
|
||||
|
||||
Some(ServerEvent::NotFound(line)) => {
|
||||
warn!("The .NET server issued a 404 error: {line}.");
|
||||
},
|
||||
|
||||
Some(ServerEvent::Warning(line)) => {
|
||||
warn!("The .NET server issued a warning: {line}.");
|
||||
},
|
||||
|
||||
Some(ServerEvent::Error(line)) => {
|
||||
error!("The .NET server issued an error: {line}.");
|
||||
},
|
||||
|
||||
Some(ServerEvent::Stopped) => {
|
||||
warn!("The .NET server was stopped.");
|
||||
*server_receive_clone.lock().unwrap() = None;
|
||||
},
|
||||
|
||||
None => {
|
||||
debug!("Server event channel was closed.");
|
||||
break;
|
||||
},
|
||||
} else {
|
||||
info!(Source = ".NET Server"; "{line}");
|
||||
}
|
||||
}
|
||||
});
|
||||
@ -267,6 +276,19 @@ async fn main() {
|
||||
.setup(move |app| {
|
||||
let window = app.get_window("main").expect("Failed to get main window.");
|
||||
*MAIN_WINDOW.lock().unwrap() = Some(window);
|
||||
|
||||
info!(Source = "Bootloader Tauri"; "Setup is running.");
|
||||
let logger_path = app.path_resolver().app_local_data_dir().unwrap();
|
||||
let logger_path = logger_path.join("data");
|
||||
|
||||
info!(Source = "Bootloader Tauri"; "Reconfigure the file logger to use the app data directory {logger_path:?}");
|
||||
logger.reset_flw(&FileLogWriter::builder(
|
||||
FileSpec::default()
|
||||
.directory(logger_path)
|
||||
.basename("events")
|
||||
.suppress_timestamp()
|
||||
.suffix("log")))?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
@ -282,15 +304,15 @@ async fn main() {
|
||||
tauri::RunEvent::WindowEvent { event, label, .. } => {
|
||||
match event {
|
||||
tauri::WindowEvent::CloseRequested { .. } => {
|
||||
warn!("Window '{label}': close was requested.");
|
||||
warn!(Source = "Tauri"; "Window '{label}': close was requested.");
|
||||
}
|
||||
|
||||
tauri::WindowEvent::Destroyed => {
|
||||
warn!("Window '{label}': was destroyed.");
|
||||
warn!(Source = "Tauri"; "Window '{label}': was destroyed.");
|
||||
}
|
||||
|
||||
tauri::WindowEvent::FileDrop(files) => {
|
||||
info!("Window '{label}': files were dropped: {files:?}");
|
||||
info!(Source = "Tauri"; "Window '{label}': files were dropped: {files:?}");
|
||||
}
|
||||
|
||||
_ => (),
|
||||
@ -302,57 +324,136 @@ async fn main() {
|
||||
|
||||
tauri::UpdaterEvent::UpdateAvailable { body, date, version } => {
|
||||
let body_len = body.len();
|
||||
info!("Updater: update available: body size={body_len} time={date:?} version={version}");
|
||||
info!(Source = "Tauri"; "Updater: update available: body size={body_len} time={date:?} version={version}");
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::Pending => {
|
||||
info!("Updater: update is pending!");
|
||||
info!(Source = "Tauri"; "Updater: update is pending!");
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::DownloadProgress { chunk_length, content_length } => {
|
||||
info!("Updater: downloaded {} of {:?}", chunk_length, content_length);
|
||||
info!(Source = "Tauri"; "Updater: downloaded {} of {:?}", chunk_length, content_length);
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::Downloaded => {
|
||||
info!("Updater: update has been downloaded!");
|
||||
warn!("Try to stop the .NET server now...");
|
||||
info!(Source = "Tauri"; "Updater: update has been downloaded!");
|
||||
warn!(Source = "Tauri"; "Try to stop the .NET server now...");
|
||||
stop_servers();
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::Updated => {
|
||||
info!("Updater: app has been updated");
|
||||
warn!("Try to restart the app now...");
|
||||
info!(Source = "Tauri"; "Updater: app has been updated");
|
||||
warn!(Source = "Tauri"; "Try to restart the app now...");
|
||||
app_handle.restart();
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::AlreadyUpToDate => {
|
||||
info!("Updater: app is already up to date");
|
||||
info!(Source = "Tauri"; "Updater: app is already up to date");
|
||||
}
|
||||
|
||||
tauri::UpdaterEvent::Error(error) => {
|
||||
warn!("Updater: failed to update: {error}");
|
||||
warn!(Source = "Tauri"; "Updater: failed to update: {error}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tauri::RunEvent::ExitRequested { .. } => {
|
||||
warn!("Run event: exit was requested.");
|
||||
warn!(Source = "Tauri"; "Run event: exit was requested.");
|
||||
}
|
||||
|
||||
tauri::RunEvent::Ready => {
|
||||
info!("Run event: Tauri app is ready.");
|
||||
info!(Source = "Tauri"; "Run event: Tauri app is ready.");
|
||||
}
|
||||
|
||||
_ => {}
|
||||
});
|
||||
|
||||
info!("Tauri app was stopped.");
|
||||
warn!(Source = "Tauri"; "Tauri app was stopped.");
|
||||
if is_prod() {
|
||||
info!("Try to stop the .NET & runtime API servers as well...");
|
||||
warn!("Try to stop the .NET server as well...");
|
||||
stop_servers();
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Data structure for iterating over key-value pairs of log messages.
|
||||
//
|
||||
struct LogKVCollect<'kvs>(BTreeMap<Key<'kvs>, Value<'kvs>>);
|
||||
|
||||
impl<'kvs> VisitSource<'kvs> for LogKVCollect<'kvs> {
|
||||
fn visit_pair(&mut self, key: Key<'kvs>, value: Value<'kvs>) -> Result<(), kv::Error> {
|
||||
self.0.insert(key, value);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_kv_pairs(w: &mut dyn std::io::Write, record: &log::Record) -> Result<(), std::io::Error> {
|
||||
if record.key_values().count() > 0 {
|
||||
let mut visitor = LogKVCollect(BTreeMap::new());
|
||||
record.key_values().visit(&mut visitor).unwrap();
|
||||
write!(w, "[")?;
|
||||
let mut index = 0;
|
||||
for (key, value) in visitor.0 {
|
||||
index += 1;
|
||||
if index > 1 {
|
||||
write!(w, ", ")?;
|
||||
}
|
||||
|
||||
write!(w, "{} = {}", key, value)?;
|
||||
}
|
||||
write!(w, "] ")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Custom logger format for the terminal:
|
||||
pub fn terminal_colored_logger_format(
|
||||
w: &mut dyn std::io::Write,
|
||||
now: &mut DeferredNow,
|
||||
record: &log::Record,
|
||||
) -> Result<(), std::io::Error> {
|
||||
let level = record.level();
|
||||
|
||||
// Write the timestamp, log level, and module path:
|
||||
write!(
|
||||
w,
|
||||
"[{}] {} [{}] ",
|
||||
flexi_logger::style(level).paint(now.format(flexi_logger::TS_DASHES_BLANK_COLONS_DOT_BLANK).to_string()),
|
||||
flexi_logger::style(level).paint(record.level().to_string()),
|
||||
record.module_path().unwrap_or("<unnamed>"),
|
||||
)?;
|
||||
|
||||
// Write all key-value pairs:
|
||||
write_kv_pairs(w, record)?;
|
||||
|
||||
// Write the log message:
|
||||
write!(w, "{}", flexi_logger::style(level).paint(record.args().to_string()))
|
||||
}
|
||||
|
||||
// Custom logger format for the log files:
|
||||
pub fn file_logger_format(
|
||||
w: &mut dyn std::io::Write,
|
||||
now: &mut DeferredNow,
|
||||
record: &log::Record,
|
||||
) -> Result<(), std::io::Error> {
|
||||
|
||||
// Write the timestamp, log level, and module path:
|
||||
write!(
|
||||
w,
|
||||
"[{}] {} [{}] ",
|
||||
now.format(flexi_logger::TS_DASHES_BLANK_COLONS_DOT_BLANK),
|
||||
record.level(),
|
||||
record.module_path().unwrap_or("<unnamed>"),
|
||||
)?;
|
||||
|
||||
// Write all key-value pairs:
|
||||
write_kv_pairs(w, record)?;
|
||||
|
||||
// Write the log message:
|
||||
write!(w, "{}", &record.args())
|
||||
}
|
||||
|
||||
#[get("/system/dotnet/port")]
|
||||
fn dotnet_port() -> String {
|
||||
let dotnet_server_port = *DOTNET_SERVER_PORT;
|
||||
@ -367,10 +468,11 @@ async fn dotnet_ready() {
|
||||
{
|
||||
Ok(url) => url,
|
||||
Err(msg) => {
|
||||
error!("Error while parsing URL: {msg}");
|
||||
error!("Error while parsing URL for navigating to the app: {msg}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
info!("The .NET server was booted successfully.");
|
||||
|
||||
// Try to get the main window. If it is not available yet, wait for it:
|
||||
@ -397,20 +499,11 @@ async fn dotnet_ready() {
|
||||
let js_location_change = format!("window.location = '{url}';");
|
||||
let location_change_result = main_window.as_ref().unwrap().eval(js_location_change.as_str());
|
||||
match location_change_result {
|
||||
Ok(_) => info!("Location was changed to {url}."),
|
||||
Err(e) => error!("Failed to change location to {url}: {e}."),
|
||||
Ok(_) => info!("The app location was changed to {url}."),
|
||||
Err(e) => error!("Failed to change the app location to {url}: {e}."),
|
||||
}
|
||||
}
|
||||
|
||||
// Enum for server events:
|
||||
enum ServerEvent {
|
||||
Started,
|
||||
NotFound(String),
|
||||
Warning(String),
|
||||
Error(String),
|
||||
Stopped,
|
||||
}
|
||||
|
||||
pub fn is_dev() -> bool {
|
||||
cfg!(debug_assertions)
|
||||
}
|
||||
@ -447,7 +540,7 @@ async fn check_for_update() -> CheckUpdateResponse {
|
||||
true => {
|
||||
*CHECK_UPDATE_RESPONSE.lock().unwrap() = Some(update_response.clone());
|
||||
let new_version = update_response.latest_version();
|
||||
info!("Updater: update to version '{new_version}' is available.");
|
||||
info!(Source = "Updater"; "An update to version '{new_version}' is available.");
|
||||
let changelog = update_response.body();
|
||||
CheckUpdateResponse {
|
||||
update_is_available: true,
|
||||
@ -461,7 +554,7 @@ async fn check_for_update() -> CheckUpdateResponse {
|
||||
},
|
||||
|
||||
false => {
|
||||
info!("Updater: no updates available.");
|
||||
info!(Source = "Updater"; "No updates are available.");
|
||||
CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
error: false,
|
||||
@ -472,7 +565,7 @@ async fn check_for_update() -> CheckUpdateResponse {
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
warn!("Failed to check updater: {e}.");
|
||||
warn!(Source = "Updater"; "Failed to check for updates: {e}.");
|
||||
CheckUpdateResponse {
|
||||
update_is_available: false,
|
||||
error: true,
|
||||
@ -501,7 +594,7 @@ async fn install_update() {
|
||||
},
|
||||
|
||||
None => {
|
||||
error!("Update installer: no update available to install. Did you check for updates first?");
|
||||
error!(Source = "Updater"; "No update available to install. Did you check for updates first?");
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -513,7 +606,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store
|
||||
let result = entry.set_password(secret.as_str());
|
||||
match result {
|
||||
Ok(_) => {
|
||||
info!("Secret for {service} and user {user_name} was stored successfully.");
|
||||
info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was stored successfully.");
|
||||
StoreSecretResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
@ -521,7 +614,7 @@ fn store_secret(destination: String, user_name: String, secret: String) -> Store
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
error!("Failed to store secret for {service} and user {user_name}: {e}.");
|
||||
error!(Source = "Secret Store"; "Failed to store secret for {service} and user {user_name}: {e}.");
|
||||
StoreSecretResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
@ -543,7 +636,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
|
||||
let secret = entry.get_password();
|
||||
match secret {
|
||||
Ok(s) => {
|
||||
info!("Secret for {service} and user {user_name} was retrieved successfully.");
|
||||
info!(Source = "Secret Store"; "Secret for {service} and user {user_name} was retrieved successfully.");
|
||||
RequestedSecret {
|
||||
success: true,
|
||||
secret: s,
|
||||
@ -552,7 +645,7 @@ fn get_secret(destination: String, user_name: String) -> RequestedSecret {
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
error!("Failed to retrieve secret for {service} and user {user_name}: {e}.");
|
||||
error!(Source = "Secret Store"; "Failed to retrieve secret for {service} and user {user_name}: {e}.");
|
||||
RequestedSecret {
|
||||
success: false,
|
||||
secret: String::from(""),
|
||||
@ -577,7 +670,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
warn!("Secret for {service} and user {user_name} was deleted successfully.");
|
||||
warn!(Source = "Secret Store"; "Secret for {service} and user {user_name} was deleted successfully.");
|
||||
DeleteSecretResponse {
|
||||
success: true,
|
||||
was_entry_found: true,
|
||||
@ -586,7 +679,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
|
||||
},
|
||||
|
||||
Err(NoEntry) => {
|
||||
warn!("No secret for {service} and user {user_name} was found.");
|
||||
warn!(Source = "Secret Store"; "No secret for {service} and user {user_name} was found.");
|
||||
DeleteSecretResponse {
|
||||
success: true,
|
||||
was_entry_found: false,
|
||||
@ -595,7 +688,7 @@ fn delete_secret(destination: String, user_name: String) -> DeleteSecretResponse
|
||||
}
|
||||
|
||||
Err(e) => {
|
||||
error!("Failed to delete secret for {service} and user {user_name}: {e}.");
|
||||
error!(Source = "Secret Store"; "Failed to delete secret for {service} and user {user_name}: {e}.");
|
||||
DeleteSecretResponse {
|
||||
success: false,
|
||||
was_entry_found: false,
|
||||
@ -618,7 +711,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
|
||||
let mut clipboard = match clipboard_result {
|
||||
Ok(clipboard) => clipboard,
|
||||
Err(e) => {
|
||||
error!("Failed to get the clipboard instance: {e}.");
|
||||
error!(Source = "Clipboard"; "Failed to get the clipboard instance: {e}.");
|
||||
return SetClipboardResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
@ -629,7 +722,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
|
||||
let set_text_result = clipboard.set_text(text);
|
||||
match set_text_result {
|
||||
Ok(_) => {
|
||||
debug!("Text was set to the clipboard successfully.");
|
||||
debug!(Source = "Clipboard"; "Text was set to the clipboard successfully.");
|
||||
SetClipboardResponse {
|
||||
success: true,
|
||||
issue: String::from(""),
|
||||
@ -637,7 +730,7 @@ fn set_clipboard(text: String) -> SetClipboardResponse {
|
||||
},
|
||||
|
||||
Err(e) => {
|
||||
error!("Failed to set text to the clipboard: {e}.");
|
||||
error!(Source = "Clipboard"; "Failed to set text to the clipboard: {e}.");
|
||||
SetClipboardResponse {
|
||||
success: false,
|
||||
issue: e.to_string(),
|
||||
|
Loading…
Reference in New Issue
Block a user