Improve ipc (#102)

This commit is contained in:
Thorsten Sommer authored and GitHub committed 2024-09-01 20:10:03 +02:00
1 parent 47f5756e5a
commit fc64c0b505
81 files changed
+2408 -799

No files matched your search

@@ -0,0 +1,13 @@
using System.Security;
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
[JsonConverter(typeof(EncryptedTextJsonConverter))]
public readonly record struct EncryptedText(string EncryptedData)
{
public EncryptedText() : this(string.Empty)
{
throw new SecurityException("Please provide the encrypted data.");
}
}
@@ -0,0 +1,8 @@
namespace AIStudio.Tools;
public static class EncryptedTextExtensions
{
public static async Task<EncryptedText> Encrypt(this string data, Encryption encryption) => await encryption.Encrypt(data);
public static async Task<string> Decrypt(this EncryptedText encryptedText, Encryption encryption) => await encryption.Decrypt(encryptedText);
}
@@ -0,0 +1,27 @@
using System.Text.Json;
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
public sealed class EncryptedTextJsonConverter : JsonConverter<EncryptedText>
{
#region Overrides of JsonConverter<EncryptedText>
public override EncryptedText Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType is JsonTokenType.String)
{
var value = reader.GetString()!;
return new EncryptedText(value);
}
throw new JsonException($"Unexpected token type when parsing EncryptedText. Expected {JsonTokenType.String}, but got {reader.TokenType}.");
}
public override void Write(Utf8JsonWriter writer, EncryptedText value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.EncryptedData);
}
#endregion
}
+142
View File
@@ -0,0 +1,142 @@
using System.Diagnostics;
using System.Security.Cryptography;
using System.Text;
namespace AIStudio.Tools;
public sealed class Encryption(ILogger<Encryption> logger, byte[] secretPassword, byte[] secretKeySalt)
{
/// <summary>
/// The number of iterations to derive the key and IV from the password. For a password manager
/// where the user has to enter their primary password, 100 iterations would be too few and
/// insecure. Here, the use case is different: We generate a 512-byte long and cryptographically
/// secure password at every start. This password already contains enough entropy. In our case,
/// we need key and IV primarily because AES, with the algorithms we chose, requires a fixed key
/// length, and our password is too long.
/// </summary>
private const int ITERATIONS = 100;
private readonly byte[] key = new byte[32];
private readonly byte[] iv = new byte[16];
public async Task Initialize()
{
logger.LogInformation("Initializing encryption service...");
var stopwatch = Stopwatch.StartNew();
if (secretPassword.Length != 512)
{
logger.LogError($"The secret password must be 512 bytes long. It was {secretPassword.Length} bytes long.");
throw new CryptographicException("The secret password must be 512 bytes long.");
}
if(secretKeySalt.Length != 16)
{
logger.LogError($"The salt data must be 16 bytes long. It was {secretKeySalt.Length} bytes long.");
throw new CryptographicException("The salt data must be 16 bytes long.");
}
// Derive key and iv vector: the operations take several seconds. Thus, using a task:
await Task.Run(() =>
{
using var keyVectorObj = new Rfc2898DeriveBytes(secretPassword, secretKeySalt, ITERATIONS, HashAlgorithmName.SHA512);
var keyBytes = keyVectorObj.GetBytes(32); // the max valid key length = 256 bit = 32 bytes
var ivBytes = keyVectorObj.GetBytes(16); // the only valid block size = 128 bit = 16 bytes
Array.Copy(keyBytes, this.key, this.key.Length);
Array.Copy(ivBytes, this.iv, this.iv.Length);
});
var initDuration = stopwatch.Elapsed;
stopwatch.Stop();
logger.LogInformation($"Encryption service initialized in {initDuration.TotalMilliseconds} milliseconds.");
}
public async Task<EncryptedText> Encrypt(string data)
{
// Create AES encryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = this.key;
aes.IV = this.iv;
aes.Mode = CipherMode.CBC;
using var encryption = aes.CreateEncryptor();
// Copy the given string data into a memory stream:
await using var plainDataStream = new MemoryStream(Encoding.UTF8.GetBytes(data));
// A memory stream for the final, encrypted data:
await using var encryptedAndEncodedData = new MemoryStream();
// A base64 stream for the encoding:
await using var base64Stream = new CryptoStream(encryptedAndEncodedData, new ToBase64Transform(), CryptoStreamMode.Write);
// Write the salt into the base64 stream:
await base64Stream.WriteAsync(secretKeySalt);
// Create the encryption stream:
await using var cryptoStream = new CryptoStream(base64Stream, encryption, CryptoStreamMode.Write);
// Write the payload into the encryption stream:
await plainDataStream.CopyToAsync(cryptoStream);
// Flush the final block. Please note that it is not enough to call the regular flush method.
await cryptoStream.FlushFinalBlockAsync();
// Convert the base64 encoded data back into a string. Uses GetBuffer due to the advantage that
// it does not create another copy of the data. ToArray would create another copy of the data.
return new EncryptedText(Encoding.ASCII.GetString(encryptedAndEncodedData.GetBuffer()[..(int)encryptedAndEncodedData.Length]));
}
public async Task<string> Decrypt(EncryptedText encryptedData)
{
// Build a memory stream to access the given base64 encoded data:
await using var encodedEncryptedStream = new MemoryStream(Encoding.ASCII.GetBytes(encryptedData.EncryptedData));
// Wrap around the base64 decoder stream:
await using var base64Stream = new CryptoStream(encodedEncryptedStream, new FromBase64Transform(), CryptoStreamMode.Read);
// A buffer for the salt's bytes:
var readSaltBytes = new byte[16]; // 16 bytes = Guid
// Read the salt's bytes out of the stream:
var readBytes = 0;
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1));
while(readBytes < readSaltBytes.Length && !cts.Token.IsCancellationRequested)
{
readBytes += await base64Stream.ReadAsync(readSaltBytes, readBytes, readSaltBytes.Length - readBytes, cts.Token);
await Task.Delay(TimeSpan.FromMilliseconds(60), cts.Token);
}
// Check the salt bytes:
if(!readSaltBytes.SequenceEqual(secretKeySalt))
{
logger.LogError("The salt bytes do not match. The data is corrupted or tampered.");
throw new CryptographicException("The salt bytes do not match. The data is corrupted or tampered.");
}
// Create AES decryption:
using var aes = Aes.Create();
aes.Padding = PaddingMode.PKCS7;
aes.Key = this.key;
aes.IV = this.iv;
using var decryption = aes.CreateDecryptor();
// A memory stream for the final, decrypted data:
await using var decryptedData = new MemoryStream();
// The crypto stream:
await using var cryptoStream = new CryptoStream(base64Stream, decryption, CryptoStreamMode.Read);
// Reads all remaining data through the decrypt stream. Note that this operation
// starts at the current position, i.e., after the salt bytes:
await cryptoStream.CopyToAsync(decryptedData);
// Convert the decrypted data back into a string. Uses GetBuffer due to the advantage that
// it does not create another copy of the data. ToArray would create another copy of the data.
return Encoding.UTF8.GetString(decryptedData.GetBuffer()[..(int)decryptedData.Length]);
}
}
@@ -0,0 +1,18 @@
using System.Net.Http.Headers;
namespace AIStudio.Tools;
public static class HttpRequestHeadersExtensions
{
private static readonly string API_TOKEN;
static HttpRequestHeadersExtensions()
{
API_TOKEN = Program.API_TOKEN;
}
public static void AddApiToken(this HttpRequestHeaders headers)
{
headers.Add("token", API_TOKEN);
}
}
-54
View File
@@ -1,54 +0,0 @@
namespace AIStudio.Tools;
/// <summary>
/// Calling Rust functions.
/// </summary>
public sealed class Rust
{
/// <summary>
/// Tries to copy the given text to the clipboard.
/// </summary>
/// <param name="jsRuntime">The JS runtime to access the Rust code.</param>
/// <param name="snackbar">The snackbar to show the result.</param>
/// <param name="text">The text to copy to the clipboard.</param>
public async Task CopyText2Clipboard(IJSRuntime jsRuntime, ISnackbar snackbar, string text)
{
var response = await jsRuntime.InvokeAsync<SetClipboardResponse>("window.__TAURI__.invoke", "set_clipboard", new SetClipboardText(text));
var msg = response.Success switch
{
true => "Successfully copied text to clipboard!",
false => $"Failed to copy text to clipboard: {response.Issue}",
};
var severity = response.Success switch
{
true => Severity.Success,
false => Severity.Error,
};
snackbar.Add(msg, severity, config =>
{
config.Icon = Icons.Material.Filled.ContentCopy;
config.IconSize = Size.Large;
config.IconColor = severity switch
{
Severity.Success => Color.Success,
Severity.Error => Color.Error,
_ => Color.Default,
};
});
}
public async Task<UpdateResponse> CheckForUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(16));
return await jsRuntime.InvokeAsync<UpdateResponse>("window.__TAURI__.invoke", cts.Token, "check_for_update");
}
public async Task InstallUpdate(IJSRuntime jsRuntime)
{
var cts = new CancellationTokenSource();
await jsRuntime.InvokeVoidAsync("window.__TAURI__.invoke", cts.Token, "install_update");
}
}
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for deleting a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully deleted or not found.</param>
/// <param name="Issue">The issue, when the secret could not be deleted.</param>
/// <param name="WasEntryFound">True, when the entry was found and deleted.</param>
public readonly record struct DeleteSecretResponse(bool Success, string Issue, bool WasEntryFound);
@@ -0,0 +1,9 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for any requested secret.
/// </summary>
/// <param name="Success">True, when the secret was successfully retrieved.</param>
/// <param name="Secret">The secret, e.g., API key.</param>
/// <param name="Issue">The issue, when the secret could not be retrieved.</param>
public readonly record struct RequestedSecret(bool Success, EncryptedText Secret, string Issue);
@@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct SelectSecretRequest(string Destination, string UserName);
@@ -1,4 +1,4 @@
namespace AIStudio.Tools;
namespace AIStudio.Tools.Rust;
/// <summary>
/// The response from the set clipboard operation.
@@ -0,0 +1,3 @@
namespace AIStudio.Tools.Rust;
public readonly record struct StoreSecretRequest(string Destination, string UserName, EncryptedText Secret);
@@ -0,0 +1,8 @@
namespace AIStudio.Tools.Rust;
/// <summary>
/// Data structure for storing a secret response.
/// </summary>
/// <param name="Success">True, when the secret was successfully stored.</param>
/// <param name="Issue">The issue, when the secret could not be stored.</param>
public readonly record struct StoreSecretResponse(bool Success, string Issue);
@@ -1,6 +1,4 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools;
namespace AIStudio.Tools.Rust;
/// <summary>
/// The response of the update check.
@@ -9,8 +7,8 @@ namespace AIStudio.Tools;
/// <param name="NewVersion">The new version, when available.</param>
/// <param name="Changelog">The changelog of the new version, when available.</param>
public readonly record struct UpdateResponse(
[property:JsonPropertyName("update_is_available")] bool UpdateIsAvailable,
[property:JsonPropertyName("error")] bool Error,
[property:JsonPropertyName("new_version")] string NewVersion,
bool UpdateIsAvailable,
bool Error,
string NewVersion,
string Changelog
);
+331
View File
@@ -0,0 +1,331 @@
using System.Security.Cryptography;
using System.Text.Json;
using AIStudio.Provider;
using AIStudio.Tools.Rust;
// ReSharper disable NotAccessedPositionalProperty.Local
namespace AIStudio.Tools;
/// <summary>
/// Calling Rust functions.
/// </summary>
public sealed class RustService : IDisposable
{
private readonly HttpClient http;
private readonly JsonSerializerOptions jsonRustSerializerOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
private ILogger<RustService>? logger;
private Encryption? encryptor;
private readonly string apiPort;
private readonly string certificateFingerprint;
public RustService(string apiPort, string certificateFingerprint)
{
this.apiPort = apiPort;
this.certificateFingerprint = certificateFingerprint;
var certificateValidationHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
{
if(certificate is null)
return false;
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
return currentCertificateFingerprint == certificateFingerprint;
},
};
this.http = new HttpClient(certificateValidationHandler)
{
BaseAddress = new Uri($"https://127.0.0.1:{apiPort}"),
DefaultRequestVersion = Version.Parse("2.0"),
DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher,
};
this.http.DefaultRequestHeaders.AddApiToken();
}
public void SetLogger(ILogger<RustService> logService)
{
this.logger = logService;
}
public void SetEncryptor(Encryption encryptionService)
{
this.encryptor = encryptionService;
}
public async Task<int> GetAppPort()
{
Console.WriteLine("Trying to get app port from Rust runtime...");
//
// Note I: In the production environment, the Rust runtime is already running
// and listening on the given port. In the development environment, the IDE
// starts the Rust runtime in parallel with the .NET runtime. Since the
// Rust runtime needs some time to start, we have to wait for it to be ready.
//
const int MAX_TRIES = 160;
var tris = 0;
var wait4Try = TimeSpan.FromMilliseconds(250);
var url = new Uri($"https://127.0.0.1:{this.apiPort}/system/dotnet/port");
while (tris++ < MAX_TRIES)
{
//
// Note II: We use a new HttpClient instance for each try to avoid
// .NET is caching the result. When we use the same HttpClient
// instance, we would always get the same result (403 forbidden),
// without even trying to connect to the Rust server.
//
using var initialHttp = new HttpClient(new HttpClientHandler
{
//
// Note III: We have to create also a new HttpClientHandler instance
// for each try to avoid .NET is caching the result. This is necessary
// because it gets disposed when the HttpClient instance gets disposed.
//
ServerCertificateCustomValidationCallback = (_, certificate, _, _) =>
{
if(certificate is null)
return false;
var currentCertificateFingerprint = certificate.GetCertHashString(HashAlgorithmName.SHA256);
return currentCertificateFingerprint == this.certificateFingerprint;
}
});
initialHttp.DefaultRequestVersion = Version.Parse("2.0");
initialHttp.DefaultVersionPolicy = HttpVersionPolicy.RequestVersionOrHigher;
initialHttp.DefaultRequestHeaders.AddApiToken();
try
{
var response = await initialHttp.GetAsync(url);
if (!response.IsSuccessStatusCode)
{
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}'");
return appPort;
}
catch (Exception e)
{
Console.WriteLine($"Error: Was not able to get the app port from Rust runtime: '{e.Message}'");
Console.WriteLine(e.InnerException);
throw;
}
}
Console.WriteLine("Failed to receive the app port from Rust runtime.");
return 0;
}
public async Task AppIsReady()
{
const string URL = "/system/dotnet/ready";
this.logger!.LogInformation("Notifying Rust runtime that the app is ready.");
try
{
var response = await this.http.GetAsync(URL);
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to notify Rust runtime that the app is ready: '{response.StatusCode}'");
}
}
catch (Exception e)
{
this.logger!.LogError(e, "Failed to notify the Rust runtime that the app is ready.");
throw;
}
}
public async Task<string> GetConfigDirectory()
{
var response = await this.http.GetAsync("/system/directories/config");
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the config directory from Rust: '{response.StatusCode}'");
return string.Empty;
}
return await response.Content.ReadAsStringAsync();
}
public async Task<string> GetDataDirectory()
{
var response = await this.http.GetAsync("/system/directories/data");
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the data directory from Rust: '{response.StatusCode}'");
return string.Empty;
}
return await response.Content.ReadAsStringAsync();
}
/// <summary>
/// Tries to copy the given text to the clipboard.
/// </summary>
/// <param name="snackbar">The snackbar to show the result.</param>
/// <param name="text">The text to copy to the clipboard.</param>
public async Task CopyText2Clipboard(ISnackbar snackbar, string text)
{
var message = "Successfully copied the text to your clipboard";
var iconColor = Color.Error;
var severity = Severity.Error;
try
{
var encryptedText = await text.Encrypt(this.encryptor!);
var response = await this.http.PostAsync("/clipboard/set", new StringContent(encryptedText.EncryptedData));
if (!response.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to copy the text to the clipboard due to an network error: '{response.StatusCode}'");
message = "Failed to copy the text to your clipboard.";
return;
}
var state = await response.Content.ReadFromJsonAsync<SetClipboardResponse>();
if (!state.Success)
{
this.logger!.LogError("Failed to copy the text to the clipboard.");
message = "Failed to copy the text to your clipboard.";
return;
}
iconColor = Color.Success;
severity = Severity.Success;
this.logger!.LogDebug("Successfully copied the text to the clipboard.");
}
finally
{
snackbar.Add(message, severity, config =>
{
config.Icon = Icons.Material.Filled.ContentCopy;
config.IconSize = Size.Large;
config.IconColor = iconColor;
});
}
}
public async Task<UpdateResponse> CheckForUpdate()
{
try
{
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(16));
return await this.http.GetFromJsonAsync<UpdateResponse>("/updates/check", cts.Token);
}
catch (Exception e)
{
this.logger!.LogError(e, "Failed to check for an update.");
return new UpdateResponse
{
Error = true,
UpdateIsAvailable = false,
};
}
}
public async Task InstallUpdate()
{
try
{
var cts = new CancellationTokenSource();
await this.http.GetAsync("/updates/install", cts.Token);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Try to get the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to get the API key for.</param>
/// <returns>The requested secret.</returns>
public async Task<RequestedSecret> GetAPIKey(IProvider provider)
{
var secretRequest = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/get", secretRequest, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new RequestedSecret(false, new EncryptedText(string.Empty), "Failed to get the API key due to an API issue.");
}
var secret = await result.Content.ReadFromJsonAsync<RequestedSecret>();
if (!secret.Success)
this.logger!.LogError($"Failed to get the API key for provider '{provider.Id}': '{secret.Issue}'");
return secret;
}
/// <summary>
/// Try to store the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to store the API key for.</param>
/// <param name="key">The API key to store.</param>
/// <returns>The store secret response.</returns>
public async Task<StoreSecretResponse> SetAPIKey(IProvider provider, string key)
{
var encryptedKey = await this.encryptor!.Encrypt(key);
var request = new StoreSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName, encryptedKey);
var result = await this.http.PostAsJsonAsync("/secrets/store", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to store the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new StoreSecretResponse(false, "Failed to get the API key due to an API issue.");
}
var state = await result.Content.ReadFromJsonAsync<StoreSecretResponse>();
if (!state.Success)
this.logger!.LogError($"Failed to store the API key for provider '{provider.Id}': '{state.Issue}'");
return state;
}
/// <summary>
/// Tries to delete the API key for the given provider.
/// </summary>
/// <param name="provider">The provider to delete the API key for.</param>
/// <returns>The delete secret response.</returns>
public async Task<DeleteSecretResponse> DeleteAPIKey(IProvider provider)
{
var request = new SelectSecretRequest($"provider::{provider.Id}::{provider.InstanceName}::api_key", Environment.UserName);
var result = await this.http.PostAsJsonAsync("/secrets/delete", request, this.jsonRustSerializerOptions);
if (!result.IsSuccessStatusCode)
{
this.logger!.LogError($"Failed to delete the API key for provider '{provider.Id}' due to an API issue: '{result.StatusCode}'");
return new DeleteSecretResponse{Success = false, WasEntryFound = false, Issue = "Failed to delete the API key due to an API issue."};
}
var state = await result.Content.ReadFromJsonAsync<DeleteSecretResponse>();
if (!state.Success)
this.logger!.LogError($"Failed to delete the API key for provider '{provider.Id}': '{state.Issue}'");
return state;
}
#region IDisposable
public void Dispose()
{
this.http.Dispose();
}
#endregion
}
@@ -6,17 +6,15 @@ namespace AIStudio.Tools.Services;
/// Wire up the clipboard service to copy Markdown to the clipboard.
/// We use our own Rust-based clipboard service for this.
/// </summary>
public sealed class MarkdownClipboardService(Rust rust, IJSRuntime jsRuntime, ISnackbar snackbar) : IMudMarkdownClipboardService
public sealed class MarkdownClipboardService(RustService rust, ISnackbar snackbar) : IMudMarkdownClipboardService
{
private IJSRuntime JsRuntime { get; } = jsRuntime;
private ISnackbar Snackbar { get; } = snackbar;
private Rust Rust { get; } = rust;
private RustService 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);
public async ValueTask CopyToClipboardAsync(string text) => await this.Rust.CopyText2Clipboard(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,10 +38,14 @@ 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))
{
var chatPath = Path.Join(tempChatDirPath, "thread.json");
@@ -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;
}
@@ -7,20 +7,16 @@ namespace AIStudio.Tools.Services;
public sealed class UpdateService : BackgroundService, IMessageBusReceiver
{
// We cannot inject IJSRuntime into our service. This is because
// the service is not a Blazor component. We need to pass the IJSRuntime from
// the MainLayout component to the service.
private static IJSRuntime? JS_RUNTIME;
private static bool IS_INITIALIZED;
private static ISnackbar? SNACKBAR;
private readonly SettingsManager settingsManager;
private readonly MessageBus messageBus;
private readonly Rust rust;
private readonly RustService rust;
private TimeSpan updateInterval;
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, Rust rust)
public UpdateService(MessageBus messageBus, SettingsManager settingsManager, RustService rust)
{
this.settingsManager = settingsManager;
this.messageBus = messageBus;
@@ -96,7 +92,7 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
if(!IS_INITIALIZED)
return;
var response = await this.rust.CheckForUpdate(JS_RUNTIME!);
var response = await this.rust.CheckForUpdate();
if (response.UpdateIsAvailable)
{
await this.messageBus.SendMessage(null, Event.UPDATE_AVAILABLE, response);
@@ -115,10 +111,9 @@ public sealed class UpdateService : BackgroundService, IMessageBusReceiver
}
}
public static void SetBlazorDependencies(IJSRuntime jsRuntime, ISnackbar snackbar)
public static void SetBlazorDependencies(ISnackbar snackbar)
{
SNACKBAR = snackbar;
JS_RUNTIME = jsRuntime;
IS_INITIALIZED = true;
}
}
@@ -1,7 +0,0 @@
namespace AIStudio.Tools;
/// <summary>
/// Model for setting clipboard text.
/// </summary>
/// <param name="Text">The text to set to the clipboard.</param>
public record SetClipboardText(string Text);
@@ -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();
}
}