//****************************************************************************************************** // ServiceHostBase.cs - Gbtc // // Copyright © 2012, Grid Protection Alliance. All Rights Reserved. // // Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See // the NOTICE file distributed with this work for additional information regarding copyright ownership. // The GPA licenses this file to you under the MIT License (MIT), the "License"; you may // not use this file except in compliance with the License. You may obtain a copy of the License at: // // http://www.opensource.org/licenses/MIT // // Unless agreed to in writing, the subject software distributed under the License is distributed on an // "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the // License for the specific language governing permissions and limitations. // // Code Modification History: // ---------------------------------------------------------------------------------------------------- // 08/20/2010 - J. Ritchie Carroll // Generated original version of source code. // 01/30/2011 - Pinal C. Patel // Updated to subscribe to new ProcessException event of MultiDestinationExporter class. // 04/21/2011 - Timothy M. Yardley / Erich A. Heine (UIUC) // Implemented an optional, but enabled by default, non-broadcast method of directly routed // measurement distribution as a system performance optimization. // 12/20/2012 - Starlynn Danyelle Gilliam // Modified Header. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Runtime; using System.Security; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.ServiceProcess; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Xml; using GSF.Collections; using GSF.Communication; using GSF.Configuration; using GSF.Console; using GSF.Data; using GSF.Diagnostics; using GSF.IO; using GSF.Net.Security; using GSF.Reflection; using GSF.Security; using GSF.ServiceProcess; using GSF.Threading; using GSF.TimeSeries.Adapters; using GSF.TimeSeries.Configuration; using GSF.TimeSeries.Reports; using GSF.TimeSeries.Statistics; using GSF.Units; namespace GSF.TimeSeries { #region [ Enumerations ] /// /// Configuration data source type enumeration. /// public enum ConfigurationType { /// /// Configuration source is a database. /// Database, /// /// Configuration source is a web-service. /// WebService, /// /// Configuration source is a binary file. /// BinaryFile, /// /// Configuration source is a XML file. /// XmlFile } #endregion /// /// Represents the time-series framework service host. /// public partial class ServiceHostBase : ServiceBase { #region [ Members ] // Constants private const int DefaultMinThreadPoolSize = 25; private const int DefaultMaxThreadPoolSize = 100; private const int DefaultConfigurationBackups = 5; private const int DefaultMaxLogFiles = 300; internal event EventHandler> UpdatedStatus; internal event EventHandler> LoggedException; // Fields private IaonSession m_iaonSession; private string m_nodeIDQueryString; private ConfigurationType m_configurationType; private IConfigurationLoader m_configurationLoader; private BinaryFileConfigurationLoader m_binaryCacheConfigurationLoader; private XMLConfigurationLoader m_xmlCacheConfigurationLoader; private string m_cachedXmlConfigurationFile; private string m_cachedBinaryConfigurationFile; private int m_configurationBackups; private bool m_uniqueAdapterIDs; private bool m_allowRemoteRestart; private bool m_preferCachedConfiguration; private MultipleDestinationExporter m_healthExporter; private MultipleDestinationExporter m_statusExporter; private ReportingProcessCollection m_reportingProcesses; private ProcessQueue>> m_reloadConfigQueue; private LongSynchronizedOperation m_configurationCacheOperation; private volatile DataSet m_latestConfiguration; private RunTimeLog m_runTimeLog; private ServiceHelper m_serviceHelper; private ServerBase m_remotingServer; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new . /// public ServiceHostBase() { InitializeComponent(); ServiceName = "IaonHost"; } /// /// Creates a new from specified parameters. /// /// Service host . public ServiceHostBase(IContainer container) : this() { container?.Add(this); } #endregion #region [ Properties ] /// /// Gets the related remote console application name. /// protected virtual string ConsoleApplicationName => ServiceName + "Console.exe"; /// /// Gets access to the . /// protected ServiceHelper ServiceHelper => m_serviceHelper; /// /// Gets reference to the based remoting server. /// protected ServerBase RemotingServer => m_remotingServer; /// /// Gets reference to the . /// protected AllAdaptersCollection AllAdapters => m_iaonSession.AllAdapters; /// /// Gets reference to the . /// protected InputAdapterCollection InputAdapters => m_iaonSession.InputAdapters; /// /// Gets reference to the . /// protected ActionAdapterCollection ActionAdapters => m_iaonSession.ActionAdapters; /// /// Gets reference to the . /// protected OutputAdapterCollection OutputAdapters => m_iaonSession.OutputAdapters; /// /// Gets the current node ID. /// protected Guid NodeID => m_iaonSession.NodeID; /// /// Gets the current node ID formatted for use in a SQL query string based on . /// protected string NodeIDQueryString => m_nodeIDQueryString; /// /// Gets the currently loaded system configuration . /// protected DataSet DataSource => m_iaonSession.DataSource; /// /// Gets the defined system . /// protected ConfigurationType ConfigurationType => m_configurationType; #endregion #region [ Methods ] #region [ Service Event Handlers ] /// /// Event handler for service starting operations. /// /// Event source. /// Event arguments containing command line arguments passed into service at startup. /// /// Time-series framework uses this handler to load settings from configuration file as service is starting. /// protected virtual void ServiceStartingHandler(object sender, EventArgs e) { ShutdownHandler.Initialize(); // Define a run-time log m_runTimeLog = new RunTimeLog(); m_runTimeLog.FileName = "RunTimeLog.txt"; m_runTimeLog.ProcessException += ProcessExceptionHandler; m_runTimeLog.Initialize(); // Use this run-time log for system up-time calculations PerformanceStatistics.SystemRunTimeLog = m_runTimeLog; // Initialize Iaon session m_iaonSession = new IaonSession(); m_iaonSession.StatusMessage += StatusMessageHandler; m_iaonSession.ProcessException += ProcessExceptionHandler; m_iaonSession.ConfigurationChanged += ConfigurationChangedHandler; // Create a handler for unobserved task exceptions TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException; // Make sure default service settings exist ConfigurationFile configFile = ConfigurationFile.Current; string servicePath = FilePath.GetAbsolutePath(""); string cachePath = string.Format("{0}{1}ConfigurationCache{1}", servicePath, Path.DirectorySeparatorChar); string defaultLogPath = string.Format("{0}{1}Logs{1}", servicePath, Path.DirectorySeparatorChar); // System settings CategorizedSettingsElementCollection systemSettings = configFile.Settings["systemSettings"]; systemSettings.Add("ConfigurationType", "Database", "Specifies type of configuration: Database, WebService, BinaryFile or XmlFile"); systemSettings.Add("ConnectionString", "Provider=Microsoft.Jet.OLEDB.4.0;Data Source=IaonHost.mdb", "Configuration database connection string"); systemSettings.Add("DataProviderString", "AssemblyName={System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089};ConnectionType=System.Data.OleDb.OleDbConnection;AdapterType=System.Data.OleDb.OleDbDataAdapter", "Configuration database ADO.NET data provider assembly type creation string"); systemSettings.Add("ConfigurationCachePath", cachePath, "Defines the path used to cache serialized configurations"); systemSettings.Add("LogPath", defaultLogPath, "Defines the path used to archive log files"); systemSettings.Add("MaxLogFiles", DefaultMaxLogFiles, "Defines the maximum number of log files to keep"); systemSettings.Add("CachedConfigurationFile", "SystemConfiguration.xml", "File name for last known good system configuration (only cached for a Database or WebService connection)"); systemSettings.Add("UniqueAdaptersIDs", "True", "Set to true if all runtime adapter ID's will be unique to allow for easier adapter specification"); systemSettings.Add("ProcessPriority", "High", "Sets desired process priority: Normal, AboveNormal, High, RealTime"); systemSettings.Add("AllowRemoteRestart", "True", "Controls ability to remotely restart the host service."); systemSettings.Add("MinThreadPoolWorkerThreads", DefaultMinThreadPoolSize, "Defines the minimum number of allowed thread pool worker threads."); systemSettings.Add("MaxThreadPoolWorkerThreads", DefaultMaxThreadPoolSize, "Defines the maximum number of allowed thread pool worker threads."); systemSettings.Add("MinThreadPoolIOPortThreads", DefaultMinThreadPoolSize, "Defines the minimum number of allowed thread pool I/O completion port threads (used by socket layer)."); systemSettings.Add("MaxThreadPoolIOPortThreads", DefaultMaxThreadPoolSize, "Defines the maximum number of allowed thread pool I/O completion port threads (used by socket layer)."); systemSettings.Add("ConfigurationBackups", DefaultConfigurationBackups, "Defines the total number of older backup configurations to maintain."); systemSettings.Add("PreferCachedConfiguration", "False", "Set to true to try the cached configuration first, before loading database configuration - typically used when cache is updated by external process."); systemSettings.Add("LocalCertificate", $"{ServiceName}.cer", "Path to the local certificate used by this server for authentication."); systemSettings.Add("RemoteCertificatesPath", @"Certs\Remotes", "Path to the directory where remote certificates are stored."); systemSettings.Add("DefaultCulture", "en-US", "Default culture to use for language, country/region and calendar formats."); // Example connection settings CategorizedSettingsElementCollection exampleSettings = configFile.Settings["exampleConnectionSettings"]; exampleSettings.Add("SqlServer.ConnectionString", "Data Source=serverName; Initial Catalog=databaseName; User ID=userName; Password=password", "Example SQL Server database connection string"); exampleSettings.Add("SqlServer.DataProviderString", "AssemblyName={System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089}; ConnectionType=System.Data.SqlClient.SqlConnection; AdapterType=System.Data.SqlClient.SqlDataAdapter", "Example SQL Server database .NET provider string"); exampleSettings.Add("MySQL.ConnectionString", "Server=serverName;Database=databaseName; Uid=root; Pwd=password; allow user variables = true;", "Example MySQL database connection string"); exampleSettings.Add("MySQL.DataProviderString", "AssemblyName={MySql.Data, Version=6.3.6.0, Culture=neutral, PublicKeyToken=c5687fc88969c44d}; ConnectionType=MySql.Data.MySqlClient.MySqlConnection; AdapterType=MySql.Data.MySqlClient.MySqlDataAdapter", "Example MySQL database .NET provider string"); exampleSettings.Add("Oracle.ConnectionString", "Data Source=tnsName; User ID=schemaUserName; Password=schemaPassword", "Example Oracle database connection string"); exampleSettings.Add("Oracle.DataProviderString", "AssemblyName={Oracle.DataAccess, Version=2.112.2.0, Culture=neutral, PublicKeyToken=89b483f429c47342}; ConnectionType=Oracle.DataAccess.Client.OracleConnection; AdapterType=Oracle.DataAccess.Client.OracleDataAdapter", "Example Oracle database .NET provider string"); exampleSettings.Add("SQLite.ConnectionString", "Data Source=databaseName.db; Version=3; Foreign Keys=True; FailIfMissing=True", "Example SQLite database connection string"); exampleSettings.Add("SQLite.DataProviderString", "AssemblyName={System.Data.SQLite, Version=1.0.109.0, Culture=neutral, PublicKeyToken=db937bc2d44ff139}; ConnectionType=System.Data.SQLite.SQLiteConnection; AdapterType=System.Data.SQLite.SQLiteDataAdapter", "Example SQLite database .NET provider string"); exampleSettings.Add("OleDB.ConnectionString", "Provider=Microsoft.Jet.OLEDB.4.0; Data Source=databaseName.mdb", "Example Microsoft Access (via OleDb) database connection string"); exampleSettings.Add("OleDB.DataProviderString", "AssemblyName={System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089}; ConnectionType=System.Data.OleDb.OleDbConnection; AdapterType=System.Data.OleDb.OleDbDataAdapter", "Example OleDb database .NET provider string"); exampleSettings.Add("Odbc.ConnectionString", "Driver={SQL Server Native Client 10.0}; Server=serverName; Database=databaseName; Uid=userName; Pwd=password;", "Example ODBC database connection string"); exampleSettings.Add("Odbc.DataProviderString", "AssemblyName={System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089}; ConnectionType=System.Data.Odbc.OdbcConnection; AdapterType=System.Data.Odbc.OdbcDataAdapter", "Example ODBC database .NET provider string"); exampleSettings.Add("WebService.ConnectionString", "http://localhost/ConfigSource/SystemConfiguration.xml", "Example web service connection string"); exampleSettings.Add("XmlFile.ConnectionString", "SystemConfiguration.xml", "Example XML configuration file connection string"); // Attempt to set default culture try { string defaultCulture = systemSettings["DefaultCulture"].ValueAs("en-US"); CultureInfo.DefaultThreadCurrentCulture = CultureInfo.CreateSpecificCulture(defaultCulture); // Defaults for date formatting, etc. CultureInfo.DefaultThreadCurrentUICulture = CultureInfo.CreateSpecificCulture(defaultCulture); // Culture for resource strings, etc. } catch (Exception ex) { DisplayStatusMessage("Failed to set default culture due to exception, defaulting to \"{1}\": {0}", UpdateType.Alarm, ex.Message, CultureInfo.CurrentCulture.Name.ToNonNullNorEmptyString("Undetermined")); LogException(ex); } // Retrieve application log path as defined in the config file string logPath = FilePath.GetAbsolutePath(systemSettings["LogPath"].Value); // Make sure log directory exists try { if (!Directory.Exists(logPath)) Directory.CreateDirectory(logPath); } catch (Exception ex) { // Attempt to default back to common log file path if (!Directory.Exists(defaultLogPath)) { try { Directory.CreateDirectory(defaultLogPath); } catch { defaultLogPath = servicePath; } } DisplayStatusMessage("Failed to create logging directory \"{0}\" due to exception, defaulting to \"{1}\": {2}", UpdateType.Alarm, logPath, defaultLogPath, ex.Message); LogException(ex); logPath = defaultLogPath; } int maxLogFiles = systemSettings["MaxLogFiles"].ValueAs(DefaultMaxLogFiles); try { Logger.FileWriter.SetPath(logPath); Logger.FileWriter.SetLoggingFileCount(maxLogFiles); } catch (Exception ex) { DisplayStatusMessage("Failed to set logging path \"{0}\" or max file count \"{1}\" due to exception: {2}", UpdateType.Alarm, logPath, maxLogFiles, ex.Message); LogException(ex); } // Retrieve configuration cache directory as defined in the config file cachePath = FilePath.GetAbsolutePath(systemSettings["ConfigurationCachePath"].Value); // Make sure configuration cache directory exists try { if (!Directory.Exists(cachePath)) Directory.CreateDirectory(cachePath); } catch (Exception ex) { DisplayStatusMessage("Failed to create configuration cache directory \"{0}\" due to exception: {1}", UpdateType.Alarm, cachePath, ex.Message); LogException(ex); } try { Directory.SetCurrentDirectory(servicePath); } catch (Exception ex) { DisplayStatusMessage("Failed to set current directory to execution path \"{0}\" due to exception: {1}", UpdateType.Alarm, servicePath, ex.Message); LogException(ex); } // Initialize system settings m_configurationType = systemSettings["ConfigurationType"].ValueAs(); m_cachedXmlConfigurationFile = FilePath.AddPathSuffix(cachePath) + systemSettings["CachedConfigurationFile"].Value; m_cachedBinaryConfigurationFile = FilePath.AddPathSuffix(cachePath) + FilePath.GetFileNameWithoutExtension(m_cachedXmlConfigurationFile) + ".bin"; m_configurationBackups = systemSettings["ConfigurationBackups"].ValueAs(DefaultConfigurationBackups); m_uniqueAdapterIDs = systemSettings["UniqueAdaptersIDs"].ValueAsBoolean(true); m_allowRemoteRestart = systemSettings["AllowRemoteRestart"].ValueAsBoolean(true); m_preferCachedConfiguration = systemSettings["PreferCachedConfiguration"].ValueAsBoolean(false); m_reloadConfigQueue = ProcessQueue>>.CreateSynchronousQueue(ExecuteReloadConfig, 500.0D, Timeout.Infinite, false, false); m_reloadConfigQueue.ProcessException += m_iaonSession.ProcessExceptionHandler; m_configurationCacheOperation = new LongSynchronizedOperation(ExecuteConfigurationCache) { IsBackground = true }; // Setup default thread pool size try { ThreadPool.SetMinThreads(systemSettings["MinThreadPoolWorkerThreads"].ValueAs(DefaultMinThreadPoolSize), systemSettings["MinThreadPoolIOPortThreads"].ValueAs(DefaultMinThreadPoolSize)); ThreadPool.SetMaxThreads(systemSettings["MaxThreadPoolWorkerThreads"].ValueAs(DefaultMaxThreadPoolSize), systemSettings["MaxThreadPoolIOPortThreads"].ValueAs(DefaultMaxThreadPoolSize)); } catch (Exception ex) { DisplayStatusMessage("Failed to set desired thread pool size due to exception: {0}", UpdateType.Alarm, ex.Message); LogException(ex); } // Define guid with query string delimiters according to database needs if (string.IsNullOrWhiteSpace(m_nodeIDQueryString)) m_nodeIDQueryString = "'" + m_iaonSession.NodeID + "'"; // Set up the configuration loader switch (m_configurationType) { case ConfigurationType.Database: m_configurationLoader = new DatabaseConfigurationLoader { ConnectionString = systemSettings["ConnectionString"].Value, DataProviderString = systemSettings["DataProviderString"].Value, NodeIDQueryString = m_nodeIDQueryString }; break; case ConfigurationType.WebService: m_configurationLoader = new WebServiceConfigurationLoader { URI = systemSettings["ConnectionString"].Value }; break; case ConfigurationType.BinaryFile: m_configurationLoader = new BinaryFileConfigurationLoader { FilePath = systemSettings["ConnectionString"].Value }; break; case ConfigurationType.XmlFile: m_configurationLoader = new XMLConfigurationLoader { FilePath = systemSettings["ConnectionString"].Value }; break; } m_binaryCacheConfigurationLoader = new BinaryFileConfigurationLoader { FilePath = m_cachedBinaryConfigurationFile }; m_xmlCacheConfigurationLoader = new XMLConfigurationLoader { FilePath = m_cachedXmlConfigurationFile }; m_configurationLoader.StatusMessage += (o, args) => DisplayStatusMessage(args.Argument, UpdateType.Information); m_binaryCacheConfigurationLoader.StatusMessage += (o, args) => DisplayStatusMessage(args.Argument, UpdateType.Information); m_xmlCacheConfigurationLoader.StatusMessage += (o, args) => DisplayStatusMessage(args.Argument, UpdateType.Information); m_configurationLoader.ProcessException += ConfigurationLoader_ProcessException; m_binaryCacheConfigurationLoader.ProcessException += ConfigurationLoader_ProcessException; m_xmlCacheConfigurationLoader.ProcessException += ConfigurationLoader_ProcessException; m_reloadConfigQueue.Start(); #if !MONO try { // Attempt to assign desired process priority. Note that process will require SeIncreaseBasePriorityPrivilege or // Administrative privileges to make this change Process.GetCurrentProcess().PriorityClass = systemSettings["ProcessPriority"].ValueAs(); } catch (Exception ex) { LogException(ex); } #endif } /// /// Event handler for service started operation. /// /// Event source. /// Event arguments. /// /// Time-series framework uses this handler to handle initialization of system objects. /// protected virtual void ServiceStartedHandler(object sender, EventArgs e) { // Define a line of asterisks for emphasis string stars = new string('*', 79); // Get current process memory usage long processMemory = GSF.Common.GetProcessMemory(); // Log startup information m_serviceHelper.UpdateStatus( UpdateType.Information, "\r\n\r\n{0}\r\n\r\n" + "Node {{{1}}} Initializing\r\n\r\n" + " System Time: {2} UTC\r\n\r\n" + " Current Path: {3}\r\n\r\n" + " Machine Name: {4}\r\n\r\n" + " OS Version: {5}\r\n\r\n" + " Product Name: {6}\r\n\r\n" + " Working Memory: {7}\r\n\r\n" + " Execution Mode: {8}-bit\r\n\r\n" + " Processors: {9}\r\n\r\n" + " GC Server Mode: {10}\r\n\r\n" + " GC Latency Mode: {11}\r\n\r\n" + " Process Account: {12}\\{13}\r\n\r\n" + "{14}\r\n", stars, m_iaonSession.NodeID, DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss.fff"), FilePath.TrimFileName(FilePath.RemovePathSuffix(FilePath.GetAbsolutePath("")), 61), Environment.MachineName, Environment.OSVersion.VersionString, GSF.Common.GetOSProductName(), processMemory > 0 ? SI2.ToScaledString(processMemory, 4, "B", SI2.IECSymbols) : "Undetermined", IntPtr.Size * 8, Environment.ProcessorCount, GCSettings.IsServerGC, GCSettings.LatencyMode, Environment.UserDomainName, Environment.UserName, stars); // Add run-time log as a service component m_serviceHelper.ServiceComponents.Add(m_runTimeLog); // Create health exporter m_healthExporter = new MultipleDestinationExporter("HealthExporter", Timeout.Infinite); m_healthExporter.Initialize(new[] { new ExportDestination(FilePath.GetAbsolutePath("Health.txt"), false) }); m_healthExporter.StatusMessage += m_iaonSession.StatusMessageHandler; m_healthExporter.ProcessException += m_iaonSession.ProcessExceptionHandler; m_serviceHelper.ServiceComponents.Add(m_healthExporter); // Create status exporter m_statusExporter = new MultipleDestinationExporter("StatusExporter", Timeout.Infinite); m_statusExporter.Initialize(new[] { new ExportDestination(FilePath.GetAbsolutePath("Status.txt"), false) }); m_statusExporter.StatusMessage += m_iaonSession.StatusMessageHandler; m_statusExporter.ProcessException += m_iaonSession.ProcessExceptionHandler; m_serviceHelper.ServiceComponents.Add(m_statusExporter); // Create reporting processes m_reportingProcesses = ReportingProcessCollection.LoadImplementations(ex => { DisplayStatusMessage("Failed to load reporting process: {0}", UpdateType.Warning, ex.Message); LogException(ex); }); m_serviceHelper.ServiceComponents.Add(m_reportingProcesses); // Define scheduled service processes m_serviceHelper.AddScheduledProcess(HealthMonitorProcessHandler, "HealthMonitor", "* * * * *"); // Every minute m_serviceHelper.AddScheduledProcess(StatusExportProcessHandler, "StatusExport", "*/30 * * * *"); // Every 30 minutes // Define processes for each report (initially unscheduled) foreach (IReportingProcess reportingProcess in m_reportingProcesses) m_serviceHelper.AddProcess(ReportingProcessHandler, $"{reportingProcess.ReportType}Reporting"); // Add key Iaon collections as service components m_serviceHelper.ServiceComponents.Add(m_iaonSession.InputAdapters); m_serviceHelper.ServiceComponents.Add(m_iaonSession.ActionAdapters); m_serviceHelper.ServiceComponents.Add(m_iaonSession.OutputAdapters); // Define remote client requests (i.e., console commands) m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("List", "Displays status for specified adapter or collection", ListRequestHandler, new[] { "ls", "dir" })); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("Connect", "Connects (or starts) specified adapter", StartRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("Disconnect", "Disconnects (or stops) specified adapter", StopRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("Invoke", "Invokes a command for specified adapter", InvokeRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("ListCommands", "Displays possible commands for specified adapter", ListCommandsRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("Initialize", "Initializes specified adapter or collection", InitializeRequestHandler, new[] { "init" })); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("ReloadConfig", "Manually reloads the system configuration", ReloadConfigRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("UpdateConfigFile", "Updates an option in the configuration file", UpdateConfigFileRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("ManageCertificate", "Manages the certificate used by the service", ManageCertificateHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("Authenticate", "Authenticates network shares for health and status exports", AuthenticateRequestHandler, new[] { "auth" })); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("Restart", "Attempts to restart the host service", RestartServiceHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("RefreshRoutes", "Spawns request to recalculate routing tables", RefreshRoutesRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("TemporalSupport", "Determines if any adapters support temporal processing", TemporalSupportRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("ListReports", "Lists the available reports that have been generated", ListReportsRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("GetReport", "Gets a previously generated report for the specified date", GetReportRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("GetReportStatus", "Gets a reporting process status", GetReportStatusRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("GenerateReport", "Generates a report for the specified date", GenerateReportRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("ReportingConfig", "Displays or modifies the configuration of the reporting process", ReportingConfigRequestHandler, false)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("LogEvent", "Logs remote event log entries.", LogEventRequestHandler, false)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("Ping", "Shells out a process to perform a ping on the device.", PingRequestHandler)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("GetInputMeasurements", "Gets adapter input measurements.", GetInputMeasurementsRequestHandler, false)); m_serviceHelper.ClientRequestHandlers.Add(new ClientRequestHandler("GetOutputMeasurements", "Gets adapter output measurements.", GetOutputMeasurementsRequestHandler, false)); // Start system initialization on an independent thread so that service responds in a timely fashion... InitializeSystem(); } /// /// Event handler for service stopping operation. /// /// Event source. /// Event arguments. /// /// Time-series framework uses this handler to un-wire events and dispose of system objects. /// protected virtual void ServiceStoppingHandler(object sender, EventArgs e) { // Dispose system health exporter if (!(m_healthExporter is null)) { m_healthExporter.Enabled = false; m_serviceHelper.ServiceComponents.Remove(m_healthExporter); m_healthExporter.Dispose(); m_healthExporter.StatusMessage -= m_iaonSession.StatusMessageHandler; m_healthExporter.ProcessException -= m_iaonSession.ProcessExceptionHandler; m_healthExporter = null; } // Dispose system status exporter if (!(m_statusExporter is null)) { m_statusExporter.Enabled = false; m_serviceHelper.ServiceComponents.Remove(m_statusExporter); m_statusExporter.Dispose(); m_statusExporter.StatusMessage -= m_iaonSession.StatusMessageHandler; m_statusExporter.ProcessException -= m_iaonSession.ProcessExceptionHandler; m_statusExporter = null; } // Dispose reload config queue if (!(m_reloadConfigQueue is null)) { m_reloadConfigQueue.ProcessException -= m_iaonSession.ProcessExceptionHandler; m_reloadConfigQueue.Dispose(); m_reloadConfigQueue = null; } // Dispose reporting processes if (!(m_reportingProcesses is null)) { m_serviceHelper.ServiceComponents.Remove(m_reportingProcesses); m_reportingProcesses = null; } // Dispose Iaon session if (!(m_iaonSession is null)) { m_serviceHelper.ServiceComponents.Remove(m_iaonSession.InputAdapters); m_serviceHelper.ServiceComponents.Remove(m_iaonSession.ActionAdapters); m_serviceHelper.ServiceComponents.Remove(m_iaonSession.OutputAdapters); m_iaonSession.Dispose(); m_iaonSession.StatusMessage -= StatusMessageHandler; m_iaonSession.ProcessException -= ProcessExceptionHandler; m_iaonSession.ConfigurationChanged -= ConfigurationChangedHandler; m_iaonSession = null; } // Dispose of run-time log if (!(m_runTimeLog is null)) { m_serviceHelper.ServiceComponents.Remove(m_runTimeLog); m_runTimeLog.ProcessException -= ProcessExceptionHandler; m_runTimeLog.Dispose(); m_runTimeLog = null; } m_serviceHelper.ServiceStarting -= ServiceStartingHandler; m_serviceHelper.ServiceStarted -= ServiceStartedHandler; m_serviceHelper.ServiceStopping -= ServiceStoppingHandler; m_serviceHelper.UpdatedStatus -= UpdatedStatusHandler; m_serviceHelper.LoggedException -= LoggedExceptionHandler; if (!(m_serviceHelper.StatusLog is null)) { m_serviceHelper.StatusLog.Flush(); m_serviceHelper.StatusLog.LogException -= StatusLogExceptionHandler; } if (!(m_serviceHelper.ErrorLogger?.ErrorLog is null)) { m_serviceHelper.ErrorLogger.ErrorLog.Flush(); m_serviceHelper.ErrorLogger.ErrorLog.LogException -= ErrorLogExceptionHandler; } if (!(m_serviceHelper.ConnectionErrorLogger?.ErrorLog is null)) { m_serviceHelper.ConnectionErrorLogger.ErrorLog.Flush(); m_serviceHelper.ConnectionErrorLogger.ErrorLog.LogException -= ConnectionErrorLogExceptionHandler; } // Detach from handler for unobserved task exceptions TaskScheduler.UnobservedTaskException -= TaskScheduler_UnobservedTaskException; ShutdownHandler.InitiateSafeShutdown(); Dispose(); } private void UpdatedStatusHandler(object sender, EventArgs e) => UpdatedStatus?.Invoke(sender, new EventArgs(e.Argument1, e.Argument2, e.Argument3)); private void LoggedExceptionHandler(object sender, EventArgs e) => LoggedException?.Invoke(sender, new EventArgs(e.Argument)); #endregion #region [ System Initialization ] private void GenerateLocalCertificate() { ConfigurationFile configurationFile; CategorizedSettingsElementCollection remotingServer; string certificatePath; CertificateGenerator certificateGenerator = null; X509Certificate2 certificate = null; if (string.IsNullOrWhiteSpace(ServiceName)) throw new InvalidOperationException("EstablishServiceProperties must be overridden and ServiceName must be set"); try { configurationFile = ConfigurationFile.Current; remotingServer = configurationFile.Settings["remotingServer"]; remotingServer.Add("CertificateFile", $"{ServiceName}.cer", "Path to the local certificate used by this server for authentication."); certificatePath = FilePath.GetAbsolutePath(remotingServer["CertificateFile"].Value); certificateGenerator = new CertificateGenerator() { Issuer = ServiceName, CertificatePath = certificatePath }; if (File.Exists(certificatePath)) certificate = new X509Certificate2(certificatePath); if (!Equals(certificate, certificateGenerator.GenerateCertificate())) EventLog.WriteEntry(ServiceName, $"Created self-signed certificate for service: \"{certificatePath}\"", EventLogEntryType.Information, 0); } catch (Exception ex) { string message = new StringBuilder() .AppendLine(ex.ToString()) .AppendLine() .AppendLine("Debug info:") .AppendLine(string.Join(Environment.NewLine, certificateGenerator?.DebugLog ?? new List())) .ToString(); EventLog.WriteEntry(ServiceName, message, EventLogEntryType.Error, 0); } } private void SetupTempPath() { string assemblyDirectory = null; try { // Setup custom temp folder for services so that dynamically compiled razor assemblies can be easily managed assemblyDirectory = FilePath.GetAbsolutePath(Common.DynamicAssembliesFolderName); if (!Directory.Exists(assemblyDirectory)) Directory.CreateDirectory(assemblyDirectory); Environment.SetEnvironmentVariable("TEMP", assemblyDirectory); Environment.SetEnvironmentVariable("TMP", assemblyDirectory); } catch (Exception ex) { // This is not catastrophic Logger.SwallowException(ex, $"Failed to assign temp folder location for \"{ServiceName}\" service to: {assemblyDirectory}"); } } /// /// Initializes the service helper. /// public void InitializeServiceHelper() { CategorizedSettingsElementCollection remotingServerSettings = ConfigurationFile.Current.Settings["remotingServer"]; m_serviceHelper = new ServiceHelper(); if (remotingServerSettings.Cast().Any(element => element.Name.Equals("EnabledSslProtocols", StringComparison.OrdinalIgnoreCase) && !element.Value.Equals("None", StringComparison.OrdinalIgnoreCase))) m_remotingServer = InitializeTlsServer(); else m_remotingServer = InitializeTcpServer(); m_serviceHelper.ErrorLogger.ErrorLog.FileName = "ErrorLog.txt"; m_serviceHelper.ErrorLogger.ErrorLog.PersistSettings = true; m_serviceHelper.ErrorLogger.ErrorLog.SettingsCategory = "ErrorLog"; m_serviceHelper.ErrorLogger.ErrorLog.Initialize(); m_serviceHelper.ErrorLogger.LogToEventLog = false; m_serviceHelper.ErrorLogger.PersistSettings = true; m_serviceHelper.ErrorLogger.SmtpServer = ""; m_serviceHelper.ErrorLogger.Initialize(); m_serviceHelper.ConnectionErrorLogger.ErrorLog.FileName = "ConnectionErrorLog.txt"; m_serviceHelper.ConnectionErrorLogger.ErrorLog.PersistSettings = true; m_serviceHelper.ConnectionErrorLogger.ErrorLog.SettingsCategory = "ConnectionErrorLog"; m_serviceHelper.ConnectionErrorLogger.ErrorLog.Initialize(); m_serviceHelper.ConnectionErrorLogger.LogToEventLog = false; m_serviceHelper.ConnectionErrorLogger.PersistSettings = true; m_serviceHelper.ConnectionErrorLogger.SmtpServer = ""; m_serviceHelper.ConnectionErrorLogger.Initialize(); m_serviceHelper.ProcessScheduler.PersistSettings = true; m_serviceHelper.ProcessScheduler.SettingsCategory = "ProcessScheduler"; m_serviceHelper.ProcessScheduler.Initialize(); m_serviceHelper.StatusLog.FileName = "StatusLog.txt"; m_serviceHelper.StatusLog.PersistSettings = true; m_serviceHelper.StatusLog.SettingsCategory = "StatusLog"; m_serviceHelper.StatusLog.Initialize(); m_serviceHelper.ParentService = this; m_serviceHelper.PersistSettings = true; m_serviceHelper.RemotingServer = m_remotingServer; m_serviceHelper.Initialize(); } private TcpServer InitializeTcpServer() { TcpServer remotingServer; remotingServer = new TcpServer(); remotingServer.ConfigurationString = "Port=8500"; remotingServer.IgnoreInvalidCredentials = true; remotingServer.PayloadAware = true; remotingServer.PersistSettings = true; remotingServer.SettingsCategory = "RemotingServer"; remotingServer.Initialize(); return remotingServer; } private TlsServer InitializeTlsServer() { TlsServer remotingServer; remotingServer = new TlsServer(); remotingServer.CertificateFile = "Internal.cer"; remotingServer.ConfigurationString = "Port=8500"; remotingServer.IgnoreInvalidCredentials = true; remotingServer.PayloadAware = true; remotingServer.PersistSettings = true; remotingServer.SettingsCategory = "RemotingServer"; remotingServer.TrustedCertificatesPath = $"Certs{Path.DirectorySeparatorChar}Remotes"; remotingServer.Initialize(); remotingServer.RemoteCertificateValidationCallback = (o, certificate, chain, errors) => true; return remotingServer; } // Perform system initialization private void InitializeSystem() { m_reloadConfigQueue.Add(Tuple.Create("Startup", new Action(success => { // Attempt to load system configuration if (success) { // Initialize and start all session adapters m_iaonSession.Initialize(); DisplayStatusMessage("System initialization complete.", UpdateType.Information); try { // If any settings have been added to configuration file, we go ahead and save them now m_serviceHelper.SaveSettings(true); ConfigurationFile.Current.Save(); } catch (Exception ex) { LogException(new InvalidOperationException($"Failed to save current service helper settings: {ex.Message}", ex)); } } else { DisplayStatusMessage("System initialization failed due to unavailable configuration.", UpdateType.Alarm); } // Log current thread pool size ThreadPool.GetMinThreads(out int minWorkerThreads, out int minIOThreads); ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxIOThreads); DisplayStatusMessage("Thread pool size: minimum {0} worker {1} I/O, maximum {2} worker {3} I/O", UpdateType.Information, minWorkerThreads, minIOThreads, maxWorkerThreads, maxIOThreads); }))); } private void ExecuteReloadConfig(Tuple>[] items) { DataSet configuration = null; bool systemConfigurationLoaded = false; List requestTypes = items .Select(tuple => tuple.Item1) .ToList(); List distinctRequestsTypes = requestTypes .Distinct() .OrderBy(type => requestTypes.LastIndexOf(type)) .ToList(); foreach (string type in distinctRequestsTypes) { string typeInner = type; if (type == m_configurationType.ToString()) { DisplayStatusMessage("Loading system configuration...", UpdateType.Information); configuration = GetConfigurationDataSet(); // Update data source on all adapters in all collections if (!(configuration is null)) PropagateDataSource(configuration); } else if (type == "Augmented") { DisplayStatusMessage("Augmenting current system configuration...", UpdateType.Information); configuration = AugmentConfigurationDataSet(DataSource); // Update data source on all adapters in all collections if (!(configuration is null)) PropagateDataSource(configuration); } else if (type == "BinaryCache") { DisplayStatusMessage("Loading binary cached configuration...", UpdateType.Information); configuration = GetBinaryCachedConfigurationDataSet(); // Update data source on all adapters in all collections if (!(configuration is null)) PropagateDataSource(configuration); } else if (type == "XmlCache") { DisplayStatusMessage("Loading XML cached configuration...", UpdateType.Information); configuration = GetXMLCachedConfigurationDataSet(); // Update data source on all adapters in all collections if (!(configuration is null)) PropagateDataSource(configuration); } else if (type == "Startup") { systemConfigurationLoaded = LoadStartupConfiguration(); } else { // No specific reload command was issued; // load system configuration as normal systemConfigurationLoaded = LoadSystemConfiguration(); } foreach (Action callback in items.Where(tuple => tuple.Item1 == typeInner).Select(tuple => tuple.Item2)) { try { callback(systemConfigurationLoaded || !(configuration is null)); } catch (Exception ex) { DisplayStatusMessage("Failed to execute callback for ReloadConfig request of type {0}: {1}", UpdateType.Alarm, type, ex.Message); LogException(ex); } } // Spawn routing table calculation updates m_iaonSession.RecalculateRoutingTables(); } } // Load the system configuration data set private bool LoadStartupConfiguration() { // If cached configuration is not preferred, // load system configuration normally if (!m_preferCachedConfiguration) return LoadSystemConfiguration(); // Cached configuration is preferred, so start by loading cached configuration bool loadedFromCache = true; DataSet configuration = GetBinaryCachedConfigurationDataSet() ?? GetXMLCachedConfigurationDataSet(); // If cached configuration failed to load, // load from regular configuration data source after all if (configuration is null) { loadedFromCache = false; configuration = GetConfigurationDataSet(); } // Update data source on all adapters in all collections if (configuration is null || !PropagateDataSource(configuration)) return false; // Cache the configuration if it wasn't already loaded from a cache if (!loadedFromCache) CacheCurrentConfiguration(configuration); return true; } // Load the system configuration data set private bool LoadSystemConfiguration() { DatabaseConfigurationLoader dbConfigurationLoader = m_configurationLoader as DatabaseConfigurationLoader; DataSet configuration = null; DataSet binaryCache = null; DataSet xmlCache = null; bool binaryCacheLoadAttempted = false; bool xmlCacheLoadAttempted = false; bool loadedFromCache = false; try { // If the configuration is a database configuration, make sure // to open the database before we start so we're not constantly // opening and closing connections to the database dbConfigurationLoader?.Open(); if (m_configurationLoader.CanAugment) { // Get the current running configuration configuration = DataSource; if (!(configuration is null)) { // Existing configuration is available so we attempt to augment that DisplayStatusMessage("Attempting to augment existing configuration data set...", UpdateType.Information); configuration = AugmentConfigurationDataSet(configuration.Copy()); } else { // Attempt to load the binary cached configuration binaryCache = GetBinaryCachedConfigurationDataSet(); binaryCacheLoadAttempted = true; if (!(binaryCache is null)) { // Binary cached configuration is available so we attempt to augment that DisplayStatusMessage("Attempting to augment binary cached configuration data set...", UpdateType.Information); configuration = AugmentConfigurationDataSet(binaryCache.Copy()); } else { // Attempt to load the XML cached configuration xmlCache = GetXMLCachedConfigurationDataSet(); xmlCacheLoadAttempted = true; if (!(xmlCache is null)) { // XML cached configuration is available so we attempt to augment that DisplayStatusMessage("Attempting to augment XML cached configuration data set...", UpdateType.Information); configuration = AugmentConfigurationDataSet(xmlCache.Copy()); } } } } if (configuration is null) { // Either we cannot augment configuration or augmentation failed, // so we attempt to load the whole configuration from the source configuration = GetConfigurationDataSet(); } if (configuration is null) { // If all else fails, load configuration from the binary cache loadedFromCache = true; configuration = binaryCacheLoadAttempted ? binaryCache : GetBinaryCachedConfigurationDataSet(); } if (configuration is null) { // If we even failed to load from the // binary cache, load from the XML cache configuration = xmlCacheLoadAttempted ? xmlCache : GetXMLCachedConfigurationDataSet(); } // Attempt to update data source on all adapters in all collections if (configuration is null || !PropagateDataSource(configuration)) return false; // Cache the configuration if it wasn't already loaded from a cache if (!loadedFromCache) CacheCurrentConfiguration(configuration); return true; } catch (Exception ex) { DisplayStatusMessage(ex.Message, UpdateType.Alarm); LogException(ex); // Most of the methods used above have a try-catch implemented to display helpful error messages // and prevent this code from executing. This should only execute when using a database configuration // as the primary configuration source and only if the system is unable to open a connection to the // database. In that case, we simply attempt to load the cached configuration and use that instead DisplayStatusMessage("Failed to open a connection to the database. Falling back on cached configuration...", UpdateType.Warning); if (configuration is null) configuration = GetBinaryCachedConfigurationDataSet(); if (configuration is null) configuration = GetXMLCachedConfigurationDataSet(); if (!(configuration is null)) return PropagateDataSource(configuration); return false; } finally { dbConfigurationLoader?.Close(); } } private DataSet GetConfigurationDataSet() { try { return m_configurationLoader.Load(); } catch (Exception ex) { DisplayStatusMessage("Failed to load {0} configuration due to exception: {1}", UpdateType.Warning, m_configurationType, ex.Message); LogException(ex); return null; } } private DataSet GetBinaryCachedConfigurationDataSet() { try { if (File.Exists(m_binaryCacheConfigurationLoader.FilePath)) return m_binaryCacheConfigurationLoader.Load(); return null; } catch (Exception ex) { DisplayStatusMessage("Failed to load cached binary configuration due to exception: {0}", UpdateType.Alarm, ex.Message); LogException(ex); return null; } } private DataSet GetXMLCachedConfigurationDataSet() { try { if (File.Exists(m_xmlCacheConfigurationLoader.FilePath)) return m_xmlCacheConfigurationLoader.Load(); return null; } catch (Exception ex) { DisplayStatusMessage("Failed to load cached XML configuration due to exception: {0}", UpdateType.Alarm, ex.Message); LogException(ex); return null; } } private DataSet AugmentConfigurationDataSet(DataSet configuration) { try { m_configurationLoader.Augment(configuration); return configuration; } catch (Exception ex) { DisplayStatusMessage("Failed to augment configuration due to exception: {0}", UpdateType.Warning, ex.Message); LogException(ex); return null; } } /// /// Handle assignment of data source to Iaon session. /// /// New data source to assign. /// true if data source was assigned; otherwise, false. protected virtual bool PropagateDataSource(DataSet dataSource) { try { m_iaonSession.DataSource = dataSource; return true; } catch (Exception ex) { DisplayStatusMessage("Unable to propagate configuration data set to adapters due to exception: {0}", UpdateType.Alarm, ex.Message); LogException(ex); return false; } } /// /// Caches the current system configuration. /// /// Configuration . /// /// This method allows caching of the current system configuration so it can be used if primary configuration source is unavailable. /// protected virtual void CacheCurrentConfiguration(DataSet configuration) { m_latestConfiguration = configuration; m_configurationCacheOperation.RunOnceAsync(); } // Executes actual serialization of current configuration private void ExecuteConfigurationCache() { DataSet configuration = m_latestConfiguration; if (configuration is null) return; // Create backups of binary configurations BackupConfiguration(ConfigurationType.BinaryFile, m_cachedBinaryConfigurationFile); // Create backups of XML configurations BackupConfiguration(ConfigurationType.XmlFile, m_cachedXmlConfigurationFile); try { // Wait a moment for write lock in case binary file is open by another process if (File.Exists(m_cachedBinaryConfigurationFile)) FilePath.WaitForWriteLock(m_cachedBinaryConfigurationFile); // Cache binary serialized version of data set using (FileStream configurationFileStream = File.OpenWrite(m_cachedBinaryConfigurationFile)) { configuration.SerializeToStream(configurationFileStream); } DisplayStatusMessage("Successfully cached current configuration to binary.", UpdateType.Information); } catch (Exception ex) { DisplayStatusMessage("Failed to cache last known configuration due to exception: {0}", UpdateType.Alarm, ex.Message); LogException(ex); } // Serialize current data set to configuration files try { // Wait a moment for write lock in case XML file is open by another process if (File.Exists(m_cachedXmlConfigurationFile)) FilePath.WaitForWriteLock(m_cachedXmlConfigurationFile); // Cache XML serialized version of data set configuration.WriteXml(m_cachedXmlConfigurationFile, XmlWriteMode.WriteSchema); DisplayStatusMessage("Successfully cached current configuration to XML.", UpdateType.Information); } catch (Exception ex) { DisplayStatusMessage("Failed to cache last known configuration due to exception: {0}", UpdateType.Alarm, ex.Message); LogException(ex); } } private void BackupConfiguration(ConfigurationType configType, string configurationFile) { try { // Create multiple backup configurations, if requested for (int i = m_configurationBackups; i > 0; i--) { string origConfigFile = configurationFile + ".backup" + (i == 1 ? "" : (i - 1).ToString()); if (!File.Exists(origConfigFile)) continue; string nextConfigFile = configurationFile + ".backup" + i; if (File.Exists(nextConfigFile)) File.Delete(nextConfigFile); File.Move(origConfigFile, nextConfigFile); } } catch (Exception ex) { DisplayStatusMessage("Failed to create extra backup {0} configurations due to exception: {1}", UpdateType.Warning, configType, ex.Message); LogException(ex); } try { if (m_configurationBackups < 1 || !File.Exists(configurationFile)) return; // Back up current configuration file string backupConfigFile = configurationFile + ".backup"; if (File.Exists(backupConfigFile)) File.Delete(backupConfigFile); File.Move(configurationFile, backupConfigFile); } catch (Exception ex) { DisplayStatusMessage("Failed to backup last known cached {0} configuration due to exception: {1}", UpdateType.Warning, configType, ex.Message); LogException(ex); } } /// /// Releases the unmanaged resources used by the object and optionally releases the managed resources. /// /// true to release both managed and unmanaged resources; false to release only unmanaged resources. protected override void Dispose(bool disposing) { if (m_disposed) return; try { if (!disposing) return; if (!(components is null)) { components.Dispose(); components = null; } if (!(m_serviceHelper is null)) { m_serviceHelper.Dispose(); m_serviceHelper = null; } if (!(m_remotingServer is null)) { m_remotingServer.Dispose(); m_remotingServer = null; } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } #endregion #region [ Service Binding ] internal void StartHostedService() { OnStart(Environment.CommandLine.Split(' ')); } internal void StopHostedService() { OnStop(); } internal void SendRequest(IPrincipal principal, Guid clientID, string userInput) { ClientRequest request = ClientRequest.Parse(userInput); if (request is null) return; ClientRequestHandler requestHandler = m_serviceHelper.FindClientRequestHandler(request.Command); if (requestHandler is null) { DisplayStatusMessage($"Command \"{request.Command}\" is not supported\r\n\r\n", UpdateType.Alarm); } else { ClientInfo clientInfo = new ClientInfo() {ClientID = clientID}; ClientRequestInfo clientRequestInfo = new ClientRequestInfo(clientInfo, request); clientInfo.SetClientUser(principal); requestHandler.HandlerMethod(clientRequestInfo); } } /// /// Handles service start event. /// /// Service startup arguments, if any. protected override void OnStart(string[] args) { try { GenerateLocalCertificate(); SetupTempPath(); InitializeServiceHelper(); // Register service level event handlers m_serviceHelper.ServiceStarting += ServiceStartingHandler; m_serviceHelper.ServiceStarted += ServiceStartedHandler; m_serviceHelper.ServiceStopping += ServiceStoppingHandler; m_serviceHelper.UpdatedStatus += UpdatedStatusHandler; m_serviceHelper.LoggedException += LoggedExceptionHandler; if (!(m_serviceHelper.StatusLog is null)) m_serviceHelper.StatusLog.LogException += StatusLogExceptionHandler; if (!(m_serviceHelper.ErrorLogger?.ErrorLog is null)) m_serviceHelper.ErrorLogger.ErrorLog.LogException += ErrorLogExceptionHandler; if (!(m_serviceHelper.ConnectionErrorLogger?.ErrorLog is null)) m_serviceHelper.ConnectionErrorLogger.ErrorLog.LogException += ConnectionErrorLogExceptionHandler; m_serviceHelper.OnStart(args); } catch (Exception ex) { LogException(ex); } } /// /// Handles service stop event. /// protected override void OnStop() { try { m_serviceHelper.OnStop(); } catch (Exception ex) { LogException(ex); } // As a final operation, shell remote console to clean up temporary razor files try { if (GSF.Common.IsPosixEnvironment) { using (Process shell = new Process()) { shell.StartInfo = new ProcessStartInfo(ConsoleApplicationName) { CreateNoWindow = true, Arguments = "-clearCache" }; shell.Start(); } } else { string batchFileName = FilePath.GetAbsolutePath("ClearRazorAssemblyCache.cmd"); File.WriteAllText(batchFileName, $@"@ECHO OFF{Environment.NewLine}TIMEOUT 2{Environment.NewLine}{ConsoleApplicationName} -clearCache"); // This is the only .NET call that will spawn an independent, non-child, process on Windows Process.Start(batchFileName); } } catch (Exception ex) { // This is not catastrophic Logger.SwallowException(ex, $"Failed to start \"{ConsoleApplicationName}\" to clear temporary RazorEngine files from dynamic assembly cache."); } } /// /// Handles service pause event. /// protected override void OnPause() { try { m_serviceHelper.OnPause(); } catch (Exception ex) { LogException(ex); } } /// /// Handles service continue event. /// protected override void OnContinue() { try { m_serviceHelper.OnResume(); } catch (Exception ex) { LogException(ex); } } /// /// Handles service shut down event. /// protected override void OnShutdown() { try { m_serviceHelper.OnShutdown(); } catch (Exception ex) { LogException(ex); } } #endregion #region [ Primary Adapter Event Handlers ] /// /// Event handler for reporting status messages. /// /// Event source of the status message. /// Event arguments containing the status message and its type to report. /// /// The time-series framework uses this event to report adapter status messages (e.g., to a log file or console window). /// private void StatusMessageHandler(object sender, EventArgs e) { DisplayStatusMessage(e.Argument1, e.Argument2); } /// /// Event handler for processing reported exceptions. /// /// Event source of the exception. /// Event arguments containing the exception to report. /// /// The time-series framework uses this event to report exceptions. /// private void ProcessExceptionHandler(object sender, EventArgs e) { LogException(e.Argument); } /// /// Event handler for processing notifications from adapters that configuration has changed. /// /// Event source of the notification. /// Event arguments, if any. /// /// The time-series framework uses this event to report configuration changes. /// private void ConfigurationChangedHandler(object sender, EventArgs e) { m_reloadConfigQueue.Add(Tuple.Create("System", (Action)(success => {}))); } // Handle task scheduler exceptions private void TaskScheduler_UnobservedTaskException(object sender, UnobservedTaskExceptionEventArgs e) { foreach (Exception ex in e.Exception.Flatten().InnerExceptions) { LogException(ex); } e.SetObserved(); } /// /// Event handler for processing exceptions encountered while writing entries to any log file. /// /// Event source of the exception. /// Event arguments containing the exception to report. /// /// This function is for derived classes wanting access to any logged exception without needing /// to override all specific log exception handlers. /// protected virtual void LogExceptionHandler(object sender, EventArgs e) { } /// /// Event handler for processing exceptions encountered while writing entries to the status log file. /// /// Event source of the exception. /// Event arguments containing the exception to report. protected virtual void StatusLogExceptionHandler(object sender, EventArgs e) { DisplayStatusMessage("Status log file exception: " + e.Argument.Message, UpdateType.Alarm); LogExceptionHandler(sender, e); } /// /// Event handler for processing exceptions encountered while writing entries to the error log file. /// /// Event source of the exception. /// Event arguments containing the exception to report. protected virtual void ErrorLogExceptionHandler(object sender, EventArgs e) { DisplayStatusMessage("Error log file exception: " + e.Argument.Message, UpdateType.Alarm); LogExceptionHandler(sender, e); } /// /// Event handler for processing exceptions encountered while writing entries to the connection log file. /// /// Event source of the exception. /// Event arguments containing the exception to report. protected virtual void ConnectionErrorLogExceptionHandler(object sender, EventArgs e) { DisplayStatusMessage("Connection log file exception: " + e.Argument.Message, UpdateType.Alarm); LogExceptionHandler(sender, e); } /// /// Event handler for scheduled health monitor display. /// /// Scheduled event name. /// Scheduled event parameters. protected virtual void HealthMonitorProcessHandler(string name, object[] parameters) { const string RequestCommand = "Health"; ClientRequestHandler requestHandler = m_serviceHelper.FindClientRequestHandler(RequestCommand); if (requestHandler is null) return; // We pretend to be a client and send a "Health" command to ourselves... requestHandler.HandlerMethod(ClientHelper.PretendRequest(RequestCommand)); // We also export human readable health information to a text file for external display m_healthExporter.ExportData(m_serviceHelper.PerformanceMonitor.Status); } /// /// Event handler for scheduled adapter status export. /// /// Scheduled event name. /// Scheduled event parameters. protected virtual void StatusExportProcessHandler(string name, object[] parameters) { // Every thirty minutes we export a human readable service status to a text file for external display m_statusExporter.ExportData(m_serviceHelper.Status); } /// /// Event handler for scheduled reporting services. /// /// Scheduled event name. /// Scheduled event parameters. protected virtual void ReportingProcessHandler(string name, object[] parameters) { // Report processing schedule names end with "Reporting", remove suffix // 123456789 if (name.EndsWith("Reporting", StringComparison.OrdinalIgnoreCase)) name = name.Substring(0, name.Length - 9); // Lookup reporting process IReportingProcess reportingProcess = m_reportingProcesses.FindReportType(name); if (reportingProcess is null) return; // Reporting process schedule is due, generate a new report reportingProcess.CleanReportLocation(); reportingProcess.GenerateReport(DateTime.UtcNow - TimeSpan.FromDays(1), true); } #endregion #region [ Remote Client Request Handlers ] /// /// Gets requested . /// /// instance containing the client request. /// Requested . protected virtual IAdapterCollection GetRequestedCollection(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.Exists("A")) return m_iaonSession.ActionAdapters; if (requestInfo.Request.Arguments.Exists("O")) return m_iaonSession.OutputAdapters; if (requestInfo.Request.Arguments.Exists("F")) return m_iaonSession.FilterAdapters; return m_iaonSession.InputAdapters; } /// /// Gets requested . /// /// instance containing the client request. /// Requested . protected virtual IAdapter GetRequestedAdapter(ClientRequestInfo requestInfo) { return GetRequestedAdapter(requestInfo, out IAdapterCollection _); } /// /// Gets requested and its containing . /// /// instance containing the client request. /// Containing for . /// Requested . protected virtual IAdapter GetRequestedAdapter(ClientRequestInfo requestInfo, out IAdapterCollection collection) { string adapterID = requestInfo.Request.Arguments["OrderedArg1"]; collection = GetRequestedCollection(requestInfo); if (string.IsNullOrWhiteSpace(adapterID)) return null; IAdapter adapter; adapterID = adapterID.Trim(); if (adapterID.IsAllNumbers() && uint.TryParse(adapterID, out uint id)) { // Adapter ID is numeric, try numeric lookup by adapter ID in requested collection if (collection.TryGetAdapterByID(id, out adapter)) return adapter; // Try looking for ID in any collection if all runtime ID's are unique if (m_uniqueAdapterIDs && m_iaonSession.AllAdapters.TryGetAnyAdapterByID(id, out adapter, out collection)) return adapter; collection = GetRequestedCollection(requestInfo); SendResponse(requestInfo, false, "Failed to find adapter with ID \"{0}\".", id); } else { // Adapter ID is alpha-numeric, try text-based lookup by adapter name in requested collection if (collection.TryGetAdapterByName(adapterID, out adapter)) return adapter; // Try looking for adapter name in any collection if (m_iaonSession.AllAdapters.TryGetAnyAdapterByName(adapterID, out adapter, out collection)) return adapter; collection = GetRequestedCollection(requestInfo); SendResponse(requestInfo, false, "Failed to find adapter named \"{0}\".", adapterID); } return null; } /// /// Displays status of specified adapter or collection. /// /// instance containing the client request. protected virtual void ListRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Displays status of specified adapter or collection."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" List [ID] [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ID:".PadRight(20)); helpMessage.Append("ID of the adapter to display, or all adapters if not specified"); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -I".PadRight(20)); helpMessage.Append("Enumerate input adapters (default)"); helpMessage.AppendLine(); helpMessage.Append(" -A".PadRight(20)); helpMessage.Append("Enumerate action adapters"); helpMessage.AppendLine(); helpMessage.Append(" -O".PadRight(20)); helpMessage.Append("Enumerate output adapters"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { StringBuilder adapterList = new StringBuilder(); IAdapterCollection collection = GetRequestedCollection(requestInfo); IEnumerable listItems = collection; bool idArgExists = requestInfo.Request.Arguments.Exists("OrderedArg1"); int enumeratedItems = 0; adapterList.AppendFormat("System Uptime: {0}", m_serviceHelper.RemotingServer.RunTime.ToString(3)); adapterList.AppendLine(); adapterList.AppendLine(); if (idArgExists) adapterList.AppendFormat(">> Selected adapter from {0}", collection.Name); else adapterList.AppendFormat(">> All defined adapters in {0} ({1} total)", collection.Name, collection.Count); // Make a collection of one item for individual adapters if (idArgExists) { IAdapter adapter = GetRequestedAdapter(requestInfo); List singleItemList = new List(); if (!(adapter is null)) singleItemList.Add(adapter); listItems = singleItemList; } adapterList.AppendLine(); adapterList.AppendLine(); adapterList.Append(" ID Name"); adapterList.AppendLine(); // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 // 1 2 3 4 5 6 7 8 adapterList.Append("---------- --------------------------------------------------------------------"); // 123456789012345678901234567890123456789012345678901234567890123456789 // 1 2 3 4 5 6 adapterList.AppendLine(); foreach (IAdapter adapter in listItems) { adapterList.AppendFormat("{0} {1}", adapter.ID.ToString().CenterText(10), adapter.Name.TruncateRight(66)); adapterList.AppendLine(); adapterList.Append(" "); adapterList.Append(adapter.GetShortStatus(68).TruncateRight(68)); // If a request was made to list a specific item, we request full status if (idArgExists) { adapterList.AppendLine(); adapterList.AppendLine(); adapterList.Append(adapter.Status); } adapterList.AppendLine(); enumeratedItems++; } if (enumeratedItems > 0) SendResponse(requestInfo, true, adapterList.ToString()); else SendResponse(requestInfo, false, "No items were available enumerate."); } } /// /// Starts specified adapter. /// /// instance containing the client request. protected virtual void StartRequestHandler(ClientRequestInfo requestInfo) { ActionRequestHandler(requestInfo, adapter => adapter.Start()); } /// /// Stops specified adapter. /// /// instance containing the client request. protected virtual void StopRequestHandler(ClientRequestInfo requestInfo) { ActionRequestHandler(requestInfo, adapter => adapter.Stop()); } /// /// Generic adapter request handler. /// /// instance containing the client request. /// Action to perform on . protected virtual void ActionRequestHandler(ClientRequestInfo requestInfo, Action adapterAction) { string actionName = requestInfo.Request.Command.ToTitleCase(); if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.AppendFormat("Handles {0} command for specified adapter.", actionName); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.AppendFormat(" {0} ID [Options]", actionName); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ID:".PadRight(20)); helpMessage.Append("ID of the adapter to execute action on"); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -I".PadRight(20)); helpMessage.AppendFormat("Perform {0} command on input adapters (default)", actionName); helpMessage.AppendLine(); helpMessage.Append(" -A".PadRight(20)); helpMessage.AppendFormat("Perform {0} command on action adapters", actionName); helpMessage.AppendLine(); helpMessage.Append(" -O".PadRight(20)); helpMessage.AppendFormat("Perform {0} command on output adapters", actionName); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { if (requestInfo.Request.Arguments.Exists("OrderedArg1")) { IAdapter adapter = GetRequestedAdapter(requestInfo); if (adapter is null) return; if (adapter.Initialized) { adapterAction(adapter); SendResponse(requestInfo, true); } else { SendResponse(requestInfo, false, "Unable to {0} -- adapter is not initialized.", actionName); } } else { SendResponse(requestInfo, false, "No ID was specified for \"{0}\" command.", actionName); } } } /// /// Invokes specified adapter command. /// /// instance containing the client request. protected virtual void InvokeRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Invokes specified adapter command."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" Invoke [ID] Command [Params] [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ID:".PadRight(20)); helpMessage.Append("ID of the adapter to execute command on"); helpMessage.AppendLine(); helpMessage.AppendLine(" If ID is not specified, action will be on collection itself."); helpMessage.AppendLine(); helpMessage.Append(" Command:".PadRight(20)); helpMessage.Append("Name of the adapter command (i.e., method) to invoke"); helpMessage.AppendLine(); helpMessage.Append(" Params:".PadRight(20)); helpMessage.Append("Command parameters, if any, separated by spaces"); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -I".PadRight(20)); helpMessage.Append("Invoke specified command on input adapter (default)"); helpMessage.AppendLine(); helpMessage.Append(" -A".PadRight(20)); helpMessage.Append("Invoke specified command on action adapter"); helpMessage.AppendLine(); helpMessage.Append(" -O".PadRight(20)); helpMessage.Append("Invoke specified command on output adapter"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { Thread invocationThread = new Thread(() => { IAdapter adapter = null; string command = null; // See if specific ID for an adapter was requested if (requestInfo.Request.Arguments.Exists("OrderedArg1")) { if (requestInfo.Request.Arguments.Exists("OrderedArg2")) { adapter = GetRequestedAdapter(requestInfo); command = requestInfo.Request.Arguments["OrderedArg2"]; } else { adapter = GetRequestedCollection(requestInfo); command = requestInfo.Request.Arguments["OrderedArg1"]; } } else { SendResponse(requestInfo, false, "No command was specified to invoke."); } if (adapter is null) return; try { // See if method exists with specified name using reflection MethodInfo method = adapter.GetType().GetMethod(command, BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase); // Invoke method if (!(method is null)) { // Make sure method is marked as invokable (i.e., AdapterCommandAttribute exists on method) if (method.TryGetAttribute(out AdapterCommandAttribute commandAttribute) && (!m_serviceHelper.SecureRemoteInteractions || commandAttribute.AllowedRoles.Any(role => requestInfo.Sender.ClientUser.IsInRole(role)))) { ParameterInfo[] parameterInfo = method.GetParameters(); object returnValue = null; bool success = true; if (parameterInfo.Length == 0) { // Invoke parameterless adapter command returnValue = method.Invoke(adapter, null); } else { int argCount = requestInfo.Request.Arguments.Count - 2; int attachmentCount = requestInfo.Request.Attachments.Count; // Create typed parameters for method and invoke if (argCount + attachmentCount >= parameterInfo.Length) { // Attempt to convert command parameters to the method parameter types object[] parameters = requestInfo.Request.Arguments.Skip(2) .Select((arg, i) => arg.Value.ConvertToType(parameterInfo[i].ParameterType)) .Concat(requestInfo.Request.Attachments) .Take(parameterInfo.Length) .ToArray(); // Invoke adapter command with specified parameters returnValue = method.Invoke(adapter, parameters); } else { success = false; SendResponse(requestInfo, false, "Parameter count mismatch, \"{0}\" command expects {1} parameters.", command, parameterInfo.Length); } } // If invoke was successful, return actionable response if (success) { // Return value, if any, will be returned to requesting client as a response attachment if (returnValue is null) SendResponse(requestInfo, true, "Command \"{0}\" successfully invoked.", command); else SendResponseWithAttachment(requestInfo, true, returnValue, "Command \"{0}\" successfully invoked, return value = {1}", command, returnValue.ToNonNullString("null")); } } else { SendResponse(requestInfo, false, "Specified command \"{0}\" is not marked as invokable for adapter \"{1}\" [Type = {2}].", command, adapter.Name, adapter.GetType().Name); } } else { SendResponse(requestInfo, false, "Specified command \"{0}\" does not exist for adapter \"{1}\" [Type = {2}].", command, adapter.Name, adapter.GetType().Name); } } catch (Exception ex) { SendResponse(requestInfo, false, "Failed to invoke command: {0}", ex.Message); LogException(ex); } }); invocationThread.IsBackground = true; invocationThread.Start(); } } /// /// Lists possible commands of specified adapter. /// /// instance containing the client request. protected virtual void ListCommandsRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Lists possible commands of specified adapter."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" ListCommands [ID] [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ID:".PadRight(20)); helpMessage.Append("ID of the adapter to execute command on"); helpMessage.AppendLine(); helpMessage.AppendLine(" If ID is not specified, action will be on collection itself."); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -I".PadRight(20)); helpMessage.Append("Lists commands on input adapter (default)"); helpMessage.AppendLine(); helpMessage.Append(" -A".PadRight(20)); helpMessage.Append("Lists commands on action adapter"); helpMessage.AppendLine(); helpMessage.Append(" -O".PadRight(20)); helpMessage.Append("Lists commands on output adapter"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { IAdapter adapter; // See if specific ID for an adapter was requested if (requestInfo.Request.Arguments.Exists("OrderedArg1")) adapter = GetRequestedAdapter(requestInfo); else adapter = GetRequestedCollection(requestInfo); if (adapter is null) return; try { // Get public command methods of specified adapter using reflection MethodInfo[] methods = adapter.GetType().GetMethods(BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance | BindingFlags.IgnoreCase); // Invoke method StringBuilder methodList = new StringBuilder(); methodList.AppendFormat("Adapter \"{0}\" [Type = {1}] Command List:", adapter.Name, adapter.GetType().Name); methodList.AppendLine(); methodList.AppendLine(); // Enumerate each public method foreach (MethodInfo method in methods) { // Only display methods marked as invokable (i.e., AdapterCommandAttribute exists on method) if (method.TryGetAttribute(out AdapterCommandAttribute commandAttribute)) { // Don't bother displaying commands to users who cannot invoke them if (m_serviceHelper.SecureRemoteInteractions && !commandAttribute.AllowedRoles.Any(role => requestInfo.Sender.ClientUser.IsInRole(role))) continue; bool firstParameter = true; methodList.Append(" "); methodList.Append(method.Name); methodList.Append('('); // Enumerate each method parameter foreach (ParameterInfo parameter in method.GetParameters()) { if (!firstParameter) methodList.Append(", "); string typeName = parameter.ParameterType.ToString(); // Assume namespace for basic System types... if (typeName.StartsWith("System.", StringComparison.OrdinalIgnoreCase) && typeName.CharCount('.') == 1) typeName = typeName.Substring(7); methodList.Append(typeName); methodList.Append(' '); methodList.Append(parameter.Name); firstParameter = false; } methodList.Append(')'); methodList.AppendLine(); if (!string.IsNullOrWhiteSpace(commandAttribute.Description)) { methodList.Append(" "); methodList.Append(commandAttribute.Description); } methodList.AppendLine(); } } methodList.AppendLine(); SendResponse(requestInfo, true, methodList.ToString()); } catch (Exception ex) { SendResponse(requestInfo, false, "Failed to list commands: {0}", ex.Message); LogException(ex); } } } /// /// Performs initialization or reinitialization of specified adapter or collection. /// /// instance containing the client request. protected virtual void InitializeRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Performs (re)initialization of specified adapter or collection."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" Initialize [ID] [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ID:".PadRight(20)); helpMessage.Append("ID of the adapter to initialize, or all adapters if not specified"); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -I".PadRight(20)); helpMessage.Append("Initialize input adapters (default)"); helpMessage.AppendLine(); helpMessage.Append(" -A".PadRight(20)); helpMessage.Append("Initialize action adapters"); helpMessage.AppendLine(); helpMessage.Append(" -O".PadRight(20)); helpMessage.Append("Initialize output adapters"); helpMessage.AppendLine(); helpMessage.Append(" -SkipReloadConfig".PadRight(20)); helpMessage.Append("Skips configuration reload before initialize"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { bool requestedAdapterExists = RequestedAdapterExists(requestInfo); if (requestInfo.Request.Arguments.Exists("SkipReloadConfig")) { if (!requestedAdapterExists) { SendResponse(requestInfo, false, "Unable to initialize disabled or nonexistent adapter with ID."); return; } HandleInitializeRequest(requestInfo); // Spawn routing table calculation updates m_iaonSession.RecalculateRoutingTables(); } else { m_reloadConfigQueue.Add(Tuple.Create("System", new Action(success => { if (!success) { SendResponse(requestInfo, false, "Failed to load system configuration."); return; } if (requestedAdapterExists) HandleInitializeRequest(requestInfo); }))); } } } private void HandleInitializeRequest(ClientRequestInfo requestInfo) { IAdapterCollection collection; // See if specific ID for an adapter was requested if (requestInfo.Request.Arguments.Exists("OrderedArg1")) { string adapterID = requestInfo.Request.Arguments["OrderedArg1"]; // Try initializing new adapter by ID searching in any collection if all runtime ID's are unique IAdapter adapter; if (m_uniqueAdapterIDs && uint.TryParse(adapterID, out uint id) && m_iaonSession.AllAdapters.TryInitializeAdapterByID(id)) { if (m_iaonSession.AllAdapters.TryGetAnyAdapterByID(id, out adapter, out collection)) SendResponse(requestInfo, true, "Adapter \"{0}\" ({1}) was successfully initialized...", adapter.Name, adapter.ID); else SendResponse(requestInfo, true, "Adapter ({1}) was successfully initialized...", id); } else { adapter = GetRequestedAdapter(requestInfo, out collection); // Initialize specified adapter if (adapter is null || collection is null) { SendResponse(requestInfo, false, "Requested adapter was not found."); } else { if (collection.TryInitializeAdapterByID(adapter.ID)) SendResponse(requestInfo, true, "Adapter \"{0}\" ({1}) was successfully initialized...", adapter.Name, adapter.ID); else SendResponse(requestInfo, false, "Adapter \"{0}\" ({1}) failed to initialize.", adapter.Name, adapter.ID); } } } else { // Get specified adapter collection collection = GetRequestedCollection(requestInfo); if (collection is null) { SendResponse(requestInfo, false, "Requested collection was unavailable."); } else { DisplayStatusMessage("Initializing all adapters in {0}...", UpdateType.Information, collection.Name); collection.Initialize(); DisplayStatusMessage("{0} initialization complete.", UpdateType.Information, collection.Name); SendResponse(requestInfo, true); } } } private void ListReportsRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Returns a list of reports that are available from the report location."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" ListReports ReportType [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ReportType:".PadRight(20)); helpMessage.Append("Type of the report to process (e.g., Completeness)"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { IReportingProcess reportingProcess; if (requestInfo.Request.Arguments.Exists("OrderedArg1")) reportingProcess = m_reportingProcesses.FindReportType(requestInfo.Request.Arguments["OrderedArg1"]); else throw new ArgumentException("ReportType not specified."); if (reportingProcess is null) throw new ArgumentException($"ReportType \"{requestInfo.Request.Arguments["OrderedArg1"]}\" undefined."); StringBuilder listBuilder; List reportsList; List pendingReportsList; int fileColumnWidth; string reportPath; FileInfo info; TimeSpan expiration; reportingProcess.CleanReportLocation(); reportsList = reportingProcess.GetReportsList(); pendingReportsList = reportingProcess.GetPendingReportsList(); fileColumnWidth = Math.Max(6, reportsList.Concat(pendingReportsList).Select(report => report.Length).DefaultIfEmpty(0).Max()) / 4 * 4 + 4; listBuilder = new StringBuilder(); listBuilder.Append("Report".PadRight(fileColumnWidth)); listBuilder.AppendLine("Status"); listBuilder.Append("------".PadRight(fileColumnWidth)); listBuilder.AppendLine("------"); foreach (string report in reportsList) { reportPath = FilePath.GetAbsolutePath(Path.Combine(reportingProcess.ReportLocation, report)); listBuilder.Append(report.PadRight(fileColumnWidth)); info = new FileInfo(reportPath); expiration = TimeSpan.FromDays(reportingProcess.IdleReportLifetime) - (DateTime.UtcNow - info.LastAccessTimeUtc); if (expiration.TotalSeconds <= 0.0D) listBuilder.Append("Expired"); if (expiration.TotalSeconds < 60.0D) listBuilder.AppendFormat("Expires in {0:0} seconds", expiration.TotalSeconds); else if (expiration.TotalMinutes < 60.0D) listBuilder.AppendFormat("Expires in {0:0} minutes", expiration.TotalMinutes); else if (expiration.TotalHours < 24.0D) listBuilder.AppendFormat("Expires in {0:0} hours", expiration.TotalHours); else listBuilder.AppendFormat("Expires in {0:0} days", expiration.TotalDays); listBuilder.AppendLine(); } foreach (string report in pendingReportsList) { listBuilder.Append(report.PadRight(fileColumnWidth)); listBuilder.AppendLine("Pending"); } SendResponse(requestInfo, true, listBuilder.ToString()); } } private void GetReportRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest || requestInfo.Request.Arguments.OrderedArgCount == 0) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Gets a report for the specified date."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" GetReport ReportType [ReportDate] [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ReportType:".PadRight(20)); helpMessage.Append("Type of the report to process (e.g., Completeness)"); helpMessage.AppendLine(); helpMessage.Append(" ReportDate:".PadRight(20)); helpMessage.Append("Date of the report to be generated (yyyy-MM-dd), or yesterday if not specified"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { IReportingProcess reportingProcess; if (requestInfo.Request.Arguments.Exists("OrderedArg1")) reportingProcess = m_reportingProcesses.FindReportType(requestInfo.Request.Arguments["OrderedArg1"]); else throw new ArgumentException("ReportType not specified."); if (reportingProcess is null) throw new ArgumentException($"ReportType \"{requestInfo.Request.Arguments["OrderedArg1"]}\" undefined."); DateTime now = DateTime.UtcNow; DateTime today = DateTime.Parse(now.ToString("yyyy-MM-dd")); string reportPath; try { if (!requestInfo.Request.Arguments.Exists("OrderedArg2") || !DateTime.TryParse(requestInfo.Request.Arguments["OrderedArg2"], out DateTime reportDate)) reportDate = today - TimeSpan.FromDays(1); if (reportDate < today) { reportPath = FilePath.GetAbsolutePath(Path.Combine(reportingProcess.ReportLocation, $"{reportingProcess.Title} {reportDate:yyyy-MM-dd}.pdf")); if (File.Exists(reportPath)) { File.SetLastAccessTimeUtc(reportPath, DateTime.UtcNow); SendResponseWithAttachment(requestInfo, true, File.ReadAllBytes(reportPath), $"Found and returned report for {reportDate:yyyy-MM-dd}."); } else { SendResponse(requestInfo, false, $"Unable to find report for {reportDate:yyyy-MM-dd}."); } } else { SendResponse(requestInfo, false, $"[{now:yyyy-MM-dd HH:mm:ss}] Unable to generate report for {reportDate:yyyy-MM-dd} until {reportDate + TimeSpan.FromDays(1):yyyy-MM-dd}. The statistics archive is not fully populated for that date."); } } catch (Exception ex) { SendResponse(requestInfo, false, "Unable to generate report due to exception: {0}", ex.Message); } } } private void GetReportStatusRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest || requestInfo.Request.Arguments.OrderedArgCount == 0) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Gets a reporting process status."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" GetReportStatus ReportType [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ReportType:".PadRight(20)); helpMessage.Append("Type of the report to process (e.g., Completeness)"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { IReportingProcess reportingProcess; if (requestInfo.Request.Arguments.Exists("OrderedArg1")) reportingProcess = m_reportingProcesses.FindReportType(requestInfo.Request.Arguments["OrderedArg1"]); else throw new ArgumentException("ReportType not specified."); if (reportingProcess is null) throw new ArgumentException($"ReportType \"{requestInfo.Request.Arguments["OrderedArg1"]}\" undefined."); try { SendResponse(requestInfo, true, reportingProcess.Status); } catch (Exception ex) { SendResponse(requestInfo, false, "Unable to get report status due to exception: {0}", ex.Message); } } } private void GenerateReportRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest || requestInfo.Request.Arguments.OrderedArgCount == 0) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Generates a report for the specified date."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" GenerateReport ReportType [ReportDate] [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ReportType:".PadRight(20)); helpMessage.Append("Type of the report to process (e.g., Completeness)"); helpMessage.AppendLine(); helpMessage.Append(" ReportDate:".PadRight(20)); helpMessage.Append("Date of the report to be generated (yyyy-MM-dd), or yesterday if not specified"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -email".PadRight(20)); helpMessage.Append("E-mail generated report, if enabled"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { IReportingProcess reportingProcess; if (requestInfo.Request.Arguments.Exists("OrderedArg1")) reportingProcess = m_reportingProcesses.FindReportType(requestInfo.Request.Arguments["OrderedArg1"]); else throw new ArgumentException("ReportType not specified."); if (reportingProcess is null) throw new ArgumentException($"ReportType \"{requestInfo.Request.Arguments["OrderedArg1"]}\" undefined."); DateTime today = DateTime.UtcNow; bool emailReport = requestInfo.Request.Arguments.Exists("email"); try { if (!requestInfo.Request.Arguments.Exists("OrderedArg2") || !DateTime.TryParse(requestInfo.Request.Arguments["OrderedArg2"], out DateTime reportDate)) reportDate = today - TimeSpan.FromDays(1); if (reportDate < today) { reportingProcess.GenerateReport(reportDate, emailReport); SendResponse(requestInfo, true, "Report was successfully queued for generation."); } else { SendResponse(requestInfo, false, $"[{today:yyyy-MM-dd HH:mm:ss}] Unable to generate report for {reportDate:yyyy-MM-dd} until {reportDate + TimeSpan.FromDays(1):yyyy-MM-dd}. The statistics archive is not fully populated for that date."); } } catch (Exception ex) { SendResponse(requestInfo, false, "Unable to generate report due to exception: {0}", ex.Message); } } } private void ReportingConfigRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Displays or modifies the configuration of the reporting process."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" ReportingConfig ReportType [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" ReportType:".PadRight(20)); helpMessage.Append("Type of the report to process (e.g., Completeness)"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -get".PadRight(20)); helpMessage.Append("Gets the configuration of the reporting process as command-line arguments"); helpMessage.AppendLine(); helpMessage.Append(" -set Args".PadRight(20)); helpMessage.Append("Sets the configuration of the reporting process using the given command-line arguments"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { IReportingProcess reportingProcess; if (requestInfo.Request.Arguments.Exists("OrderedArg1")) reportingProcess = m_reportingProcesses.FindReportType(requestInfo.Request.Arguments["OrderedArg1"]); else throw new ArgumentException("ReportType not specified."); if (reportingProcess is null) throw new ArgumentException($"ReportType \"{requestInfo.Request.Arguments["OrderedArg1"]}\" undefined."); if (requestInfo.Request.Arguments.Exists("get") || !requestInfo.Request.Arguments.Exists("set")) { try { string configuration = $"{reportingProcess.GetArguments()} --idleReportLifetime=\" {reportingProcess.IdleReportLifetime} \""; SendResponse(requestInfo, true, configuration); } catch (Exception ex) { SendResponse(requestInfo, false, "Unable to send reporting configuration due to exception: {0}", ex.Message); } } else { try { lock (reportingProcess) { reportingProcess.SetArguments(requestInfo.Request.Arguments); reportingProcess.SaveSettings(); SendResponse(requestInfo, true, "Reporting configuration saved successfully."); } } catch (Exception ex) { SendResponse(requestInfo, false, "Unable to save settings reporting configuration due to exception: {0}", ex.Message); } } } } /// /// Sends remote entry for logging. /// /// protected virtual void LogEventRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Logs remote entry to event log."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" LogEvent [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -Message=\"Event Message\"".PadRight(20)); helpMessage.Append("Specifies message for event log entry (required)"); helpMessage.AppendLine(); helpMessage.Append(" -Type=[Error|Warning|Information|...]".PadRight(20)); helpMessage.Append("Specifies EventLogEntryType setting (optional)"); helpMessage.AppendLine(); helpMessage.Append(" -ID=0".PadRight(20)); helpMessage.Append("Specifies application event log ID (optional)"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { if (requestInfo.Request.Arguments.Exists("Message")) { try { string message = requestInfo.Request.Arguments["Message"]; if (!(requestInfo.Request.Arguments.TryGetValue("Type", out string type) && Enum.TryParse(type, out EventLogEntryType entryType))) entryType = EventLogEntryType.Information; if (!(requestInfo.Request.Arguments.TryGetValue("ID", out string id) && ushort.TryParse(id, out ushort eventID))) eventID = 0; EventLog.WriteEntry(ServiceName, message, entryType, eventID); SendResponse(requestInfo, true, "Successfully wrote event log entry."); } catch (Exception ex) { SendResponse(requestInfo, false, "Failed to write event log entry: {0}", ex.Message); } } else { SendResponse(requestInfo, false, "Failed to write event log entry: required \"message\" parameter was not specified."); } } } /// /// Gets adapter input measurements. /// /// protected virtual void GetInputMeasurementsRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Gets adapter input measurements."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" GetInputMeasurements AdapterName [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -actionable".PadRight(20)); helpMessage.Append("Returns results via an actionable event"); helpMessage.AppendLine(); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { if (requestInfo.Request.Arguments.Exists("OrderedArg1")) { IAdapter adapter = GetRequestedAdapter(requestInfo); IEnumerable signalIDs = adapter.InputMeasurementKeys?.Select(key => key.SignalID) ?? Enumerable.Empty(); if (requestInfo.Request.Arguments.Exists("actionable")) m_serviceHelper.SendActionableResponse(requestInfo, true, signalIDs.Select(id => id.ToByteArray()).ToArray()); else DisplayResponseMessage(requestInfo, string.Join(";", signalIDs.Select(id => id.ToString()))); } else { SendResponse(requestInfo, false, "No target adapter was specified."); } } } /// /// Gets adapter output measurements. /// /// protected virtual void GetOutputMeasurementsRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Gets adapter output measurements."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" GetOutputMeasurements AdapterName [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -actionable".PadRight(20)); helpMessage.Append("Returns results via an actionable event"); helpMessage.AppendLine(); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { if (requestInfo.Request.Arguments.Exists("OrderedArg1")) { IAdapter adapter = GetRequestedAdapter(requestInfo); IEnumerable signalIDs = adapter.OutputMeasurements?.Select(m => m.Key.SignalID) ?? Enumerable.Empty(); if (requestInfo.Request.Arguments.Exists("actionable")) m_serviceHelper.SendActionableResponse(requestInfo, true, signalIDs.Select(id => id.ToByteArray()).ToArray()); else DisplayResponseMessage(requestInfo, string.Join(";", signalIDs.Select(id => id.ToString()))); } else { SendResponse(requestInfo, false, "No target adapter was specified."); } } } /// /// Determines support for temporal processing from existing adapters. /// /// instance containing the client request. protected virtual void TemporalSupportRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Checks support for temporal processing."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" TemporalSupport [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -I".PadRight(20)); helpMessage.Append("Checks support for input adapters (default)"); helpMessage.AppendLine(); helpMessage.Append(" -A".PadRight(20)); helpMessage.Append("Checks support for action adapters"); helpMessage.AppendLine(); helpMessage.Append(" -O".PadRight(20)); helpMessage.Append("Checks support for output adapters"); helpMessage.AppendLine(); helpMessage.Append(" -System".PadRight(20)); helpMessage.Append("Checks support for any adapter"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { if (requestInfo.Request.Arguments.Exists("System")) { bool supported = m_iaonSession.TemporalProcessingSupportExists(); SendResponseWithAttachment(requestInfo, true, supported, "Temporal processing support {0}", supported ? "exists" : "does not exist"); } else { string collectionName = GetRequestedCollection(requestInfo).DataMember; bool supported = m_iaonSession.TemporalProcessingSupportExists(collectionName); SendResponseWithAttachment(requestInfo, true, supported, "Temporal processing support {0} for {1}", supported ? "exists" : "does not exist", collectionName); } } } /// /// Recalculates routing tables. /// /// instance containing the client request. protected virtual void RefreshRoutesRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Recalculates routing tables."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" RefreshRoutes [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { // Spawn routing table calculation updates m_iaonSession.RecalculateRoutingTables(); SendResponse(requestInfo, true, "Spawned request to refresh routing tables."); } } /// /// Manually reloads system configuration. /// /// instance containing the client request. protected virtual void ReloadConfigRequestHandler(ClientRequestInfo requestInfo) { Arguments arguments = requestInfo.Request.Arguments; ClientRequestInfo invokeInfo = null; ClientRequestHandler invokeHandler = null; string resource; bool invoking; if (arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Manually reloads system configuration."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" ReloadConfig [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -Invoke=\"Command [Args]\"".PadRight(20)); helpMessage.AppendFormat("Invokes another command once the reload config is complete"); helpMessage.AppendLine(); helpMessage.Append(" -Augmented".PadRight(20)); helpMessage.AppendFormat("Augments the current running configuration from the {0}, if supported", m_configurationType); helpMessage.AppendLine(); helpMessage.Append($" -{m_configurationType}".PadRight(20)); helpMessage.AppendFormat("Loads configuration from the {0}", m_configurationType); helpMessage.AppendLine(); helpMessage.Append(" -BinaryCache".PadRight(20)); helpMessage.Append("Loads configuration from the latest cached binary file"); helpMessage.AppendLine(); helpMessage.Append(" -XmlCache".PadRight(20)); helpMessage.Append("Loads configuration from the latest cached XML file"); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { invoking = arguments.Exists("Invoke"); if (invoking) { invokeInfo = new ClientRequestInfo(requestInfo.Sender, ClientRequest.Parse(arguments["Invoke"])); resource = invokeInfo.Request.Command; // Check if remote client has permission to invoke the requested command. if (m_serviceHelper.SecureRemoteInteractions) { // Validate current client principal if (SecurityProviderUtility.IsResourceSecurable(resource) && !SecurityProviderUtility.IsResourceAccessible(resource, requestInfo.Sender.ClientUser)) throw new SecurityException($"Access to '{resource}' is denied"); } invokeHandler = m_serviceHelper.FindClientRequestHandler(resource); } if (arguments.Count == (invoking ? 2 : 1)) { if (!arguments.Exists(m_configurationType.ToString()) && !arguments.Exists("Augmented") && !arguments.Exists("BinaryCache") && !arguments.Exists("XmlCache")) { SendResponse(requestInfo, false, "Invalid argument supplied to ReloadConfig command."); return; } } else if (arguments.Count > (invoking ? 2 : 1)) { SendResponse(requestInfo, false, "Invalid arguments supplied to ReloadConfig command."); return; } m_reloadConfigQueue.Add(Tuple.Create(GetReloadConfigType(requestInfo), new Action(success => { if (success) { if (!invoking || invokeHandler is null) SendResponse(requestInfo, true, "System configuration was successfully reloaded."); else invokeHandler.HandlerMethod(invokeInfo); } else { SendResponse(requestInfo, false, "System configuration failed to reload."); } }))); } } private string GetReloadConfigType(ClientRequestInfo requestInfo) { Arguments arguments = requestInfo.Request.Arguments; if (arguments.Exists("Augmented")) return "Augmented"; if (arguments.Exists(m_configurationType.ToString())) return m_configurationType.ToString(); if (arguments.Exists("BinaryCache")) return "BinaryCache"; if (arguments.Exists("XmlCache")) return "XmlCache"; return "System"; } /// /// Updates an option in the configuration file. /// /// instance containing the client request. protected virtual void UpdateConfigFileRequestHandler(ClientRequestInfo requestInfo) { int orderedArgCount = requestInfo.Request.Arguments.OrderedArgCount; bool listSetting = requestInfo.Request.Arguments.Exists("list"); bool deleteSetting = requestInfo.Request.Arguments.Exists("delete"); bool addSetting = requestInfo.Request.Arguments.Exists("add"); if (requestInfo.Request.Arguments.ContainsHelpRequest || (!listSetting && orderedArgCount < 2) || (!listSetting && !deleteSetting && orderedArgCount < 3)) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Updates an option in the configuration file."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" UpdateConfigFile [Category Name] -list"); helpMessage.AppendLine(); helpMessage.Append(" UpdateConfigFile \"Category Name\" \"Setting Name\" -delete"); helpMessage.AppendLine(); helpMessage.Append(" UpdateConfigFile \"Category Name\" \"Setting Name\" \"Setting Value\" [-add]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -add".PadRight(20)); helpMessage.Append("Adds specified setting to the specified category"); helpMessage.AppendLine(); helpMessage.Append(" -delete".PadRight(20)); helpMessage.Append("Deletes specified setting from the specified category"); helpMessage.AppendLine(); helpMessage.Append(" -list".PadRight(20)); helpMessage.Append("Lists categories or settings under a specified category"); helpMessage.AppendLine(); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { string categoryName = requestInfo.Request.Arguments["OrderedArg1"]; string settingName = requestInfo.Request.Arguments["OrderedArg2"]; string settingValue = requestInfo.Request.Arguments["OrderedArg3"]; ConfigurationFile config = ConfigurationFile.Current; if (listSetting) { if (orderedArgCount == 0) { StringBuilder categoryList = new StringBuilder(); categoryList.Append("List of categories in the configuration file:"); categoryList.AppendLine(); categoryList.AppendLine(); string xml = config.Settings.SectionInformation.GetRawXml(); XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(xml); // List settings categories. if (!(xmlDoc.DocumentElement is null)) { foreach (XmlNode node in xmlDoc.DocumentElement) { categoryList.Append(" "); categoryList.Append(node.Name); categoryList.AppendLine(); } } categoryList.AppendLine(); DisplayResponseMessage(requestInfo, categoryList.ToString()); } else { CategorizedSettingsElementCollection settings = config.Settings[categoryName]; StringBuilder settingsList = new StringBuilder(); settingsList.Append($"List of settings under the category {categoryName}:"); settingsList.AppendLine(); settingsList.AppendLine(); // List settings under specified category. foreach (CategorizedSettingsElement settingsElement in settings) { // Skip encrypted settings for security purpose. if (settingsElement.Encrypted) continue; settingsList.Append(" Name: "); settingsList.Append(settingsElement.Name); settingsList.AppendLine(); settingsList.Append(" Value: "); settingsList.Append(settingsElement.Value); settingsList.AppendLine(); settingsList.Append(" Description: "); settingsList.Append(settingsElement.Description); settingsList.AppendLine(); settingsList.AppendLine(); } settingsList.Replace("{", "{{{{"); settingsList.Replace("}", "}}}}"); DisplayResponseMessage(requestInfo, settingsList.ToString()); } } else { CategorizedSettingsElementCollection settings = config.Settings[categoryName]; CategorizedSettingsElement setting = settings[settingName]; if (deleteSetting) { // Delete existing setting. if (setting is null) { SendResponse(requestInfo, false, "Failed to delete setting \"{0}\" under category \"{1}\". Setting does not exist.\r\n\r\n", settingName, categoryName); } else { settings.Remove(setting); config.Save(); SendResponse(requestInfo, true, "Successfully deleted setting \"{0}\" under category \"{1}\".\r\n\r\n", settingName, categoryName); } } else if (addSetting) { // Add new setting. if (setting is null) { settings.Add(settingName, settingValue); config.Save(); SendResponse(requestInfo, true, "Successfully added setting \"{0}\" under category \"{1}\".\r\n\r\n", settingName, categoryName); } else { SendResponse(requestInfo, false, "Failed to add setting \"{0}\" under category \"{1}\". Setting already exists.\r\n\r\n", settingName, categoryName); } } else { // Update existing setting. if (setting is null) { SendResponse(requestInfo, false, "Failed to update value of setting \"{0}\" under category \"{1}\" . Setting does not exist.\r\n\r\n", settingName, categoryName); } else { setting.Value = settingValue; config.Save(); SendResponse(requestInfo, true, "Successfully updated setting \"{0}\" under category \"{1}\".\r\n\r\n", settingName, categoryName); } } } } } /// /// Manages the certificate used by the service. /// /// instance containing the client request. protected virtual void ManageCertificateHandler(ClientRequestInfo requestInfo) { bool import = requestInfo.Request.Arguments.Exists("import"); bool export = requestInfo.Request.Arguments.Exists("export"); string certificateFile = requestInfo.Request.Arguments["OrderedArg1"]; SecureString password = requestInfo.Request.Arguments["OrderedArg2"].ToSecureString(); if (requestInfo.Request.Arguments.ContainsHelpRequest || !(import ^ export) || string.IsNullOrEmpty(certificateFile) || (password?.Length ?? 0) == 0) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Updates an option in the configuration file."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" ManageCertificate { -import | -export } \"Certificate File\" \"Password\""); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" -import".PadRight(20)); helpMessage.Append("Imports the given certificate into the current user store"); helpMessage.AppendLine(); helpMessage.Append(" -export".PadRight(20)); helpMessage.Append("Exports the current certificate from the user store"); helpMessage.AppendLine(); helpMessage.AppendLine(); DisplayResponseMessage(requestInfo, "{0}", helpMessage); } else { ConfigurationFile configurationFile = ConfigurationFile.Current; CategorizedSettingsElementCollection remotingServer = configurationFile.Settings["remotingServer"]; remotingServer.Add("CertificateFile", $"{ServiceName}.cer", "Path to the local certificate used by this server for authentication."); string certificatePath = FilePath.GetAbsolutePath(remotingServer["CertificateFile"].Value); CertificateGenerator certificateGenerator = new CertificateGenerator() { Issuer = ServiceName, CertificatePath = certificatePath }; if (import) certificateGenerator.ImportCertificateWithPrivateKey(certificateFile, password); if (export) certificateGenerator.ExportCertificateWithPrivateKey(certificateFile, password); SendResponse(requestInfo, true); } } /// /// Attempts to authenticate or re-authenticate to network shares. /// /// instance containing the client request. protected virtual void AuthenticateRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Attempts to (re)authenticate to network shares."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" Authenticate [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { DisplayStatusMessage("Attempting to re-authenticate network shares for health and status exports...", UpdateType.Information); try { m_healthExporter.Initialize(); m_statusExporter.Initialize(); SendResponse(requestInfo, true); } catch (Exception ex) { SendResponse(requestInfo, false, "Failed to re-authenticate network shares: {0}", ex.Message); LogException(ex); } } } /// /// Attempts to restart the hose service. /// /// instance containing the client request. protected virtual void RestartServiceHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Attempts to restart the host service."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" Restart [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); DisplayResponseMessage(requestInfo, helpMessage.ToString()); } else { if (m_allowRemoteRestart) { DisplayStatusMessage("Attempting to restart host service...", UpdateType.Information); try { if (GSF.Common.IsPosixEnvironment) { using (Process shell = new Process()) { shell.StartInfo = new ProcessStartInfo(ConsoleApplicationName) { CreateNoWindow = true, Arguments = $"{ServiceName} -restart" }; shell.Start(); } } else { string batchFileName = FilePath.GetAbsolutePath("RestartService.cmd"); File.WriteAllText(batchFileName, $@"@ECHO OFF{Environment.NewLine}{ConsoleApplicationName} {ServiceName} -restart"); // This is the only .NET call that will spawn an independent, non-child, process on Windows Process.Start(batchFileName); } SendResponse(requestInfo, true); } catch (Exception ex) { SendResponse(requestInfo, false, "Failed to restart host service: {0}", ex.Message); LogException(ex); } } else { DisplayStatusMessage("Remote restart request denied, this is currently disallowed in the system configuration.", UpdateType.Warning); } } } /// /// Determines whether the requested adapter exists. /// /// instance containing the client request. /// True if the requested adapter exists; false otherwise. private bool RequestedAdapterExists(ClientRequestInfo requestInfo) { string adapterID = requestInfo.Request.Arguments["OrderedArg1"]; IAdapterCollection collection = GetRequestedCollection(requestInfo); if (!string.IsNullOrWhiteSpace(adapterID)) { adapterID = adapterID.Trim(); if (adapterID.IsAllNumbers() && uint.TryParse(adapterID, out uint id)) { // Adapter ID is numeric, try numeric lookup by adapter ID in requested collection if (collection.TryGetAdapterByID(id, out _)) return true; // Try looking for ID in any collection if all runtime ID's are unique if (m_uniqueAdapterIDs && m_iaonSession.AllAdapters.TryGetAnyAdapterByID(id, out _, out _)) return true; } else { // Adapter ID is alpha-numeric, try text-based lookup by adapter name in requested collection if (collection.TryGetAdapterByName(adapterID, out _)) return true; // Try looking for adapter name in any collection if (m_iaonSession.AllAdapters.TryGetAnyAdapterByName(adapterID, out _, out _)) return true; } } return false; } /// /// Used to ping a device. /// /// instance containing the client request. /// True if the requested adapter exists; false otherwise. protected virtual void PingRequestHandler(ClientRequestInfo requestInfo) { if (requestInfo.Request.Arguments.ContainsHelpRequest) { StringBuilder helpMessage = new StringBuilder(); helpMessage.Append("Attempts to ping a device."); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Usage:"); helpMessage.AppendLine(); helpMessage.Append(" Ping [Options]"); helpMessage.AppendLine(); helpMessage.AppendLine(); helpMessage.Append(" Options:"); helpMessage.AppendLine(); helpMessage.Append(" -?".PadRight(20)); helpMessage.Append("Displays this help message"); helpMessage.AppendLine(); helpMessage.Append(" [ID] ".PadRight(20)); helpMessage.Append("Attempts to ping the specified device"); SendResponse(requestInfo, true, helpMessage.ToString()); } else { IAdapter adapter = GetRequestedAdapter(requestInfo); if (adapter.Settings.TryGetValue("server", out string server)) { string hostName = server.Split(':')[0]; SendResponse(requestInfo, true, $"Attempting to ping {adapter.Name} @ {hostName}..."); try { ProcessStartInfo psi = new ProcessStartInfo(ConsoleApplicationName) { CreateNoWindow = true, FileName = "ping", Arguments = hostName, RedirectStandardOutput = true, RedirectStandardError = true }; Task.Run(() => { using (Process shell = new Process()) { shell.StartInfo = psi; shell.OutputDataReceived += (sender, processArgs) => { if (string.IsNullOrWhiteSpace(processArgs.Data)) return; SendResponse(requestInfo, true, processArgs.Data); }; shell.ErrorDataReceived += (sender, processArgs) => { if (string.IsNullOrWhiteSpace(processArgs.Data)) return; SendResponse(requestInfo, false, processArgs.Data); }; shell.Start(); shell.BeginOutputReadLine(); shell.BeginErrorReadLine(); if (!shell.WaitForExit(30000)) shell.Kill(); } SendResponse(requestInfo, true); }); } catch (Exception ex) { SendResponse(requestInfo, false, $"Failed to ping {adapter.Name} @ {hostName}: {ex.Message}"); LogException(ex); } } else { SendResponse(requestInfo, false, "Unable to ping this adapter because it does not have a host name or IP address defined by the 'server' connection string parameter."); } } } #endregion #region [ Broadcast Message Handling ] /// /// Sends an actionable response to client. /// /// instance containing the client request. /// Flag that determines if this response to client request was a success. protected virtual void SendResponse(ClientRequestInfo requestInfo, bool success) { SendResponseWithAttachment(requestInfo, success, null, null); } /// /// Sends an actionable response to client with a formatted message. /// /// instance containing the client request. /// Flag that determines if this response to client request was a success. /// Formatted status message to send with response. /// Arguments of the formatted status message. protected virtual void SendResponse(ClientRequestInfo requestInfo, bool success, string status, params object[] args) { SendResponseWithAttachment(requestInfo, success, null, status, args); } /// /// Sends an actionable response to client with a formatted message and attachment. /// /// instance containing the client request. /// Flag that determines if this response to client request was a success. /// Attachment to send with response. /// Formatted status message to send with response. /// Arguments of the formatted status message. protected virtual void SendResponseWithAttachment(ClientRequestInfo requestInfo, bool success, object attachment, string status, params object[] args) { try { // Send actionable response m_serviceHelper.SendActionableResponse(requestInfo, success, attachment, status, args); // Log details of client request as well as response if (m_serviceHelper.LogStatusUpdates && m_serviceHelper.StatusLog.IsOpen) { string responseType = $"{requestInfo.Request.Command}:{(success ? "Success" : "Failure")}"; string arguments = requestInfo.Request.Arguments.ToString(); string message = $"{responseType}{(string.IsNullOrWhiteSpace(arguments) ? "" : $"({arguments})")}"; if (!(status is null)) message = $"{message} - {(args.Length == 0 ? status : string.Format(status, args))}"; m_serviceHelper.StatusLog.WriteTimestampedLine(message); } } catch (Exception ex) { LogException(ex); m_serviceHelper.UpdateStatus(requestInfo.Sender.ClientID, UpdateType.Alarm, $"Failed to send client response due to an exception: {ex.Message}\r\n\r\n"); } } /// /// Displays a response message to client requester. /// /// instance containing the client request. /// Formatted status message to send to client. /// Arguments of the formatted status message. protected virtual void DisplayResponseMessage(ClientRequestInfo requestInfo, string status, params object[] args) { try { m_serviceHelper.UpdateStatus(requestInfo.Sender.ClientID, UpdateType.Information, $"{status}\r\n\r\n", args); } catch (Exception ex) { LogException(ex); m_serviceHelper.UpdateStatus(requestInfo.Sender.ClientID, UpdateType.Alarm, $"Failed to update client status \"{status.ToNonNullString()}\" due to an exception: {ex.Message}\r\n\r\n"); } } /// /// Displays a broadcast message to all subscribed clients. /// /// Status message to send to all clients. /// of message to send. protected virtual void DisplayStatusMessage(string status, UpdateType type) { DisplayStatusMessage(status, type, true); } /// /// Displays a broadcast message to all subscribed clients. /// /// Status message to send to all clients. /// of message to send. /// Determines if messages should be sent logging engine. protected virtual void DisplayStatusMessage(string status, UpdateType type, bool publishToLog) { try { status = status.Replace("{", "{{").Replace("}", "}}"); m_serviceHelper.UpdateStatus(type, publishToLog, $"{status}\r\n\r\n"); } catch (Exception ex) { LogException(ex); m_serviceHelper.UpdateStatus(UpdateType.Alarm, $"Failed to update client status \"{status.ToNonNullString()}\" due to an exception: {ex.Message}\r\n\r\n"); } } /// /// Displays a broadcast message to all subscribed clients. /// /// Formatted status message to send to all clients. /// of message to send. /// Arguments of the formatted status message. protected virtual void DisplayStatusMessage(string status, UpdateType type, params object[] args) { DisplayStatusMessage(status, type, true, args); } /// /// Displays a broadcast message to all subscribed clients. /// /// Formatted status message to send to all clients. /// of message to send. /// Determines if messages should be sent logging engine. /// Arguments of the formatted status message. protected virtual void DisplayStatusMessage(string status, UpdateType type, bool publishToLog, params object[] args) { try { DisplayStatusMessage(string.Format(status, args), type, publishToLog); } catch (Exception ex) { LogException(ex); m_serviceHelper.UpdateStatus(UpdateType.Alarm, $"Failed to update client status \"{status.ToNonNullString()}\" due to an exception: {ex.Message}\r\n\r\n"); } } /// /// Logs an exception to the service helper . /// /// to log. protected virtual void LogException(Exception ex) { if (m_serviceHelper is null) { Logger.CreatePublisher(typeof(ServiceHostBase), MessageClass.Framework). Publish(MessageLevel.Critical, "Service Initialization", "LogException", exception: ex); } else { // Connection exceptions are logged separately since these types of exceptions // can be so frequent when a device is offline it makes looking for specific, // non-connection related, exceptions more difficult. if (ex is ConnectionException connectionException) m_serviceHelper.LogConnectionException(connectionException); else m_serviceHelper.LogException(ex); } } // Processes exceptions coming from the configuration loaders. private void ConfigurationLoader_ProcessException(object sender, EventArgs args) { DisplayStatusMessage(args.Argument.Message, UpdateType.Warning); LogException(args.Argument); } #endregion #endregion } }