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