diff --git a/src/GuiApp/FileTime.Avalonia/App.axaml.cs b/src/GuiApp/FileTime.Avalonia/App.axaml.cs index c3687ed..386b9b7 100644 --- a/src/GuiApp/FileTime.Avalonia/App.axaml.cs +++ b/src/GuiApp/FileTime.Avalonia/App.axaml.cs @@ -1,26 +1,32 @@ -using Avalonia; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using FileTime.App.Core; using FileTime.Avalonia.ViewModels; using FileTime.Avalonia.Views; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using System; namespace FileTime.Avalonia { public class App : global::Avalonia.Application { - public static IServiceProvider ServiceProvider { get; private set; } + public static IServiceProvider ServiceProvider { get; } static App() { ServiceProvider ??= DependencyInjection .RegisterDefaultServices() + .AddConfiguration() + .InitSerilog() + .RegisterLogging() .AddViewModels() .AddServices() .RegisterCommandHandlers() .BuildServiceProvider(); + + var _logger = ServiceProvider.GetService>(); + _logger?.LogInformation("App initialization completed."); } public override void Initialize() diff --git a/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs new file mode 100644 index 0000000..2897136 --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/Configuration/MainConfiguration.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace FileTime.Avalonia.Configuration +{ + public static class MainConfiguration + { + public static readonly Dictionary Configuration = new(); + } +} \ No newline at end of file diff --git a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj index da357e2..36ce53b 100644 --- a/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj +++ b/src/GuiApp/FileTime.Avalonia/FileTime.Avalonia.csproj @@ -8,6 +8,7 @@ + @@ -21,9 +22,14 @@ + + + + + diff --git a/src/GuiApp/FileTime.Avalonia/Program.cs b/src/GuiApp/FileTime.Avalonia/Program.cs index 07a28b3..aff7bea 100644 --- a/src/GuiApp/FileTime.Avalonia/Program.cs +++ b/src/GuiApp/FileTime.Avalonia/Program.cs @@ -1,18 +1,85 @@ -using System; +using System.Reflection; +using System; +using System.IO; using Avalonia; -using Avalonia.Controls.ApplicationLifetimes; using Avalonia.ReactiveUI; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System.Collections.Generic; namespace FileTime.Avalonia { - class Program + public class Program { + public static string AppDataRoot { get; } + public static string EnvironmentName { get; } + + static Program() + { + +#if DEBUG + EnvironmentName = "Development"; + + AppDataRoot = Path.Combine(Environment.CurrentDirectory, "appdata"); +#else + EnvironmentName = "Release"; + + var possibleDataRootsPaths = new List() + { + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FileTime"), + Path.Combine(Assembly.GetEntryAssembly()?.Location ?? ".", "fallbackDataRoot") + }; + + string? appDataRoot = null; + foreach (var possibleAppDataRoot in possibleDataRootsPaths) + { + try + { + var appDataRootDirectory = new DirectoryInfo(possibleAppDataRoot); + if (!appDataRootDirectory.Exists) appDataRootDirectory.Create(); + + //TODO write test + appDataRoot = possibleAppDataRoot; + break; + } + catch { } + } + + if (appDataRoot == null) throw new UnauthorizedAccessException(); + AppDataRoot = appDataRoot; +#endif + } + // Initialization code. Don't use any Avalonia, third-party APIs or any // SynchronizationContext-reliant code before AppMain is called: things aren't initialized // yet and stuff might break. [STAThread] - public static void Main(string[] args) => BuildAvaloniaApp() - .StartWithClassicDesktopLifetime(args); + public static void Main(string[] args) + { + try + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + catch (Exception e) + { + var message = $"Ciritcal error cought in {nameof(Program)}"; + if (App.ServiceProvider?.GetService>() is var logger && logger != null) + { + logger.LogCritical(0, e, message); + return; + } + + var logsPath = Path.Combine(AppDataRoot, "logs"); + var logsDirectory = new DirectoryInfo(logsPath); + if (!logsDirectory.Exists) logsDirectory.Create(); + + var logPath = Path.Combine(logsPath, "_criticalError.log"); + + using var fileWriter = File.Open(logPath, FileMode.OpenOrCreate, FileAccess.Write); + using var streamWriter = new StreamWriter(fileWriter); + streamWriter.WriteLine(DateTime.Now.ToString() + ": " + message + "\n" + e.ToString() + "\n\n"); + } + } // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() diff --git a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs index cc5f786..ae8b892 100644 --- a/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs +++ b/src/GuiApp/FileTime.Avalonia/Services/StatePersistenceService.cs @@ -34,7 +34,7 @@ namespace FileTime.Avalonia.Services _itemNameConverterService = itemNameConverterService; _contentProviders = contentProviders; _localContentProvider = localContentProvider; - _settingsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FileTime", "savedState.json"); + _settingsPath = Path.Combine(Program.AppDataRoot, "savedState.json"); _jsonOptions = new JsonSerializerOptions() { diff --git a/src/GuiApp/FileTime.Avalonia/Startup.cs b/src/GuiApp/FileTime.Avalonia/Startup.cs index 4c22887..e5457af 100644 --- a/src/GuiApp/FileTime.Avalonia/Startup.cs +++ b/src/GuiApp/FileTime.Avalonia/Startup.cs @@ -1,11 +1,15 @@ -using System.Runtime.InteropServices; +using System.IO; +using System.Runtime.InteropServices; using FileTime.Avalonia.Application; +using FileTime.Avalonia.Configuration; using FileTime.Avalonia.IconProviders; using FileTime.Avalonia.Services; using FileTime.Avalonia.ViewModels; using FileTime.Core.Command; using FileTime.Core.Interactions; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Serilog; namespace FileTime.Avalonia { @@ -21,7 +25,6 @@ namespace FileTime.Avalonia internal static IServiceCollection AddServices(this IServiceCollection serviceCollection) { serviceCollection = serviceCollection - .AddLogging() .AddSingleton() .AddSingleton() .AddSingleton(); @@ -39,12 +42,47 @@ namespace FileTime.Avalonia } internal static IServiceCollection RegisterCommandHandlers(this IServiceCollection serviceCollection) { - foreach (var commandHandler in FileTime.Providers.Local.Startup.GetCommandHandlers()) + foreach (var commandHandler in Providers.Local.Startup.GetCommandHandlers()) { serviceCollection.AddTransient(typeof(ICommandHandler), commandHandler); } return serviceCollection; } + + internal static IServiceCollection RegisterLogging(this IServiceCollection serviceCollection) + { + return serviceCollection.AddLogging(loggingBuilder => + loggingBuilder.AddSerilog(dispose: true) + ); + } + + internal static IServiceCollection AddConfiguration(this IServiceCollection serviceCollection) + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(MainConfiguration.Configuration) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{Program.EnvironmentName}.json", true, true) + .Build(); + + return serviceCollection + //.Configure(configuration.GetSection("server")) + .AddSingleton(configuration); + } + + internal static IServiceCollection InitSerilog(this IServiceCollection serviceCollection) + { + Log.Logger = new LoggerConfiguration() + .ReadFrom.Configuration(serviceCollection.BuildServiceProvider().GetService()) + .Enrich.FromLogContext() + .WriteTo.File( + Path.Combine(Program.AppDataRoot, "logs", "appLog.log"), + fileSizeLimitBytes: 10*1024*1024, + rollOnFileSizeLimit: true, + rollingInterval: RollingInterval.Day) + .CreateLogger(); + + return serviceCollection; + } } } diff --git a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs index c61f519..910bc48 100644 --- a/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs +++ b/src/GuiApp/FileTime.Avalonia/ViewModels/MainPageViewModel.cs @@ -26,6 +26,7 @@ using FileTime.Core.Providers; using Syroot.Windows.IO; using FileTime.Avalonia.IconProviders; using Avalonia.Threading; +using Microsoft.Extensions.Logging; namespace FileTime.Avalonia.ViewModels { @@ -34,6 +35,7 @@ namespace FileTime.Avalonia.ViewModels [Inject(typeof(AppState), PropertyAccessModifier = AccessModifier.Public)] [Inject(typeof(StatePersistenceService), PropertyName = "StatePersistence", PropertyAccessModifier = AccessModifier.Public)] [Inject(typeof(ItemNameConverterService))] + [Inject(typeof(ILogger), PropertyName = "_logger")] public partial class MainPageViewModel : IMainPageViewModelBase { const string RAPIDTRAVEL = "rapidTravel"; @@ -87,6 +89,7 @@ namespace FileTime.Avalonia.ViewModels async partial void OnInitialize() { + _logger?.LogInformation($"Starting {nameof(MainPageViewModel)} initialization..."); _clipboard = App.ServiceProvider.GetService()!; _timeRunner = App.ServiceProvider.GetService()!; _contentProviders = App.ServiceProvider.GetService>()!; @@ -205,6 +208,7 @@ namespace FileTime.Avalonia.ViewModels Places = places; await Task.Delay(100); Loading = false; + _logger?.LogInformation($"{nameof(MainPageViewModel)} initialized."); } private void UpdateParalellCommands(object? sender, EventArgs e) diff --git a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs index d1b71cb..1d2cd81 100644 --- a/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs +++ b/src/GuiApp/FileTime.Avalonia/Views/MainWindow.axaml.cs @@ -7,6 +7,7 @@ using FileTime.Avalonia.Misc; using FileTime.Avalonia.Models; using FileTime.Avalonia.ViewModels; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using System; using System.Linq; @@ -14,6 +15,7 @@ namespace FileTime.Avalonia.Views { public partial class MainWindow : Window { + private readonly ILogger? _logger; public MainPageViewModel? ViewModel { get => DataContext as MainPageViewModel; @@ -30,6 +32,8 @@ namespace FileTime.Avalonia.Views public MainWindow() { + _logger = App.ServiceProvider.GetService>(); + _logger?.LogInformation($"Starting {nameof(MainWindow)} initialization..."); InitializeComponent(); #if DEBUG this.AttachDevTools(); @@ -123,6 +127,7 @@ namespace FileTime.Avalonia.Views { if (ViewModel is not MainPageViewModel) { + _logger?.LogInformation($"{nameof(MainWindow)} opened, starting {nameof(MainPageViewModel)} initialization..."); ViewModel = App.ServiceProvider.GetService(); } } diff --git a/src/GuiApp/FileTime.Avalonia/appsettings.Development.json b/src/GuiApp/FileTime.Avalonia/appsettings.Development.json new file mode 100644 index 0000000..d9ffaae --- /dev/null +++ b/src/GuiApp/FileTime.Avalonia/appsettings.Development.json @@ -0,0 +1,11 @@ +{ + "Serilog": { + "MinimalLevel": { + "Default": "Debug", + "Override": { + "Microsoft": "Information", + "System": "Warning" + } + } + } +}