diff --git a/Design/States.drawio b/Design/States.drawio index e5b430b..5f1ca7a 100644 --- a/Design/States.drawio +++ b/Design/States.drawio @@ -1,6 +1,6 @@ - + - + @@ -477,15 +477,27 @@ - + - + + + + + + + + + + + + + @@ -737,13 +749,13 @@ - + - + diff --git a/FlightRecorder.Client.Logics/IDialogLogic.cs b/FlightRecorder.Client.Logics/IDialogLogic.cs new file mode 100644 index 0000000..4cf0e9a --- /dev/null +++ b/FlightRecorder.Client.Logics/IDialogLogic.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace FlightRecorder.Client.Logics; + +public interface IDialogLogic +{ + bool Confirm(string message); + void Error(string error); + Task SaveAsync(SavedData data); + Task<(string? fileName, SavedData? data)> LoadAsync(); +} diff --git a/FlightRecorder.Client.Logics/RecorderLogic.cs b/FlightRecorder.Client.Logics/RecorderLogic.cs index fddf969..1d23aba 100644 --- a/FlightRecorder.Client.Logics/RecorderLogic.cs +++ b/FlightRecorder.Client.Logics/RecorderLogic.cs @@ -4,97 +4,99 @@ using System.Collections.Generic; using System.Diagnostics; -namespace FlightRecorder.Client.Logics +namespace FlightRecorder.Client.Logics; + +public class RecorderLogic : IRecorderLogic, IDisposable { - public class RecorderLogic : IRecorderLogic, IDisposable - { - public event EventHandler? RecordsUpdated; + public event EventHandler? RecordsUpdated; - private readonly ILogger logger; - private readonly IConnector connector; - private readonly Stopwatch stopwatch = new(); + private readonly ILogger logger; + private readonly IConnector connector; + private readonly Stopwatch stopwatch = new(); - private long? startMilliseconds; - private long? endMilliseconds; - private SimStateStruct startState; - private List<(long milliseconds, AircraftPositionStruct position)> records = new(); + private long? startMilliseconds; + private long? endMilliseconds; + private SimStateStruct startState; + private List<(long milliseconds, AircraftPositionStruct position)> records = new(); - private SimStateStruct simState; + private SimStateStruct simState; - private bool IsStarted => startMilliseconds.HasValue && records != null; - private bool IsEnded => startMilliseconds.HasValue && endMilliseconds.HasValue; + private bool IsStarted => startMilliseconds.HasValue && records != null; + private bool IsEnded => startMilliseconds.HasValue && endMilliseconds.HasValue; - public RecorderLogic(ILogger logger, IConnector connector) - { - logger.LogDebug("Creating instance of {class}", nameof(RecorderLogic)); - this.logger = logger; - this.connector = connector; + public RecorderLogic(ILogger logger, IConnector connector) + { + logger.LogDebug("Creating instance of {class}", nameof(RecorderLogic)); + this.logger = logger; + this.connector = connector; - connector.SimStateUpdated += Connector_SimStateUpdated; - } + connector.SimStateUpdated += Connector_SimStateUpdated; + } - public void Dispose() - { - logger.LogDebug("Disposing {class}", nameof(RecorderLogic)); - Dispose(true); - GC.SuppressFinalize(this); - } + public void Dispose() + { + logger.LogDebug("Disposing {class}", nameof(RecorderLogic)); + Dispose(true); + GC.SuppressFinalize(this); + } - protected virtual void Dispose(bool disposing) + protected virtual void Dispose(bool disposing) + { + if (disposing) { - if (disposing) - { - connector.SimStateUpdated -= Connector_SimStateUpdated; - } + connector.SimStateUpdated -= Connector_SimStateUpdated; } + } - private void Connector_SimStateUpdated(object? sender, SimStateUpdatedEventArgs e) - { - simState = e.State; - } + private void Connector_SimStateUpdated(object? sender, SimStateUpdatedEventArgs e) + { + simState = e.State; + } - #region Public Functions + #region Public Functions - public void Initialize() - { - logger.LogDebug("Initializing recorder..."); + public void Initialize() + { + logger.LogDebug("Initializing recorder..."); - stopwatch.Start(); - } + stopwatch.Start(); + } - public void Record() - { - logger.LogInformation("Start recording..."); + public void Record() + { + logger.LogInformation("Start recording..."); - startMilliseconds = stopwatch.ElapsedMilliseconds; - endMilliseconds = null; - startState = simState; - records = new List<(long milliseconds, AircraftPositionStruct position)>(); - } + startMilliseconds = stopwatch.ElapsedMilliseconds; + endMilliseconds = null; + startState = simState; + records = new List<(long milliseconds, AircraftPositionStruct position)>(); + } - public void StopRecording() + public void StopRecording() + { + if (endMilliseconds == null) { endMilliseconds = stopwatch.ElapsedMilliseconds; logger.LogDebug("Recording stopped. {totalFrames} frames recorded.", records.Count); } + } - public void NotifyPosition(AircraftPositionStruct? value) - { - if (IsStarted && !IsEnded && value.HasValue) - { - records.Add((stopwatch.ElapsedMilliseconds, value.Value)); - RecordsUpdated?.Invoke(this, new(null, startState.AircraftTitle, records.Count)); - } - } - - public SavedData ToData(string clientVersion) + public void NotifyPosition(AircraftPositionStruct? value) + { + if (IsStarted && !IsEnded && value.HasValue) { - if (startMilliseconds == null) throw new InvalidOperationException("Cannot get data before started recording!"); - if (endMilliseconds == null) throw new InvalidOperationException("Cannot get data before finished recording!"); - return new(clientVersion, startMilliseconds.Value, endMilliseconds.Value, startState, records); + records.Add((stopwatch.ElapsedMilliseconds, value.Value)); + RecordsUpdated?.Invoke(this, new(null, startState.AircraftTitle, records.Count)); } + } - #endregion + public SavedData ToData(string clientVersion) + { + if (startMilliseconds == null) throw new InvalidOperationException("Cannot get data before started recording!"); + if (endMilliseconds == null) throw new InvalidOperationException("Cannot get data before finished recording!"); + return new(clientVersion, startMilliseconds.Value, endMilliseconds.Value, startState, records); } + + #endregion } diff --git a/FlightRecorder.Client.ViewModels/ICrashLogic.cs b/FlightRecorder.Client.ViewModels/ICrashLogic.cs new file mode 100644 index 0000000..ac46d52 --- /dev/null +++ b/FlightRecorder.Client.ViewModels/ICrashLogic.cs @@ -0,0 +1,13 @@ +using FlightRecorder.Client.Logics; +using System.Threading.Tasks; + +namespace FlightRecorder.Client; + +/// +/// Handle emergency save on crash +/// +public interface ICrashLogic +{ + Task LoadDataAsync(StateMachine stateMachine, IReplayLogic replayLogic); + void SaveData(); +} diff --git a/FlightRecorder.Client.ViewModels/IDialogLogic.cs b/FlightRecorder.Client.ViewModels/IDialogLogic.cs deleted file mode 100644 index 6b07755..0000000 --- a/FlightRecorder.Client.ViewModels/IDialogLogic.cs +++ /dev/null @@ -1,13 +0,0 @@ -using FlightRecorder.Client.Logics; -using System.Threading.Tasks; - -namespace FlightRecorder.Client -{ - public interface IDialogLogic - { - bool Confirm(string message); - void Error(string error); - Task SaveAsync(SavedData data); - Task<(string? fileName, SavedData? data)> LoadAsync(); - } -} diff --git a/FlightRecorder.Client.ViewModels/StateMachine.cs b/FlightRecorder.Client.ViewModels/StateMachine.cs index d940be2..8b4221d 100644 --- a/FlightRecorder.Client.ViewModels/StateMachine.cs +++ b/FlightRecorder.Client.ViewModels/StateMachine.cs @@ -31,6 +31,7 @@ public enum Event Save, Load, LoadAI, + RestoreCrashData, Exit } @@ -71,6 +72,8 @@ private void InitializeStateMachine() { Register(Transition.From(State.Start).To(State.DisconnectedEmpty).By(Event.StartUp).ThenUpdate(viewModel)); + Register(Transition.From(State.DisconnectedEmpty).To(State.DisconnectedUnsaved).By(Event.RestoreCrashData).ThenUpdate(viewModel)); + Register(Transition.From(State.DisconnectedEmpty).To(State.IdleEmpty).By(Event.Connect).Then(Connect).ThenUpdate(viewModel)); Register(Transition.From(State.DisconnectedEmpty).To(State.LoadingDisconnected).By(Event.RequestLoading).ThenUpdate(viewModel)); Register(Transition.From(State.DisconnectedEmpty).To(State.End).By(Event.Exit)); // NO-OP diff --git a/FlightRecorder.Client.ViewModels/StateMachineCore.cs b/FlightRecorder.Client.ViewModels/StateMachineCore.cs index b09b776..4fc1150 100644 --- a/FlightRecorder.Client.ViewModels/StateMachineCore.cs +++ b/FlightRecorder.Client.ViewModels/StateMachineCore.cs @@ -1,4 +1,5 @@ -using FlightRecorder.Client.ViewModels.States; +using FlightRecorder.Client.Logics; +using FlightRecorder.Client.ViewModels.States; using Microsoft.Extensions.Logging; using System; using System.Collections.Concurrent; @@ -7,224 +8,223 @@ using System.Threading.Tasks; using static FlightRecorder.Client.StateMachine; -namespace FlightRecorder.Client +namespace FlightRecorder.Client; + +public abstract class StateMachineCore { - public abstract class StateMachineCore - { - public event EventHandler? StateChanged; + public event EventHandler? StateChanged; - protected readonly ILogger logger; - protected readonly IDialogLogic dialogLogic; - private readonly MainViewModel viewModel; - private readonly ConcurrentDictionary> waitingTasks = new(); - private readonly Dictionary> stateLogics = new(); + protected readonly ILogger logger; + protected readonly IDialogLogic dialogLogic; + private readonly MainViewModel viewModel; + private readonly ConcurrentDictionary> waitingTasks = new(); + private readonly Dictionary> stateLogics = new(); - /// - /// This is to store the events that trigger during another long running transition, - /// so that if the long running transition is reverted, we can replay those events. - /// - private List? transitioningEvents = null; + /// + /// This is to store the events that trigger during another long running transition, + /// so that if the long running transition is reverted, we can replay those events. + /// + private List? transitioningEvents = null; - public State CurrentState { get; private set; } = State.Start; + public State CurrentState { get; private set; } = State.Start; - public StateMachineCore(ILogger logger, IDialogLogic dialogLogic, MainViewModel viewModel) - { - this.logger = logger; - this.dialogLogic = dialogLogic; - this.viewModel = viewModel; - } + public StateMachineCore(ILogger logger, IDialogLogic dialogLogic, MainViewModel viewModel) + { + this.logger = logger; + this.dialogLogic = dialogLogic; + this.viewModel = viewModel; + } - /// - /// - /// - /// True to indicate that user did not cancel any prompt - public async Task TransitAsync(Event e) - { - logger.LogDebug("Triggering event {event} from state {state}", e, CurrentState); + /// + /// + /// + /// True to indicate that user did not cancel any prompt + public async Task TransitAsync(Event e) + { + logger.LogDebug("Triggering event {event} from state {state}", e, CurrentState); - if (stateLogics.TryGetValue(CurrentState, out var transitions) && transitions.TryGetValue(e, out var transition)) + if (stateLogics.TryGetValue(CurrentState, out var transitions) && transitions.TryGetValue(e, out var transition)) + { + if (transitioningEvents != null && !waitingTasks.Keys.Contains(e)) { - if (transitioningEvents != null && !waitingTasks.Keys.Contains(e)) - { - logger.LogInformation("{event} is triggered from {state} during a multiple transition.", e, CurrentState); - transitioningEvents.Add(e); - } + logger.LogInformation("{event} is triggered from {state} during a multiple transition.", e, CurrentState); + transitioningEvents.Add(e); + } - if (transition.ViaEvents != null) - { - return await ExecuteMultipleTransitionsAsync(e, transition.ViaEvents, transition.WaitForEvents, transition.RevertErrorMessage); - } - else - { - return await ExecuteSingleTransitionAsync(e, transition); - } + if (transition.ViaEvents != null) + { + return await ExecuteMultipleTransitionsAsync(e, transition.ViaEvents, transition.WaitForEvents, transition.RevertErrorMessage); } else { - logger.LogError("Cannot trigger {event} from {state}", e, CurrentState); - throw new InvalidOperationException($"Cannot trigger {e} from {CurrentState}!"); + return await ExecuteSingleTransitionAsync(e, transition); } } - - protected async Task ExecuteSingleTransitionAsync(Event originatingEvent, Transition transition) + else { - var oldState = CurrentState; - var resultingState = await transition.ExecuteAsync(); + logger.LogError("Cannot trigger {event} from {state}", e, CurrentState); + throw new InvalidOperationException($"Cannot trigger {e} from {CurrentState}!"); + } + } - var success = true; + protected async Task ExecuteSingleTransitionAsync(Event originatingEvent, Transition transition) + { + var oldState = CurrentState; + var resultingState = await transition.ExecuteAsync(); - if (resultingState.HasValue) - { - // TODO: might need to suppress this if waitingTask is applicable - // to avoid race condition with state setting in the multiple transition - CurrentState = resultingState.Value; + var success = true; - StateChanged?.Invoke(this, new StateChangedEventArgs(oldState, resultingState.Value, originatingEvent)); - } - else - { - success = false; - } + if (resultingState.HasValue) + { + // TODO: might need to suppress this if waitingTask is applicable + // to avoid race condition with state setting in the multiple transition + CurrentState = resultingState.Value; - logger.LogInformation("Triggered event {event} from state {state} to {resultingState}", originatingEvent, oldState, resultingState); + StateChanged?.Invoke(this, new StateChangedEventArgs(oldState, resultingState.Value, originatingEvent)); + } + else + { + success = false; + } - if (waitingTasks.TryGetValue(originatingEvent, out var waitingTask)) - { - waitingTask.SetResult(CurrentState); - } + logger.LogInformation("Triggered event {event} from state {state} to {resultingState}", originatingEvent, oldState, resultingState); - return success; + if (waitingTasks.TryGetValue(originatingEvent, out var waitingTask)) + { + waitingTask.SetResult(CurrentState); } - /// - /// Handle the case when a single event should be resolved into multiple events to leverage other existing transition. - /// - /// The event that is triggered externally - /// The events that state machine should triggered instead - /// Some events in the viaEvents list should not be triggered by the state machine itself. Instead, the state machine should wait for the event to be triggered externally before continue with the viaEvents list. - /// Revert state if there is a an error and show the error message - protected async Task ExecuteMultipleTransitionsAsync(Event originatingEvent, Event[] viaEvents, Event[]? waitForEvents, string? revertErrorMessage) - { - var success = true; + return success; + } - var originalState = CurrentState; + /// + /// Handle the case when a single event should be resolved into multiple events to leverage other existing transition. + /// + /// The event that is triggered externally + /// The events that state machine should triggered instead + /// Some events in the viaEvents list should not be triggered by the state machine itself. Instead, the state machine should wait for the event to be triggered externally before continue with the viaEvents list. + /// Revert state if there is a an error and show the error message + protected async Task ExecuteMultipleTransitionsAsync(Event originatingEvent, Event[] viaEvents, Event[]? waitForEvents, string? revertErrorMessage) + { + var success = true; + + var originalState = CurrentState; - if (transitioningEvents != null) + if (transitioningEvents != null) + { + logger.LogError("{event} is triggered when another multiple transition is happening!", originatingEvent); + } + + var localTransitioningEvents = new List(); + transitioningEvents = localTransitioningEvents; + try + { + // NOTE: we have to initialize all the waiting tasks here to prevent concurrency issue + // when the waiting event triggered before the waiting task is initialized + if (waitForEvents != null) { - logger.LogError("{event} is triggered when another multiple transition is happening!", originatingEvent); + foreach (var waitForEvent in waitForEvents) + { + var tcs = new TaskCompletionSource(); + waitingTasks.TryAdd(waitForEvent, tcs); + } } - var localTransitioningEvents = new List(); - transitioningEvents = localTransitioningEvents; - try + foreach (var via in viaEvents) { - // NOTE: we have to initialize all the waiting tasks here to prevent concurrency issue - // when the waiting event triggered before the waiting task is initialized - if (waitForEvents != null) + logger.LogDebug("Processing {via} at state {state} due to {event}.", via, CurrentState, originatingEvent); + if (waitForEvents != null && waitForEvents.Contains(via)) { - foreach (var waitForEvent in waitForEvents) + // This event is completed asynchronously in another thread + if (waitingTasks.TryGetValue(via, out var waitingTask)) { - var tcs = new TaskCompletionSource(); - waitingTasks.TryAdd(waitForEvent, tcs); + logger.LogInformation("Waiting for {via} at state {state} due to {event}.", via, CurrentState, originatingEvent); + await waitingTask.Task; + logger.LogInformation("Finished waiting for {via} at state {state} due to {event}.", via, CurrentState, originatingEvent); + + waitingTasks.Remove(via, out _); + } + else + { + throw new InvalidOperationException($"Cannot find Task for {via}!"); } } - - foreach (var via in viaEvents) + else { - logger.LogDebug("Processing {via} at state {state} due to {event}.", via, CurrentState, originatingEvent); - if (waitForEvents != null && waitForEvents.Contains(via)) + // TODO: maybe recurse here? + + var oldState = CurrentState; + var resultingState = await stateLogics[oldState][via].ExecuteAsync(); + if (resultingState.HasValue) { - // This event is completed asynchronously in another thread - if (waitingTasks.TryGetValue(via, out var waitingTask)) - { - logger.LogInformation("Waiting for {via} at state {state} due to {event}.", via, CurrentState, originatingEvent); - await waitingTask.Task; - logger.LogInformation("Finished waiting for {via} at state {state} due to {event}.", via, CurrentState, originatingEvent); + CurrentState = resultingState.Value; - waitingTasks.Remove(via, out _); - } - else - { - throw new InvalidOperationException($"Cannot find Task for {via}!"); - } + StateChanged?.Invoke(this, new StateChangedEventArgs(oldState, resultingState.Value, via)); } else { - // TODO: maybe recurse here? - - var oldState = CurrentState; - var resultingState = await stateLogics[oldState][via].ExecuteAsync(); - if (resultingState.HasValue) - { - CurrentState = resultingState.Value; + success = false; + } + logger.LogInformation("Triggered event {via} due to {event} from state {state} to {resultingState}", via, originatingEvent, oldState, resultingState); - StateChanged?.Invoke(this, new StateChangedEventArgs(oldState, resultingState.Value, via)); - } - else + if (!success) + { + if (revertErrorMessage != null) { - success = false; + logger.LogInformation("Transition from {state} by {via} was cancelled! Revert back to orignal state {original}.", oldState, via, originalState); + await RevertStateAsync(originalState, originatingEvent, localTransitioningEvents); } - logger.LogInformation("Triggered event {via} due to {event} from state {state} to {resultingState}", via, originatingEvent, oldState, resultingState); - if (!success) - { - if (revertErrorMessage != null) - { - logger.LogInformation("Transition from {state} by {via} was cancelled! Revert back to orignal state {original}.", oldState, via, originalState); - await RevertStateAsync(originalState, originatingEvent, localTransitioningEvents); - } - - break; - } + break; } } } - catch (Exception ex) when (revertErrorMessage != null) - { - logger.LogError(ex, "Cannot complete the transition from {state} by {event}! Revert back to orignal state.", originalState, originatingEvent); - await RevertStateAsync(originalState, originatingEvent, localTransitioningEvents); - dialogLogic.Error(revertErrorMessage); - } - finally - { - transitioningEvents = null; - } - - return success; } - - protected async Task RevertStateAsync(State originalState, Event originatingEvent, List localTransitioningEvents) + catch (Exception ex) when (revertErrorMessage != null) { - logger.LogInformation("Reverting to {originalState} from transition by {originatingEvent}", originalState, originatingEvent); + logger.LogError(ex, "Cannot complete the transition from {state} by {event}! Revert back to orignal state.", originalState, originatingEvent); + await RevertStateAsync(originalState, originatingEvent, localTransitioningEvents); + dialogLogic.Error(revertErrorMessage); + } + finally + { + transitioningEvents = null; + } - var oldState = CurrentState; - CurrentState = originalState; - StateChanged?.Invoke(this, new StateChangedEventArgs(oldState, originalState, originatingEvent)); - viewModel.State = originalState; + return success; + } + + protected async Task RevertStateAsync(State originalState, Event originatingEvent, List localTransitioningEvents) + { + logger.LogInformation("Reverting to {originalState} from transition by {originatingEvent}", originalState, originatingEvent); - if (localTransitioningEvents != null) + var oldState = CurrentState; + CurrentState = originalState; + StateChanged?.Invoke(this, new StateChangedEventArgs(oldState, originalState, originatingEvent)); + viewModel.State = originalState; + + if (localTransitioningEvents != null) + { + transitioningEvents = null; + foreach (var e in localTransitioningEvents) { - transitioningEvents = null; - foreach (var e in localTransitioningEvents) - { - logger.LogInformation("Replay {event} from {state}", e, CurrentState); - await TransitAsync(e); - } + logger.LogInformation("Replay {event} from {state}", e, CurrentState); + await TransitAsync(e); } } + } - protected void Register(Transition logic) + protected void Register(Transition logic) + { + if (!stateLogics.ContainsKey(logic.FromState)) { - if (!stateLogics.ContainsKey(logic.FromState)) - { - stateLogics.Add(logic.FromState, new()); - } - var fromLogic = stateLogics[logic.FromState]; - if (fromLogic.ContainsKey(logic.ByEvent)) - { - throw new InvalidOperationException($"There is already a transition from {logic.FromState} that use {logic.ByEvent} (to {fromLogic[logic.ByEvent].ToState})!"); - } - fromLogic.Add(logic.ByEvent, logic); + stateLogics.Add(logic.FromState, new()); + } + var fromLogic = stateLogics[logic.FromState]; + if (fromLogic.ContainsKey(logic.ByEvent)) + { + throw new InvalidOperationException($"There is already a transition from {logic.FromState} that use {logic.ByEvent} (to {fromLogic[logic.ByEvent].ToState})!"); } + fromLogic.Add(logic.ByEvent, logic); } } diff --git a/FlightRecorder.Client/App.xaml b/FlightRecorder.Client/App.xaml index a561c1b..570ec85 100644 --- a/FlightRecorder.Client/App.xaml +++ b/FlightRecorder.Client/App.xaml @@ -2,7 +2,8 @@ xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="clr-namespace:FlightRecorder.Client.Converters" - xmlns:local="clr-namespace:FlightRecorder.Client"> + xmlns:local="clr-namespace:FlightRecorder.Client" + DispatcherUnhandledException="Application_DispatcherUnhandledException"> diff --git a/FlightRecorder.Client/App.xaml.cs b/FlightRecorder.Client/App.xaml.cs index f04a881..c1359c2 100644 --- a/FlightRecorder.Client/App.xaml.cs +++ b/FlightRecorder.Client/App.xaml.cs @@ -6,120 +6,138 @@ using Microsoft.Extensions.DependencyInjection; using Serilog; using Serilog.Sink.AppCenter; +using System; using System.Linq; using System.Windows; -namespace FlightRecorder.Client +namespace FlightRecorder.Client; + +/// +/// Interaction logic for App.xaml +/// +public partial class App : Application { - /// - /// Interaction logic for App.xaml - /// - public partial class App : Application - { - #region Single Instance Enforcer + #region Single Instance Enforcer - readonly SingletonApplicationEnforcer enforcer = new SingletonApplicationEnforcer(args => + readonly SingletonApplicationEnforcer enforcer = new SingletonApplicationEnforcer(args => + { + Current.Dispatcher.Invoke(() => { - Current.Dispatcher.Invoke(() => + var mainWindow = Current.MainWindow as MainWindow; + if (mainWindow != null && args != null) { - var mainWindow = Current.MainWindow as MainWindow; - if (mainWindow != null && args != null) - { - mainWindow.RestoreWindow(); - } - }); - }, "FlightRecorder.Client"); + mainWindow.RestoreWindow(); + } + }); + }, "FlightRecorder.Client"); - #endregion + #endregion - public ServiceProvider? ServiceProvider { get; private set; } + public ServiceProvider? ServiceProvider { get; private set; } - protected override void OnStartup(StartupEventArgs e) + protected override void OnStartup(StartupEventArgs e) + { + if (!e.Args.Contains("--multiple-instances") && enforcer.ShouldApplicationExit()) { - if (!e.Args.Contains("--multiple-instances") && enforcer.ShouldApplicationExit()) + try { - try - { - Shutdown(); - } - catch { } + Shutdown(); } + catch { } + } #if DEBUG - AppCenter.Start("cd6afedd-0333-4624-af0c-91eaf5273f15", typeof(Analytics), typeof(Crashes)); + AppCenter.Start("cd6afedd-0333-4624-af0c-91eaf5273f15", typeof(Analytics), typeof(Crashes)); #else - AppCenter.Start("5525090f-eddc-4bca-bdd9-5b5fdc301ed0", typeof(Analytics), typeof(Crashes)); + AppCenter.Start("5525090f-eddc-4bca-bdd9-5b5fdc301ed0", typeof(Analytics), typeof(Crashes)); #endif - var serviceCollection = new ServiceCollection(); - ConfigureServices(serviceCollection); + var serviceCollection = new ServiceCollection(); + ConfigureServices(serviceCollection); - ServiceProvider = serviceCollection.BuildServiceProvider(); + ServiceProvider = serviceCollection.BuildServiceProvider(); - MainWindow = ServiceProvider.GetRequiredService().Create(ServiceProvider); - MainWindow.Show(); + MainWindow = ServiceProvider.GetRequiredService().Create(ServiceProvider); + MainWindow.Show(); + } + + protected override void OnExit(ExitEventArgs e) + { + try + { + // This create a new instance of ReplayLogic, which is not desirable. + // However, it can still unfreeze the aircraft. + var replayLogic = ServiceProvider?.GetService(); + replayLogic?.Unfreeze(); + } + catch + { + // Ignore } - protected override void OnExit(ExitEventArgs e) + if (Log.Logger != null) { - try - { - // This create a new instance of ReplayLogic, which is not desirable. - // However, it can still unfreeze the aircraft. - var replayLogic = ServiceProvider?.GetService(); - replayLogic?.Unfreeze(); - } - catch - { - // Ignore - } + Log.CloseAndFlush(); + } - if (Log.Logger != null) - { - Log.CloseAndFlush(); - } + base.OnExit(e); + } - base.OnExit(e); - } + private void ConfigureServices(ServiceCollection services) + { + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Debug() + .WriteTo.Logger(config => config + .MinimumLevel.Information() + .WriteTo.File("flightrecorder.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 3, buffered: true) + ) + .WriteTo.Logger(config => config + .MinimumLevel.Information() + .WriteTo.AppCenterSink(target: AppCenterTarget.ExceptionsAsEvents) + ) + .CreateLogger(); + + services.AddLogging(configure => + { + configure.AddSerilog(); + }); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + + services.AddScoped(); + services.AddScoped(); + + // NOTE: For recorder, we leave it as singleton as it is supported only on main windows (not AI) and to allow saving on crash. + services.AddSingleton(); + // NOTE: For replay, we set it as scoped as we need to create a new instance for each window. + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddTransient(); + services.AddTransient(); + services.AddTransient(); + } - private void ConfigureServices(ServiceCollection services) + private void Application_DispatcherUnhandledException(object sender, System.Windows.Threading.DispatcherUnhandledExceptionEventArgs e) + { + try { - Log.Logger = new LoggerConfiguration() - .MinimumLevel.Debug() - .WriteTo.Debug() - .WriteTo.Logger(config => config - .MinimumLevel.Information() - .WriteTo.File("flightrecorder.log", rollingInterval: RollingInterval.Day, retainedFileCountLimit: 3, buffered: true) - ) - .WriteTo.Logger(config => config - .MinimumLevel.Information() - .WriteTo.AppCenterSink(target: AppCenterTarget.ExceptionsAsEvents) - ) - .CreateLogger(); - - services.AddLogging(configure => - { - configure.AddSerilog(); - }); - - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); + var crashLogic = ServiceProvider?.GetRequiredService(); + crashLogic?.SaveData(); + } + catch + { + // Ignore } } } diff --git a/FlightRecorder.Client/CrashLogic.cs b/FlightRecorder.Client/CrashLogic.cs new file mode 100644 index 0000000..b796c68 --- /dev/null +++ b/FlightRecorder.Client/CrashLogic.cs @@ -0,0 +1,100 @@ +using FlightRecorder.Client.Logics; +using ICSharpCode.SharpZipLib.Zip; +using Microsoft.Extensions.Logging; +using System; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; + +namespace FlightRecorder.Client; + +public class CrashLogic : ICrashLogic +{ + private const string CrashFileName = "crashed_flight.dat"; + private readonly ILogger logger; + private readonly IRecorderLogic recorderLogic; + private readonly IDialogLogic dialogLogic; + private readonly VersionLogic versionLogic; + + public CrashLogic(ILogger logger, IRecorderLogic recorderLogic, IDialogLogic dialogLogic, VersionLogic versionLogic) + { + this.logger = logger; + this.recorderLogic = recorderLogic; + this.dialogLogic = dialogLogic; + this.versionLogic = versionLogic; + } + + public async Task LoadDataAsync(StateMachine stateMachine, IReplayLogic replayLogic) + { + if (File.Exists(CrashFileName)) + { + logger.LogInformation("Crash file detected"); + + if (!dialogLogic.Confirm("An auto-save from a previous Flight Recorder crash was found. Do you want to load it?")) + { + CleanUp(); + return; + } + + using (var file = File.OpenRead(CrashFileName)) + { + using var zipFile = new ZipFile(file); + + foreach (ZipEntry entry in zipFile) + { + if (entry.IsFile && entry.Name == "data.json") + { + var stream = zipFile.GetInputStream(entry); + + var crashData = await JsonSerializer.DeserializeAsync(stream); + + if (crashData == null) + { + logger.LogWarning("Crash data is null."); + dialogLogic.Error("Cannot load auto-save flight!"); + return; + } + + replayLogic.FromData(null, crashData); + logger.LogDebug("Loaded crash file"); + + await stateMachine.TransitAsync(StateMachine.Event.RestoreCrashData); + + break; + } + } + } + + CleanUp(); + } + } + + public void SaveData() + { + recorderLogic.StopRecording(); + var data = recorderLogic.ToData(versionLogic.GetVersion()); + using var fileStream = new FileStream(CrashFileName, FileMode.Create); + using var outStream = new ZipOutputStream(fileStream); + + outStream.SetLevel(9); + + var entry = new ZipEntry("data.json") { DateTime = DateTime.Now }; + outStream.PutNextEntry(entry); + + JsonSerializer.Serialize(outStream, data); + + outStream.Finish(); + } + + private void CleanUp() + { + try + { + File.Delete(CrashFileName); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Failed to delete crash file."); + } + } +} diff --git a/FlightRecorder.Client/MainWindow.xaml.cs b/FlightRecorder.Client/MainWindow.xaml.cs index f44c516..5a3adde 100644 --- a/FlightRecorder.Client/MainWindow.xaml.cs +++ b/FlightRecorder.Client/MainWindow.xaml.cs @@ -21,6 +21,7 @@ public partial class MainWindow : BaseWindow { private readonly ILogger logger; private readonly IConnector connector; + private readonly ICrashLogic crashLogic; private readonly DrawingLogic drawingLogic; private readonly ExportLogic exportLogic; private readonly WindowFactory windowFactory; @@ -32,6 +33,7 @@ public partial class MainWindow : BaseWindow public MainWindow(ILogger logger, IConnector connector, + ICrashLogic crashLogic, DrawingLogic drawingLogic, ExportLogic exportLogic, VersionLogic versionLogic, @@ -43,6 +45,7 @@ public partial class MainWindow : BaseWindow this.logger = logger; this.connector = connector; + this.crashLogic = crashLogic; this.drawingLogic = drawingLogic; this.exportLogic = exportLogic; this.windowFactory = windowFactory; @@ -73,6 +76,8 @@ protected async override Task Window_LoadedAsync(object sender, RoutedEventArgs { await base.Window_LoadedAsync(sender, e); + await crashLogic.LoadDataAsync(stateMachine, replayLogic); + // Create an event handle for the WPF window to listen for SimConnect events Handle = new WindowInteropHelper(sender as Window).Handle; // Get handle of main WPF Window var HandleSource = HwndSource.FromHwnd(Handle); // Get source of handle in order to add event handlers to it