using NLog; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; using System.Timers; namespace VRCX { // I don't think this applies to our use case, but I'm leaving it here for reference. // https://stackoverflow.com/questions/2519673/process-hasexited-returns-true-even-though-process-is-running // "When a process is started, it is assigned a PID. If the User is then prompted with the User Account Control dialog and selects 'Yes', the process is re-started and assigned a new PID." // There's no docs for this, but Process.HasExited also seems to be checked every time the property is accessed, so it's not cached. Which means Process.Refresh() is not needed for our use case. /// /// A class that monitors given processes and raises events when they are started or exited. /// Intended to be used to monitor VRChat and VRChat-related processes. /// internal class ProcessMonitor { private readonly Dictionary monitoredProcesses; private readonly Timer monitorProcessTimer; private static readonly Logger logger = LogManager.GetCurrentClassLogger(); static ProcessMonitor() { Instance = new ProcessMonitor(); Instance.ProcessStarted += Program.AppApiInstance.OnProcessStateChanged; Instance.ProcessExited += Program.AppApiInstance.OnProcessStateChanged; } public ProcessMonitor() { monitoredProcesses = new Dictionary(); monitorProcessTimer = new Timer(); monitorProcessTimer.Interval = 1000; monitorProcessTimer.Elapsed += MonitorProcessTimer_Elapsed; } public static ProcessMonitor Instance { get; private set; } /// /// Raised when a monitored process is started. /// public event Action ProcessStarted; /// /// Raised when a monitored process is exited. /// public event Action ProcessExited; public void Init() { AddProcess("vrchat"); AddProcess("vrserver"); monitorProcessTimer.Start(); } public void Exit() { monitorProcessTimer.Stop(); monitoredProcesses.Values.ToList().ForEach(x => x.ProcessExited()); } private void MonitorProcessTimer_Elapsed(object sender, ElapsedEventArgs e) { var processesNeedingUpdate = new List(); // Check if any of the monitored processes have been opened or closed. foreach (var keyValuePair in monitoredProcesses) { var monitoredProcess = keyValuePair.Value; if (monitoredProcess.IsRunning) { if (monitoredProcess.Process == null || WinApi.HasProcessExited(monitoredProcess.Process.Id)) { monitoredProcess.ProcessExited(); ProcessExited?.Invoke(monitoredProcess); logger.Info($"Monitored process {monitoredProcess.ProcessName} (PID: {(monitoredProcess.Process?.Id.ToString() ?? "null")}) exited."); } } else { processesNeedingUpdate.Add(monitoredProcess); } } // We do it this way so we're not constantly polling for processes if we don't actually need to (aka, all processes are already accounted for). if (processesNeedingUpdate.Count == 0) return; var processes = Process.GetProcesses(); foreach (var monitoredProcess in processesNeedingUpdate) { var process = processes.FirstOrDefault(p => string.Equals(p.ProcessName, monitoredProcess.ProcessName, StringComparison.OrdinalIgnoreCase)); // We are also checking to see if the process is exiting before adding it, otherwise we'll keep adding it and then removing it constantly in an endless loop. if (process == null || WinApi.HasProcessExited(process.Id)) continue; monitoredProcess.ProcessStarted(process); ProcessStarted?.Invoke(monitoredProcess); logger.Info($"Monitored process {monitoredProcess.ProcessName} (PID: {process.Id}) started."); } } /// /// Checks if a process if currently being monitored and if it is running. /// /// The name of the process to check for. /// If true, will manually check if the given process is running should the the monitored process not be initialized yet. /// Whether the given process is monitored and currently running. public bool IsProcessRunning(string processName, bool ensureCheck = false) { processName = processName.ToLower(); if (!monitoredProcesses.TryGetValue(processName, out var process)) return false; if (ensureCheck && process.Process == null) { var processes = Process.GetProcessesByName(processName); var isProcessRunning = processes.Length > 0; foreach (var proc in processes) proc.Dispose(); return isProcessRunning; } return process.IsRunning; } /// /// Adds a process to be monitored. /// /// public void AddProcess(Process process) { var processName = process.ProcessName.ToLower(); if (monitoredProcesses.ContainsKey(processName)) return; monitoredProcesses.Add(processName, new MonitoredProcess(process)); logger.Debug($"Added process {processName} to process monitor."); } /// /// Adds a process to be monitored. /// /// public void AddProcess(string processName) { processName = processName.ToLower(); if (monitoredProcesses.ContainsKey(processName)) return; monitoredProcesses.Add(processName, new MonitoredProcess(processName)); logger.Debug($"Added process {processName} to process monitor."); } /// /// Removes a process from being monitored. /// /// public void RemoveProcess(string processName) { processName = processName.ToLower(); monitoredProcesses.Remove(processName); logger.Debug($"Removed process {processName} from process monitor."); } } public class MonitoredProcess { public MonitoredProcess(Process process) { Process = process; ProcessName = process.ProcessName.ToLower(); if (!WinApi.HasProcessExited(process.Id)) IsRunning = true; } public MonitoredProcess(string processName) { ProcessName = processName; IsRunning = false; } public Process Process { get; private set; } public string ProcessName { get; private set; } public bool IsRunning { get; private set; } public bool HasName(string processName) { return ProcessName.Equals(processName, StringComparison.OrdinalIgnoreCase); } public void ProcessExited() { IsRunning = false; Process?.Dispose(); Process = null; } public void ProcessStarted(Process process) { Process = process; ProcessName = process.ProcessName.ToLower(); IsRunning = true; } } }