namespace Pluto.Api { using System; using RemObjects.SDK; using RemObjects.SDK.Types; using RemObjects.SDK.Server; using RemObjects.SDK.Server.ClassFactories; using PlutoServer.MSL.Connectors; using Yaulw.File; using Sdaleo; using Sdaleo.Systems.SQLServer; using Sdaleo.Systems.Advantage; using Sdaleo.Systems; using System.Net; using PlutoServer.MSL; using Yaulw.Assembly; using Yaulw.Tools; [RemObjects.SDK.Server.ClassFactories.StandardClassFactory()] [RemObjects.SDK.Server.Service(Name = "MSLSpecific", InvokerClass = typeof(MSLSpecific_Invoker), ActivatorClass = typeof(MSLSpecific_Activator))] public class MSLSpecific : RemObjects.SDK.Server.Service, IMSLSpecific { private System.ComponentModel.Container components = null; public MSLSpecific() : base() { this.InitializeComponent(); } private void InitializeComponent() { } protected override void Dispose(bool aDisposing) { if (aDisposing) { if ((this.components != null)) { this.components.Dispose(); } } base.Dispose(aDisposing); } /// /// Keep track of the Host GUID /// private static ISReadWrite s_ISReadWrite = new ISReadWrite("host"); #region Logger Functions (public one is for Test Framework) /// /// For Logging to a File /// internal static Logging Logger = null; /// /// Test Log File and Path /// internal static string TestLogFileNameNPath = String.Empty; /// /// Setup the Logger Object, should be done only once at when the application starts /// internal static void Setup_Logger() { if (Logger == null) { // Is Setup_Test_Logger() Called? bool bIsTestLogFileSet = !String.IsNullOrEmpty(TestLogFileNameNPath); // Default Log File Settings const int LOG_FILE_FILE_SIZE_IN_MB = 4; const int LOG_FILE_NUM_OF_BACKUPS = 2; const string FILE_EXTENSION_LOG_DEFAULT = "log"; // Default Log File Name N Path Settings string LogFileNameNPath = String.Empty; if (!bIsTestLogFileSet) { string Path = AssemblyW.SpecializedAssemblyInfo.GetAssemblyPath(AssemblyW.AssemblyST.Entry); LogFileNameNPath = PathNaming.PathEndsWithSlash(Path) + AssemblyW.GetAssemblyName(AssemblyW.AssemblyST.Entry) + "." + FILE_EXTENSION_LOG_DEFAULT; } else { // Allow Unit test framework to set the log file path LogFileNameNPath = TestLogFileNameNPath; } // Log Config Logging_Configuration config = new Logging_Configuration(); config.LogFileNameNPath = LogFileNameNPath; config.maxFileSizeInMB = LOG_FILE_FILE_SIZE_IN_MB; config.numOfBackupLogFiles = LOG_FILE_NUM_OF_BACKUPS; config.Log4NetDetailPatternLayout = "%date{dd MMM HH:mm:ss,fff} %level - %message%newline"; config.LogCallingFunction = false; config.LogCallingType = false; if (PlutoService.IsOnMcKessonNetwork || bIsTestLogFileSet) config.Detail = Logging_Detail.DEBUG; else config.Detail = Logging_Detail.INFO; config.UseExclusiveFileLock = false; // Now just create the Logger if (!bIsTestLogFileSet) Logger = Logging.AddGlobalLoggerConfiguration(AssemblyW.GetAssemblyName(AssemblyW.AssemblyST.Entry), config); else Logger = Logging.AddGlobalLoggerConfiguration("Pluto.MSL.Test", config); // Make a Debug Message show, if we are on McKesson if (PlutoService.IsOnMcKessonNetwork) Logger.Info("**** NOTE **** This is a McKesson Networked Machine. DEBUG Messages will only show when service is run from a McKesson Networked Machine."); else Logger.Info("**** NOTE **** This is a Regularly Networked Machine. Only INFO and ERROR Messages will show when service in this Environment."); Logger.Info("Logger Started at:{0} with Service Version {1}", DateTime.Now.ToString(), AssemblyW.GetAssemblyVersion(AssemblyW.AssemblyST.Entry)); } } /// /// Setup the Test Logger Object, should be done by the Unit test framework to put the file in c:\\ /// public static void Setup_Test_Logger() { TestLogFileNameNPath = @"C:\\Pluto.MSL.Test.log"; Setup_Logger(); } #endregion #region Internal Static Helper Functions // Wipes all information known to use, both on the registration server as well as here // and starts over. logs the information to the user. System is auto-fixing itelf. // performing an internal re-install. The user may not like it, but that is just what // happend, we are in a bad state ERROR IT * BOLDLY in the Error log* internal static void MAJOR_ERROR_INTEGRITY_EVENT_OCCURED_ENTER_REINSTALL_STATE() { Logger.Fatal("******** FATAL ******** State occured. Service integrity invalid. All UserApiKeys will get deregistered as well as all hostGuid keys"); Logger.Fatal("******** FATAL ******** Someone must have tried to tamper with the system (registry / files) loosing vital information needed to continue."); Logger.Fatal("******** FATAL ******** Service is attempting to auto-fix itself. All users must re-open Mobile->Help about and obtain a new user api key."); } /// /// Retrieve the UserApiKey for a given Practice /// /// /// /// /// internal static string GetUserApiKeyForPractice(bool bIsMedisoft, IConnectDb credential, string DataSetOrDataBaseName, out bool bErrorOccured) { string UserApiKey = ""; bErrorOccured = true; if (!String.IsNullOrEmpty(DataSetOrDataBaseName) && credential != null) { if (bIsMedisoft) { string retVal = MedisoftConnector.GetUserApiKeyForPractice((AdvantageCredential)credential, DataSetOrDataBaseName, out bErrorOccured); if (!String.IsNullOrEmpty(retVal)) UserApiKey = retVal; } else { string retVal = LytecConnector.GetUserApiKeyForPractice((SQLServerCredential)credential, DataSetOrDataBaseName, out bErrorOccured); if (!String.IsNullOrEmpty(retVal)) UserApiKey = retVal; } } return UserApiKey; } /// /// Set the UserApiKey for a given Practice /// /// /// /// /// internal static bool SetUserApiKeyForPractice(bool bIsMedisoft, IConnectDb credential, string DataSetOrDataBaseName, string UserApiKey) { if (!String.IsNullOrEmpty(DataSetOrDataBaseName) && credential != null) { if (bIsMedisoft) { bool bSuccess = MedisoftConnector.SetUserApiKeyForPractice((AdvantageCredential)credential, DataSetOrDataBaseName, UserApiKey); if(!bSuccess) Logger.Error("Error setting UserApiKey for DataSetorDataBaseName {0}", DataSetOrDataBaseName); return bSuccess; } else { bool bSuccess = LytecConnector.SetUserApiKeyForPractice((SQLServerCredential)credential, DataSetOrDataBaseName, UserApiKey); if (!bSuccess) Logger.Error("Error setting UserApiKey for DataSetorDataBaseName {0}", DataSetOrDataBaseName); return bSuccess; } } return false; } /// /// Retrieve the Host Guid for this instance,k if not exists, /// create one. Host Guid is stored in isolated storage * why not * /// /// internal static Guid GetHostGUID() { string s = s_ISReadWrite.ReadFromIS(); if (String.IsNullOrEmpty(s)) { Guid g = Guid.NewGuid(); Logger.Debug("Creating new Host GUID {0}", g.ToString()); s_ISReadWrite.WriteToIS(g.ToString()); return g; } return new Guid(s); // New Untested Code * Still needs some hashing out * //string s = s_ISReadWrite.ReadFromIS(); //Guid g = Guid.NewGuid(); //if (String.IsNullOrEmpty(s)) //{ // // Store bck HostGuid in registry to make sure this value is never lost // Yaulw.Registry.RegKey.SetKey(Yaulw.Registry.HKEYRoot.LocalMachine, PlutoService.LOCAL_MACHINE_SUBKEY, PlutoService.LOCAL_MACHINE_HOSTGUID, g.ToString()); // Logger.Debug("Creating new Host GUID {0}", g.ToString()); // s_ISReadWrite.WriteToIS(g.ToString()); // return g; //} //else //{ // // Read registry and retrieve backup guid first * if empty set it, Upgrade service issues * feature added in 1.0.2.3 // string bckGuid = Yaulw.Registry.RegKey.GetKey(Yaulw.Registry.HKEYRoot.LocalMachine, PlutoService.LOCAL_MACHINE_SUBKEY, PlutoService.LOCAL_MACHINE_HOSTGUID, String.Empty); // if (String.IsNullOrEmpty(bckGuid.ToString())) // { // Yaulw.Registry.RegKey.SetKey(Yaulw.Registry.HKEYRoot.LocalMachine, PlutoService.LOCAL_MACHINE_SUBKEY, PlutoService.LOCAL_MACHINE_HOSTGUID, g.ToString()); // } // else // { // // if not empty compare them (they should always be equal if not, we are kind of in mayor trouble) // // we will pick the guid that is stored in online, that is our only choice, doubtful that both got // // stored online (*that state is a major error state and would require wiping the host guid, and wiping // // all hostkeys online and all userapikeys online. Even maybe the DBCacheFile. basically an internal re-install // if (String.Compare(s, bckGuid, true) != 0) // { // // chech which guid is online, pick that one first // // else Mayor event occured // MAJOR_ERROR_INTEGRITY_EVENT_OCCURED_ENTER_REINSTALL_STATE(); // } // } //} //return new Guid(s); } /// /// Quick check to see if the connection is a Medisoft / Advantage DB Connection /// /// /// internal static bool ConnectionContainsAdvServerType(string ConnectionString) { if (!String.IsNullOrEmpty(ConnectionString)) return (ConnStr.ContainsKey("Advantage Server Type", ConnectionString) || ConnStr.ContainsKey("ServerType", ConnectionString)); return false; } #endregion #region Private DBCache Related Helper Functions /// /// Shared Database Connection needs to be added /// /// Connection String of the Shared DB to be added to Cache /// Will try to connect to the DB, if set to true /// if bTryToConnect is true, it will try to connect and assign out this value /// true if it needed to be added and, if TryToConnect is set, and communication is working, false otherwise private bool AddConnectionIfNotAlreadyThere(string SharedConnectionDataSource, bool bTryToConnect) { if (!String.IsNullOrEmpty(SharedConnectionDataSource)) { IConnectDb credential = ConvertSharedDataSourceToProperCredential(SharedConnectionDataSource); if (credential != null) { // Is Connection of Type Medisoft if (ConnectionContainsAdvServerType(SharedConnectionDataSource)) { DBCache.AddSharedDataConnectionToDataStore((AdvantageCredential)credential, true); // Test the Connection = see if error occurs calling practice list if (bTryToConnect) { bool bConnect = MedisoftConnector.GetPracticeList((AdvantageCredential)credential).IsValid; return bConnect; } else return true; } else // Or Lytec { DBCache.AddSharedDataConnectionToDataStore((SQLServerCredential)credential, true); // Test the Connection = see if error occurs calling practice list if (bTryToConnect) { bool bConnect = LytecConnector.GetPracticeList((SQLServerCredential)credential).IsValid; return bConnect; } else return true; } } } return false; } /// /// Converts a passed in SharedConnection (from Medisoft/Lytec) into a proper IConnectDb credential /// we can use to pass arround /// /// /// private IConnectDb ConvertSharedDataSourceToProperCredential(string SharedConnectionDataSource) { if (!String.IsNullOrEmpty(SharedConnectionDataSource)) { // Is Connection of Type Medisoft if (ConnectionContainsAdvServerType(SharedConnectionDataSource)) { string Type = ConnStr.RetrieveValue("Advantage Server Type", SharedConnectionDataSource); if (String.IsNullOrEmpty(Type)) Type = ConnStr.RetrieveValue("ServerType", SharedConnectionDataSource); bool bIsRemote = Type.ToUpper().Contains("REMOTE"); AdvantageCredential credential = new AdvantageCredential(Yaulw.Net.IPHostHelper.GetDataSource(SharedConnectionDataSource), "SharedDataUser", "AndPassword", bIsRemote ? AdvantageCredential.ServerType.REMOTE : AdvantageCredential.ServerType.LOCAL); return credential; } else // Or Lytec { string Server; string Instance; if (SQLServerUtilities.SplitServerOrServerNInstance(Yaulw.Net.IPHostHelper.GetDataSource(SharedConnectionDataSource), out Server, out Instance)) { string UserID = ConnStr.RetrieveValue("User ID", SharedConnectionDataSource); string Password = ConnStr.RetrieveValue("Password", SharedConnectionDataSource); SQLServerCredential credential = new SQLServerCredential(Server, Instance, "Lytec SharedData", UserID, Password); return credential; } } } return null; } /// /// Reload the UserCredentials for a given SharedCredentials in the Cache /// /// /// private void ReloadUserDBConnectionForSharedCredential(IConnectDb SharedCredential, bool bIsMedisoft) { if (bIsMedisoft) DBCache.IterateSharedConnectionAndLoadUserApiKeyConnections((AdvantageCredential)SharedCredential); else DBCache.IterateSharedConnectionAndLoadUserApiKeyConnections((SQLServerCredential)SharedCredential); } #endregion #region IMSLMobileConnect /// /// Common Tasks to do for all of our MSLSpecific Calls /// /// Medisoft/Lytec SharedDataConnection /// if true, add the connection to our DataStore /// if true, trying to connect to the db to test /// out is Medisoft /// out Credential /// return true when everything is ok, false otherwise private bool CommonThingsToDoUponEveryMSLCall(string SharedConnectionDataSource, bool bAddSharedConnection, bool bTryToConnect, out bool bIsMedisoft, out IConnectDb Credential) { bIsMedisoft = false; Credential = null; if(!String.IsNullOrEmpty(SharedConnectionDataSource)) { // Is Lytec or Medisoft? bIsMedisoft = ConnectionContainsAdvServerType(SharedConnectionDataSource); Credential = ConvertSharedDataSourceToProperCredential(SharedConnectionDataSource); if (Credential != null) { // Local Host name and IP if(bIsMedisoft) Logger.Info("Determined DataSource to be Medisoft"); else Logger.Info("Determined DataSource to be Lytec"); Logger.Info("Determined Host:{0} with Host Guid:'{1}'", Dns.GetHostName(), GetHostGUID()); // Should we add the connection? if (bAddSharedConnection) { // will always return true, unless bTryToConnect is true and it will try to connect and fail bool bConnect = AddConnectionIfNotAlreadyThere(SharedConnectionDataSource, bTryToConnect); if (bTryToConnect && bConnect) Logger.Debug("Successfully connected to SharedDataSource: {0}", SharedConnectionDataSource); else if(bTryToConnect && !bConnect) Logger.Error("Failed to Connect to passed in DataSource"); // Ensure that all UserApiKeys are unique - running into issues // with Lytec and Medisoft somehow if (bConnect) { if (bIsMedisoft) MedisoftConnector.CleanUpDuplicateUserApiKeyExists((AdvantageCredential)Credential); else LytecConnector.CleanUpDuplicateUserApiKeyExists((SQLServerCredential)Credential); } return bConnect; } } } Logger.Debug("Something is wrong with this DataSource {0},", SharedConnectionDataSource); Logger.Error("Failed to Retrieve the information needed to continue"); return false; } /// /// Mobile About Dialog got called by Lytec/Medisoft. Here we do the main work of registering everything /// /// /// /// /// public virtual ApiKeyNPin MobileAboutDialogCalled(string SharedConnectionDataSource, string RegisteredName, string PracticeName, string DataBaseNameOrDataSetName) { Logger.Debug("called with SharedDataSource: {0}, RegisteredName: {1}, PracticeName: {2}, DataSetOrDBName: {3}", SharedConnectionDataSource, RegisteredName, PracticeName, DataBaseNameOrDataSetName); ApiKeyNPin apikey = new ApiKeyNPin(); try { // Perform this for all MSL Entry Calls * for Sanity * bool bIsMedisoft = false; IConnectDb Credential = null; bool bIsValid = CommonThingsToDoUponEveryMSLCall(SharedConnectionDataSource, true, false, out bIsMedisoft, out Credential); if (!bIsValid) return apikey; // Try To Fetch the UserApikey bool bErrorOccured = true; string UserApiKey = GetUserApiKeyForPractice(bIsMedisoft, Credential, DataBaseNameOrDataSetName, out bErrorOccured); if (bErrorOccured) { Logger.Error("Database Error Occured fetching UserApiKey - Mobile About Dialog cannot continue for DataSetorDataBaseName {0}", DataBaseNameOrDataSetName); return apikey; } // To Do: UserApiKey could not match with the current host, if that ever happens, // we should ideally re-register and not retrieve the PIN, because now the systems are out of sync // (TBD) // If there is no UserApiKey already in the System, // it means we must register this practice as * new * if (String.IsNullOrEmpty(UserApiKey)) { string strApiKey; string strPin; bool bSuccess = RegistrationWrapper.RegisterNewPractice(PracticeName, bIsMedisoft, out strApiKey, out strPin); if (bSuccess) { Logger.Debug("Retrieved new UserApiKey {0} and Pin {1}", strApiKey, strPin); // Set the Service State PlutoService.state = RegistrationWrapper.HostGUIDstate.exists; // Save the UserApiKey in the DB (if successful save, then continue to reload user DB Connections) // and only then assign out the api key! ~if it didn't save in the db it shouldn't show on the dialog if (SetUserApiKeyForPractice(bIsMedisoft, Credential, DataBaseNameOrDataSetName, strApiKey)) { apikey.UserApiKey = strApiKey; apikey.UserPin = strPin; // Reload the UserDB Cache ReloadUserDBConnectionForSharedCredential(Credential, bIsMedisoft); } else { Logger.Error("Database Error Occured saving UserApiKey - Mobile About Dialog cannot continue"); } return apikey; } } else { // Practice already registered! So just retrieve the pin for the UserApikey and we are done string Pin = RegistrationWrapper.RetrievePinForUserApiKey(UserApiKey); if (!String.IsNullOrEmpty(Pin)) { apikey.UserApiKey = UserApiKey; apikey.UserPin = Pin; Logger.Debug("MobileAboutDialogCalled() Retrieving Pin:{0} for UserApiKey:{1}", Pin, UserApiKey); } else { Logger.Error("Failed to retrieve Pin - Mobile About Dialog cannot continue"); } return apikey; } } catch (Exception e) { Logger.Error("MobileAboutDialogCalled() Error", e); } return apikey; } /// /// Called upon every lytec/medisoft launch. Currently doesn't do very much. (It was kind of /// intended for Registration Purposes). /// /// /// /// public virtual bool ProductSetupCompleted(string SharedConnectionDataSource, string RegisteredName) { Logger.Debug("called with SharedDataSource: {0}, RegisteredName: {1}", SharedConnectionDataSource, RegisteredName); try { // Perform this for all MSL Entry Calls * for Sanity * bool bIsMedisoft = false; IConnectDb Credential = null; CommonThingsToDoUponEveryMSLCall(SharedConnectionDataSource, true, true, out bIsMedisoft, out Credential); // Always return true, Medisoft/Lytec Checking that it can connect to this service expects // a true back, if it doesn't it will hinder the CheckConnectivity Test at step 1, // not allowing the real step 2 and step 3 to continue return true; } catch (Exception e) { Logger.Error("ProductSetupCompleted() Error", e); } return false; } /// /// Practice Name Change Occured /// /// /// /// public virtual bool PracticeNameChangeOccured(string SharedConnectionDataSource, string NewPracticeName, string DataBaseNameOrDataSetName) { Logger.Debug("called with SharedDataSource: {0}, NewPracticeName: {1}, DataSetOrDBName: {2}", SharedConnectionDataSource, NewPracticeName, DataBaseNameOrDataSetName); try { // Perform this for all MSL Entry Calls * for Sanity * bool bIsMedisoft = false; IConnectDb Credential = null; bool bIsValid = CommonThingsToDoUponEveryMSLCall(SharedConnectionDataSource, true, false, out bIsMedisoft, out Credential); if (!bIsValid) return false; // Try To Fetch the UserApikey bool bErrorOccured = true; string UserApiKey = GetUserApiKeyForPractice(bIsMedisoft, Credential, DataBaseNameOrDataSetName, out bErrorOccured); if (bErrorOccured) { Logger.Error("Database Error Occured fetching UserApiKey - PracticeNameChangeOccured cannot continue for DataSetorDataBaseName {0}", DataBaseNameOrDataSetName); return false; } if (!String.IsNullOrEmpty(UserApiKey)) { // Auto-Retrieve the Pin string Pin = RegistrationWrapper.RetrievePinForUserApiKey(UserApiKey); if (!String.IsNullOrEmpty(Pin)) { Logger.Debug("Retrieved UserApiKey:{0} Pin:{1} for Practice:{2}", UserApiKey, Pin, NewPracticeName); // Update the Practice Name bool bSuccess = RegistrationWrapper.UpdatePracticeName(UserApiKey, Pin, NewPracticeName); return bSuccess; } else { Logger.Error("Failed to retrieve Pin - PracticeNameChangeOccured cannot continue"); } } return false; } catch (Exception e) { Logger.Error("PracticeNameChangeOccured() Error", e); } return false; } /// /// Check to see if the external IP can be accessed by the McKesson's Mobile Gateway /// /// /// public virtual bool IsServerReachableFromTheInternet(string SharedConnectionDataSource) { Logger.Debug("called with SharedDataSource: {0}", SharedConnectionDataSource); try { // Perform this for all MSL Entry Calls * for Sanity * bool bIsMedisoft = false; IConnectDb Credential = null; bool bIsValid = CommonThingsToDoUponEveryMSLCall(SharedConnectionDataSource, true, false, out bIsMedisoft, out Credential); if (!bIsValid) return false; // Check to see if this server is reachable from the internet bool bIsReachable = RegistrationWrapper.IsServerReachable(); return bIsReachable; } catch (Exception e) { Logger.Error("IsServerReachableFromTheInternet() Error", e); } return false; } /// /// Update the Service if needed /// public virtual void UpdateServiceIfNeeded() { PlutoService.AppLogInfo("UpdateServiceIfNeeded() called - Will try to Update the Service, if new Service was found online"); // Callback from MSLConnect, to try to update the service, // we only need a little delay here, let this call return as quickly as possible AutoUpdate.TryToUpdateIfNeeded(5, false); } #endregion #region Pluto AddOns /// /// Retrieve the Host GUID for this machine /// /// public virtual string GetHostSpecGUID() { return GetHostGUID().ToString(); } /// /// Retrieve the External Port Set /// /// a valid port or -1 for none public virtual int GetExternalPort() { if (PlutoService.IsExtIPandExtPortSet) return (int) PlutoService.ExternalPort; else return -1; } /// /// Set and reload the External CHannel on a different port /// /// /// public virtual bool SetExternalPort(int Port) { // never tested (coded), but never tested // must test ReloadExtIPandPortConfiguration() at some point, // won't get called by Diagnose mobile at this time return PlutoService.ReloadExtIPandPortConfiguration(Port); } /// /// Retrieve the latest external IP /// /// public virtual string GetExternalIP() { if (PlutoService.IsExtIPandExtPortSet) return PlutoService.ExternalIP.ToString(); else return IPAddress.None.ToString(); } /// /// Retrieve the latest Internal IP /// /// public virtual string GetLocalIP() { return PlutoService.InternalIP.ToString(); } #endregion #region Pluto Dev AddOns (To Do) /// /// Go thru all the Shared Database Connections and auto-generate a /// user api key for all practices * good for testing and debugging * /// this means lytec/medisoft is not needed to create the userapikeys /// public virtual void GenerateApiKeysForPracticesInLoadedSharedCredentials() { // TO DO: } /// /// Add a custom Shared Database Credential Medisoft or Lytec to the system /// *good for testing and debugging* /// /// /// public virtual bool AddSharedCredential(ApiKeyNCredential credential) { // TO DO: return false; } /// /// Retrieve all currently loaded credentials good for support, /// dev, qa, etc to see what is going on /// /// public virtual ApiKeyNCredential[] GetLoadedCredentials() { // TO DO: return null; } #endregion } }