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
}
}