//****************************************************************************************************** // ProcessLauncher.cs - Gbtc // // Copyright © 2017, 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: // ---------------------------------------------------------------------------------------------------- // 11/29/2017 - J. Ritchie Carroll // Generated original version of source code. // //****************************************************************************************************** using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using GSF; using GSF.Diagnostics; using GSF.IO; using GSF.Threading; using GSF.TimeSeries; using GSF.TimeSeries.Adapters; using GSF.TimeSeries.Statistics; using GSF.Units; // Commands are passed via configuration #pragma warning disable SG0001 // ReSharper disable AssignNullToNotNullAttribute namespace FileAdapters { /// /// Represents an adapter that will launch a configured executable process. /// /// /// Unless credentials are provided to create an authentication context, rights of /// any launched executable will be limited to those available to time-series host. /// [Description("Process Launcher: Launches a configured executable process on initialization")] public class ProcessLauncher : FacileActionAdapterBase { #region [ Members ] // Constants /// /// Default value for the property. /// public const bool DefaultSupportsTemporalProcessing = false; /// /// Default value for the property. /// public const string DefaultArguments = ""; /// /// Default value for the property. /// public const string DefaultWorkingDirectory = ""; /// /// Default value for the property. /// public const string DefaultEnvironmentalVariables = ""; /// /// Default value for the property. /// public const bool DefaultCreateNoWindow = true; /// /// Default value for the property. /// public const string DefaultWindowStyle = "Normal"; /// /// Default value for the property. /// public const bool DefaultErrorDialog = false; /// /// Default value for the property. /// public const string DefaultDomain = ""; /// /// Default value for the property. /// public const string DefaultUserName = ""; /// /// Default value for the property. /// public const string DefaultPassword = ""; /// /// Default value for the property. /// public const bool DefaultLoadUserProfile = false; /// /// Default value for the property. /// public const string DefaultInitialInputFileName = ""; /// /// Default value for the property. /// public const int DefaultInitialInputProcessingDelay = 2000; /// /// Default value for the property. /// public const bool DefaultRedirectOutputToHostEnvironment = true; /// /// Default value for the property. /// public const bool DefaultRedirectErrorToHostEnvironment = true; /// /// Default value for the property. /// public const bool DefaultProcessOutputAsLogMessages = false; /// /// Default value for the property. /// public const string DefaultLogMessageTextExpression = @"(?<=.*msg\s*\=\s*\"")[^\""]*(?=\"")"; /// /// Default value for the property. /// public const string DefaultLogMessageLevelExpression = @"(?<=.*lvl\s*\=\s*)[^\s]*(?=\s|$)"; /// /// Default value for the property. /// public const string DefaultLogMessageLevelMappings = "info=Info; warn=Waning; error=Error; critical=Critical; debug=Debug"; /// /// Default value for the property. /// public const bool DefaultForceKillOnDispose = true; /// /// Default value for the property. /// public const bool DefaultTrackProcessStatistics = true; // Fields private readonly Process m_process; private readonly Dictionary m_messageLevelMap; private readonly ProcessUtilizationCalculator m_processUtilizationCalculator; private readonly ChildProcessManager m_childProcessManager; private Regex m_logMessageTextExpression; private Regex m_logMessageLevelExpression; private bool m_supportsTemporalProcessing; private long m_inputLinesProcessed; private long m_outputLinesProcessed; private long m_errorLinesProcessed; private bool m_disposed; #endregion #region [ Constructors ] /// /// Creates a new instance of the class. /// public ProcessLauncher() { m_process = new Process(); m_messageLevelMap = new Dictionary(StringComparer.OrdinalIgnoreCase); m_processUtilizationCalculator = new ProcessUtilizationCalculator(); // In Windows environments, make sure child processes can be terminated if parent is terminated - even if termination is not graceful if (!Common.IsPosixEnvironment) m_childProcessManager = new ChildProcessManager(); } #endregion #region [ Properties ] /// /// Gets the flag indicating if this adapter supports temporal processing. /// [ConnectionStringParameter, Description("Define the flag indicating if this adapter supports temporal processing."), DefaultValue(DefaultSupportsTemporalProcessing)] public override bool SupportsTemporalProcessing => m_supportsTemporalProcessing; /// /// Gets or sets the path and filename of executable to launch as a new process. /// [ConnectionStringParameter, Description("Define the path and filename of executable to launch as a new process.")] public string FileName { get; set; } /// /// Gets or sets any command line arguments to use when launching the process. /// [ConnectionStringParameter, Description("Define any command line arguments to use when launching the process."), DefaultValue(DefaultArguments)] public string Arguments { get; set; } = DefaultArguments; /// /// Gets or sets working directory to use when launching the process. /// [ConnectionStringParameter, Description("Define working directory to use when launching the process. Defaults to path of executable to launch."), DefaultValue(DefaultWorkingDirectory)] public string WorkingDirectory { get; set; } = DefaultWorkingDirectory; /// /// Gets or sets any needed environmental variables that should be set or updated before launching the process. /// [ConnectionStringParameter, Description("Define any needed environmental variables that should be set or updated before launching the process. Use connection string format to define variables, for example: variable1=value1; variable2=value2"), DefaultValue(DefaultEnvironmentalVariables)] public string EnvironmentalVariables { get; set; } = DefaultEnvironmentalVariables; /// /// Gets or sets flag that determines if a new window should be created when launching the process. /// [ConnectionStringParameter, Description("Define flag that determines if a new window should be created when launching the process. This property is only applicable when adapter hosting environment can interact with a user interface."), DefaultValue(DefaultCreateNoWindow)] public bool CreateNoWindow { get; set; } = DefaultCreateNoWindow; /// /// Gets or sets window style to use when launching the process if creating a window. /// [ConnectionStringParameter, Description("Define window style to use when launching the process if creating a window. This property is only applicable when adapter hosting environment can interact with a user interface."), DefaultValue(typeof(ProcessWindowStyle), DefaultWindowStyle)] public ProcessWindowStyle WindowStyle { get; set; } = (ProcessWindowStyle)Enum.Parse(typeof(ProcessWindowStyle), DefaultWindowStyle); /// /// Gets or sets flag that determines if an error dialog should be displayed if the process cannot be launched. /// [ConnectionStringParameter, Description("Define flag that determines if an error dialog should be displayed if the process cannot be launched. This property is only applicable when adapter hosting environment can interact with a user interface."), DefaultValue(DefaultErrorDialog)] public bool ErrorDialog { get; set; } = DefaultErrorDialog; /// /// Gets or sets the user domain to use when creating an authentication context before launching the process. /// [ConnectionStringParameter, Description("Define the user domain to use when creating an authentication context before launching the process. Leave blank to use authentication context of hosting environment."), DefaultValue(DefaultDomain)] public string Domain { get; set; } = DefaultDomain; /// /// Gets or sets the user name to use when creating an authentication context before launching the process. /// [ConnectionStringParameter, Description("Define the user name to use when creating an authentication context before launching the process. Leave blank to use authentication context of hosting environment."), DefaultValue(DefaultUserName)] public string UserName { get; set; } = DefaultUserName; /// /// Gets or sets the user password to use when creating an authentication context before launching the process. /// [ConnectionStringParameter, Description("Define the user password to use when creating an authentication context before launching the process. Leave blank to use authentication context of hosting environment."), DefaultValue(DefaultPassword)] public string Password { get; set; } = DefaultPassword; /// /// Gets or sets flag that determines if Windows user profile should be loaded from the registry before launching the process. /// [ConnectionStringParameter, Description("Define flag that determines if Windows user profile should be loaded from the registry before launching the process."), DefaultValue(DefaultLoadUserProfile)] public bool LoadUserProfile { get; set; } = DefaultLoadUserProfile; /// /// Gets or sets filename that contains text to use as standard input into process after it has been launched. /// [ConnectionStringParameter, Description("Define filename that contains text to use as input into process after it has been launched. UTF-8 encoding assumed."), DefaultValue(DefaultInitialInputFileName)] public string InitialInputFileName { get; set; } = DefaultInitialInputFileName; /// /// Gets or sets the processing delay, in milliseconds, before text defined in the is sent to the launched process. /// [ConnectionStringParameter, Description("Define the processing delay, in milliseconds, before text defined in the InitialInputFileName is sent to the launched process."), DefaultValue(DefaultInitialInputProcessingDelay)] public int InitialInputProcessingDelay { get; set; } = DefaultInitialInputProcessingDelay; /// /// Gets or sets flag that determines if process standard output should be redirected to the hosting environment. /// [ConnectionStringParameter, Description("Define flag that determines if process standard output should be redirected to the hosting environment."), DefaultValue(DefaultRedirectOutputToHostEnvironment)] public bool RedirectOutputToHostEnvironment { get; set; } = DefaultRedirectOutputToHostEnvironment; /// /// Gets or sets flag that determines if process standard error output should be redirected to the hosting environment. /// [ConnectionStringParameter, Description("Define flag that determines if process standard error output should be redirected to the hosting environment."), DefaultValue(DefaultRedirectErrorToHostEnvironment)] public bool RedirectErrorToHostEnvironment { get; set; } = DefaultRedirectErrorToHostEnvironment; /// /// Gets or sets flag that determines if redirected output should be attempted to be parsed as formatted log messages, e.g.: lvl=info msg="log message". /// [ConnectionStringParameter, Description("Define flag that determines if redirected output should be attempted to be parsed as formatted log messages, e.g.: lvl=info msg=\"log message\"."), DefaultValue(DefaultProcessOutputAsLogMessages)] public bool ProcessOutputAsLogMessages { get; set; } = DefaultProcessOutputAsLogMessages; /// /// Gets or sets regular expression used to find log message text when redirected output is processed as formatted log messages. /// [ConnectionStringParameter, Description("Define regular expression used to find log message text when redirected output is processed as formatted log messages. Multiple matches will be joined as a single string."), DefaultValue(DefaultLogMessageTextExpression)] public string LogMessageTextExpression { get; set; } = DefaultLogMessageTextExpression; /// /// Gets or sets regular expression used to find log message level when redirected output is processed as formatted log messages. /// [ConnectionStringParameter, Description("Define regular expression used to find log message level when redirected output is processed as formatted log messages."), DefaultValue(DefaultLogMessageLevelExpression)] public string LogMessageLevelExpression { get; set; } = DefaultLogMessageLevelExpression; /// /// Gets or sets log level mappings to use when redirected output is processed as formatted log messages. /// [ConnectionStringParameter, Description("Define log level mappings to use when redirected output is processed as formatted log messages. Use connection string format as \"log_level_name=GSF.Diagnostics.MessageLevel\", for example: info=Info; warn=Waning; error=Error; critical=Critical; debug=Debug"), DefaultValue(DefaultLogMessageLevelMappings)] public string LogMessageLevelMappings { get; set; } = DefaultLogMessageLevelMappings; /// /// Gets or sets flag that determines if launched process should be forcibly terminated when adapter is disposed. /// [ConnectionStringParameter, Description("Define flag that determines if launched process should be forcibly terminated when adapter is disposed."), DefaultValue(DefaultForceKillOnDispose)] public bool ForceKillOnDispose { get; set; } = DefaultForceKillOnDispose; /// /// Gets or sets the interval over which to calculate lunched process utilization. /// [ConnectionStringParameter, Description("Define the interval over which to update the lunched process CPU utilization. Set to 0 to disable utilization calculations."), DefaultValue(ProcessUtilizationCalculator.DefaultUpdateInterval)] public int UtilizationUpdateInterval { get; set; } = ProcessUtilizationCalculator.DefaultUpdateInterval; /// /// Gets or sets flag that determines if statistics should be tracked for launched process. /// [ConnectionStringParameter, Description("Define flag that determines if statistics should be tracked for launched process."), DefaultValue(DefaultTrackProcessStatistics)] public bool TrackProcessStatistics { get; set; } = DefaultTrackProcessStatistics; #region [ Hidden Properties ] // The following common adapter properties are marked as never browse by an // editor since the properties are not used by the ProcessLauncher adapter /// /// Property hidden - not used by . /// [EditorBrowsable(EditorBrowsableState.Never)] public new MeasurementKey[] InputMeasurementKeys { get { return base.InputMeasurementKeys; } set { base.InputMeasurementKeys = value; } } /// /// Property hidden - not used by . /// [EditorBrowsable(EditorBrowsableState.Never)] public new IMeasurement[] OutputMeasurements { get { return base.OutputMeasurements; } set { base.OutputMeasurements = value; } } /// /// Property hidden - not used by . /// [EditorBrowsable(EditorBrowsableState.Never)] public new int FramesPerSecond { get { return base.FramesPerSecond; } set { base.FramesPerSecond = value; } } /// /// Property hidden - not used by . /// [EditorBrowsable(EditorBrowsableState.Never)] public new double LagTime { get { return base.LagTime; } set { base.LagTime = value; } } /// /// Property hidden - not used by . /// [EditorBrowsable(EditorBrowsableState.Never)] public new double LeadTime { get { return base.LeadTime; } set { base.LeadTime = value; } } #endregion /// /// Returns the detailed status of the data input source. /// /// /// Derived classes should extend status with implementation specific information. /// public override string Status { get { StringBuilder status = new StringBuilder(); if (string.IsNullOrEmpty(WorkingDirectory)) WorkingDirectory = Directory.GetCurrentDirectory(); status.Append(base.Status); status.AppendLine($" Executable filename: {FilePath.TrimFileName(FileName, 51)}"); status.AppendLine($" Working directory: {FilePath.TrimFileName(WorkingDirectory, 51)}"); if (Initialized) { try { FileVersionInfo version = m_process.MainModule.FileVersionInfo; if (!string.IsNullOrWhiteSpace(version.FileVersion)) status.AppendLine($" Executable file version: {version.FileVersion}"); if (!string.IsNullOrWhiteSpace(version.FileDescription)) status.AppendLine($" Executable description: {version.FileDescription}"); if (!string.IsNullOrWhiteSpace(version.Comments)) status.AppendLine($" Executable comments: {version.Comments}"); if (!string.IsNullOrWhiteSpace(version.CompanyName)) status.AppendLine($" Executable company name: {version.CompanyName}"); if (!string.IsNullOrWhiteSpace(version.LegalCopyright)) status.AppendLine($" Executable copyright: {version.LegalCopyright}"); if (!string.IsNullOrWhiteSpace(version.Language)) status.AppendLine($" Executable language: {version.Language}"); if (!string.IsNullOrWhiteSpace(version.OriginalFilename)) status.AppendLine($" Executable original name: {version.OriginalFilename}"); if (!string.IsNullOrWhiteSpace(version.InternalName)) status.AppendLine($" Executable internal name: {version.InternalName}"); if (!string.IsNullOrWhiteSpace(version.ProductName)) status.AppendLine($" Executable product name: {version.ProductName}"); if (!string.IsNullOrWhiteSpace(version.ProductVersion)) status.AppendLine($"Executable product version: {version.ProductVersion}"); status.AppendLine($" Executable is debug build: {version.IsDebug}"); } catch { status.AppendLine(" Version information: [unavailable]"); } try { if (m_process.HasExited) { status.AppendLine(" Process has exited: True"); status.AppendLine($" Process exit code: {m_process.ExitCode}"); status.AppendLine($" Process exit time: {m_process.ExitTime:yyyy-MM-dd HH:mm:ss.fff}"); status.AppendLine($" Total processor time: {m_process.TotalProcessorTime.ToElapsedTimeString()}"); status.AppendLine($" Total run-time: {(m_process.ExitTime - ((DateTime)StartTime).ToLocalTime()).ToElapsedTimeString()}"); } else { status.AppendLine($" Process is responding: {m_process.Responding}"); status.AppendLine($" Process name: {m_process.ProcessName}"); status.AppendLine($" Process start time: {m_process.StartTime:yyyy-MM-dd HH:mm:ss.fff}"); status.AppendLine($" OS Process ID: {m_process.Id}"); status.AppendLine($" Process base priority: {m_process.BasePriority}"); status.AppendLine($" Process thread count: {m_process.Threads.Count:N0}"); status.AppendLine($" Process handle count: {m_process.HandleCount:N0}"); status.AppendLine($" Process memory usage: {SI2.ToScaledString(m_process.WorkingSet64, 2, "B")} (working set)"); if (m_processUtilizationCalculator.UpdateInterval > 0) status.AppendLine($" Process utilization: {m_processUtilizationCalculator.Utilization:##0.0%}"); status.AppendLine($" Total processor time: {m_process.TotalProcessorTime.ToElapsedTimeString()}"); status.AppendLine($" Total run-time: {(DateTime.Now - m_process.StartTime).ToElapsedTimeString()}"); } } catch { status.AppendLine(" Process state: [unavailable]"); } } status.AppendLine($" Initial input filename: {FilePath.TrimFileName(InitialInputFileName, 51)}"); status.AppendLine($" CPU usage update interval: {(m_processUtilizationCalculator.UpdateInterval > 0 ? $"{m_processUtilizationCalculator.UpdateInterval:N0}ms" : "Disabled")}"); status.AppendLine($" Input lines processed: {m_inputLinesProcessed:N0}"); status.AppendLine($" Output lines processed: {m_outputLinesProcessed:N0}"); status.AppendLine($" Error lines processed: {m_errorLinesProcessed:N0}"); status.AppendLine($" Interpret output for logs: {ProcessOutputAsLogMessages}"); status.AppendLine($" Forcing kill on dispose: {ForceKillOnDispose}"); status.AppendLine($" Tracking statistics: {TrackProcessStatistics}"); return status.ToString(); } } #endregion #region [ Methods ] /// /// 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) { if (ForceKillOnDispose) { try { m_process.Kill(); m_childProcessManager?.Dispose(); } catch (Exception ex) { OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Exception encountered while attempting to stop process launched from \"{FileName}\": {ex.Message}", ex)); } } m_process.Dispose(); m_process.OutputDataReceived -= ProcessOutputDataReceived; m_process.ErrorDataReceived -= ProcessErrorDataReceived; m_processUtilizationCalculator.Dispose(); } } finally { m_disposed = true; // Prevent duplicate dispose. base.Dispose(disposing); // Call base class Dispose(). } } /// /// Initializes . /// public override void Initialize() { base.Initialize(); Dictionary settings = Settings; ProcessWindowStyle windowStyle; int initialInputProcessingDelay, utilizationCalculationInterval; string setting; ProcessStartInfo startInfo = m_process.StartInfo; // Load required parameters if (settings.TryGetValue(nameof(FileName), out setting)) { setting = FilePath.GetAbsolutePath(setting.Trim()); if (File.Exists(setting)) { FileName = setting; startInfo.FileName = FileName; } else { throw new FileNotFoundException($"Cannot launch process: specified executable path and filename \"{setting}\" does not exist."); } } else { throw new ArgumentException($"Cannot launch process: required \"{nameof(FileName)}\" parameter is missing from connection string."); } // Load optional parameters if (settings.TryGetValue(nameof(SupportsTemporalProcessing), out setting)) m_supportsTemporalProcessing = setting.ParseBoolean(); else m_supportsTemporalProcessing = DefaultSupportsTemporalProcessing; if (settings.TryGetValue(nameof(Arguments), out setting) && setting.Length > 0) startInfo.Arguments = setting; if (settings.TryGetValue(nameof(WorkingDirectory), out setting)) { setting = setting.Trim(); if (Directory.Exists(setting)) { WorkingDirectory = setting; startInfo.WorkingDirectory = WorkingDirectory; } else { throw new DirectoryNotFoundException($"Cannot launch process: specified working directory \"{setting}\" does not exist."); } } else { WorkingDirectory = FilePath.GetDirectoryName(FileName); startInfo.WorkingDirectory = WorkingDirectory; } if (settings.TryGetValue(nameof(EnvironmentalVariables), out setting)) { foreach (KeyValuePair item in setting.ParseKeyValuePairs()) startInfo.Environment[item.Key] = item.Value; } // Note that it's possible that time-series framework is being hosted by an application // running in a Window making many of the following process start properties relevant. // Even when hosted as a service, the user may start the service logging on using the // local system account and select to allow the service interact with the desktop. if (settings.TryGetValue(nameof(CreateNoWindow), out setting)) startInfo.CreateNoWindow = setting.ParseBoolean(); else startInfo.CreateNoWindow = DefaultCreateNoWindow; if (settings.TryGetValue(nameof(WindowStyle), out setting) && Enum.TryParse(setting, true, out windowStyle)) startInfo.WindowStyle = windowStyle; else startInfo.WindowStyle = (ProcessWindowStyle)Enum.Parse(typeof(ProcessWindowStyle), DefaultWindowStyle); if (settings.TryGetValue(nameof(ErrorDialog), out setting)) startInfo.ErrorDialog = setting.ParseBoolean(); else startInfo.ErrorDialog = DefaultErrorDialog; if (settings.TryGetValue(nameof(Domain), out setting) && setting.Length > 0) startInfo.Domain = setting; if (settings.TryGetValue(nameof(UserName), out setting) && setting.Length > 0) startInfo.UserName = setting; if (settings.TryGetValue(nameof(Password), out setting) && setting.Length > 0) startInfo.Password = setting.ToSecureString(); if (settings.TryGetValue(nameof(LoadUserProfile), out setting)) startInfo.LoadUserProfile = setting.ParseBoolean(); if (settings.TryGetValue(nameof(InitialInputFileName), out setting)) { setting = FilePath.GetAbsolutePath(setting.Trim()); if (File.Exists(setting)) InitialInputFileName = setting; else throw new FileNotFoundException($"Cannot launch process: specified initial input filename \"{setting}\" does not exist."); } if (settings.TryGetValue(nameof(InitialInputProcessingDelay), out setting) && int.TryParse(setting, out initialInputProcessingDelay) && initialInputProcessingDelay > -1) InitialInputProcessingDelay = initialInputProcessingDelay; if (settings.TryGetValue(nameof(RedirectOutputToHostEnvironment), out setting)) RedirectOutputToHostEnvironment = setting.ParseBoolean(); if (settings.TryGetValue(nameof(RedirectErrorToHostEnvironment), out setting)) RedirectErrorToHostEnvironment = setting.ParseBoolean(); if (settings.TryGetValue(nameof(UtilizationUpdateInterval), out setting) && int.TryParse(setting, out utilizationCalculationInterval)) UtilizationUpdateInterval = utilizationCalculationInterval; startInfo.RedirectStandardOutput = RedirectOutputToHostEnvironment; startInfo.RedirectStandardError = RedirectErrorToHostEnvironment; startInfo.RedirectStandardInput = true; startInfo.UseShellExecute = false; m_process.EnableRaisingEvents = true; m_process.OutputDataReceived += ProcessOutputDataReceived; m_process.ErrorDataReceived += ProcessErrorDataReceived; if (settings.TryGetValue(nameof(ProcessOutputAsLogMessages), out setting)) ProcessOutputAsLogMessages = setting.ParseBoolean(); if (ProcessOutputAsLogMessages) { if (settings.TryGetValue(nameof(LogMessageTextExpression), out setting) && setting.Length > 0) LogMessageTextExpression = setting; m_logMessageTextExpression = new Regex(LogMessageTextExpression, RegexOptions.Compiled); if (settings.TryGetValue(nameof(LogMessageLevelExpression), out setting) && setting.Length > 0) LogMessageLevelExpression = setting; m_logMessageLevelExpression = new Regex(LogMessageLevelExpression, RegexOptions.Compiled); if (settings.TryGetValue(nameof(LogMessageLevelMappings), out setting)) LogMessageLevelMappings = setting; foreach (KeyValuePair item in LogMessageLevelMappings.ParseKeyValuePairs()) { MessageLevel level; if (Enum.TryParse(item.Value, true, out level)) m_messageLevelMap[item.Key] = level; } } if (settings.TryGetValue(nameof(ForceKillOnDispose), out setting)) ForceKillOnDispose = setting.ParseBoolean(); if (settings.TryGetValue(nameof(TrackProcessStatistics), out setting)) TrackProcessStatistics = setting.ParseBoolean(); m_process.Start(); if (ForceKillOnDispose) m_childProcessManager?.AddProcess(m_process); if (RedirectOutputToHostEnvironment) m_process.BeginOutputReadLine(); if (RedirectErrorToHostEnvironment) m_process.BeginErrorReadLine(); m_processUtilizationCalculator.UpdateInterval = UtilizationUpdateInterval; m_processUtilizationCalculator.Initialize(m_process); // Register launched process with the statistics engine if (TrackProcessStatistics) StatisticsEngine.Register(this, "Process", "PROC"); if (string.IsNullOrEmpty(InitialInputFileName)) return; // Send any defined initial input to launched application new Action(() => { try { using (StreamReader reader = File.OpenText(InitialInputFileName)) { string line; while ((object)(line = reader.ReadLine()) != null) Input(line); } } catch (Exception ex) { OnProcessException(MessageLevel.Warning, new InvalidOperationException($"Failed while sending text from \"{InitialInputFileName}\" to launched process standard input: {ex.Message}", ex)); } }) .DelayAndExecute(InitialInputProcessingDelay); } /// /// Gets a short one-line status of this . /// /// Maximum number of available characters for display. /// A short one-line summary of the current status of this . public override string GetShortStatus(int maxLength) { string filename = FilePath.GetFileName(FileName); if (!Initialized) return $"\"{filename}\" process has not started...".CenterText(maxLength); try { if (m_process.HasExited) return $"\"{filename}\" process exited with {m_process.ExitCode}.".CenterText(maxLength); // Ignoring this check on Mono since it seems to report incorrect state for Responding property if (!Common.IsMono && !m_process.Responding) return $"\"{filename}\" process is not responding...".CenterText(maxLength); string utilization = m_processUtilizationCalculator.UpdateInterval > 0 ? $" at {m_processUtilizationCalculator.Utilization:##0.0%}" : ""; return $"\"{filename}\" process running{utilization} for {(DateTime.Now - m_process.StartTime).ToElapsedTimeString()}.".CenterText(maxLength); } catch { return $"\"{filename}\" process state could not be determined...".CenterText(maxLength); } } /// /// Sends specified value as input to launched process, new line will be automatically appended to text. /// /// Input string to send to launched process. [AdapterCommand("Sends specified value as input to launched process, new line will be automatically appended to text.")] public void Input(string value) { m_process.StandardInput.WriteLine(value); m_process.StandardInput.Flush(); m_inputLinesProcessed++; } /// /// Clears any cached information associated with the launched process. /// [AdapterCommand("Clears any cached information associated with the launched process.")] public void Refresh() { m_process.Refresh(); } /// /// Stops the launched process. /// [AdapterCommand("Stops the launched process.")] public void Kill() { m_process.Kill(); } private void ProcessOutputDataReceived(object sender, DataReceivedEventArgs e) { MessageLevel level = MessageLevel.Info; string status = ProcessOutputAsLogMessages ? ParseLogMessage(e.Data, out level) : e.Data; if (string.IsNullOrEmpty(status)) return; OnStatusMessage(level, status); m_outputLinesProcessed++; } private void ProcessErrorDataReceived(object sender, DataReceivedEventArgs e) { MessageLevel level = MessageLevel.Error; string status = ProcessOutputAsLogMessages ? ParseLogMessage(e.Data, out level) : e.Data; if (string.IsNullOrEmpty(status)) return; OnStatusMessage(level, status); m_errorLinesProcessed++; } private string ParseLogMessage(string message, out MessageLevel level) { level = MessageLevel.NA; if (string.IsNullOrWhiteSpace(message)) return ""; // Parse log message level if ((object)m_logMessageLevelExpression != null) { Match match = m_logMessageLevelExpression.Match(message); if (match.Success) { if (!m_messageLevelMap.TryGetValue(match.Value, out level)) level = MessageLevel.NA; } } // Parse log message text if ((object)m_logMessageTextExpression != null) { MatchCollection matches = m_logMessageTextExpression.Matches(message); if (matches.Count > 0) return string.Join(" ", matches.Cast().Select(match => match.Value)); } return message; } #endregion #region [ Static ] // Static Methods // ReSharper disable UnusedMember.Local // ReSharper disable UnusedParameter.Local private static double GetProcessStatistic_CPUUsage(object source, string arguments) { double statistic = double.NaN; ProcessLauncher launcher = source as ProcessLauncher; if ((object)launcher != null && !launcher.m_disposed && launcher.m_processUtilizationCalculator.UpdateInterval > 0) statistic = launcher.m_processUtilizationCalculator.Utilization; return statistic; } private static double GetProcessStatistic_MemoryUsage(object source, string arguments) { double statistic = double.NaN; ProcessLauncher launcher = source as ProcessLauncher; if ((object)launcher != null && !launcher.m_disposed && !launcher.m_process.HasExited) statistic = launcher.m_process.WorkingSet64 / (double)SI2.Mega; return statistic; } private static double GetProcessStatistic_UpTime(object source, string arguments) { double statistic = double.NaN; ProcessLauncher launcher = source as ProcessLauncher; if ((object)launcher != null && !launcher.m_disposed && !launcher.m_process.HasExited) statistic = (DateTime.Now - launcher.m_process.StartTime).TotalSeconds; return statistic; } // ReSharper restore UnusedMember.Local // ReSharper restore UnusedParameter.Local #endregion } }