cemu-DS4Windows/DS4Windows/AutoProfileChecker.cs

252 lines
12 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security;
using System.Text;
using System.Diagnostics; // StopWatch
using System.Threading; // Sleep
using System.Threading.Tasks;
using DS4Windows;
namespace DS4WinWPF
{
[SuppressUnmanagedCodeSecurity]
public class AutoProfileChecker
{
private AutoProfileHolder profileHolder;
private IntPtr prevForegroundWnd = IntPtr.Zero;
private uint prevForegroundProcessID;
private string prevForegroundProcessName = string.Empty;
private string prevForegroundWndTitleName = string.Empty;
private StringBuilder autoProfileCheckTextBuilder = new StringBuilder(1000);
private int autoProfileDebugLogLevel = 0;
private bool turnOffTemp;
private AutoProfileEntity tempAutoProfile;
private bool running;
public int AutoProfileDebugLogLevel { get => autoProfileDebugLogLevel; set => autoProfileDebugLogLevel = value; }
public bool Running { get => running; set => running = value; }
public delegate void ChangeServiceHandler(AutoProfileChecker sender, bool state);
public event ChangeServiceHandler RequestServiceChange;
public AutoProfileChecker(AutoProfileHolder holder)
{
profileHolder = holder;
}
public void Process()
{
string topProcessName, topWindowTitle;
bool turnOffDS4WinApp = false;
AutoProfileEntity matchedProfileEntity = null;
if (GetTopWindowName(out topProcessName, out topWindowTitle))
{
// Find a profile match based on autoprofile program path and wnd title list.
// The same program may set different profiles for each of the controllers, so we need an array of newProfileName[controllerIdx] values.
for (int i = 0, pathsLen = profileHolder.AutoProfileColl.Count; i < pathsLen; i++)
{
AutoProfileEntity tempEntity = profileHolder.AutoProfileColl[i];
if (tempEntity.IsMatch(topProcessName, topWindowTitle))
{
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. Rule#{i + 1} Path={tempEntity.path} Title={tempEntity.title}", false);
// Matching autoprofile rule found
turnOffDS4WinApp = tempEntity.Turnoff;
matchedProfileEntity = tempEntity;
break;
}
}
if (matchedProfileEntity != null)
{
bool forceLoadProfile = false;
if (!turnOffDS4WinApp && turnOffTemp)
{
// DS4Win was temporarily turned off by another auto-profile rule. Turn DS4Win on before trying to load a new profile because otherwise the new profile won't do anything.
// Force load the profile when DS4Win service afer waking up DS4Win service to make sure that the new profile will be active.
turnOffTemp = false;
SetAndWaitServiceStatus(true);
forceLoadProfile = true;
}
// Program match found. Check if the new profile is different than current profile of the controller. Load the new profile only if it is not already loaded.
for (int j = 0; j < 4; j++)
{
string tempname = matchedProfileEntity.ProfileNames[j];
if (tempname != string.Empty && tempname != "(none)")
{
if ((Global.useTempProfile[j] && tempname != Global.tempprofilename[j]) ||
(!Global.useTempProfile[j] && tempname != Global.ProfilePath[j]) ||
forceLoadProfile)
{
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. LoadProfile Controller {j + 1}={tempname}", false);
Global.LoadTempProfile(j, tempname, true, Program.rootHub); // j is controller index, i is filename
//if (LaunchProgram[j] != string.Empty) Process.Start(LaunchProgram[j]);
}
else
{
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. LoadProfile Controller {j + 1}={tempname} (already loaded)", false);
}
}
}
if (turnOffDS4WinApp)
{
turnOffTemp = true;
if (App.rootHub.running)
{
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. Turning DS4Windows temporarily off", false);
SetAndWaitServiceStatus(false);
}
}
tempAutoProfile = matchedProfileEntity;
}
else if (tempAutoProfile != null)
{
if (turnOffTemp && DS4Windows.Global.AutoProfileRevertDefaultProfile)
{
turnOffTemp = false;
if (!App.rootHub.running)
{
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. Turning DS4Windows on before reverting to default profile", false);
SetAndWaitServiceStatus(true);
}
}
tempAutoProfile = null;
for (int j = 0; j < 4; j++)
{
if (Global.useTempProfile[j])
{
if (DS4Windows.Global.AutoProfileRevertDefaultProfile)
{
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. Unknown process. Reverting to default profile. Controller {j + 1}={Global.ProfilePath[j]} (default)", false);
Global.LoadProfile(j, false, Program.rootHub);
}
else
{
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. Unknown process. Existing profile left as active. Controller {j + 1}={Global.tempprofilename[j]}", false);
}
}
}
}
}
}
private bool GetTopWindowName(out string topProcessName, out string topWndTitleName)
{
IntPtr hWnd = GetForegroundWindow();
if (hWnd == IntPtr.Zero)
{
// Top window unknown or cannot acquire a handle. Return FALSE and return unknown process and wndTitle values
prevForegroundWnd = IntPtr.Zero;
prevForegroundProcessID = 0;
topProcessName = topWndTitleName = String.Empty;
return false;
}
//
// If this function was called from "auto-profile watcher timer" then check cached "previous hWnd handle". If the current hWnd is the same
// as during the previous check then return cached previous wnd and name values (ie. foreground app and window are assumed to be the same, so no need to re-query names).
// This should optimize the auto-profile timer check process and causes less burden to .NET GC collector because StringBuffer is not re-allocated every second.
//
// Note! hWnd handles may be re-cycled but not during the lifetime of the window. This "cache" optimization still works because when an old window is closed
// then foreground window changes to something else and the cached prevForgroundWnd variable is updated to store the new hWnd handle.
// It doesn't matter even when the previously cached handle is recycled by WinOS to represent some other window (it is no longer used as a cached value anyway).
//
if (hWnd == prevForegroundWnd)
{
// The active window is still the same. Return cached process and wndTitle values and FALSE to indicate caller that no changes since the last call of this method
topProcessName = prevForegroundProcessName;
topWndTitleName = prevForegroundWndTitleName;
return false;
}
prevForegroundWnd = hWnd;
IntPtr hProcess = IntPtr.Zero;
uint lpdwProcessId = 0;
GetWindowThreadProcessId(hWnd, out lpdwProcessId);
if (lpdwProcessId == prevForegroundProcessID)
{
topProcessName = prevForegroundProcessName;
}
else
{
prevForegroundProcessID = lpdwProcessId;
hProcess = OpenProcess(0x0410, false, lpdwProcessId);
if (hProcess != IntPtr.Zero) GetModuleFileNameEx(hProcess, IntPtr.Zero, autoProfileCheckTextBuilder, autoProfileCheckTextBuilder.Capacity);
else autoProfileCheckTextBuilder.Clear();
prevForegroundProcessName = topProcessName = autoProfileCheckTextBuilder.Replace('/', '\\').ToString().ToLower();
}
GetWindowText(hWnd, autoProfileCheckTextBuilder, autoProfileCheckTextBuilder.Capacity);
prevForegroundWndTitleName = topWndTitleName = autoProfileCheckTextBuilder.ToString().ToLower();
if (hProcess != IntPtr.Zero) CloseHandle(hProcess);
if (autoProfileDebugLogLevel > 0)
DS4Windows.AppLogger.LogToGui($"DEBUG: Auto-Profile. PID={lpdwProcessId} Path={topProcessName} | WND={hWnd} Title={topWndTitleName}", false);
return true;
}
private void SetAndWaitServiceStatus(bool serviceRunningStatus)
{
// Start or Stop the service only if it is not already in the requested state
if (App.rootHub.running != serviceRunningStatus)
{
RequestServiceChange?.Invoke(this, serviceRunningStatus);
// Wait until DS4Win app service is running or stopped (as requested by serviceRunningStatus value) or timeout.
// LoadProfile call fails if a new profile is loaded while DS4Win service is still in stopped state (ie the loaded temp profile doesn't do anything).
Stopwatch sw = new Stopwatch();
sw.Start();
while (App.rootHub.running != serviceRunningStatus && sw.Elapsed.TotalSeconds < 10)
{
Thread.Sleep(500);
}
Thread.Sleep(500);
}
}
[DllImport("user32.dll")]
private static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);
[DllImport("kernel32.dll")]
private static extern IntPtr OpenProcess(uint dwDesiredAccess, bool bInheritHandle, uint dwProcessId);
[DllImport("kernel32.dll")]
private static extern bool CloseHandle(IntPtr handle);
[DllImport("psapi.dll")]
private static extern uint GetModuleFileNameEx(IntPtr hWnd, IntPtr hModule, StringBuilder lpFileName, int nSize);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
private static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nSize);
}
}