using System; using System.Collections.Generic; using System.Data.Entity; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Text; using System.Threading; using System.Threading.Tasks; using Tango.BL; using Tango.BL.Entities; using Tango.Core; using Tango.Core.DI; using Tango.Core.ExtensionMethods; using Tango.Core.Threading; using Tango.Emulations.ExternalBridge; using Tango.FSE.BL; using Tango.FSE.Common; using Tango.FSE.Common.Authentication; using Tango.FSE.Common.Build; using Tango.FSE.Common.Connection; using Tango.FSE.Common.FSEApplication; using Tango.FSE.Common.Notifications; using Tango.FSE.UI.Dialogs; using Tango.Integration.ExternalBridge; using Tango.Integration.Operation; using Tango.Logging; using Tango.PMR.Integration; using Tango.SharedUI; using Tango.Transport; namespace Tango.FSE.UI.Connection { /// /// Represents the default implementation. /// /// /// public class DefaultMachineProvider : FSEExtendedObject, IMachineProvider { private List _eventRegistrations; private String _lastMachineSerialNumber; private DateTime _connectionStartTime; private System.Timers.Timer _connectionTimer; private FSEMachineEventsStateProvider _machineEventsStateProvider; private bool _autoReconnect; [TangoInject] private INotificationProvider NotificationProvider { get; set; } [TangoInject] private IAuthenticationProvider AuthenticationProvider { get; set; } [TangoInject] private FSEServicesContainer Services { get; set; } [TangoInject] private IBuildProvider BuildProvider { get; set; } #region Events /// /// Occurs when machine operator has connected. /// public event EventHandler MachineConnected; /// /// Occurs when the machine operator has disconnected. /// public event EventHandler MachineDisconnected; #endregion #region Properties private Machine _machine; /// /// Gets the database machine entity associated with the current machine. /// public Machine Machine { get { return _machine; } private set { _machine = value; RaisePropertyChangedAuto(); } } private bool _isConnected; /// /// Gets or sets a value indicating whether a machine is currently connected. /// public bool IsConnected { get { return _isConnected; } private set { _isConnected = value; RaisePropertyChangedAuto(); RaisePropertyChanged(nameof(ConnectionType)); RaisePropertyChanged(nameof(IsPPCAvailable)); } } /// /// Gets the current connected machine connection type. /// public MachineConnectionTypes ConnectionType { get { if (MachineOperator.GetType() == typeof(ExternalBridgeUsbClient)) { return MachineConnectionTypes.USB; } else if (MachineOperator.GetType() == typeof(ExternalBridgeTcpClient)) { return MachineConnectionTypes.Wifi; } else if (MachineOperator.GetType() == typeof(ExternalBridgeSignalRClient)) { return MachineConnectionTypes.SignalR; } else { return MachineConnectionTypes.Emulator; } } } /// /// Gets a value indicating whether the equals or . /// public bool IsPPCAvailable { get { return IsConnected && ConnectionType.IsRemote(); } } private TimeSpan _connectionTime; /// /// Gets the connection time span. /// public TimeSpan ConnectionTime { get { return _connectionTime; } set { _connectionTime = value; RaisePropertyChangedAuto(); } } private bool _isBusy; /// /// Gets a value indicating whether this instance is busy connecting/disconnecting. /// public bool IsBusy { get { return _isBusy; } private set { _isBusy = value; RaisePropertyChangedAuto(); } } private IExternalBridgeClient _machineOperator; /// /// Gets the machine operator. /// public IExternalBridgeClient MachineOperator { get { return _machineOperator; } private set { _machineOperator = value; RaisePropertyChangedAuto(); } } #endregion #region Constructors /// /// Initializes a new instance of the class. /// public DefaultMachineProvider() : base() { _machineEventsStateProvider = new FSEMachineEventsStateProvider(); MachineOperator = new ExternalBridgeTcpClient("N/A", "N/A"); MachineOperator.MachineEventsStateProvider = _machineEventsStateProvider; MachineOperator.StateChanged += MachineOperator_StateChanged; MachineOperator.As().SessionClosed += DefaultMachineProvider_SessionClosed; _connectionTimer = new System.Timers.Timer(1000); _connectionTimer.Elapsed += _connectionTimer_Elapsed; _connectionTimer.Start(); } #endregion #region Public Methods /// /// Connects to the specified machine. /// /// The machine. /// public async Task ConnectToMachine(IExternalBridgeClient machine) { try { _autoReconnect = false; LogManager.Log($"Connecting to machine '{machine.GetType().Name}' => '{machine.SerialNumber}' => '{machine.Adapter?.Address}'..."); IsBusy = true; MachineConnectionBaseViewVM vm = null; LogManager.Log("Invoking machine connection dialog..."); if (machine.GetType() == typeof(ExternalBridgeUsbClient)) { vm = await NotificationProvider.ShowDialog(new MachineConnectionUsbViewVM() { Machine = machine }); } else if (machine.GetType() == typeof(EmulatorExternalBridge)) { vm = await NotificationProvider.ShowDialog(new MachineConnectionEmulatorViewVM() { Machine = machine }); } else if (machine is ExternalBridgeTcpClient) { var secureClient = machine.As(); if (machine.GetType() == typeof(ExternalBridgeTcpClient)) { vm = await NotificationProvider.ShowDialog(new MachineConnectionWifiViewVM(secureClient)); _autoReconnect = (vm as MachineConnectionWifiViewVM).AutoReconnection; } else { vm = await NotificationProvider.ShowDialog(new MachineConnectionSignalRViewVM(secureClient)); } secureClient.LoginRequest = new ExternalBridgeLoginRequest() { AppID = BuildProvider.BuildName, HostName = Environment.MachineName, Password = (vm as MachineConnectionWifiViewVM).Password, Intent = (vm as MachineConnectionWifiViewVM).SelectedIntent, RequireSafetyLevelOperations = (vm as MachineConnectionWifiViewVM).RequireSafetyLevelOperations, UserGuid = AuthenticationProvider.CurrentUser.Guid, UserName = AuthenticationProvider.CurrentUser.Contact.FullName }; secureClient.ForceProtocolConfiguration = Settings.ForceExternalBridgeProtocolConfiguration; secureClient.ConfigureProtocolRequest = new ConfigureProtocolRequest() { EnableCompression = true, GenericProtocol = GenericMessageProtocol.Bson }; LogManager.Log($"Target machine requires a secure connection. Login Request:\n{secureClient.LoginRequest.ToJsonString()}"); } else { throw LogManager.Log(new NotSupportedException($"The specified type {machine.GetType().Name} is not supported.")); } if (vm.DialogResult) { if (IsConnected) { LogManager.Log("Another machine is currently connected. Disconnecting current machine..."); await DisconnectMachine(); } IsBusy = true; String serial = vm.GetMachineSerialNumber(); machine.SerialNumber = serial; using (var task = NotificationProvider.PushTaskItem($"Connecting to machine '{serial}'...")) { LogManager.Log("Retrieving full machine details..."); var machineDB = await Services.MachinesService.GetMachineFull(serial); if (machineDB == null) { throw LogManager.Log(new KeyNotFoundException("The specified machine was not found on the database.")); } LogManager.Log("Reassigning previous machine operator event handlers..."); ReassignEventHandlers(MachineOperator, machine); MachineOperator = machine; ApplyOperatorSettings(); LogManager.Log("Connecting..."); await MachineOperator.Connect(); Machine = machineDB; LogManager.Log("Machine successfully connected."); IsConnected = true; OnMachineConnected(MachineOperator); if (machine.GetType() == typeof(ExternalBridgeUsbClient) || machine.GetType() == typeof(EmulatorExternalBridge)) { LogManager.Log($"Connected machine is of type '{machine.GetType()}' requires uploading hardware configuration upon connection."); task.UpdateProgress("Uploading hardware configuration..."); await Task.Delay(1000); try { LogManager.Log("Uploading hardware configuration..."); await machine.UploadHardwareConfiguration(Machine.Configuration.HardwareVersion, Machine.Configuration); } catch (Exception ex) { LogManager.Log(ex, "Error uploading hardware configuration. The connection will remain but in an unsafe state."); await NotificationProvider.ShowWarning($"Error uploading hardware configuration.\n{ex.FlattenMessage()}"); } } return true; } } else { return false; } } catch { throw; } finally { IsBusy = false; } } /// /// Disconnects the current connected machine. /// /// public async Task DisconnectMachine() { if (IsConnected) { LogManager.Log($"Disconnecting currently connected machine..."); using (var task = NotificationProvider.PushTaskItem("Disconnecting from current machine...")) { try { await Task.Delay(1500); IsBusy = true; await MachineOperator.Disconnect(); } catch (Exception ex) { LogManager.Log(ex, "Error disconnecting machine."); } finally { IsConnected = false; IsBusy = false; Machine = null; OnMachineDisconnected(MachineOperator, null); } } } } /// /// Disconnects the currently connected machine and displays a "waiting for reconnection dialog" with a timeout. /// Useful when doing remote application restart. /// /// The amount of time to wait before starting reconnection attempts. /// The timeout for when to drop the reconnection attempt. /// The message to display to the user /// public async Task DisconnectAndWaitForReconnection(TimeSpan beginDelay, TimeSpan timeout, String message = null) { IsBusy = true; if (!_autoReconnect) { LogManager.Log("Starting disconnect and wait for machine procedure..."); LogManager.Log("Disconnecting from current machine..."); try { IsConnected = false; await MachineOperator.Disconnect(); LogManager.Log("Machine disconnected."); } catch (Exception ex) { LogManager.Log(ex, "Error disconnecting machine. Never mind..."); } finally { OnMachineDisconnected(MachineOperator, null); } } bool aborted = false; MachineWaitForConnectionViewVM vm = new MachineWaitForConnectionViewVM(MachineOperator, (int)timeout.TotalSeconds, message); vm.RequestCancel += (_, __) => { vm.BlockCancel = true; aborted = true; }; ThreadFactory.StartNew(() => { Thread.Sleep(beginDelay); while (!aborted) { LogManager.Log("Trying to reconnect (controlled disconnection)..."); try { MachineOperator.Connect().Wait(); IsConnected = true; OnMachineConnected(MachineOperator); LogManager.Log("Machine reconnected successfully. Closing dialog..."); InvokeUI(() => { vm.Close(false); }); break; } catch (Exception ex) { LogManager.Log(ex, "Error reconnecting to machine. Maybe next time..."); } if (aborted) break; Thread.Sleep(10000); //Try every 10 seconds.. } if (!IsConnected) { InvokeUI(() => { vm.Close(false); }); } }); System.Timers.Timer connectionTimer = new System.Timers.Timer(); connectionTimer.Interval = 1000; connectionTimer.Elapsed += (_, __) => { vm.RemainingSeconds--; if (vm.RemainingSeconds == 0 || IsConnected || aborted) { vm.BlockCancel = true; aborted = true; connectionTimer.Stop(); connectionTimer.Dispose(); } }; connectionTimer.Start(); await NotificationProvider.ShowDialog(vm); aborted = true; IsBusy = false; bool abortedByUser = vm.RemainingSeconds > 0; if (!IsConnected) { if (abortedByUser) { LogManager.Log("Auto reconnection aborted by user."); } else { LogManager.Log("Failed to auto-reconnect to the last machine (controlled disconnection).", LogCategory.Error); await NotificationProvider.ShowError($"Failed to reconnect to the last connected machine."); } } } #endregion #region Machine Operator Event Handlers private void MachineOperator_StateChanged(object sender, TransportComponentState state) { if (IsConnected && state == TransportComponentState.Failed) { IsConnected = false; LogManager.Log($"Current machine connection has failed and disconnected after a period of {ConnectionTime} with exception: {MachineOperator.FailedStateException.FlattenMessage()}"); LogManager.Log($"Trying reconnection procedure within {Settings.AutoMachineReconnectionTimeoutSeconds} seconds..."); OnMachineDisconnected(MachineOperator, MachineOperator.FailedStateException); if (_autoReconnect) return; InvokeUI(async () => { var vm = await NotificationProvider.ShowDialog(new MachineConnectionLostViewVM(MachineOperator, MachineOperator.FailedStateException.FlattenMessage(), Settings.AutoMachineReconnectionTimeoutSeconds)); if (vm.DialogResult) { try { LogManager.Log("Auto reconnection dialog accepted..."); IsBusy = true; using (var task = NotificationProvider.PushTaskItem($"Reconnecting to machine '{Machine.SerialNumber}'...")) { await Task.Delay(2000); await MachineOperator.Connect(); IsConnected = true; OnMachineConnected(MachineOperator); LogManager.Log("Machine reconnected successfully."); } } catch (Exception ex) { LogManager.Log(ex, "Failed to reconnect to the last machine."); await NotificationProvider.ShowError($"Failed to reconnect to the last connected machine.\n{ex.FlattenMessage()}"); } finally { IsBusy = false; } } else { LogManager.Log("Auto reconnection dialog canceled. Reconnection aborted."); } }); } } private void DefaultMachineProvider_SessionClosed(object sender, EventArgs e) { IsConnected = false; String message = "The connection has been terminated by the remote machine."; LogManager.Log(message); OnMachineDisconnected(MachineOperator, new Exception(message)); InvokeUI(() => { NotificationProvider.ShowError(message); }); } #endregion #region Event Handlers private void _connectionTimer_Elapsed(object sender, System.Timers.ElapsedEventArgs e) { if (IsConnected) { ConnectionTime = DateTime.Now - _connectionStartTime; } } #endregion #region Virtual Methods protected virtual void OnMachineConnected(IExternalBridgeClient machineOperator) { Settings.LastConnectedMachineType = (int)machineOperator.MachineType; MachineConnected?.Invoke(this, new MachineConnectedEventArgs() { MachineOperator = machineOperator, DifferentFromPrevious = machineOperator.SerialNumber != _lastMachineSerialNumber, }); _lastMachineSerialNumber = machineOperator.SerialNumber; _connectionStartTime = DateTime.Now; } protected virtual void OnMachineDisconnected(IExternalBridgeClient machineOperator, Exception exception) { machineOperator?.MachineEventsStateProvider.Reset(); LogManager.Log($"Machine disconnected. Total connection time: {DateTime.Now - _connectionStartTime}."); MachineDisconnected?.Invoke(this, new MachineDisconnectedEventArgs() { MachineOperator = machineOperator, Exception = exception }); if (_autoReconnect && exception != null) { LogManager.Log("Auto reconnection is activated, trying to reconnect."); InvokeUI(async () => { await DisconnectAndWaitForReconnection(TimeSpan.FromSeconds(10), TimeSpan.FromHours(1), "The connection with the remote machine has lost, trying to reconnect."); }); } } #endregion #region Private Methods private void ApplyOperatorSettings() { LogManager.Log("Applying machine operator settings..."); MachineOperator.MachineEventsStateProvider = _machineEventsStateProvider; MachineOperator.EnableDiagnostics = true; MachineOperator.EnableMachineStatusUpdates = true; MachineOperator.EnableEmbeddedDebugging = true; MachineOperator.EnableEventsNotification = true; MachineOperator.EnableJobResume = false; MachineOperator.UseKeepAlive = true; MachineOperator.JobUnitsMethod = JobUnitsMethods.Device; MachineOperator.JobRunsLogger.JobSource = Tango.BL.Enumerations.JobSource.Remote; MachineOperator.KeepAliveTimeout = TimeSpan.FromSeconds(5); MachineOperator.KeepAliveRetries = 4; MachineOperator.RequestTimeout = TimeSpan.FromSeconds(10); MachineOperator.ContinuousRequestTimeout = TimeSpan.FromSeconds(10); if (ConnectionType.IsRemote()) { ExternalBridgeTcpClient tcpClient = MachineOperator.As(); tcpClient.EnableApplicationLogs = true; tcpClient.InjectApplicationLogsToDefaultLogManager = false; tcpClient.LogEmbeddedDebuggingToFile = false; } } #endregion #region Machine Operator Event & Request Handlers Reassignment private class EventRegistration { public Delegate Handler { get; set; } public EventInfo EventInfo { get; set; } public void RemoveHandler(Object instance) { try { EventInfo.RemoveEventHandler(instance, Handler); } catch { Debug.WriteLine($"Cannot remove handler '{Handler.Method.Name}' for event '{instance.GetType().Name}.{EventInfo.Name}'."); } } public void AddHandler(Object instance) { try { EventInfo.AddEventHandler(instance, Handler); } catch { Debug.WriteLine($"Cannot add handler '{Handler.Method.Name}' for event '{instance.GetType().Name}.{EventInfo.Name}'."); } } } /// /// Reassigns the event handlers from the previous connected machine to a new one. /// /// The old instance. /// The new instance. /// private void ReassignEventHandlers(IMachineOperator oldInstance, IMachineOperator newInstance) { oldInstance.CopyRequestHandlers(newInstance); if (_eventRegistrations == null) { _eventRegistrations = GetEventsRegistrations(oldInstance); } foreach (var registration in _eventRegistrations) { registration.RemoveHandler(oldInstance); registration.AddHandler(newInstance); } } private List GetEventsRegistrations(IMachineOperator instance) { List registrations = new List(); var instanceEvents = instance.GetType().GetEvents(BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static).ToList(); foreach (var ev in instanceEvents) { if (ev.IsMulticast) { FieldInfo eventField = null; Type type = instance.GetType(); while (eventField == null) { eventField = type.GetField(ev.Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static); if (eventField == null) { type = type.BaseType; } if (type == typeof(Object)) { break; } } if (eventField == null) { throw new NullReferenceException($"Could not locate event field for '{ev.Name}'."); } var eventDelegate = (MulticastDelegate)type.GetField(ev.Name, BindingFlags.Public | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static).GetValue(instance); if (eventDelegate != null) { foreach (var handler in eventDelegate.GetInvocationList()) { registrations.Add(new EventRegistration() { EventInfo = ev, Handler = handler, }); } } } } return registrations; } #endregion #region Application Ready /// /// Called when is ready and user is logged-in. (happens every time a user logs-in) /// public void OnApplicationReady(IFSEApplicationManager applicationManager) { _machineEventsStateProvider.Init(); } #endregion } }