using DTS.Common.Interface.StatusAndProgressBar; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.IO; using System.Linq; using System.Net; using System.Net.NetworkInformation; using System.Net.Sockets; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using DTS.Common.Utilities.Logging; namespace DTS.Common.Utils { public class PingUtils { public static void EliminateBadHosts(ref List list, string[] ips) { var hosts = list.ToArray(); foreach (var host in hosts) { if (!HostMatchesAnyIPs(host, ips)) { APILogger.Log($"Removing host {host.HostIpAddress} as it can't reach any ips"); list.Remove(host); } } } /// /// returns false if the host can not access any ips provided after looking at it's ip address and netmask /// returns true if it can reach an ip or it can't be determined if it can reach an IP this might happen if someone used /// a host name instead of a xxx.yyy.zzz.aaa style address or an empty string somewhere /// /// /// /// protected static bool HostMatchesAnyIPs(HostInfo host, string[] ips) { if (ips.Length == 0) { return true; } if (string.Empty.Equals(host.StartAddress) || string.Empty.Equals(host.EndAddress)) { return true; } //FB 43919 List result = new List(); foreach (var ip in ips) { try { if (!IPAddress.TryParse(ip, out var ipToConnectTo)) { //it's not a traditional format ip, give up trying to match it return true; } if (!IPAddress.TryParse(host.StartAddress, out var startAddress)) { //start address is not a traditional format, give up trying to match return true; } if (!IPAddress.TryParse(host.EndAddress, out var endAddress)) { //start address is not in traditional format, give up trying to match return true; } var startBytes = startAddress.GetAddressBytes(); var endBytes = endAddress.GetAddressBytes(); var ipByte = ipToConnectTo.GetAddressBytes(); for (var i = 0; i < startBytes.Length && i < endBytes.Length && i < ipByte.Length; i++) { //FB 43919 If this condition is true as first iteration then break immediately and result collection should have only one false //which means we need to remove this ip since it's not in the network if (ipByte[i] < startBytes[i] || ipByte[i] > endBytes[i]) { result.Add(false); break; } else { result.Add(true); } } //FB 43919 if any of the segment was true then don't remove the ip return result.Exists(p => p); } catch (Exception ex) { APILogger.Log(ex); } } //FB 43919 return false; } //FB 18152 & 25642 This dictionary keeps the host info for each DAS public static ConcurrentDictionary DasToHost { get; set; } = new ConcurrentDictionary(); public class PingDevice { private const int MAX_PING_ATTEMPTS = 3; private const string ALPHABET = "abcdefghijklmnopqrstuvwxyz"; #region Ping public bool PingDevices(List ipList) { //FB 18152 Get the avilable hosts var hostInfos = NetworkUtils.GetAvailableHosts(); EliminateBadHosts(ref hostInfos, ipList?.ToArray()); return PingAllDevices(hostInfos, ipList); } #region Ping All Devices /// /// ICMP ping /// /// host IP address /// List of connected devices IP addresses /// false - if as least one device disconnected private bool PingAllDevices(List hostInfos, List ipList) { var hostStringList = new List(); foreach (var host in hostInfos) { hostStringList.Add(host.HostIpAddress); } APILogger.Log($"trying to ping {string.Join(", ", ipList.ToArray())} using hosts: {string.Join(", ", hostStringList.ToArray())}"); var allConnected = true; Parallel.ForEach(ipList, (item, state) => { if (hostInfos.Any()) { var lastHost = hostInfos.Last(); //FB 18152 & 25642 Perform the ping on all the devices in all hosts foreach (var hostInfo in hostInfos) { var eachConnected = PingOneDevice(item, hostInfo.HostIpAddress); if (eachConnected) { //We found the host update the dictionary and break from the loop DasToHost[item] = hostInfo; allConnected = eachConnected; APILogger.Log($"pinging {string.Join(", ", ipList.ToArray())} worked for {hostInfo.HostIpAddress}"); break; } if (hostInfo.Equals(lastHost)) { //all the hosts are processed allConnected = eachConnected; } } if (!allConnected) state.Break(); } }); if (allConnected) { APILogger.Log("An interface could ping"); } else { APILogger.Log("no intefaces could ping"); } return allConnected; } /// /// TCP ping /// /// List of connected devices IP addresses /// false - if as least one device disconnected private bool PingAllDevices(List ipList) { var allConnected = true; Parallel.ForEach(ipList, (item, state) => { allConnected = PingOneDevice(item); if (!allConnected) state.Break(); }); return allConnected; } #endregion Ping All Devices #region Ping One Device /// /// TCP ping /// /// device IP addresses /// false - if device disconnected private bool PingOneDevice(string deviceIpAddress) { for (var i = 0; i < MAX_PING_ATTEMPTS; i++) { var pingSender = new Ping(); var reply = pingSender.Send(deviceIpAddress); if (reply != null && reply.Status == IPStatus.Success) return true; } return false; } /// /// ICMP ping /// /// device IP addresses /// host IP address /// false - if device disconnected private bool PingOneDevice(string deviceIp, string hostIpAddress) { for (var i = 0; i < MAX_PING_ATTEMPTS; i++) { if (PingICMP(IPAddress.Parse(hostIpAddress), IPAddress.Parse(deviceIp), Constants.PING_ICMP_TIMEOUT, Encoding.ASCII.GetBytes(ALPHABET)).Status == IPStatus.Success) return true; } return false; } #endregion Ping One Device #region ICMP Ping /// /// ICMP ping /// /// host address /// device address /// timeout /// Function will fail without buffer (used alphabet) /// Ping options - not been usexd /// PingReplyUtils class private static PingReplyUtils PingICMP(IPAddress srcAddress, IPAddress destAddress, int timeout = 5000, byte[] buffer = null, PingOptions po = null) { var ipSrc = string.Empty; var ipDest = string.Empty; try { var ipSrcBytes = srcAddress?.GetAddressBytes() ?? new byte[0]; if (4 <= ipSrcBytes.Length) { ipSrc = $"{ipSrcBytes[0]}.{ipSrcBytes[1]}.{ipSrcBytes[2]}.{ipSrcBytes[3]}"; } var destBytes = destAddress?.GetAddressBytes() ?? new byte[0]; if (4 <= destBytes.Length) { ipDest = $"{destBytes[0]}.{destBytes[1]}.{destBytes[2]}.{destBytes[3]}"; } } catch (Exception ex) { APILogger.Log(ex); } if (destAddress == null || destAddress.AddressFamily != AddressFamily.InterNetwork || destAddress.Equals(IPAddress.Any)) { APILogger.Log($"Ping -S {ipSrc} {ipDest} failed - Argument exception"); throw new ArgumentException(string.Format("PingAll failed on IP:{0}", srcAddress)); } //Defining pinvoke args var source = srcAddress == null ? 0 : BitConverter.ToUInt32(srcAddress.GetAddressBytes(), 0); var destination = BitConverter.ToUInt32(destAddress.GetAddressBytes(), 0); var sendbuffer = buffer ?? new byte[] { }; var options = new Interop.Option { Ttl = (po == null ? (byte)255 : (byte)po.Ttl), Flags = (po == null ? (byte)0 : po.DontFragment ? (byte)0x02 : (byte)0) //0x02 }; var fullReplyBufferSize = Interop.ReplyMarshalLength + sendbuffer.Length; //Size of Reply struct and the transmitted buffer length. var allocSpace = Marshal.AllocHGlobal(fullReplyBufferSize); // unmanaged allocation of reply size. TODO Maybe should be allocated on stack try { var start = DateTime.Now; var nativeCode = Interop.IcmpSendEcho2Ex( /* _In_ HANDLE IcmpHandle, */ Interop.IcmpHandle, /* _In_opt_ HANDLE Event, */ default(IntPtr), /* _In_opt_ PIO_APC_ROUTINE ApcRoutine, */ default(IntPtr), /* _In_opt_ PVOID ApcContext */ default(IntPtr), /* _In_ IPAddr SourceAddress, */ source, /* _In_ IPAddr DestinationAddress, */ destination, /* _In_ LPVOID RequestData, */ sendbuffer, /* _In_ WORD RequestSize, */ (short)sendbuffer.Length, /* _In_opt_ PIP_OPTION_INFORMATION RequestOptions, */ ref options, /* _Out_ LPVOID ReplyBuffer, */ allocSpace, /* _In_ DWORD ReplySize, */ fullReplyBufferSize, /* _In_ DWORD Timeout */ timeout); var duration = DateTime.Now - start; var reply = (Interop.Reply)Marshal.PtrToStructure(allocSpace, typeof(Interop.Reply)); // Parse the beginning of reply memory to reply struct byte[] replyBuffer = null; if (sendbuffer.Length != 0) { replyBuffer = new byte[sendbuffer.Length]; Marshal.Copy(allocSpace + Interop.ReplyMarshalLength, replyBuffer, 0, sendbuffer.Length); //copy the rest of the reply memory to managed byte[] } var errorCode = 0; if (0 != nativeCode) { errorCode = Marshal.GetLastWin32Error(); } APILogger.Log($"Ping -S {ipSrc} {ipDest} result: {reply.Status} LastError: {errorCode}"); return nativeCode == 0 ? new PingReplyUtils(nativeCode, reply.Status, new IPAddress(reply.Address), duration) : new PingReplyUtils(nativeCode, reply.Status, new IPAddress(reply.Address), reply.RoundTripTime, replyBuffer); } finally { Marshal.FreeHGlobal(allocSpace); /*free allocated space*/ } } #endregion ICMP Ping #region Support ICMP Ping /// Interoperability Helper /// /// private static class Interop { private static IntPtr? _icmpHandle; private static int? _replyStructLength; /// Returns the application legal icmp handle. Should be close by IcmpCloseHandle /// /// public static IntPtr IcmpHandle { get { if (_icmpHandle == null) { _icmpHandle = IcmpCreateFile(); //TODO Close Icmp Handle appropiate } return _icmpHandle.GetValueOrDefault(); } } /// Returns the the marshaled size of the reply struct. public static int ReplyMarshalLength { get { if (_replyStructLength == null) { _replyStructLength = Marshal.SizeOf(typeof(Reply)); } return _replyStructLength.GetValueOrDefault(); } } [DllImport("Iphlpapi.dll", SetLastError = true)] private static extern IntPtr IcmpCreateFile(); [DllImport("Iphlpapi.dll", SetLastError = true)] private static extern bool IcmpCloseHandle(IntPtr handle); [DllImport("Iphlpapi.dll", SetLastError = true)] public static extern uint IcmpSendEcho2Ex(IntPtr icmpHandle, IntPtr Event, IntPtr apcroutine, IntPtr apccontext, uint sourceAddress, uint destinationAddress, byte[] requestData, short requestSize, ref Option requestOptions, IntPtr replyBuffer, int replySize, int timeout); [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct Option { public byte Ttl; public readonly byte Tos; public byte Flags; public readonly byte OptionsSize; public readonly IntPtr OptionsData; } [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] public struct Reply { public readonly uint Address; public readonly int Status; public readonly int RoundTripTime; public readonly short DataSize; public readonly short Reserved; public readonly IntPtr DataPtr; public readonly Option Options; } } #endregion Support ICMP Ping #endregion Ping private static readonly object LogLock = new object(); public static void PingLog(params string[] args) { var sb = new StringBuilder(200); sb.AppendFormat("=== {0} ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff")); foreach (var arg in args) { sb.Append(" "); sb.Append(arg); } sb.Append(Environment.NewLine); lock (LogLock) { try { File.AppendAllText("Logs/ping.log", sb.ToString()); } catch (Exception ex) { Trace.WriteLine(ex); } } } #region OldPing public void PingFunction(object o) { var td = o as PingThreadData; if (td == null) return; td.UpdateProgress(0D); if (null == td.DoneEvent) { td.DoneEvent = new ManualResetEvent(false); } if (null == td.CancelEvent) { td.CancelEvent = new ManualResetEvent(false); } var sb = new StringBuilder(); sb.AppendFormat("PingFunction "); foreach (var item in td.Entries) { sb.AppendFormat("{0}, ", item.IPAddress); } PingLog(sb.ToString()); if (0 == td.Entries.Length) { td.UpdateProgress(100D); Thread.Sleep(100); td.Complete(); return; } // Kick off the background thread while we monitor/simulate progress. Task.Run(() => CheckOnline(td)).ConfigureAwait(false); // FB 25642 Consider the number of hosts as well var maxTime = Constants.PING_ICMP_TIMEOUT * td.Entries.Length * NetworkUtils.GetAvailableHosts().Count; if (maxTime > 0 && maxTime < 1000) { maxTime = 1000; //minimum of 1 second } // Tick along progress as if it will take MAXPINGTIME. If we finish early, great. var timeWaited = 0D; while (!td.DoneEvent.WaitOne(150, false) && !td.CancelEvent.WaitOne(0, false)) { timeWaited += 150D; if (timeWaited >= maxTime) { PingLog("WARNING timewaited >= maxpingtime: ", timeWaited.ToString(), ">=", maxTime.ToString()); } } if (td.CancelEvent.WaitOne(0, false)) { td.Complete(); } //else if (timeWaited >= Maxpingtime) //{ // PingLog("WARNING timewaited >= maxpingtime: ", timeWaited.ToString(), ">=", // Maxpingtime.ToString()); // //td.CancelEvent.Set(); // //td.Complete(); // //td.DoneEvent.WaitOne(); // Thread.Sleep(100); //} else { td.UpdateProgress(100D); Thread.Sleep(100); td.Complete(); } } //used to hold the number of ips pinged completed while actively pinging private volatile int _pingsDone = 0; /// /// Pinging device from specific IP address function will use IMC protocol, TC Protocol will be used if host IP address is empty /// /// A object private void CheckOnline(object o) { var td = o as PingThreadData; if (td != null && null == td.CancelEvent) { td.CancelEvent = new ManualResetEvent(false); } try { var maxPingTime = Convert.ToInt32(Constants.PING_ICMP_TIMEOUT); if (td == null) return; var ipList = new List(); foreach (var entry in td.Entries) { ipList.Add(entry.IPAddress); } var hostInfos = NetworkUtils.GetAvailableHosts(); EliminateBadHosts(ref hostInfos, ipList.ToArray()); if (!hostInfos.Any()) { return; } var lastHost = hostInfos.Last(); var alphabet = Encoding.ASCII.GetBytes(ALPHABET); var toPing = td.Entries.Length; _pingsDone = 0; _ = Parallel.ForEach(td.Entries, item => { if (td.CancelEvent.WaitOne(0, false)) { td.UpdateEntry(item, PingProgressStates.Cancel, 0); return; } td.UpdateEntry(item, PingProgressStates.Pinging, 0); var succeded = false; var attempt = 0; //FB 25642 Get available hosts //avoid having to re-parse the ip address x thousand of times var lookup = new Dictionary(); foreach (var hostInfo in hostInfos) { lookup[hostInfo] = IPAddress.Parse(hostInfo.HostIpAddress); } while (!succeded && attempt < MAX_PING_ATTEMPTS && !td.CancelEvent.WaitOne(0, false)) { if (td.CancelEvent.WaitOne(0, false)) { td.UpdateEntry(item, PingProgressStates.Cancel, 0); return; } try { PingReplyUtils allReply = null; //FB 25642 we need to go through all the hosts foreach (var hostInfo in hostInfos) { if (!IPAddress.TryParse(item.IPAddress, out var ip)) { attempt = MAX_PING_ATTEMPTS; continue; } //won't wpork if buffer is empty var eachReply = PingICMP(lookup[hostInfo], ip, Constants.PING_ICMP_TIMEOUT, alphabet); if ((eachReply.Status == IPStatus.Success) && (eachReply.NativeCode != 0)) { //we found the host, update the dictionary & break DasToHost[item.IPAddress] = hostInfo; allReply = eachReply; break; } if (hostInfo.Equals(lastHost)) { //if it's the last host update the overall reply allReply = eachReply; } } //If the default gateway of the network interface isn't set appropriately, the call to PingICMP will //return with a Status of IPStatus.Success. So, to differentiate this from a valid successful ping, we check NativeCode if (allReply != null) { //FB 25642 allReply shows the ping was successfull or not if ((allReply.Status == IPStatus.Success) && (allReply.NativeCode != 0)) { succeded = true; td.UpdateEntry(item, PingProgressStates.Ping_Good, allReply.RoundTripTime.Milliseconds); PingLog("ping succeeded: ", DasToHost[item.IPAddress]?.HostIpAddress, " -> ", item.IPAddress); } else { attempt++; } } else { PingLog("couldn't parse ip: ", item.IPAddress); attempt++; } } catch (Exception) { td.UpdateEntry(item, PingProgressStates.InvalidIPAddress, 0); break; } } if (succeded) { _pingsDone++; td.UpdateProgress(100D * _pingsDone / toPing); } else if (MAX_PING_ATTEMPTS <= attempt) { _pingsDone++; td.UpdateEntry(item, PingProgressStates.NoReply, 0); td.UpdateProgress(100D * _pingsDone / toPing); PingLog("failed (maxpings <= attempt: ", item.IPAddress, " - ", MAX_PING_ATTEMPTS.ToString(), "<=", attempt.ToString()); } if (td.CancelEvent.WaitOne(0, false)) { td.UpdateEntry(item, PingProgressStates.Cancel, 0); } }); } catch (Exception ex) { APILogger.Log(ex); throw; } finally { _ = td.DoneEvent.Set(); } } #endregion OldPing } } #region PingReply [Serializable] public class PingReplyUtils { private Win32Exception _exception; public PingReplyUtils() { } internal PingReplyUtils(uint nativeCode, int replystatus, IPAddress ipAddress, TimeSpan duration) { NativeCode = nativeCode; IpAddress = ipAddress; if (Enum.IsDefined(typeof(IPStatus), replystatus)) { Status = (IPStatus)replystatus; } } internal PingReplyUtils(uint nativeCode, int replystatus, IPAddress ipAddress, int roundTripTime, byte[] buffer) { NativeCode = nativeCode; IpAddress = ipAddress; RoundTripTime = TimeSpan.FromMilliseconds(roundTripTime); Buffer = buffer; if (Enum.IsDefined(typeof(IPStatus), replystatus)) { Status = (IPStatus)replystatus; } } /// Native result from IcmpSendEcho2Ex. public uint NativeCode { get; } = 0; public IPStatus Status { get; } = IPStatus.Unknown; /// The source address of the reply. public IPAddress IpAddress { get; } = null; public byte[] Buffer { get; } = null; public TimeSpan RoundTripTime { get; } = TimeSpan.Zero; /// Resolves the Win32Exception from native code public Win32Exception Exception { get { if (Status != IPStatus.Success) { return _exception ?? (_exception = new Win32Exception((int)NativeCode, Status.ToString())); } return null; } } public override string ToString() { if (Status == IPStatus.Success) { return Status + " from " + IpAddress + " in " + RoundTripTime + " ms with " + Buffer.Length + " bytes"; } if (Status != IPStatus.Unknown) { return Status + " from " + IpAddress; } return Exception.Message + " from " + IpAddress; } } #endregion PingReply #region Entry /// /// Trivial class to represent an IP address entry, its type, and a visual representation /// public class Entry { public string IPAddress { get; set; } public int DasType { get; set; } public DataRow DR { get; set; } public object UserData { get; set; } } #endregion Entry #region HostInfo //FB 25642 & 25590 & 18152 This class encapsulate information for the host in one place public class HostInfo { public string HostIpAddress { get; set; } = string.Empty; public string HostMacAddress { get; set; } = string.Empty; public string HostNetworkId { get; set; } = string.Empty; public string StartAddress { get; set; } = string.Empty; public string EndAddress { get; set; } = string.Empty; } #endregion #region PingProgressStates /// /// The possible states for a given . /// public enum PingProgressStates { Pinging, Connecting, Querying, Added, Updated, InvalidIPAddress, NoReply, Ping_Good, Online, Connected, NoConnection, Cancel, Ready, Armed, QueryFailed, QueryTimedOut, QueryComplete, UnexpectedMaxMemory, UnexpectedFirmwareVersion, Passed, ExpiredCalDate, ChannelCountMismatch, MissingModules, Canceled, NoMemory, AutoArmed, Realtime, ReadyToStream, Streaming, LostDock, Rebooting, UnexpectedFirstUseDate, InvalidRecordingMode, InvalidStreamingMode, StreamingNotAvailable } #endregion PingProgressStates #region PingThreadData /// /// A wrapper class to represent the progress and feedback for ping activity from background threads. /// public class PingThreadData { /// /// list to act on. /// public Entry[] Entries { get; set; } /// /// Optional to tell the process to cancel. /// public ManualResetEvent CancelEvent { get; set; } /// /// Optional to fire when the entire process is done. /// public ManualResetEvent DoneEvent { get; set; } /// /// An optional delgate to be called whenever status text has changed. /// public SetStatusTextDelegate setStatusTextCallback; /// /// An optional delgate to be called whenever progress has changed. /// public SetProgressValueDelegate progressCallback; /// /// A delegate type used for reporting progress on an individual entry /// /// that is being reported on /// indicating the new state public delegate void EntryProgressCallback(Entry entry, PingProgressStates state, long msRoundTrip); /// /// An optional delegate to be called whenever an individual entry has changed. /// public EntryProgressCallback entryProgressCallback; /// /// An optional delegate to be called when the entire process is complete. Called before is fired. /// public ActionCompleteDelegate completeCallback; /// /// Wrapper for posting progress /// /// From 0 to 100 to indicate progress public void UpdateProgress(double Percent) { progressCallback?.Invoke(Percent); } /// /// Wrapper for posting entry progress /// /// The entry being updated /// The new state public void UpdateEntry(Entry entry, PingProgressStates state, long msResponseTime) { entryProgressCallback?.Invoke(entry, state, msResponseTime); } /// /// Wrapper for posting complete /// public void Complete() { completeCallback?.Invoke(); DoneEvent?.Set(); } public bool AvoidConnectPortion { get; set; } = false; public bool PingFailed { get; set; } = false; } #endregion PingThreadData }