using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Windows.Forms; using DTS.Common.DASResource; using DTS.Common.Interface.DASFactory; using DTS.DASLib.Command.Classes; using DTS.DASLib.Service; using DTS.Common.Utilities.Logging; using DTS.DASLib.Command.SLICE.MulticastCommands; using DTS.Common.Utilities; using System.Net; using DTS.Common.Interface.Communication; using DTS.Common.Enums.Communication; using DTS.Common.Enums.DASFactory; using DTS.Common.Interface.StatusAndProgressBar; using System.Net.Sockets; using System.Threading.Tasks; using DTS.Common.Enums; using System.Collections.Concurrent; using static DTS.DASLib.DASFactory.DeviceHandling; using DASFactoryDb; using DTS.Common.Utils; using DTS.DASLib.Service.Classes.CAN; using DTS.DASLib.Connection; // ReSharper disable once CheckNamespace namespace DTS.DASLib.DASFactory { /// /// Delegate for event handler to handle the device events /// /// who? /// stuff public delegate void DASFactoryEventHandler(Object sender, DASFactoryEventArgs e); /// /// Our class for passing in custom arguments to our event handlers /// /// public class DASFactoryEventArgs : EventArgs { public string[] SerialNumbers { get; set; } public CommunicationConstantsAndEnums.CommunicationResult Result { get; set; } /// /// construct a new object /// /// das serial numbers /// communication result public DASFactoryEventArgs(string[] serialNumbers, CommunicationConstantsAndEnums.CommunicationResult result) { SerialNumbers = serialNumbers; Result = result; } } /// /// This exception is thrown when the user tries to run more than one instance. /// public class SingletonFaultException : ApplicationException { /// /// construct exception with a message /// /// description of error public SingletonFaultException(string msg) : base(msg) { } /// /// Construct an exception with message and throwing exception /// /// description of error /// exception causing error public SingletonFaultException(string msg, Exception inner) : base(msg, inner) { } } internal static class SocketExtension { public static Task ConnectAsync(this Socket socket, string host, int port) { if (socket == null) throw new ArgumentNullException("socket"); return Task.Factory.FromAsync(socket.BeginConnect, socket.EndConnect, host, port, null); } public static Task ReceiveAsync(this Socket socket, byte[] buffer, int offset, int size, SocketFlags socketFlags) { if (socket == null) throw new ArgumentNullException("socket"); return Task.Factory.FromAsync( (callback, state) => socket.BeginReceive(buffer, offset, size, socketFlags, callback, state), socket.EndReceive, state: null ); } } /// /// the DAS factory handles acquiring, listing, enumerating DAS devices. /// this factory abstracts communicating with groups of hardware and notifications of /// new devices or removed devices. /// public class DASFactory : IDASFactory, IDisposable { public void StartMulticastAutoDiscovery() { _autoDiscovery.StartMulticastAutoDiscovery(); } public void StopMulticastAutoDiscovery() { _autoDiscovery.StopMulticastAutoDiscovery(); } /// /// returns any discovered devices /// /// public IDiscoveredDevice[] GetDiscoveredDevices() { return _autoDiscovery.GetDiscoveredDevices(); } private readonly AutoDiscovery _autoDiscovery; #region Private Area private BlockingCollection> _queueActionPerDevice; private TextLogger _autoDiscoveryLog; private static void OnLoggerException(Exception ex) { } private static string _language; private const int MAX_LOG_SIZE = 4194304; public string Language { get => _language; set { _language = value; if (_language.Length > 0) { Thread.CurrentThread.CurrentUICulture = new CultureInfo(_language); } } } private WinUSBHandling _winUsbHandler; private WinUSBHandling _cdcusbHandler; private WinUSBHandling _winUsb15Handler; private WinUSBHandling _winTsrAirHandler; private readonly List _ethernetHandler = new List(); private EthernetTDASHandling _ethernetTDASHandler; private SerialTDASHandling SerialTDASHandler; private RESTSPFDHandling _restSPFDHandler; #endregion #region Singleton enforcement /// /// This mutex prevents multiple instances. Not only inside one application. /// private static Mutex _singletonFlag; private static readonly object SingletonFlagLock = new object(); private const string SINGLETON_FLAG_NAME = "DTSRecorderMutex"; private static void TakeDASFactoryOwnership() { lock (SingletonFlagLock) { if (_singletonFlag != null) { // There's already one running throw new SingletonFaultException("You can only run one instance of DASFactory at a time, exiting"); } bool createdNew; try { _singletonFlag = new Mutex(true, SINGLETON_FLAG_NAME, out createdNew); } catch (Exception ex) { throw new SingletonFaultException("DASFactory failed to create mutex", ex); } if (!createdNew) { // Application not ok to start _singletonFlag = null; throw new SingletonFaultException("You can only run one of the DTS applications at a time, exiting"); } } } public void TakeOwnership() { TakeDASFactoryOwnership(); } public static bool CanDASFactoryStart() { return CanDASFactoryStart(false); } /// /// tests whether it is okay for DASFactory to start /// /// true if DASFactory can start, false otherwise /// briefly creates and destroys a named mutex to determine whether /// an instance of the application is running or not. /// public static bool CanDASFactoryStart(bool bTakeControl) { lock (SingletonFlagLock) { if (_singletonFlag != null) { // There's already one running return false; } Mutex tmpFlag = null; try { bool createdNew; tmpFlag = new Mutex(false, SINGLETON_FLAG_NAME, out createdNew); // did we create it? if (createdNew) return true; bTakeControl = false; // no, someone already had return false; // yes! } catch (Exception ex) { APILogger.Log("MessageBox", Strings.DASFactory_FailedToCreateMutex, ex); return false; } finally { if (!bTakeControl) { // we just needed to know if we can create it so close it now if (tmpFlag != null) { tmpFlag.Close(); } } else { _singletonFlag = tmpFlag; } } } } #endregion private bool _bAllowSDBCommandPort = true; public bool AllowSDBCommandPort { get => _bAllowSDBCommandPort; set => _bAllowSDBCommandPort = value; } //FB 25642 & 18152 reference to the DasToHost dictionary public ConcurrentDictionary DasToHost { get; set; } = Common.Utils.PingUtils.DasToHost; public double S6ConnectNewTimeout { get; set; } = 60000; private void ProcessConnectionQueue() { while (!_queueActionPerDevice.IsCompleted) { Tuple queueActionPerDevice; try { queueActionPerDevice = _queueActionPerDevice.Take(); } catch { break; } switch (queueActionPerDevice.Item1) { case QueueActions.Connect: queueActionPerDevice.Item2.UpdateConnectedDevices(); break; case QueueActions.Disconnect: queueActionPerDevice.Item2.UpdateDisconnectedDevices(); break; case QueueActions.DisconnectAndExit: Task.Run(() => { queueActionPerDevice.Item2.UpdateDisconnectedDevices(); }); break; case QueueActions.Exit: continue; } UpdateFinishedCallback(queueActionPerDevice.Item2); } } /// /// host names for SLICE Distributors /// public string[] SliceDBHostNames { get { lock (MyLock) { var existingIPs = new List(); for (var i = _ethernetHandler.Count - 1; i >= 0; i--) { existingIPs.Add(_ethernetHandler[i].SliceDBHostName); } return existingIPs.ToArray(); } } set { lock (MyLock) { var existingIPs = new List(); var buckets = GetBuckets(value); var removed = false; for (var i = _ethernetHandler.Count - 1; i >= 0; i--) { var matches = Array.FindAll(value, s => s.Equals(_ethernetHandler[i].SliceDBHostName)); if (matches.Length < 1) { //FB 17556 disconnect when exit _ethernetHandler[i].EnqueueDisconnectAndExit(); _ethernetHandler[i].Dispose(); _ethernetHandler.RemoveAt(i); removed = true; } else { existingIPs.Add(_ethernetHandler[i].SliceDBHostName); } } if (removed) { ReportRemoved(); } var waitingHandlers = new List(); foreach (var s in value) { if (existingIPs.Contains(s)) { APILogger.Log($"Existing IPs already contains {s} - skipping"); continue; } else { APILogger.Log($"Adding new IP {s}"); } var eh = new EthernetHandling(this, UpdateFinishedCallback, new EthernetSliceHandling(_inUtilityMode), new RibeyeHandling(_inUtilityMode), new EthernetSlice2Handling(_inUtilityMode), new EthernetSlice1_5Handling(_inUtilityMode), new EthernetSlice6Handling(_inUtilityMode), new EthernetSlice6AirHandling(_inUtilityMode), new EthernetSlice6AirBridgeHandling(_inUtilityMode), new EthernetSlice6DBHandling(_inUtilityMode, ConnectedDevicesUpdated), new EthernetPowerProHandling(_inUtilityMode), new EthernetTsrAirHandling(_inUtilityMode), new EthernetSlice6DB3Handling(_inUtilityMode, ConnectedDevicesUpdated), new EthernetSliceProDBHandling(_inUtilityMode, SLICEPRODBConnectedDevicesUpdated), new EthernetSlice6AirThermocouplerHandling(_inUtilityMode), _queueActionPerDevice ) { HandlerIndex = null == buckets ? 0 : GetBucket(buckets, s), SliceDBHostName = s }; eh.UpdateDeviceSetups(); _ethernetHandler.Add(eh); waitingHandlers.Add(eh); } Task.Run(() => ConnectHosts(waitingHandlers.ToArray())).ConfigureAwait(false); } } } /// /// tries to ping the given ip address /// this is for http://manuscript.dts.local/f/cases/30297/ISF-import-not-working-correctly-with-S6DB3-and-ISF /// /// public static void TryPing(string ipAddress) { try { var p = new PingUtils.PingDevice(); p.PingDevices(new List(new[] { ipAddress })); } catch (Exception ex) { APILogger.Log(ex); } } private const int RECEIVE_BUFFER_SIZE = 65536; private void ConnectHosts(EthernetHandling[] waitingHandlings) { if (!waitingHandlings.Any()) { return; } //var ips = waitingHandlings.Select(wh => wh.SliceDBHostName).Distinct().ToArray(); //var buckets = GetBuckets(ips); //var r = new Random(DateTime.Now.Second); //Parallel.ForEach(waitingHandlings, h=> for (var idx = 0; idx < waitingHandlings.Length; idx++) { var h = waitingHandlings[idx]; Task.Run(async () => { //we may have to tear down and construct sockets multiple times, so //we'll loop through till we have a connection bool bTryingToConnect = true; Socket sock = null; while (bTryingToConnect && !h.SLICEDBShouldDisconnect) { //var bucket = GetBucket(buckets, h.SliceDBHostName); if (idx > 0) { var delay = idx * DFConstantsAndEnums.WaitTimeBetweenUnitConnects; await Task.Delay(delay); } sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); sock.SetSocketOption(SocketOptionLevel.Tcp, SocketOptionName.NoDelay, true); sock.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.KeepAlive, true); uint dummy = 0; //length = 4 var inOptionValues = new byte[System.Runtime.InteropServices.Marshal.SizeOf(dummy) * 3]; var onOff = true; var keepAliveTimeOutMs = DFConstantsAndEnums.LocalKeepAliveTimeOutMS; var keepAliveRetryIntervalMs = DFConstantsAndEnums.LocalKeepAliveRetryIntervalMS; BitConverter.GetBytes((uint)(onOff ? 1 : 0)).CopyTo(inOptionValues, 0); BitConverter.GetBytes(keepAliveTimeOutMs) .CopyTo(inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf(dummy)); BitConverter.GetBytes(keepAliveRetryIntervalMs) .CopyTo(inOptionValues, System.Runtime.InteropServices.Marshal.SizeOf(dummy) * 2); sock.IOControl(IOControlCode.KeepAliveValues, inOptionValues, null); //FB 25642 & 18152 Get the host ip if (!DasToHost.ContainsKey(h.SliceDBHostName)) { APILogger.Log($"no known host for {h.SliceDBHostName} we'll try pinging for it"); TryPing(h.SliceDBHostName); } else { APILogger.Log($"we have a known host for {h.SliceDBHostName} and it's {DasToHost[h.SliceDBHostName].HostIpAddress}"); } string hostIpAddress = string.Empty; if (DasToHost.ContainsKey(h.SliceDBHostName)) { hostIpAddress = DasToHost[h.SliceDBHostName].HostIpAddress; } if (!string.IsNullOrEmpty(hostIpAddress)) { APILogger.Log($"DASFactory.ConnectHosts binding to {hostIpAddress}"); sock.Bind(new IPEndPoint(IPAddress.Parse(hostIpAddress), 0)); } //before connecting a heartbeat port, make sure any corresponding //command ports are disconnected, if there's one active, disconnect it! //as SLICE doesn't really handle talking on the HB port while command port is still //open try { var commandSocket = DeviceHandling.GetCommandPortSocket(h.SliceDBHostName); if (null != commandSocket) { if (commandSocket.Connected) { APILogger.Log( $"Found a command port still active while attempting to connect heartbeat port {h.SliceDBHostName}"); commandSocket.Disconnect(false); DeviceHandling.RemoveCommandPortSocket(h.SliceDBHostName); } else { RemoveCommandPortSocket(h.SliceDBHostName); } } } catch (Exception ex) { APILogger.Log($"Exception in ConnectHosts, commandSocket {ex}"); } //connect heartbeat port Task connectTask = null; try { APILogger.Log($"Connecting to {h.SliceDBHostName} on {hostIpAddress}"); connectTask = sock.ConnectAsync(h.SliceDBHostName, 8200); connectTask.Wait(DFConstantsAndEnums.HeartbeatAsyncConnectTimeoutMS); if (connectTask.IsCompleted) { APILogger.Log($"Failed to connect to {h.SliceDBHostName} will retry"); bTryingToConnect = false; } else { //FB 18389 sock.Shutdown(SocketShutdown.Both); sock.Close(); sock.Dispose(); } //FB18389 Only dispose the tasks in one these states, same as using statement except we don't throw the exception //in case the task is still working. disposing the sock should cause the wrapper connectTask finishes eventually and the .net grabage collector should take care of the task later if (connectTask.IsCompleted || connectTask.IsFaulted || connectTask.IsCanceled) { connectTask.Dispose(); } } catch (Exception ex) { APILogger.Log($"Failed to connect to {h.SliceDBHostName}", ex); } if (!bTryingToConnect) { try { var buffer = new byte[RECEIVE_BUFFER_SIZE]; int ret = 0; //FB18389 make sure the sock is connected before receive if (sock.Connected) { ret = await sock.ReceiveAsync(buffer, 0, RECEIVE_BUFFER_SIZE, SocketFlags.None); } // only dispose the sock if the sock is not connected and can not process the data if (sock.Connected) { //FB18389 make sure the sock is connected before process await ProcessResult(ret, sock, h, buffer); } else { sock.Dispose(); } } catch (Exception ex) { //hit an error trying to read, even though we connected, so try again if (sock != null) { sock.Dispose(); } APILogger.Log("Exception in ConnectHosts, bTryingToConnect = false ", ex); bTryingToConnect = true; } } } }).ConfigureAwait(false); } } //FB43867 applied the patch public const string COMMAND_PORT_CONNECT_OVERRIDE_MESSAGE = "commandPort"; private async Task ProcessResult(int ret, Socket sock, EthernetHandling h, byte[] buffer) { //FB18389 Make sure socket is not null, there is no point to continue if the sock is null if (sock == null) { APILogger.Log("sock is null"); return; } var distributorSocket = new DistributorSocket(sock, h.SliceDBHostName, "logs"); if (0 == ret && null != sock) { APILogger.Log("DistributorSocket 0 bytes, disposing"); sock.Dispose(); APILogger.Log("DistributorSocket 0 bytes, disposed"); return; } var bytes = buffer.Take(ret).ToArray(); //17649 Issues connecting/disconnecting to SLICE Distributor //a disconnect has been requested, do it ... if (h.SLICEDBShouldDisconnect) { APILogger.Log($"Disconnect requested for {h.SliceDBHostName}"); try { // https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.shutdown?view=net-5.0 //FB 18389 Call Shutdown before Close to disable any send and receives on socket sock.Shutdown(SocketShutdown.Both); sock.Close(); sock.Dispose(); } catch (Exception ex) { APILogger.Log($"Exception in ProcessResult closing & disposing the sock {ex}"); } } var msg = System.Text.Encoding.ASCII.GetString(bytes); //FB18389 make sure msg has value if (string.IsNullOrEmpty(msg)) { APILogger.Log($"ProcessResult msg is empty."); return; } var lines = msg.Split(new[] { "\r", "\n" }, StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var thisMsg = line; if (string.IsNullOrWhiteSpace(line)) { continue; } //FB43867 applied the patch if (line.Equals("heartbeat") && !h.CommandPortConnected && !h.CommandPortConnectSignaled && !line.EndsWith("8301")) { //Get the Distributor Connected h.CommandPortConnectSignaled = true; } if (line.Equals("heartbeat") && !h.CommandPortConnected && !h.CommandPortConnectSignaled && !line.EndsWith("8301")) { //Get the Distributor Connected thisMsg = $"{COMMAND_PORT_CONNECT_OVERRIDE_MESSAGE} {h.SliceDBHostName}:8201"; h.CommandPortConnectSignaled = true; } if (EthernetHandling.DistributorMsg.IsValidMsg(thisMsg)) { var dbMessage = new EthernetHandling.DistributorMsg(thisMsg); distributorSocket.SendAck(); if (dbMessage.SupportsKeepAlive) { h.KeepAliveEnabled = distributorSocket.KeepAliveEnabled(); } else { h.KeepAliveEnabled = false; } if (h.UpdateConnectedDBDevices(h.SliceDBHostName, dbMessage)) { if (dbMessage.IsConnect) { h.EnqueueConnect(); } else { h.EnqueueDisconnect(); } } if (h.KeepAliveEnabled) { try { // https://docs.microsoft.com/en-us/dotnet/api/system.net.sockets.socket.shutdown?view=net-5.0 //FB 18389 Call Shutdown before Close to disable any send and receives on socket sock.Shutdown(SocketShutdown.Both); sock.Close(); sock.Dispose(); break; } catch (Exception ex) { APILogger.Log($"Exception in ProcessResult, the KeepAliveEnabled = true {ex}"); break; } } } else { //FB 18389 make sure the socket is connected before using it if (distributorSocket.IsConnected()) { distributorSocket.SendNak(); } } } if (!h.KeepAliveEnabled) { try { if (!sock.Connected) { return; } int res = await sock.ReceiveAsync(buffer, 0, RECEIVE_BUFFER_SIZE, SocketFlags.None); if (!sock.Connected) { return; } await ProcessResult(res, sock, h, buffer); } catch (Exception ex) { APILogger.Log($"Exception in ProcessResult, the KeepAliveEnabled = false {ex}"); } } } /// /// if all the input strings are in ip form (aaa.bbb.ccc.ddd) /// returns all the unique ccc portion of the ip addresses /// otherwise returns null /// /// /// private static string[] GetBuckets(string[] array) { var useSubnets = Array.TrueForAll(array, s => s.Split('.').Length == 4); if (useSubnets) { var list = new List(); foreach (var s in array) { var subnet = s.Split('.')[2]; if (!list.Contains(subnet)) { list.Add(subnet); } } return list.ToArray(); } return null; } /// /// if target is in ip form (aaa.bbb.ccc.ddd) /// returns which index the subnet of target is in among the subnets passed in /// /// /// /// private static int GetBucket(string[] subnets, string target) { var tokens = target.Split('.'); if (tokens.Length < 4) { return 0; } return Array.IndexOf(subnets, tokens[2]); } /// /// 14157 Refresh/Stability Issue in Data Acquisition Tile /// returns the list of all devices known connectable via ECM/SDB/S6DB /// these distributors have a HELLO SLICEBASE x.x.x.x:yyyy format, so /// we keep track of what the db told us /// /// public string[] GetConnectedDevices() { var devices = new List(); foreach (var eh in _ethernetHandler) { var list = eh.GetConnectedEthernetDeviceStrings(); devices.AddRange(list.ToArray()); } var spfd = _restSPFDHandler.GetDASList(); return devices.ToArray(); } /// /// callback when discovery was performed on a device and we discovered more devices /// this function starts the communication with the newly discovered devices /// /// private void SLICEPRODBConnectedDevicesUpdated(IDASConnectedDevice[] devices) { if (RunTestVariables.InRunTest) { return; } if (devices.Any()) { lock (MyLock) { var existingIPs = new HashSet(); var ips = SliceDBHostNames.ToList(); foreach (var ip in ips) { existingIPs.Add(ip); } var connectedDevices = devices.Select(device => device.IPAddress).ToArray(); APILogger.Log($"SLICE PRO DB has attached {string.Join(", ", connectedDevices)}"); var added = false; foreach (var ip in connectedDevices) { if (!existingIPs.Contains(ip)) { existingIPs.Add(ip); ips.Add(ip); added = true; } } if (added) { SliceDBHostNames = ips.ToArray(); } } } } /// /// callback when discovery was performed on a device and we discovered more devices /// this function starts the communication with the newly discovered devices /// /// private void ConnectedDevicesUpdated(IDASConnectedDevice[] devices) { if (RunTestVariables.InRunTest) { return; } if (devices.Any()) { lock (MyLock) { var existingIPs = new HashSet(); var ips = SliceDBHostNames.ToList(); foreach (var ip in ips) { existingIPs.Add(ip); } var connectedDevices = devices.Select(device => device.IPAddress).ToArray(); APILogger.Log($"S6DB has attached {string.Join(", ", connectedDevices)}"); var added = false; foreach (var ip in connectedDevices) { if (!existingIPs.Contains(ip)) { existingIPs.Add(ip); ips.Add(ip); added = true; } } if (added) { SliceDBHostNames = ips.ToArray(); } } } } private static readonly object MyLock = new object(); /// /// TDAS Host Names /// public string[] TDASHostNames { get => _ethernetTDASHandler.TDASHostNames; set { var toSet = value; if ( null != value && value.Length > 1) { toSet = value.Distinct().ToArray(); } _ethernetTDASHandler.TDASHostNames = toSet; } } public string[] SPFDHostNames { get => _restSPFDHandler.SPFDHostNames; set { var toSet = value; if (null != value && value.Length > 1) { toSet = value.Distinct().ToArray(); } _restSPFDHandler.SPFDHostNames = toSet; } } public string[] TDASSerialPortNames { get { return SerialTDASHandler.TDASPortNames; } set { SerialTDASHandler.TDASPortNames = value; } } public string TDASSerialRackSerialNumber { get { return SerialTDASHandler.RackSerialNumber; } set { SerialTDASHandler.RackSerialNumber = value; } } #region Events for arrived, removed and failed /// /// Event signalized to the client app. /// Add handler for this event in your form to be notified of new device events /// public event DASFactoryEventHandler DeviceArrived; internal void ReportArrived(ConnectedDevice device) { if (DASFactoryDb.DbWrapper.Connected && device.Dev.Comm.SerialNumber != null) { InsertDeviceIfNeeded(device); } //FB14628: Improve "connected units have changed" message //Send added serial up with event DeviceArrived?.Invoke(this, new DASFactoryEventArgs(null != device?.Dev?.Comm?.SerialNumber ? new string[] { device.Dev.Comm.SerialNumber } : new string[] { }, CommunicationConstantsAndEnums.CommunicationResult.ConnectOK)); } private void InsertDeviceIfNeeded(ConnectedDevice device) { try { var id = DASFactoryDb.DbWrapper.GetDeviceId(device.Dev.Comm.SerialNumber); if (id > 0) { ((IDASCommunication)device.Dev.Comm).RecordId = id; return; } InsertRecord(device); } catch (Exception ex) { APILogger.Log(ex); } } /// /// inserts a new record into DASFactory.IDASCommunicationTable /// /// private void InsertRecord(ConnectedDevice device) { try { if (!DbWrapper.Connected) { return; } var id = DASFactoryDb.DAS.DAS.InsertDASSimple(device.Dev.Comm.SerialNumber, device.Dev.Comm.FirmwareVersion, device.Dev.Comm.ConnectString); ((IDASCommunication)device.Dev.Comm).RecordId = id; } catch (Exception ex) { APILogger.Log(ex); } } /// /// Event signalized to the client app. /// Add handler for this event in your form to be notified of removed device events /// public event DASFactoryEventHandler DeviceRemoved; internal void ReportRemoved(object o = null) { //FB14628: Improve "connected units have changed" message //Send removed serials up with event var serials = new string[] { }; switch (o) { case string[] _: serials = o as string[]; break; case ICommunication icom: serials = new string[] { icom.SerialNumber }; if (ShouldReconnect(icom)) { var connection = ((DTS.Common.EthernetConnection)icom.Transport); var host = connection.ConnectString.Substring(0, connection.ConnectString.IndexOf(':')); foreach (var handler in _ethernetHandler) { if (handler.SliceDBHostName == host && !handler.SLICEDBShouldDisconnect) { Task.Run(() => { ConnectHosts(new[] { handler }); }).ConfigureAwait(false); break; } } } break; } DeviceRemoved?.Invoke(this, new DASFactoryEventArgs(serials, CommunicationConstantsAndEnums.CommunicationResult.DisconnectOK)); } private bool ShouldReconnect(ICommunication icom) { return icom is EthernetSlice6 || icom is EthernetSlice6Air || icom is EthernetSlice6AirBridge || icom is EthernetSlice6DB || icom is EthernetTsrAir || icom is EthernetSlice6AirThermocoupler; } /// /// Event signalized to the client app. /// Add handler for this event in your form to be notified of failed device events /// public event DASFactoryEventHandler DeviceFailed; internal void ReportFailed(object o = null) { //FB14628: Improve "connected units have changed" message DeviceFailed?.Invoke(this, new DASFactoryEventArgs(new string[] { }, CommunicationConstantsAndEnums.CommunicationResult.ConnectFailed)); } #endregion #region Constructor private bool _inUtilityMode; // http://manuscript.dts.local/f/cases/18700/Add-ability-to-instantiate-DASFactory-without-USB // Mostly done for use in Linux without Windows USB drivers and listeners private bool _bUsingUSB = true; /// /// The easiest way to use DASFactory. /// It will create hidden form for processing Windows messages about USB drives /// You do not need to override WndProc in your form. /// public DASFactory(bool inUtilityMode) { _autoDiscovery = new AutoDiscovery(this); _autoDiscoveryLog = new TextLogger(@"Logs\AutoDiscovery.log", OnLoggerException, MAX_LOG_SIZE); Init(inUtilityMode, true); } public DASFactory(bool inUtilityMode, bool takeControl) { _autoDiscovery = new AutoDiscovery(this); _autoDiscoveryLog = new TextLogger(@"Logs\AutoDiscovery.log", OnLoggerException, MAX_LOG_SIZE); Init(inUtilityMode, takeControl); } public DASFactory(bool inUtilityMode, bool takeControl, bool bUsingUSB) { _autoDiscovery = new AutoDiscovery(this); _autoDiscoveryLog = new TextLogger(@"Logs\AutoDiscovery.log", OnLoggerException, MAX_LOG_SIZE); Init(inUtilityMode, takeControl, bUsingUSB); } private void Init(bool inUtilityMode, bool takeOwnership, bool bUsingUSB = true) { _inUtilityMode = inUtilityMode; // http://manuscript.dts.local/f/cases/18700/Add-ability-to-instantiate-DASFactory-without-USB // Mostly done for use in Linux without Windows USB drivers and listeners _bUsingUSB = bUsingUSB; // first make sure we're allowed to start. Will throw if we can't if (takeOwnership) { TakeDASFactoryOwnership(); } _queueActionPerDevice = new BlockingCollection>(); Task.Factory.StartNew(ProcessConnectionQueue, CancellationToken.None, TaskCreationOptions.LongRunning, TaskScheduler.Default).ConfigureAwait(false); if (_bUsingUSB) { // now do WinUSB _winUsbHandler = new WinUSBHandling(this, UpdateFinishedCallback, new WinUSBSliceHandling(inUtilityMode), _queueActionPerDevice); // now do CDCUSB _cdcusbHandler = new WinUSBHandling(this, UpdateFinishedCallback, new WinUSBSlice2Handling(inUtilityMode), _queueActionPerDevice); // now do WinUSB1.5 _winUsb15Handler = new WinUSBHandling(this, UpdateFinishedCallback, new WinUSBSlice1_5Handling(inUtilityMode), _queueActionPerDevice); // now do TSR AIR _winTsrAirHandler = new WinUSBHandling(this, UpdateFinishedCallback, new WinUSBTsrAirHandling(inUtilityMode), _queueActionPerDevice); } // now ethernet _ethernetTDASHandler = new EthernetTDASHandling(this, UpdateFinishedCallback, new TDASEthernetSetup(inUtilityMode), _queueActionPerDevice); _ethernetTDASHandler.UpdateDeviceSetups(); //now SPFD _restSPFDHandler = new RESTSPFDHandling(this, UpdateFinishedCallback, new SPFDRESTSetup(), _queueActionPerDevice); _restSPFDHandler.UpdateDeviceSetups(); } #endregion /// /// gets a list of all connected DAS devices. /// /// List of all connected DAS hardware public List GetDASList() { var dasList = new List(); if (null != _winUsbHandler) { dasList.AddRange(_winUsbHandler.GetDASList()); } if (null != _cdcusbHandler) { dasList.AddRange(_cdcusbHandler.GetDASList()); } if (null != _winUsb15Handler) { dasList.AddRange(_winUsb15Handler.GetDASList()); } if (null != _winTsrAirHandler) { dasList.AddRange(_winTsrAirHandler.GetDASList()); } lock (MyLock) { foreach (var eh in _ethernetHandler) { dasList.AddRange(eh.GetDASList()); } } dasList.AddRange(_ethernetTDASHandler.GetDASList()); dasList.AddRange(_restSPFDHandler.GetDASList()); return dasList; } public List GetSortedDASList() { var list = GetDASList(); var sdbs = new List(); for (var i = list.Count - 1; i >= 0; i--) { if (!list[i].IsEthernetDistributor()) continue; sdbs.Add(list[i]); list.RemoveAt(i); } list.Sort(CompareDas); foreach (var t in sdbs) { var ip = ((ICommunication)t).ConnectString.Split(':')[0]; for (var z = 0; z < list.Count; z++) { if (!((ICommunication)list[z]).ConnectString.Contains(ip)) continue; list.Insert(z, t); break; } } return list; } public static int CompareDas(IDASCommunication left, IDASCommunication right) { if (Equals(left, right)) { return 0; } if (null == left.ConfigData) { return -1; } if (null == right.ConfigData) { return 1; } return left.ConfigData.DasDisplayOrder == right.ConfigData.DasDisplayOrder ? string.Compare(left.SerialNumber, right.SerialNumber, StringComparison.Ordinal) : left.ConfigData.DasDisplayOrder.CompareTo(right.ConfigData.DasDisplayOrder); } /// /// Retrieves the ICommunication object of all hardware currently connected. /// /// List of ICommunications representing all hardware connected. public List GetDevList() { var devList = new List(); if (null != _winTsrAirHandler) { devList.AddRange(_winTsrAirHandler.GetDevList()); } if (null != _cdcusbHandler) { devList.AddRange(_cdcusbHandler.GetDevList()); } if (null != _winUsb15Handler) { devList.AddRange(_winUsb15Handler.GetDevList()); } if (null != _winUsbHandler) { devList.AddRange(_winUsbHandler.GetDevList()); } lock (MyLock) { foreach (var eh in _ethernetHandler) { devList.AddRange(eh.GetDevList()); } } devList.AddRange(_ethernetTDASHandler.GetDevList()); devList.AddRange(_restSPFDHandler.GetDevList()); return devList; } /// /// Make DASFactory forget about all devices. /// public void DetachAllDevices(bool detachUSB = false) { // FB14290: Hardware step fails with USB SLICE the first time, succeeds the second. // Detaching USB devices every call is unnecessary var devicesWereRemoved = false; if (detachUSB) { if (_bUsingUSB) { devicesWereRemoved = _winUsbHandler.DetachAllDevices() || _cdcusbHandler.DetachAllDevices() || _winUsb15Handler.DetachAllDevices() || _winTsrAirHandler.DetachAllDevices(); } } lock (MyLock) { foreach (var eh in _ethernetHandler) { if (eh.DetachAllDevices()) { devicesWereRemoved = true; } } } if (_ethernetTDASHandler.DetachAllDevices()) { devicesWereRemoved = true; } if (_restSPFDHandler.DetachAllDevices()) { devicesWereRemoved = true; } // notify our subscribers if something was removed if (devicesWereRemoved) { ReportRemoved(); } //these should be the same instance, I'm just being extra careful here. PingUtils.DasToHost.Clear(); DasToHost.Clear(); } /// /// Unregister and close the file we may have opened on the removable drive. /// Garbage collector will call this method. /// public void Dispose() { // Prevent garbage collector getting rid of your mutex object... GC.KeepAlive(_singletonFlag); if (_winUsbHandler != null) { _winUsbHandler.Dispose(); _winUsbHandler = null; } if (_cdcusbHandler != null) { _cdcusbHandler.Dispose(); _cdcusbHandler = null; } if (_winUsb15Handler != null) { _winUsb15Handler.Dispose(); _winUsb15Handler = null; } if (_winTsrAirHandler != null) { _winTsrAirHandler.Dispose(); _winTsrAirHandler = null; } foreach (var eh in _ethernetHandler) { eh.Dispose(); } _ethernetHandler.Clear(); if (_ethernetTDASHandler != null) { _ethernetTDASHandler.Dispose(); _ethernetTDASHandler = null; } _restSPFDHandler?.Dispose(); _restSPFDHandler = null; _queueActionPerDevice.CompleteAdding(); } #region Refresh handling /// /// Lock variable to serialize calls to Refresh /// private int _refreshLock; /// /// What to call after refresh is done (only not null when a refresh is active) /// private ActionCompleteDelegate _currentRefreshFinishedAction; /// /// Lock to serialize calls from HID and WinUSB /// private static readonly object UpdateFinishedCallbackLock = new object(); /// /// This keeps track of the state of one device /// private class RefreshFlags { private bool ConnectDone { get; set; } private bool DisconnectDone { get; set; } private DeviceHandling Target { get; set; } public RefreshFlags(DeviceHandling dev) { Target = dev; ConnectDone = false; DisconnectDone = false; } public void Start() { Target.EnqueueConnect(); } public void DoNext() { Target.EnqueueDisconnect(); } public void ActionFinished() { if (ConnectDone) { DisconnectDone = true; } else { ConnectDone = true; } } public bool AllFinished() { return ConnectDone && DisconnectDone; } } /// /// Indicates if the HID disconnect is done /// private Dictionary _deviceStates; /// /// Callback function for HID and WinUSB updates. /// If we need to support more than HID and WinUSB, this function and the *Done /// variables above needs to be changed to a proper statemachine mechanism. /// /// the device belonging /// to this update /// private void UpdateFinishedCallback(DeviceHandling dev) { // only one at a time lock (UpdateFinishedCallbackLock) { if (_currentRefreshFinishedAction == null) { // not currently running refresh return; } if (!_deviceStates.ContainsKey(dev)) { return; } _deviceStates[dev].ActionFinished(); if (!_deviceStates[dev].AllFinished()) { _deviceStates[dev].DoNext(); } else { // check if all done if (_deviceStates.Count(flags => flags.Value.AllFinished()) != _deviceStates.Count) return; // yes we are, unhook and call var temp = _currentRefreshFinishedAction; _currentRefreshFinishedAction = null; temp(); } } } /// /// Setup the "statemachines" above to do a refresh /// /// The action to perform when finished private void SetupRefreshActions(ActionCompleteDelegate action) { lock (UpdateFinishedCallbackLock) { if (_bUsingUSB) { _deviceStates = new Dictionary { {_winUsbHandler, new RefreshFlags(_winUsbHandler)}, {_cdcusbHandler, new RefreshFlags(_cdcusbHandler)}, {_winUsb15Handler, new RefreshFlags(_winUsb15Handler)}, {_winTsrAirHandler, new RefreshFlags(_winTsrAirHandler)} }; } else { _deviceStates = new Dictionary(); } foreach (var eh in _ethernetHandler) { _deviceStates.Add(eh, new RefreshFlags(eh)); } _deviceStates.Add(_ethernetTDASHandler, new RefreshFlags(_ethernetTDASHandler)); _deviceStates.Add(_restSPFDHandler, new RefreshFlags(_restSPFDHandler)); // it's all ours _currentRefreshFinishedAction = action; if (_bUsingUSB) { // start the action _deviceStates[_winUsbHandler].Start(); _deviceStates[_cdcusbHandler].Start(); _deviceStates[_winUsb15Handler].Start(); _deviceStates[_winTsrAirHandler].Start(); } foreach (var eh in _ethernetHandler) { _deviceStates[eh].Start(); } _deviceStates[_ethernetTDASHandler].Start(); _deviceStates[_restSPFDHandler].Start(); } } /// /// Initiate a refresh of the DASFactory (i.e. make sure GetDASList() reflects what's actually connected) /// /// The action to perform when the refresh is done public void Refresh(ActionCompleteDelegate action) { if (action == null) { throw new ArgumentException("Refresh: You must provide an action"); } // only one call at a time if (Interlocked.Exchange(ref _refreshLock, 1) != 0) { // no, we're too late throw new BusyException("only one refresh at a time"); } // we can only have one refresh running at a time if (_currentRefreshFinishedAction != null) { // someone is already waiting for refresh to finish Interlocked.Exchange(ref _refreshLock, 0); throw new BusyException("only one refresh at a time"); } // get going SetupRefreshActions(action); // let go off the lock Interlocked.Exchange(ref _refreshLock, 0); } #endregion #region Multicast AutoDiscovery private readonly SortableBindingList _discoveredSlices = new SortableBindingList(); /// /// configures the default timeout in ms for Multicast auto discovery receive functions /// public int MultiCastAutoDiscoveryDefaultTimeoutMS { get => MulticastCommandBase.DEFAULT_RECEIVE_TIMEOUT_MS; set => MulticastCommandBase.DEFAULT_RECEIVE_TIMEOUT_MS = value; } /// /// configures the transmit address for Multicast auto discovery functions /// public string MulticastAutoDiscoveryAddress { get => MulticastCommandBase.MulticastAddress; set => MulticastCommandBase.MulticastAddress = value; } /// /// configures the transmit port for Multicast auto discovery functions /// public int MulticastAutoDiscoveryPort { get => MulticastCommandBase.CommandPort; set => MulticastCommandBase.CommandPort = value; } /// /// configures the receive port for Multicast auto discovery functions /// public int MulticastAutoDiscoveryResponsePort { get => MulticastCommandBase.ResponsePort; set => MulticastCommandBase.ResponsePort = value; } private void ParseDiscoveryReturns(DiscoveredConnectedSlice[] ConnectedDevices, ref Dictionary macAddressToDevice) { foreach (var record in ConnectedDevices) { var hostMac = record.DeviceMAC.Replace('-', ':').ToUpper(); if (!macAddressToDevice.ContainsKey(hostMac)) { continue; } var device = macAddressToDevice[hostMac]; var connections = new List(); foreach (var connection in record.ConnectedDevices) { var strMac = MulticastDiscoverSlice6.MACAddressToString(connection.MAC); if (strMac == hostMac) { continue; } if (!macAddressToDevice.ContainsKey(strMac)) { continue; } connections.Add( new ConnectedEthernetDevice(strMac, connection.Port) { SerialNumber = macAddressToDevice[strMac].Serial }); } device.Connections = connections.ToArray(); } } //FB 25590 support multiple udp hosts private readonly List _udpQATSList = new List(); /// /// starts the qats listening thread /// public void StartQATSListening() { if (!_udpQATSList.Any()) { UpdateQATSList(); } else { //FB 25590 first stop each foreach (var qats in _udpQATSList) { //just in case there's already a QATS listening (there shouldn't be), stop it and then recreate //so that we have updated binding address, etc qats.StopListening(); } _udpQATSList.Clear(); UpdateQATSList(); } //FB 25590 then start each foreach (var qats in _udpQATSList) { qats.StartListening(); } } private void UpdateQATSList() { //FB 25590 support multiple udp hosts, create multiple MulticastUdpQueryQATS for each host ip that supports multicast var hosts = Common.Utils.NetworkUtils.GetAvailableHosts(true); foreach (var host in hosts) { var bindToAdapterIPAddress = IPAddress.Any; if (IPAddress.TryParse(host.HostIpAddress, out var newAddress)) { bindToAdapterIPAddress = newAddress; } var queryQATS = new MulticastUdpQueryQATS(host.HostMacAddress) { BindToAdapterIPAddress = bindToAdapterIPAddress }; _udpQATSList.Add(queryQATS); } } /// /// stops QATS listening /// public void StopQATSListening() { //FB 25590 stop each foreach (var qats in _udpQATSList) { qats?.StopListening(); } } /// /// broadcasts a request for QATS /// public void SendQATSRequest() { //FB 25590 send to each one foreach (var qats in _udpQATSList) { qats?.SendCommand(); } } /// /// return any stored QATS, clear list /// /// public IUDPQATSEntry[] GetQATS() { List qatsEntryList = new List(); if (!_udpQATSList.Any()) { return new IUDPQATSEntry[0]; } //Add all the result in one list foreach (var qats in _udpQATSList) { qatsEntryList.AddRange(qats.GetUDPQATs()); } return qatsEntryList.ToArray(); } public SortableBindingList AutoDiscoverMulticast(CancellationToken cancelToken, bool discoverParents = true) { var list = new List(); //FB 25642 support multiple NICs var hostInfos = Common.Utils.NetworkUtils.GetAvailableHosts(true); foreach (var hostInfo in hostInfos) { IPAddress bindToAdapterIPAddress = IPAddress.Any; if (IPAddress.TryParse(hostInfo.HostIpAddress, out var newAddress)) { bindToAdapterIPAddress = newAddress; } var cmd = new MulticastAutoDiscover(hostInfo.HostMacAddress) { Logger = _autoDiscoveryLog, BindToAdapterIPAddress = bindToAdapterIPAddress }; cmd.MulticastExecute(cancelToken); var macAddressToDevice = new Dictionary(); foreach (var device in cmd.DiscoveredDevices) { if (!list.Any(p => p.Ip == device.Ip)) { list.Add(device); if (discoverParents) { macAddressToDevice[device.Mac.Replace('-', ':').ToUpper()] = device; } } } if (discoverParents) { var cmd2 = new MulticastDiscoverSlice6(MulticastCommandBase.DeviceClasses.Any, hostInfo.HostMacAddress) { MACAddressToDevice = macAddressToDevice, BindToAdapterIPAddress = bindToAdapterIPAddress, Logger = _autoDiscoveryLog }; cmd2.MulticastExecute(cancelToken); ParseDiscoveryReturns(cmd2.ConnectedDevices, ref macAddressToDevice); //this will determine everyone's parents foreach (var device in cmd.DiscoveredDevices) { DiscoverParent(device, macAddressToDevice); } //now we can determine port on SDB foreach (var device in cmd.DiscoveredDevices) { var ultimateParent = DiscoveredDevice.GetParent(device); if (null != ultimateParent) { device.Port = ultimateParent.GetPort(device); } } //now we can set the "slot" for each device foreach (var device in cmd.DiscoveredDevices) { DiscoverSlot(device, macAddressToDevice); } } } _discoveredSlices.Clear(); foreach( var d in list) { _discoveredSlices.Add(d); } return _discoveredSlices; } private static void DiscoverSlot(IDiscoveredDevice device, Dictionary lookup) { if (null == device.Parent) { device.PositionOnDistributor = 0; return; } //we want to know what slot we are on our ULTIMATE parent, so we have to go up through the list of parents till there is no parent var ultimateParent = DiscoveredDevice.GetParent(device); if (ultimateParent.DevClass == DFConstantsAndEnums.MultiCastDeviceClasses.S6DB) { device.IsModule = true; device.PositionOnDistributor = ultimateParent.GetSlot(device, lookup); device.PositionOnChain = ultimateParent.GetSlotOnPort(device, lookup); } else { device.IsModule = false; } } private static void DiscoverParent(IDiscoveredDevice device, Dictionary lookup) { if (null == device) { return; } var eAllDevices = lookup.GetEnumerator(); while (eAllDevices.MoveNext()) { if (eAllDevices.Current.Value.IsParent(device)) { device.Parent = eAllDevices.Current.Value; } } } #endregion /// /// returns IP address for connected device /// /// /// private string GetIPAddress(IDASCommunication das) { if (!(das is ICommunication icom)) { return string.Empty; } var s = icom.ConnectString; if (string.IsNullOrWhiteSpace(s)) { return string.Empty; } var idx = s.IndexOf(':'); if (idx > 0) { s = s.Substring(0, idx); } return s; } /// /// returns all unique IP addresses minus USB and empty connection strings /// /// private string[] GetUniqueIPAddresses() { var devices = GetDASList(); var ipList = devices.Select(device => GetIPAddress(device)).AsEnumerable().Distinct().Where(ip => !ip.ToUpper().Contains("USB") && !string.IsNullOrWhiteSpace(ip)); return ipList.ToArray(); } /// /// pings all attached devices, returns true if there are only USB devices connected or all attached devices /// ping successfully /// /// public bool PingAll() { var pingResult = false; try { var ping = new PingUtils.PingDevice(); var ipList = GetUniqueIPAddresses().ToList(); if (ipList.Any()) { APILogger.Log("IPS: ", string.Join(",", ipList)); } //FB 18152 host selction will be automatic pingResult = ipList.Count < 1 || ping.PingDevices(ipList); } catch (Exception ex) { APILogger.LogException(ex); // Temporarily, if we're using the windows API to ping, dont rely on it for this functionality return pingResult; } return pingResult; } } }