//****************************************************************************************************** // ReportingProcessBase.cs - Gbtc // // Copyright © 2015, 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://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: // ---------------------------------------------------------------------------------------------------- // 06/08/2015 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Configuration; using System.Diagnostics; using System.IO; using System.Linq; using System.Management; using System.Security; using System.Security.Principal; using System.Text; using System.Text.RegularExpressions; using GSF.Configuration; using GSF.Console; using GSF.IO; using GSF.Security.Cryptography; using GSF.Threading; namespace GSF.TimeSeries.Reports { /// /// Represents the base functionality for reporting processes. /// public abstract class ReportingProcessBase : IReportingProcess { #region [ Members ] // Fields private readonly string m_reportType; private readonly ConcurrentQueue> m_reportGenerationQueue; private readonly LongSynchronizedOperation m_executeOperation; private bool m_persistSettings; private string m_settingsCategory; private string m_archiveFilePath; private string m_reportLocation; private string m_title; private string m_company; private double m_idleReportLifetime; private bool m_enableReportEmail; private string m_smtpServer; private string m_fromAddress; private string m_toAddresses; private string m_smtpUsername; private SecureString m_smtpPassword; private long m_generatedReports; private DateTime m_lastReportGenerationTime; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// /// Report type - passed into StatHistorianReportGenerator. protected ReportingProcessBase(string reportType) { m_reportType = reportType; m_reportGenerationQueue = new ConcurrentQueue>(); m_executeOperation = new LongSynchronizedOperation(Execute) { IsBackground = true }; m_persistSettings = true; m_settingsCategory = string.Format("{0}Reporting", m_reportType); m_archiveFilePath = "Eval(statArchiveFile.FileName)"; m_reportLocation = "Reports"; m_title = string.Format("Eval(securityProvider.ApplicationName) {0} Report", m_reportType); m_company = "Eval(systemSettings.CompanyName)"; m_idleReportLifetime = 14.0D; m_enableReportEmail = false; m_smtpServer = "localhost"; m_fromAddress = "reports@gridprotectionalliance.org"; m_toAddresses = "wile.e.coyote@acme.com"; } #endregion #region [ Properties ] /// /// Determines whether the object settings are to be persisted to the config file. /// public bool PersistSettings { get { return m_persistSettings; } set { m_persistSettings = value; } } /// /// Gets report type, i.e., basically the report name associated with this reporting process. /// /// /// This value is passed to StatHistorianReportGenerator as "reportType" parameter. /// public string ReportType { get { return m_reportType; } } string IProvideStatus.Name { get { return GetType().Name; } } /// /// Gets or sets the category name under which the object settings are persisted in the config file. /// public string SettingsCategory { get { return m_settingsCategory; } set { m_settingsCategory = value; } } /// /// Gets or sets the path to the archive file to which /// the statistics required for reporting are archived. /// public string ArchiveFilePath { get { return m_archiveFilePath; } set { m_archiveFilePath = value; } } /// /// Gets or sets the directory to which reports will be written. /// public string ReportLocation { get { return m_reportLocation; } set { m_reportLocation = value; } } /// /// Gets or sets the title to be displayed on reports. /// public string Title { get { return m_title; } set { m_title = value; } } /// /// Gets or sets the name of the company to be displayed on reports. /// public string Company { get { return m_company; } set { m_company = value; } } /// /// Gets or sets the minimum lifetime of a report /// since the last time it was accessed, in days. /// public double IdleReportLifetime { get { return m_idleReportLifetime; } set { m_idleReportLifetime = value; } } /// /// Gets or sets flag to enable e-mailing of reports. /// public bool EnableReportEmail { get { return m_enableReportEmail; } set { m_enableReportEmail = value; } } /// /// Gets or sets SMTP server to use when e-mailing reports. /// public string SmtpServer { get { return m_smtpServer; } set { m_smtpServer = value; } } /// /// Gets or sets the "from" address to use when e-mailing reports. /// public string FromAddress { get { return m_fromAddress; } set { m_fromAddress = value; } } /// /// Gets or sets the comma separated "to" addresses to use when e-mailing reports. /// public string ToAddresses { get { return m_toAddresses; } set { m_toAddresses = value; } } /// /// Gets or sets the username used to authenticate to the SMTP server. /// public string SmtpUsername { get { return m_smtpUsername; } set { m_smtpUsername = value; } } /// /// Gets or sets the password used to authenticate to the SMTP server. /// public string SmtpPassword { get { return m_smtpPassword.ToUnsecureString(); } set { m_smtpPassword = value.ToSecureString(); } } /// /// Gets or sets the password used to authenticate to the SMTP server as a secure string. /// public SecureString SmtpSecurePassword { get { return m_smtpPassword; } set { m_smtpPassword = value; } } /// /// Gets the current status details about reporting process. /// public virtual string Status { get { StringBuilder status = new StringBuilder(); status.AppendFormat(" Report type: {0}", ReportType ?? "undefined"); status.AppendLine(); status.AppendFormat(" Archive file path: {0}", FilePath.TrimFileName(ArchiveFilePath ?? "undefined", 51)); status.AppendLine(); status.AppendFormat(" Report location: {0}", FilePath.TrimFileName(ReportLocation ?? "undefined", 51)); status.AppendLine(); status.AppendFormat(" Report title: {0}", Title ?? "undefined"); status.AppendLine(); status.AppendFormat(" Idle report lifetime: {0:N2} days", IdleReportLifetime); status.AppendLine(); status.AppendFormat(" Queued reports: {0:N0}", m_reportGenerationQueue.Count); status.AppendLine(); status.AppendFormat(" Generated reports: {0:N0}", m_generatedReports); status.AppendLine(); status.AppendFormat(" Last report generation: {0:yyyy-MM-dd HH:mm:ss} UTC", m_lastReportGenerationTime); status.AppendLine(); status.AppendFormat(" Report e-mailing: {0}", EnableReportEmail ? "Enabled" : "Disabled"); status.AppendLine(); if (EnableReportEmail) { status.AppendFormat(" Defined SMTP server: {0}", SmtpServer ?? "undefined"); status.AppendLine(); status.AppendFormat(" From address: {0}", FromAddress ?? "undefined"); status.AppendLine(); status.AppendFormat(" To addresses: {0}", ToAddresses ?? "undefined"); status.AppendLine(); status.AppendFormat(" SMTP username: {0}", SmtpUsername ?? "undefined"); status.AppendLine(); status.AppendFormat(" SMTP password: {0}", new string('*', SmtpSecurePassword?.Length ?? 0)); status.AppendLine(); } return status.ToString(); } } #endregion #region [ Methods ] /// /// Returns the list of reports that are available from the report location. /// /// The list of generated reports. public List GetReportsList() { string reportLocation = FilePath.GetAbsolutePath(m_reportLocation) .EnsureEnd(Path.DirectorySeparatorChar); if (Directory.Exists(reportLocation)) { return FilePath.GetFileList(reportLocation) .Select(FilePath.GetFileName) .Where(filename => IsReportFileName(filename) && Path.GetExtension(filename).ToLower() == ".pdf") .ToList(); } return new List(); } /// /// Returns the list of reports which are in the queue but are yet to be generated. /// /// The list of pending reports. public List GetPendingReportsList() { return m_reportGenerationQueue.ToArray() .Select(reportDate => string.Format("{0} {1:yyyy-MM-dd}.pdf", m_title, reportDate)) .ToList(); } /// /// Queues up a report to be generated on a separate thread. /// /// The date of the report to be generated. /// Flag that determines if report should be e-mailed, if enabled. public void GenerateReport(DateTime reportDate, bool emailReport) { // Align reportDate with the top of the day before using it with the report generation queue reportDate = reportDate.Date; // ToArray is a thread-safe operation on ConcurrentQueue whereas using an enumerator // directly on a ConcurrentQueue can cause collection modified errors while iterating if (m_reportGenerationQueue.ToArray().Count(tuple => tuple.Item1 == reportDate) == 0) { m_reportGenerationQueue.Enqueue(new Tuple(reportDate, emailReport)); m_executeOperation.RunOnceAsync(); } } /// /// Deletes reports from the that have /// been idle for the length of the . /// public void CleanReportLocation() { FileInfo info; foreach (string report in GetReportsList()) { if (IsReportFileName(report)) { info = new FileInfo(FilePath.GetAbsolutePath(Path.Combine(m_reportLocation, report))); if ((DateTime.UtcNow - info.LastAccessTimeUtc).TotalDays > m_idleReportLifetime) File.Delete(info.FullName); } } } /// /// Loads saved settings from the config file. /// public virtual void LoadSettings() { if (!m_persistSettings) return; // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Load settings from the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings.Add("ArchiveFilePath", m_archiveFilePath, "Path to the archive file to which the statistics required for reporting are archived."); settings.Add("ReportLocation", m_reportLocation, "Directory to which reports will be written."); settings.Add("Title", m_title, "Title to be displayed on reports."); settings.Add("Company", m_company, "Name of the company to be displayed on reports."); settings.Add("IdleReportLifetime", m_idleReportLifetime, "The minimum lifetime of a report since the last time it was accessed, in days."); settings.Add("EnableReportEmail", m_enableReportEmail, "Set to true to enable daily e-mailing of reports."); settings.Add("SmtpServer", m_smtpServer, "The SMTP relay server from which to send e-mails."); settings.Add("FromAddress", m_fromAddress, "The from address for the report e-mails."); settings.Add("ToAddresses", m_toAddresses, "Comma separated list of destination addresses for the report e-mails."); settings.Add("SmtpUsername", m_smtpUsername, "Username to authenticate to the SMTP server."); settings.Add("SmtpPassword", m_smtpPassword, "Password to authenticate to the SMTP server."); ArchiveFilePath = settings["ArchiveFilePath"].ValueAs(m_archiveFilePath); ReportLocation = settings["ReportLocation"].ValueAs(m_reportLocation); Title = settings["Title"].ValueAs(m_title); Company = settings["Company"].ValueAs(m_company); IdleReportLifetime = settings["IdleReportLifetime"].ValueAs(m_idleReportLifetime); EnableReportEmail = settings["EnableReportEmail"].ValueAsBoolean(); SmtpServer = settings["SmtpServer"].ValueAs(m_smtpServer); FromAddress = settings["FromAddress"].ValueAs(m_fromAddress); ToAddresses = settings["ToAddresses"].ValueAs(m_toAddresses); SmtpUsername = settings["SmtpUsername"].ValueAs(m_smtpUsername); SmtpPassword = settings["SmtpPassword"].ValueAs(string.Empty); } /// /// Saves settings to the config file. /// public virtual void SaveSettings() { if (!m_persistSettings) return; // Ensure that settings category is specified. if (string.IsNullOrEmpty(m_settingsCategory)) throw new ConfigurationErrorsException("SettingsCategory property has not been set"); // Save settings under the specified category. ConfigurationFile config = ConfigurationFile.Current; CategorizedSettingsElementCollection settings = config.Settings[m_settingsCategory]; settings["ArchiveFilePath", true].Update(m_archiveFilePath); settings["ReportLocation", true].Update(m_reportLocation); settings["Title", true].Update(m_title); settings["Company", true].Update(m_company); settings["IdleReportLifetime", true].Update(m_idleReportLifetime); settings["EnableReportEmail", true].Update(m_enableReportEmail); settings["SmtpServer", true].Update(m_smtpServer); settings["FromAddress", true].Update(m_fromAddress); settings["ToAddresses", true].Update(m_toAddresses); settings["SmtpUsername", true].Update(m_smtpUsername); settings["SmtpPassword", true].Update(SmtpPassword); config.Save(); } /// /// Executes the reporting process to generate the report. /// private void Execute() { WindowsIdentity currentOwner = WindowsIdentity.GetCurrent(); Tuple tuple; if ((object)currentOwner != null) { // Wait for existing processes to exit, for instance if the // service was restarted while a report was being generated foreach (Process process in Process.GetProcessesByName("StatHistorianReportGenerator.exe")) { if (currentOwner.Name == GetProcessOwner(process.Id)) process.WaitForExit(); } } while (m_reportGenerationQueue.TryPeek(out tuple)) { // Execute the reporting process using (Process process = new Process()) { process.StartInfo.FileName = FilePath.GetAbsolutePath("StatHistorianReportGenerator.exe"); process.StartInfo.Arguments = GetArguments(tuple.Item1, tuple.Item2); process.Start(); process.WaitForExit(); process.Close(); } // Dequeue only after the report is generated so that it // remains in the list of pending reports during generation m_reportGenerationQueue.TryDequeue(out tuple); m_generatedReports++; m_lastReportGenerationTime = DateTime.UtcNow; } } /// /// Gets the command line arguments for the reporting process. /// public virtual string GetArguments() { // Because we may have a archive location like "C:\Program Files\MyPath" with quotes, // the arguments below have an extra leading and trailing space around quoted values // for proper parsing. return string.Format( "--reportType=\" {0} \" " + "--archiveLocation=\" {1} \" " + "--reportLocation=\" {2} \" " + "--title=\" {3} \" " + "--company=\" {4} \"", ReportType.Replace("\"", "\\\""), FilePath.GetDirectoryName(ArchiveFilePath).Replace("\"", "\\\""), ReportLocation.Replace("\"", "\\\""), Title.Replace("\"", "\\\""), Company.Replace("\"", "\\\"")); } /// /// Gets the command line arguments for the reporting process for a given report date. /// /// The date of the report to be generated. /// Flag that determines if report should be e-mailed, if enabled. public string GetArguments(DateTime reportDate, bool emailReport) { const string CryptoKey = "0679d9ae-aca5-4702-a3f5-604415096987"; string arguments = string.Format( "{0} " + "--reportDate=\" {1:yyyy-MM-dd} \"", GetArguments(), reportDate); if (emailReport && EnableReportEmail) arguments = string.Format( "{0} " + "--smtpServer=\" {1} \" " + "--fromAddress=\" {2} \" " + "--toAddresses=\" {3} \" " + "--username=\" {4} \" " + "--password=\" {5} \"", arguments, SmtpServer, FromAddress, ToAddresses, SmtpUsername, SmtpPassword.Encrypt(CryptoKey, CipherStrength.Aes256)); return arguments; } /// /// Applies any received command line arguments for the reporting process. /// /// Received command line arguments. public virtual void SetArguments(Arguments args) { double value; string arg = args["reportLocation"]; if ((object)arg != null) ReportLocation = arg.Trim(); arg = args["title"]; if ((object)arg != null) Title = arg.Trim(); arg = args["company"]; if ((object)arg != null) Company = arg.Trim(); arg = args["idleReportLifetime"]; if ((object)arg != null && double.TryParse(arg.Trim(), out value)) IdleReportLifetime = value; } /// /// Determines whether the given path is a path to a report, based on the file name. /// public virtual bool IsReportFileName(string fileName) { string regex = string.Format(@"{0} (?[^.]+)\.pdf", m_title); Match match = Regex.Match(fileName, regex); DateTime reportDate; return match.Success && DateTime.TryParse(match.Groups["Date"].Value, out reportDate); } /// /// Determines who is the owner of the given process. /// private string GetProcessOwner(int processId) { try { string query = "Select * From Win32_Process Where ProcessID = " + processId; ManagementObjectSearcher searcher = new ManagementObjectSearcher(query); ManagementObjectCollection processList = searcher.Get(); object[] argList; int returnVal; // ReSharper disable once PossibleInvalidCastExceptionInForeachLoop foreach (ManagementObject obj in processList) { argList = new object[] { string.Empty, string.Empty }; returnVal = Convert.ToInt32(obj.InvokeMethod("GetOwner", argList)); if (returnVal == 0) return argList[1] + "\\" + argList[0]; } return "NO OWNER"; } catch { return "NO OWNER"; } } #endregion } }