using System; using System.Diagnostics; using System.Linq; using System.ServiceProcess; using System.Reflection; using PlutoServer.MSL.Connectors; //using TCMPortMapper; using System.Net; using Pluto.Api; using System.Xml; using System.IO; using System.Text; using Sdaleo.Systems.Advantage; using Sdaleo; using Sdaleo.Systems.SQLServer; using Yaulw.File; using Yaulw.Thread; using TCMPortMapper; using Yaulw.Assembly; namespace PlutoServer.MSL { public partial class PlutoService : ServiceBase { //Portmapper singleton instance PortMapper pMapper = PortMapper.SharedInstance; #region Internal Consts // Hard-coded in * Internal channel always uses these ports internal const int INTERNAL_CHANNEL_PORT = 1945; internal const int EXTERNAL_MINIMUM_PORT_NUMBER = 49000; internal const int EXTERNAL_MAXIMUM_PORT_NUMBER = 50000; // Hard-coded in * Registry Strings / Settings internal const string LOCAL_MACHINE_SUBKEY = "Software\\MSLMobile"; internal const string LOCAL_MACHINE_EXTERNALPORT = "ExternalPort"; internal const string LOCAL_MACHINE_HOSTGUID = "HostGuid"; internal const string LOCAL_MACHINE_INITIALSETUPDT = "InitialSetup"; // Threshold used to determine if system just rebooted, and service just // started. We need to wait a little because we need to make sure SQLServer // and Advantage Server are up internal const int NUMBER_OF_MINUTES_SERVICE_UP_THRESHOLD = 5; internal const int NUMBER_OF_MINUTES_SYSTEM_UP_THRESHOLD = 10; // Threshold used to query WhatIsMyIP (they allow every 5 Minutes) internal const int NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP = 15; // Use this host to check, if we are on the McKesson Network internal const string MCK_HOST_TO_CHECK_ON_MCK_NETWORK = "ndh1fs01.mckesson.com"; #endregion #region Internal Statics (Important for getting Information from Pluto Service) /// /// Service Start Time /// internal static DateTime ServiceStartDT = DateTime.MinValue; /// /// Did the service statup on the McKesson Network? Initialized upon Service Start /// internal static bool IsOnMcKessonNetwork { get; private set; } /// /// Are the External IP and Port Loaded (and set) /// internal static bool IsExtIPandExtPortSet { get { return s_PlutoService.InitialExternalIPConfigurationIsSet; } } /// /// LocalIPAddress, automatically set by our Connectivity Thread /// internal static IPAddress InternalIP { get; private set; } /// /// ExternalIPAddress, automatically set by our Connectivity Thread /// updated upon service startup and then at interval NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP /// internal static IPAddress ExternalIP { get; private set; } /// /// External Port, automatically set by our Connectivity Thread (Radomly or from the Registry) /// LOCAL_MACHINE_EXTERNALPORT /// internal static uint ExternalPort { get; private set; } /// /// There is no need for us to update the server if the Host GUID doesn't exist, hence /// we need to at start up know whether or not to automatically update or not, or whether to /// just wait /// internal static RegistrationWrapper.HostGUIDstate state = RegistrationWrapper.HostGUIDstate.no_connectivity; /// /// Quick Check to get the local IP Address /// /// internal static IPAddress IPFetchLocalIPAddress { get { IPAddress IP = Yaulw.Net.IPHostHelper.GetFirstLocalIPAddress(); if (IP == IPAddress.None) MSLSpecific.Logger.Error("No Local IP Address Found using IP {0} as Local Address", IPAddress.None); return IP; } } /// /// Reloads the External IP, and Port Configuration, /// by setting ReloadExternalIPandPortConfiguration to true /// internal static bool ReloadExtIPandPortConfiguration(int Port) { if (Port > 0 && Port != ExternalPort) { s_PlutoService.DeactivateROExternalChannelIfItHasBeenActivated(false); // Set the Port in the Registry Yaulw.Registry.RegKey.SetKey(Yaulw.Registry.HKEYRoot.LocalMachine, LOCAL_MACHINE_SUBKEY, LOCAL_MACHINE_EXTERNALPORT, Port); // Reload the Ports IPAddress extIP = IPAddress.None; uint nPort = 0; // A user could already have the router pointed to us while we are still set on another port, // then decide to change port. We can check via PlutoCheck if this new port is actually pointing to us already // without the Pluto check we will see it as used and jump to another port (unexpected behavior for the user) if (s_PlutoService.Retrieve_ExternalIP_ExternalPort(true, true, true, out extIP, out nPort)) { ExternalIP = extIP; ExternalPort = nPort; s_PlutoService.LastExternalIPCheckRun = DateTime.Now; // Re-initialize the McKesson's Mobile Gateway RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort); // Re-activate the Channel s_PlutoService.ActivateROInternalChannelIfHasNotBeenActivated(); // If the port is now what the user asked for than we are good to go bool bSuccess = (nPort == Port); if (!bSuccess) MSLSpecific.Logger.Error("ReloadExtIPandPortConfiguration Failed nPort:{0} Port:{1}", nPort, Port); return bSuccess; } } return false; } #endregion #region Private Members /// /// Check if this is the first time the network got enabled /// private bool FirstTimeNetworkConnected = true; /// /// Was the Internal Channel Activated? /// private bool ROInternalHasBeenActivated = false; /// /// Was the External Channel Activated? /// private bool ROExternalHasBeenActivated = false; /// /// Did a System Reboot just occur? /// private bool SystemJustStarted_SoUseServiceThreshold = false; private bool ServiceUpTimeReached = false; private bool SystemThresholdNotReached = true; /// /// Has the DataStore been read in? Try up to Number of Times /// private bool DataStoreHasBeenRead = false; private uint Number_Of_Tries_to_Read_DataStore = 3; private bool DataStoreReadErroredOut = false; /// /// Has the PortMapper been started? /// private bool IsPortMapperRunning = false; /// /// Is the Logger Setup? /// private bool LoggerIsSetup = false; /// /// Try to Read in External IP configuration. Try up to Number of Times /// private bool InitialExternalIPConfigurationIsSet = false; private uint Number_Of_Tries_to_Read_ExternalIP_Configuration = 3; private bool InitialExternalIPRetrieveErrorerOut = false; /// /// What is my IP only allows us to check external IP every NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP /// private DateTime LastExternalIPCheckRun = DateTime.MinValue; /// /// Our Connectivity Timer Thread /// private SingleThreadTimer connectivityTimer = null; /// /// Allows us to skip cycles in the Connectivity Thread /// private uint nConnectivitySkipCounter = 0; /// /// If for some reason the network goes down, we should re-initialize /// everything as if nothing happened /// private bool NetworkOutageDetected = false; /// /// When we start up we have no knowledge whether we can connect to the /// registrat/gateway server. Also if it goes down. If we detect a switch from /// no to exist we force an external UpdateServer Refresh /// *by default this is true* because at startup we want to do this also /// private bool SwitchFromNoConnectivityToConnectivityOccured = true; /// /// Send the service version upon Service start-up /// private bool ServiceStartVersionSent = false; #endregion #region Construction /// /// Singleton Instance of the Service /// private static PlutoService s_PlutoService = null; /// /// Constructor /// public PlutoService() { InitializeComponent(); // Log to the Application System Log this.EventLog.Log = "Application"; s_PlutoService = this; // Handle all unexpected errors AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException); } /// /// Unhandled Domain Exception /// /// /// static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e) { Exception ex = (Exception)e.ExceptionObject; String msg = "UnhandledException Occured: " + ex.Message + "\n\n" + ex.InnerException.Message; AppLogError(msg); } #endregion #region Windows Event Logging /// /// Log into the Event Viewer as well as the Service Log (if possible) /// /// internal static void AppLogError(string errMsg) { s_PlutoService.EventLog.WriteEntry(errMsg, EventLogEntryType.Error); if (MSLSpecific.Logger != null) MSLSpecific.Logger.Error(errMsg); } /// /// Log into the Event Viewer as well as the Service Log (if possible) /// /// internal static void AppLogInfo(string infoMsg) { s_PlutoService.EventLog.WriteEntry(infoMsg, EventLogEntryType.Information); if (MSLSpecific.Logger != null) MSLSpecific.Logger.Info(infoMsg); } /// /// Log into the Event Viewer as well as the Service Log (if possible) /// /// internal static void AppLogWarning(string warnMsg) { s_PlutoService.EventLog.WriteEntry(warnMsg, EventLogEntryType.Warning); if (MSLSpecific.Logger != null) MSLSpecific.Logger.Info(warnMsg); } #endregion // usefull for debugging Service in visual studio #if DEBUG internal void DebugStart(string[] args) { DEBUGGING_ONLY.DEBUGSTEP_INTO(); OnStart(args); } #endif #region System / Service Up Time /// /// Get Service UpTime /// /// private TimeSpan GetServiceUpTime() { return (DateTime.Now - ServiceStartDT); } /// /// For Service Handling, see if the system has been up long enough /// Once it becomes true, it will always return true /// private bool HasServiceBeenUpLongEnough() { if (!ServiceUpTimeReached) ServiceUpTimeReached = GetServiceUpTime().TotalMinutes >= NUMBER_OF_MINUTES_SERVICE_UP_THRESHOLD; return ServiceUpTimeReached; } /// /// For Error Handling, see if the system has been up till this threshold /// Once it becomes false, will always return false /// /// private bool IsSystemBeenUpWithinMaxThreshold() { if (SystemThresholdNotReached) SystemThresholdNotReached = (Yaulw.Installer.Common.GetSystemUpTime().TotalMinutes <= NUMBER_OF_MINUTES_SYSTEM_UP_THRESHOLD); return SystemThresholdNotReached; } #endregion /// /// Service Start-Up /// /// protected override void OnStart(string[] args) { try { // Service Started // System just started, so make sure that service is timing out a little to make // sure everything is up and running SystemJustStarted_SoUseServiceThreshold = IsSystemBeenUpWithinMaxThreshold(); AppLogInfo(String.Format("MSLMobile Api Service version:'{0}' started up at:'{1}'. System Reboot detected:'{2}'", AssemblyW.GetAssemblyVersion(AssemblyW.AssemblyST.Entry).ToString(), DateTime.Now.ToLongTimeString(), SystemJustStarted_SoUseServiceThreshold)); ServiceStartDT = DateTime.Now; // Are we on the McKesson Network? IsOnMcKessonNetwork = Yaulw.Net.IPHostHelper.CanResolveHost(PlutoService.MCK_HOST_TO_CHECK_ON_MCK_NETWORK, true); if (IsOnMcKessonNetwork) AppLogInfo("McKesson Network Detected"); else AppLogInfo("Regular Network Detected"); // Load the AppConfig if(Configuration.Load(false)) AppLogInfo("Using the .config File as main configuration"); else AppLogInfo("Using hard-coded in values as main configuration"); // Load the Determine External IP Url Yaulw.Net.IPHostHelper.WHAT_IS_MY_IP_AUTOMATION_URL = Configuration.DETERMINE_EXTERNAL_IP_ADDRESS_URL; AppLogInfo(String.Format("Using '{0}' as our determine external IP address url", Configuration.DETERMINE_EXTERNAL_IP_ADDRESS_URL)); // Internet / Network Connectivity Timer // * Ran into issues when computer is rebooted, we need certain things working before the service can start, // * therefore decided to put DataStore Read(), AutoUpdate, RO Connection into a Connection Timer, // that keeps checking every 10 seconds if we have network connectivity * Enables / Disables RO Channels as needed, just in case * AppLogInfo("MSLMobile Api Service starting connectivity timer."); connectivityTimer = new SingleThreadTimer(ConnectivityHandler, (uint)TimeSpan.FromSeconds(10).TotalMilliseconds, true); } catch (Exception e) { AppLogError(String.Format("Service OnStart() Error thrown. Fatal. Can't continue: {0}", e.Message)); this.Stop(); } } #region Private Connectivity Timed Thread (runs every 10 Seconds) /// /// Single Threaded Connectivity Handler (Handles Auto-Update, ReadInDataStore(), and RO Channels) /// /// /// private void ConnectivityHandler(object sender, Yaulw.Thread.SingleThreadTimer.STElapsedEventArgs e) { // System just started, we won't do anyting in here until our // Service Startup 'NUMBER_OF_MINUTES_SERVICE_UP_THRESHOLD' timeout of is reached if (SystemJustStarted_SoUseServiceThreshold && !HasServiceBeenUpLongEnough()) return; // If the skip counter is set, use it to skip iterations if (nConnectivitySkipCounter > 0) { nConnectivitySkipCounter--; return; } // Check Connectivity, // ~withouth basic networking being up there is nothing really to do here bool bHasConnection = Yaulw.Net.IPHostHelper.HasConnection(); if (!bHasConnection) { DeactivateROChannelsIfTheyHaveBeenActivated(true); return; } // Initialize Internal IP if (InternalIP == null) InternalIP = PlutoService.IPFetchLocalIPAddress; // Always set up the Logger First (Needed for all subsequent Calls) if (!LoggerIsSetup) { AppLogInfo("Setting up Logger"); MSLSpecific.Setup_Logger(); LoggerIsSetup = true; } // Check To See if we need to update, This is our first time that // the system has been up long enough and that we have a network connection if (FirstTimeNetworkConnected) { AppLogInfo("System has been up long enough. Starting Initialization."); // Upon Every Startup of the Service, let Auto-Update know to update the service when needed #if !DEBUG AppLogInfo("Starting Auto-Update Timer."); AutoUpdate.StartAutoTimer_ServiceStarted(); #endif FirstTimeNetworkConnected = false; // just to make sure wait another cycle * due to Auto-Update * Feature return; } // * Check to Make sure the System has been up long enough * // Calling ReadInDataStore() requires Advantage and/or SQL Server to be up // and running and hence, we can't do it right at service start when the // server just rebooted, we wait 5 of service uptime to make sure after a server reboot, // that the services needed are up if (!FirstTimeNetworkConnected) { // If Network disconnection occured, we should try to re-read in everything, // to make sure we are doing the best we can if (NetworkOutageDetected) { AppLogInfo("Network Outage Detected - Rereading Data and External IP settings"); // Re-read in Data Store DataStoreHasBeenRead = false; DataStoreReadErroredOut = false; // Re-initialize initial ExternalIP Retrival InitialExternalIPConfigurationIsSet = false; InitialExternalIPRetrieveErrorerOut = false; } // First step try to load the DataBase Cache if (!LoadInDataStore()) return; // Second is to do is to figgure out the external IP and Port Settings if (!Initialize_ExternalIP_And_ExternalPort()) return; // Lastly, activate any of the RO Channels ActivateROChannelsIfTheyHaveNotBeenActivated(); // There is no need to call update Server automatically, // if the host guid doesn't exist, the MSLSpecific calls would // have to call RegisterNewPractice First (in case of no connectivity // we can't call registration server anyways, so may as well time out // for 5 minutes and try again) switch (state) { case RegistrationWrapper.HostGUIDstate.no_connectivity: { state = RegistrationWrapper.DoesServerHostGuidExist(); if (state == RegistrationWrapper.HostGUIDstate.no_connectivity) { MSLSpecific.Logger.Info("Communication to Mobile Gateway Server could not be established, trying again in 5 minutes."); // try again in 5 minutes... nConnectivitySkipCounter = 30; // force a UpdateServerIntExt right away once connectivity is restored SwitchFromNoConnectivityToConnectivityOccured = true; return; } else if (state == RegistrationWrapper.HostGUIDstate.exists) { goto case RegistrationWrapper.HostGUIDstate.exists; } break; } case RegistrationWrapper.HostGUIDstate.exists: { // Try to force the UpdateServerIntExt Right away, // once a switch from no connectivity to connectivity occurs if (SwitchFromNoConnectivityToConnectivityOccured && InitialExternalIPConfigurationIsSet) { RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort); SwitchFromNoConnectivityToConnectivityOccured = false; } else if (SwitchFromNoConnectivityToConnectivityOccured) { // We must also initialize internal, in case external was never set // i.e. we are on the McKesson Network or WIFI only mode RegistrationWrapper.UpdateServerInternal(InternalIP); SwitchFromNoConnectivityToConnectivityOccured = false; } // Let the Gateway know upon service start-up what version we are running // (only do this once per service start), so after a fresh install, we know what they are running if (!ServiceStartVersionSent) { Version LocalVersion = Yaulw.Assembly.AssemblyW.GetAssemblyVersion(Yaulw.Assembly.AssemblyW.AssemblyST.Entry); RegistrationWrapper.ServerHasBeenUpdated(LocalVersion.ToString()); ServiceStartVersionSent = true; } // Handle Internal Address Changes every 10 seconds HandleLocalIPAddressChange(); // Handle External Address Change every 5 minutes HandleExternalIPAddressChange(); break; } default: { break; } } } } #endregion #region Connectivity Important Handlers /// /// First_LoadInDataStore() First thing to do, we should connect to the data store, /// we try up to 3 times, should be enough. Each time has 1 minute. /// /// true, if network connection is up /// True for the Caller to continue, false otherwise private bool LoadInDataStore() { if (!DataStoreHasBeenRead && !DataStoreReadErroredOut) { AppLogInfo("Trying to load the DataStore"); DataStoreHasBeenRead = DBCache.ReadInDataStore(); if (DataStoreHasBeenRead) { // This is good news! AppLogInfo("DataStore has been read successfully or DataStore is empty"); return true; } else if (!DataStoreHasBeenRead && Number_Of_Tries_to_Read_DataStore > 0) { // We are still erroring out, try again in a minute, up to Number_Of_Tries_to_Read_DataStore // Skip six cycles, aka. 60 seconds for each try Number_Of_Tries_to_Read_DataStore--; nConnectivitySkipCounter = 6; // Don't allow to continue until this is complete return false; } else if (!DataStoreHasBeenRead && Number_Of_Tries_to_Read_DataStore == 0) { DataStoreReadErroredOut = true; AppLogWarning("Failed to read a single DataStore item. Mobile Api Service may not be in a good state. Contact support if problem persist."); // Failed to many times, warn the user but nothing totally critical yet return true; } } // always continue, once we reach here return true; } /// /// Trues to retrieve and set the External IP, and calculatest a possible Port To Use, /// we try up to 3 times, should be enough. Each time has 1 minute. /// /// true, if network connection is up /// True for the Caller to continue, false otherwise private bool Initialize_ExternalIP_And_ExternalPort() { try { #if DEBUG if (!InitialExternalIPConfigurationIsSet && !InitialExternalIPRetrieveErrorerOut) #else if (!IsOnMcKessonNetwork && !InitialExternalIPConfigurationIsSet && !InitialExternalIPRetrieveErrorerOut) #endif { AppLogInfo("Trying to load External IP Configuration"); IPAddress extIP = IPAddress.None; uint Port = 0; // During initialization/startup we just will only check that the port is not in use via // Port Check. A pluto check is overkill and doesn't make sense. Only a port check should be enough. // Whether this get's called when network card is down or upon Service Startup, we will be fine // by auto-calculating the port and not pluto checking if (Retrieve_ExternalIP_ExternalPort(true, true, false, out extIP, out Port)) { InitialExternalIPConfigurationIsSet = true; ExternalIP = extIP; ExternalPort = Port; LastExternalIPCheckRun = DateTime.Now; AppLogInfo(String.Format("Loaded initial External IP:{0} and Port:{1} at {2}", extIP, ExternalPort, LastExternalIPCheckRun)); return true; } else if (!InitialExternalIPConfigurationIsSet && Number_Of_Tries_to_Read_ExternalIP_Configuration > 0) { // We are still erroring out, try again in a minute, up to Number_Of_Tries_to_Read_ExternalIP_Configuration // Skip six cycles, aka. 60 seconds for each try Number_Of_Tries_to_Read_ExternalIP_Configuration--; nConnectivitySkipCounter = 6; // Don't allow to continue until this is complete return false; } else if (!InitialExternalIPConfigurationIsSet && Number_Of_Tries_to_Read_ExternalIP_Configuration == 0) { // Failed too many times, we can work arround this via *Wifi Only* InitialExternalIPRetrieveErrorerOut = true; AppLogWarning("Failed to load External IP Configuration for now. Will try again in 5 minutes"); // Failed to many times, warn the user but nothing totally critical yet LastExternalIPCheckRun = DateTime.Now; return true; } } // always continue, once we reach here return true; } catch (Exception e) { MSLSpecific.Logger.Error("Something is seriously wrong with Initialize_ExternalIP_And_Port.", e); MSLSpecific.Logger.Fatal("Dev has to look into this. Program may not be able to recover.", e); } return false; } /// /// Handle Local IP Address Change /// private void HandleLocalIPAddressChange() { IPAddress intIP = IPAddress.None; bool bChangeOccured = Yaulw.Net.IPHostHelper.AreIPAddressesDifferent(InternalIP, PlutoService.IPFetchLocalIPAddress, out intIP); if (bChangeOccured && Yaulw.Net.IPHostHelper.IsValidIPv4Address(intIP, false)) { InternalIP = intIP; if (InitialExternalIPConfigurationIsSet) { // Update the McKesson's Mobile Gateway RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort); } else { // Update the McKesson's Mobile Gateway RegistrationWrapper.UpdateServerInternal(InternalIP); } } } /// /// Handle External IP Address Change /// private void HandleExternalIPAddressChange() { // What is my ip automation rules allow every NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP minutes to check, so that is what'll do TimeSpan ts = DateTime.Now - LastExternalIPCheckRun; bool bTimePassed = (ts.TotalMilliseconds >= TimeSpan.FromMinutes(NUMBER_OF_MINUTES_TO_QUERY_FOR_EXTERNAL_IP).TotalMilliseconds); // Also Check that we are not updating between the hours of 2am to 6am DateTime dt2 = DateTime.Now.ToLocalTime(); TimeSpan ts2 = dt2.TimeOfDay; bool bIsBetween2amAnd6am = (ts2.TotalHours >= 2.0) && (ts2.TotalHours <= 6.0); if (bIsBetween2amAnd6am && bTimePassed) bTimePassed = false; // We need to automatically keep track of the external IP on External Networks // and update it with the McKesson's Mobile Gateway, as needed #if DEBUG if (bTimePassed) #else if (!IsOnMcKessonNetwork && bTimePassed) #endif { // Retrieve both Port and IP configuration to set the initial configuration if (!InitialExternalIPConfigurationIsSet) { IPAddress extIP = IPAddress.None; uint Port = 0; // For some reason the InitialExternalIPConfiguration was never set, which means // the port also was never calculated / used. Because we need an external ip to check the external port. // try to retrieve the external ip and do a port check , don't force multiple checks and don't do a pluto check on // the port (that will only be done via ReloadExIPandPortConfiguration()) if (Retrieve_ExternalIP_ExternalPort(false, true, false, out extIP, out Port)) { InitialExternalIPConfigurationIsSet = true; ExternalIP = extIP; ExternalPort = Port; LastExternalIPCheckRun = DateTime.Now; AppLogInfo(String.Format("Loaded External IP:{0} and Port:{1} at {2}", extIP, ExternalPort, LastExternalIPCheckRun)); // Initialize the McKesson's Mobile Gateway RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort); } } else { // We only check if the External IP has changed. The Port will only change if the user // modifies the registry and reboots the server or the user calls ReloadExtIPandPortConfiguration() // via the Diagnose Mobile Tool. Hence we don't want to do any port checking here at all IPAddress extIP = Yaulw.Net.IPHostHelper.GetExternalIP(); if (extIP != IPAddress.None) { bool bChangeOccured = Yaulw.Net.IPHostHelper.AreIPAddressesDifferent(ExternalIP, extIP, out extIP); if (bChangeOccured && Yaulw.Net.IPHostHelper.IsValidIPv4Address(extIP, false)) { ExternalIP = extIP; LastExternalIPCheckRun = DateTime.Now; AppLogInfo(String.Format("Updated External IP:{0} at {1}", extIP, LastExternalIPCheckRun)); // Update the McKesson's Mobile Gateway RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort); } } } } } #endregion #region Private Helper Functions /// /// Core logic to retrieve External IP and Port to use (should be called only when not on McKesson Network) /// /// External Host IP /// set to true during initialization of the service, to check more than once, to make sure /// if set to true, will perform Port Sockets Checks to make sure the port isn't used, if false won't do any port checking and just use what is given * should be set to true upon initialization * /// if bCheckPort is set to true and we encounter an open port will perform a pluto check on it and see if it's hostguid matches ours, if so, decides we can use this port * RARE Reason this should ever be set to true * /// Random Port that can be used /// true if all of our parameters were passed out and valid, false otherwise private bool Retrieve_ExternalIP_ExternalPort(bool bForceMultipleExternalChecks, bool bCheckPort, bool bPlutoCheckPort, out IPAddress ExternalIP, out uint Port) { ExternalIP = IPAddress.None; Port = 0; // First Try, is to use the Port that is either random or saved in the registry, // if that fails (which the odds are pretty low), we fall back to a second try, // which will be random for sure. But just in case, let's do it a 3rd Random time, // if bForceMultipleExternalChecks is set to true. int nTrys = bForceMultipleExternalChecks ? 3 : 2; // Try to Fetch the External IP, this should be easy, one would think bool bFetchedExternalIP = false; for (int n = 0; n < nTrys && !bFetchedExternalIP; n++) { // Try to get the external IP (should work most of the time, if we have internet) // But once we have it, we shouldn't have to try 3 times if (!bFetchedExternalIP) { ExternalIP = Yaulw.Net.IPHostHelper.GetExternalIP(); if (ExternalIP != IPAddress.None) { bFetchedExternalIP = true; MSLSpecific.Logger.Info("External IP Fetched:{0}", ExternalIP.ToString()); } } } // Try to find a suitable Port, by trying to // Get the Port Number (should always work), but we // need to make sure that the port is free to use bool bFoundFreePort = false; for (int n = 0; n < nTrys && !bFoundFreePort && bFetchedExternalIP && bCheckPort; n++) { Port = (uint)GetRandomOrSavedPortNumber(n > 0); MSLSpecific.Logger.Info("Checking Port:{0} to use", Port); // Check to see if the port is open via external IP bool bIsReachable = RegistrationWrapper.IsServerReachable(ExternalIP, Port); if (bIsReachable) { if (bPlutoCheckPort) { string FoundHostGuid = RegistrationWrapper.RetrieveHostGUIDForServer(ExternalIP, Port); if (!String.IsNullOrEmpty(FoundHostGuid)) { // for whatever miracelous reason this is us (maybe thru registry) if (String.Compare(FoundHostGuid, MSLSpecific.GetHostGUID().ToString(), true) == 0) bFoundFreePort = true; } } } else { // Noting Reachable, assume we can use it. (DiagnoseMobile Tool can later make changes, if needed) bFoundFreePort = true; } if(bFoundFreePort) MSLSpecific.Logger.Info("Port:{0} free to use", Port); else MSLSpecific.Logger.Info("Port:{0} not free to use", Port); } // Yeah, we have a very likely way that our configuration that should // work 99% of the time without issue bool bWeCanUseThisConfig = false; if (bCheckPort && bFetchedExternalIP && bFoundFreePort) bWeCanUseThisConfig = true; else if (!bCheckPort && bFetchedExternalIP) bWeCanUseThisConfig = true; if (bWeCanUseThisConfig) MSLSpecific.Logger.Info("Found valid External IP:{0} and Port:{1} to use", ExternalIP, Port); else MSLSpecific.Logger.Error("Haven't found a valid External IP:{0} and Port:{1} to use", ExternalIP, Port); return bWeCanUseThisConfig; } /// /// Retrieve a Random Port Number * Auto Generated and retrieved/stored in Registry * /// /// private int GetRandomOrSavedPortNumber(bool bForceRandomNumberCreation) { int SavedPort = 0; if (!bForceRandomNumberCreation) SavedPort = Yaulw.Registry.RegKey.GetKey(Yaulw.Registry.HKEYRoot.LocalMachine, LOCAL_MACHINE_SUBKEY, LOCAL_MACHINE_EXTERNALPORT, 0); if (SavedPort <= 0 || SavedPort < EXTERNAL_MINIMUM_PORT_NUMBER || SavedPort > EXTERNAL_MAXIMUM_PORT_NUMBER) { Random random = new Random((int)DateTime.Now.Ticks); int nRand = random.Next(EXTERNAL_MINIMUM_PORT_NUMBER, EXTERNAL_MAXIMUM_PORT_NUMBER); SavedPort = random.Next(EXTERNAL_MINIMUM_PORT_NUMBER, EXTERNAL_MAXIMUM_PORT_NUMBER); Yaulw.Registry.RegKey.SetKey(Yaulw.Registry.HKEYRoot.LocalMachine, LOCAL_MACHINE_SUBKEY, LOCAL_MACHINE_EXTERNALPORT, SavedPort); } return SavedPort; } #endregion #region Private RO Channel Helpers /// /// Activate all RO Channels /// private void ActivateROChannelsIfTheyHaveNotBeenActivated() { ActivateROInternalChannelIfHasNotBeenActivated(); ActivateROExternalChannelIfHasNotBeenActivated(); } /// /// Deactivate all RO Channels /// private void DeactivateROChannelsIfTheyHaveBeenActivated(bool bNetworkOutage) { DeactivateROInternalChannelIfItHasBeenActivated(bNetworkOutage); DeactivateROExternalChannelIfItHasBeenActivated(bNetworkOutage); } #endregion #region Private RO Internal Channel Helper /// /// Activate Internal /// private void ActivateROInternalChannelIfHasNotBeenActivated() { if (!ROInternalHasBeenActivated) { SetInternalPort(INTERNAL_CHANNEL_PORT); MSLTcpServerChannelInternal.Activate(); ROInternalHasBeenActivated = true; AppLogInfo("MSLMobile Api Service Internal Channel Activated."); NetworkOutageDetected = false; } } /// /// Deactivate Internal /// private void DeactivateROInternalChannelIfItHasBeenActivated(bool bNetworkOutage) { if (ROInternalHasBeenActivated) { MSLTcpServerChannelInternal.Deactivate(); ROInternalHasBeenActivated = false; AppLogInfo("MSLMobile Api Service Internal Channel Deactivated."); NetworkOutageDetected = bNetworkOutage; } } #endregion #region Private RO External Channel Helper /// /// Activate External /// private void ActivateROExternalChannelIfHasNotBeenActivated() { if (!ROExternalHasBeenActivated && InitialExternalIPConfigurationIsSet) { SetExternalPort((int)ExternalPort); MSLTcpServerChannelExternal.Activate(); ROExternalHasBeenActivated = true; AppLogInfo(String.Format("MSLMobile Api Service External Channel on Port:{0} Activated.", ExternalPort)); NetworkOutageDetected = false; StartThePortMapper(); } } /// /// Deactivate External /// private void DeactivateROExternalChannelIfItHasBeenActivated(bool bNetworkOutage) { if (ROExternalHasBeenActivated) { StopThePortMapper(); MSLTcpServerChannelExternal.Deactivate(); ROExternalHasBeenActivated = false; AppLogInfo(String.Format("MSLMobile Api Service External Channel on Port:{0} Deactivated.", ExternalPort)); NetworkOutageDetected = bNetworkOutage; } } #endregion #region Port Mapper /// /// Launch Port Mapper if not already running /// private void StartThePortMapper() { #if DEBUG if (!IsPortMapperRunning) #else if (!IsOnMcKessonNetwork && !IsPortMapperRunning) #endif { try { AppLogInfo("Trying to start PortMapper"); Guid hostGuid = MSLSpecific.GetHostGUID(); // We have already done the port calculation, // just pass in one port, that should be mapped * always * //string minPort = ExternalPort.ToString(); //string maxPort = ExternalPort.ToString(); string externalPortToMap = ExternalPort.ToString(); string portMapDescription = "MSLMobile-" + ExternalPort.ToString(); // Call PORT FORWARDING Start method passing in the guid BindPortMapperEvent(); //pMapper.Start(hostGuid.ToString(), minPort, maxPort, portMapDescription, LOCAL_MACHINE_SUBKEY, MSLSpecific.Logger); pMapper.Start(ExternalIP, externalPortToMap, portMapDescription, MSLSpecific.Logger); AppLogInfo("PortMapper Started."); IsPortMapperRunning = true; } catch (Exception ex) { AppLogError(String.Format("Error when attempting to start PortMapper: {0}. PortMapper Stopped.", ex.Message)); UnBindPortMapperEvent(); pMapper.Stop(); } } } /// /// Stop Port Mapper if running /// private void StopThePortMapper() { // Stop the Port Mapper, if for any reason it started AppLogInfo("PortMapper stopping."); if (IsPortMapperRunning) { AppLogInfo("PortMapper stopped."); UnBindPortMapperEvent(); pMapper.Stop(); } } #endregion #region Port Mapper Event Bindings private void BindPortMapperEvent() { pMapper.DidFinishSearchForRouter += new PortMapper.PMDidFinishSearchForRouter(pMapper_DidFinishSearchForRouter); pMapper.ExternalIPAddressDidChange += new PortMapper.PMExternalIPAddressDidChange(pMapper_ExternalIPAddressDidChange); pMapper.CanAddPortMapping += new PortMapper.PMCanAddPortMapping(pMapper_CanAddPortMapping); } private void UnBindPortMapperEvent() { pMapper.DidFinishSearchForRouter -= new PortMapper.PMDidFinishSearchForRouter(pMapper_DidFinishSearchForRouter); pMapper.ExternalIPAddressDidChange -= new PortMapper.PMExternalIPAddressDidChange(pMapper_ExternalIPAddressDidChange); pMapper.CanAddPortMapping -= new PortMapper.PMCanAddPortMapping(pMapper_CanAddPortMapping); } #endregion #region Port Mapper Event Handlers void pMapper_DidFinishSearchForRouter(PortMapper sender) { AppLogInfo("MSLMobile Api Add port mapping."); pMapper.AddPortMapping(); } void pMapper_ExternalIPAddressDidChange(PortMapper sender, IPAddress ip) { if (Yaulw.Net.IPHostHelper.IsValidIPv4Address(ip, false)) { IPAddress extIP = IPAddress.None; bool bChangeOccured = Yaulw.Net.IPHostHelper.AreIPAddressesDifferent(ExternalIP, ip, out extIP); if (bChangeOccured) { // We probably should call McKesson's Mobile Gateway? // IF the router actually sends us the external IP! then yes, otherwise, to be discussed ExternalIP = extIP; RegistrationWrapper.UpdateServerIntExt(InternalIP, ExternalIP, ExternalPort); //Let's log this for now MSLSpecific.Logger.Info("PMapper_ExternalIPAddress Changed Occured OldIP:{0} NewIP:{1}", ExternalIP, extIP); } } } void pMapper_CanAddPortMapping(PortMapper sender) { //This is fired after an external ip address change is detected and resolved MSLSpecific.Logger.Info("PMapper_CanAddPortMapping Occured Port:{0} ExternalIP:{1}", sender.ExternalPortToMap, sender.ExternalIPAddress); pMapper.AddPortMapping(); } #endregion // usefull for debugging Service in visual studio #if DEBUG internal void DebugStop() { OnStop(); } #endif #region OnStop ///// ///// Stop Now (let's try this) ///// internal static void StopNow() { s_PlutoService.Stop(); } /// /// SCM OnStop() call-in /// protected override void OnStop() { // Stop the Connectivity timer try { if (connectivityTimer != null) { connectivityTimer.Stop(); connectivityTimer = null; AppLogInfo("End Connectivity Timer stopped."); } } catch (Exception) { /* ignore */ } // Deactivate RO Channels try { DeactivateROChannelsIfTheyHaveBeenActivated(false); AppLogInfo("End DeactivateROChannelsIfTheyHaveBeenActivated."); } catch (Exception) { /* ignore */ } #if !DEBUG // Disable Auto-Update Timer try { AutoUpdate.StopAutoTimer_ServiceStopped(); AppLogInfo("End Auto-Update Timer stopped."); } catch (Exception) { /* ignore */ } #endif } /// /// Force close this process (to make sure we are dead), /// we can't have open handles remaining open due to auto-update /// However, doing this means an error gets thrown in the SCM when the user manually /// stops the service!, so thie bool let's us know that it is us who is trying to do it. /// Last Resort, Try to kill the process to make sure the whole service is down. /// internal static void KillCurrentProccess() { try { var p = System.Diagnostics.Process.GetCurrentProcess(); if (p != null) p.Kill(); } catch (Exception) { /* ignore */ } } #endregion } }