From 1c67d6145e6eed5454991218f7c50ab3caafdfd3 Mon Sep 17 00:00:00 2001 From: mika-n Date: Mon, 1 Jul 2019 02:45:55 +0300 Subject: [PATCH 1/5] Added cmdline options to send start/stop/shutdown/LoadProfile/LoadTempProfile commands to a background DS4Win app via IPC messaging interface. --- DS4Windows/DS4Forms/DS4Form.cs | 59 ++++++++++++++++++++++++++++++++++ DS4Windows/Program.cs | 56 ++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) diff --git a/DS4Windows/DS4Forms/DS4Form.cs b/DS4Windows/DS4Forms/DS4Form.cs index abbc0b0..16a7e87 100644 --- a/DS4Windows/DS4Forms/DS4Form.cs +++ b/DS4Windows/DS4Forms/DS4Form.cs @@ -85,6 +85,9 @@ namespace DS4Windows.Forms [DllImport("psapi.dll")] private static extern uint GetModuleFileNameEx(IntPtr hWnd, IntPtr hModule, StringBuilder lpFileName, int nSize); + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + public DS4Form(string[] args) { Global.FindConfigLocation(); @@ -386,6 +389,17 @@ namespace DS4Windows.Forms if (!(StartMinimized || mini)) Form_Resize(null, null); + // Write current window class name as InterProcessCommunication identifier. The IPC classname is used in WM_DATACOPY messaging interface. .NET WinForms creates a semi-random clas names per application. + StringBuilder wndClassNameStr = new StringBuilder(256); + if (GetClassName(this.Handle, wndClassNameStr, wndClassNameStr.Capacity) != 0 && wndClassNameStr.Length > 0) + { + if (System.IO.File.Exists(appdatapath + "\\IPCClassName.dat") && System.IO.File.ReadAllText(appdatapath + "\\IPCClassName.dat") == wndClassNameStr.ToString()) + wndClassNameStr.Clear(); // The wnd classname is still the same, so no need to re-write it + + if(wndClassNameStr.Length > 0) + System.IO.File.WriteAllText(appdatapath + "\\IPCClassName.dat", wndClassNameStr.ToString()); + } + Program.rootHub.Debug += On_Debug; AppLogger.GuiLog += On_Debug; @@ -1137,6 +1151,51 @@ Properties.Resources.DS4Update, MessageBoxButtons.YesNo, MessageBoxIcon.Question systemShutdown = true; break; } + case Program.WM_COPYDATA: + { + // Received InterProcessCommunication (IPC) message. Handle the requested command + Program.COPYDATASTRUCT cds = (Program.COPYDATASTRUCT)m.GetLParam(typeof(Program.COPYDATASTRUCT)); + if (cds.cbData >= 4) + { + int tdevice = -1; + + byte[] buffer = new byte[cds.cbData]; + Marshal.Copy(cds.lpData, buffer, 0, cds.cbData); + string[] strData = Encoding.ASCII.GetString(buffer).Split('.'); + + if (strData.Length >= 1) + { + strData[0] = strData[0].ToLower(); + + if (strData[0] == "start") + ServiceStartup(true); + else if (strData[0] == "stop") + ServiceShutdown(true); + else if (strData[0] == "shutdown") + ScpForm_Closing(this, new FormClosingEventArgs(CloseReason.ApplicationExitCall, false)); + else if (strData[0] == "loadprofile" && strData.Length >= 3) + { + // Command syntax: LoadProfile.device#.profileName (fex LoadProfile.1.GameSnake) + if(int.TryParse(strData[1], out tdevice)) tdevice--; + + if (tdevice >= 0 && tdevice < 4) + { + ProfilePath[tdevice] = strData[2]; + LoadProfile(tdevice, true, Program.rootHub); + } + } + else if (strData[0] == "loadtempprofile" && strData.Length >= 3) + { + // Command syntax: LoadTempProfile.device#.profileName (fex LoadTempProfile.1.GameSnake) + if (int.TryParse(strData[1], out tdevice)) tdevice--; + + if (tdevice >= 0 && tdevice < 4) + LoadTempProfile(tdevice, strData[2], true, Program.rootHub); + } + } + } + break; + } default: break; } diff --git a/DS4Windows/Program.cs b/DS4Windows/Program.cs index 81dc8e7..3f0ad2d 100644 --- a/DS4Windows/Program.cs +++ b/DS4Windows/Program.cs @@ -11,6 +11,22 @@ namespace DS4Windows { static class Program { + [DllImport("user32.dll", EntryPoint = "FindWindow")] + private static extern IntPtr FindWindow(string sClass, string sWindow); + + [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] + private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam); + + public const int WM_COPYDATA = 0x004A; + + [StructLayout(LayoutKind.Sequential)] + public struct COPYDATASTRUCT + { + public IntPtr dwData; + public int cbData; + public IntPtr lpData; + } + // Add "global\" in front of the EventName, then only one instance is allowed on the // whole system, including other users. But the application can not be brought // into view, of course. @@ -66,6 +82,46 @@ namespace DS4Windows Environment.ExitCode = 0; return; } + else if (s == "command" || s == "-command") + { + i++; + + IntPtr hWndDS4WindowsForm = IntPtr.Zero; + + Global.FindConfigLocation(); + if (args[i].Length > 0 && !string.IsNullOrEmpty(Global.appdatapath) && System.IO.File.Exists(Global.appdatapath + "\\IPCClassName.dat")) + { + hWndDS4WindowsForm = FindWindow(System.IO.File.ReadAllText(Global.appdatapath + "\\IPCClassName.dat"), "DS4Windows"); + if (hWndDS4WindowsForm != IntPtr.Zero) + { + COPYDATASTRUCT cds; + cds.lpData = IntPtr.Zero; + + try + { + cds.dwData = IntPtr.Zero; + cds.cbData = args[i].Length; + cds.lpData = Marshal.StringToHGlobalAnsi(args[i]); + SendMessage(hWndDS4WindowsForm, WM_COPYDATA, IntPtr.Zero, ref cds); + } + finally + { + if(cds.lpData != IntPtr.Zero) + Marshal.FreeHGlobal(cds.lpData); + } + } + else + { + Environment.ExitCode = 2; + } + } + else + { + Environment.ExitCode = 1; + } + + return; + } } try From 7061a5c6c4d5b8af9012bbf9185ba15143a683af Mon Sep 17 00:00:00 2001 From: mika-n Date: Mon, 1 Jul 2019 13:34:36 +0300 Subject: [PATCH 2/5] Tweaked inter-process message posting (added safety checks to ignore too large data packets). Added LogDebug message when a profile is loaded via cmdline commands. --- DS4Windows/DS4Forms/DS4Form.cs | 37 ++++++++++++++++++---------------- DS4Windows/Program.cs | 13 +++++++----- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/DS4Windows/DS4Forms/DS4Form.cs b/DS4Windows/DS4Forms/DS4Form.cs index 16a7e87..bc80523 100644 --- a/DS4Windows/DS4Forms/DS4Form.cs +++ b/DS4Windows/DS4Forms/DS4Form.cs @@ -68,6 +68,8 @@ namespace DS4Windows.Forms private const string UPDATER_VERSION = "1.3.1"; private const int WM_QUERYENDSESSION = 0x11; private const int WM_CLOSE = 0x10; + public const int WM_COPYDATA = 0x004A; + internal string updaterExe = Environment.Is64BitProcess ? "DS4Updater.exe" : "DS4Updater_x86.exe"; [DllImport("user32.dll")] @@ -86,7 +88,7 @@ namespace DS4Windows.Forms private static extern uint GetModuleFileNameEx(IntPtr hWnd, IntPtr hModule, StringBuilder lpFileName, int nSize); [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] - static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); public DS4Form(string[] args) { @@ -1151,11 +1153,11 @@ Properties.Resources.DS4Update, MessageBoxButtons.YesNo, MessageBoxIcon.Question systemShutdown = true; break; } - case Program.WM_COPYDATA: + case WM_COPYDATA: { - // Received InterProcessCommunication (IPC) message. Handle the requested command + // Received InterProcessCommunication (IPC) message. DS4Win command is embedded as a string value in lpData buffer Program.COPYDATASTRUCT cds = (Program.COPYDATASTRUCT)m.GetLParam(typeof(Program.COPYDATASTRUCT)); - if (cds.cbData >= 4) + if (cds.cbData >= 4 && cds.cbData <= 256) { int tdevice = -1; @@ -1173,24 +1175,25 @@ Properties.Resources.DS4Update, MessageBoxButtons.YesNo, MessageBoxIcon.Question ServiceShutdown(true); else if (strData[0] == "shutdown") ScpForm_Closing(this, new FormClosingEventArgs(CloseReason.ApplicationExitCall, false)); - else if (strData[0] == "loadprofile" && strData.Length >= 3) + else if ( (strData[0] == "loadprofile" || strData[0] == "loadtempprofile") && strData.Length >= 3) { - // Command syntax: LoadProfile.device#.profileName (fex LoadProfile.1.GameSnake) + // Command syntax: LoadProfile.device#.profileName (fex LoadProfile.1.GameSnake or LoadTempProfile.1.WebBrowserSet) if(int.TryParse(strData[1], out tdevice)) tdevice--; - if (tdevice >= 0 && tdevice < 4) + if (tdevice >= 0 && tdevice < ControlService.DS4_CONTROLLER_COUNT && File.Exists(Global.appdatapath + "\\Profiles\\" + strData[2] + ".xml")) { - ProfilePath[tdevice] = strData[2]; - LoadProfile(tdevice, true, Program.rootHub); - } - } - else if (strData[0] == "loadtempprofile" && strData.Length >= 3) - { - // Command syntax: LoadTempProfile.device#.profileName (fex LoadTempProfile.1.GameSnake) - if (int.TryParse(strData[1], out tdevice)) tdevice--; + if (strData[0] == "loadprofile") + { + ProfilePath[tdevice] = strData[2]; + LoadProfile(tdevice, true, Program.rootHub); + } + else + { + LoadTempProfile(tdevice, strData[2], true, Program.rootHub); + } - if (tdevice >= 0 && tdevice < 4) - LoadTempProfile(tdevice, strData[2], true, Program.rootHub); + Program.rootHub.LogDebug(Properties.Resources.UsingProfile.Replace("*number*", (tdevice + 1).ToString()).Replace("*Profile name*", strData[2])); + } } } } diff --git a/DS4Windows/Program.cs b/DS4Windows/Program.cs index 3f0ad2d..d31fce2 100644 --- a/DS4Windows/Program.cs +++ b/DS4Windows/Program.cs @@ -15,9 +15,7 @@ namespace DS4Windows private static extern IntPtr FindWindow(string sClass, string sWindow); [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)] - private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam); - - public const int WM_COPYDATA = 0x004A; + private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, ref COPYDATASTRUCT lParam); [StructLayout(LayoutKind.Sequential)] public struct COPYDATASTRUCT @@ -89,8 +87,13 @@ namespace DS4Windows IntPtr hWndDS4WindowsForm = IntPtr.Zero; Global.FindConfigLocation(); - if (args[i].Length > 0 && !string.IsNullOrEmpty(Global.appdatapath) && System.IO.File.Exists(Global.appdatapath + "\\IPCClassName.dat")) + if (args[i].Length > 0 && args[i].Length <= 256 && !string.IsNullOrEmpty(Global.appdatapath) && System.IO.File.Exists(Global.appdatapath + "\\IPCClassName.dat")) { + // Find the main DS4Form window handle and post WM_COPYDATA inter-process message. IPCClasName.dat file was created by the main DS4Windows process + // and it contains the name of the DS4Form .NET window class name string (the hash key part of the string is dynamically generated by .NET engine and it may change, + // so that's why the main process re-creates the file if the window class name is changed by .NET framework). Finding the WND handle usig both class name and window name + // limits chances that WM_COPYDATA message is sent to a wrong window. + hWndDS4WindowsForm = FindWindow(System.IO.File.ReadAllText(Global.appdatapath + "\\IPCClassName.dat"), "DS4Windows"); if (hWndDS4WindowsForm != IntPtr.Zero) { @@ -102,7 +105,7 @@ namespace DS4Windows cds.dwData = IntPtr.Zero; cds.cbData = args[i].Length; cds.lpData = Marshal.StringToHGlobalAnsi(args[i]); - SendMessage(hWndDS4WindowsForm, WM_COPYDATA, IntPtr.Zero, ref cds); + SendMessage(hWndDS4WindowsForm, Forms.DS4Form.WM_COPYDATA, IntPtr.Zero, ref cds); } finally { From c6daa180324b54542448ca16aa255de16c8d0561 Mon Sep 17 00:00:00 2001 From: mika-n Date: Wed, 3 Jul 2019 00:02:29 +0300 Subject: [PATCH 3/5] Changed IPC communication to use memory mapped memory instead of physical file to store the name of the DS4Form window class. --- DS4Windows/DS4Forms/DS4Form.cs | 16 ++------- DS4Windows/Program.cs | 65 ++++++++++++++++++++++++++++++++-- 2 files changed, 64 insertions(+), 17 deletions(-) diff --git a/DS4Windows/DS4Forms/DS4Form.cs b/DS4Windows/DS4Forms/DS4Form.cs index bc80523..1f84e2a 100644 --- a/DS4Windows/DS4Forms/DS4Form.cs +++ b/DS4Windows/DS4Forms/DS4Form.cs @@ -59,7 +59,7 @@ namespace DS4Windows.Forms private ManagementEventWatcher managementEvWatcher; private DS4Forms.LanguagePackComboBox languagePackComboBox1; private AdvancedColorDialog advColorDialog; - Dictionary hoverTextDict = new Dictionary(); + Dictionary hoverTextDict = new Dictionary(); // 0 index is used for application version text. 1 - 4 indices are used for controller status string[] notifyText = new string[5] { "DS4Windows v" + FileVersionInfo.GetVersionInfo(Assembly.GetExecutingAssembly().Location).FileVersion, @@ -87,9 +87,6 @@ namespace DS4Windows.Forms [DllImport("psapi.dll")] private static extern uint GetModuleFileNameEx(IntPtr hWnd, IntPtr hModule, StringBuilder lpFileName, int nSize); - [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] - private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); - public DS4Form(string[] args) { Global.FindConfigLocation(); @@ -391,16 +388,7 @@ namespace DS4Windows.Forms if (!(StartMinimized || mini)) Form_Resize(null, null); - // Write current window class name as InterProcessCommunication identifier. The IPC classname is used in WM_DATACOPY messaging interface. .NET WinForms creates a semi-random clas names per application. - StringBuilder wndClassNameStr = new StringBuilder(256); - if (GetClassName(this.Handle, wndClassNameStr, wndClassNameStr.Capacity) != 0 && wndClassNameStr.Length > 0) - { - if (System.IO.File.Exists(appdatapath + "\\IPCClassName.dat") && System.IO.File.ReadAllText(appdatapath + "\\IPCClassName.dat") == wndClassNameStr.ToString()) - wndClassNameStr.Clear(); // The wnd classname is still the same, so no need to re-write it - - if(wndClassNameStr.Length > 0) - System.IO.File.WriteAllText(appdatapath + "\\IPCClassName.dat", wndClassNameStr.ToString()); - } + Program.CreateIPCClassNameMMF(this.Handle); Program.rootHub.Debug += On_Debug; diff --git a/DS4Windows/Program.cs b/DS4Windows/Program.cs index d31fce2..81dabe2 100644 --- a/DS4Windows/Program.cs +++ b/DS4Windows/Program.cs @@ -6,11 +6,16 @@ using Process = System.Diagnostics.Process; using System.ComponentModel; using System.Globalization; using Microsoft.Win32.TaskScheduler; +using System.IO.MemoryMappedFiles; +using System.Text; namespace DS4Windows { static class Program { + [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)] + private static extern int GetClassName(IntPtr hWnd, StringBuilder lpClassName, int nMaxCount); + [DllImport("user32.dll", EntryPoint = "FindWindow")] private static extern IntPtr FindWindow(string sClass, string sWindow); @@ -36,6 +41,9 @@ namespace DS4Windows private static Thread controlThread; private static Form ds4form; + private static MemoryMappedFile ipcClassNameMMF = null; // MemoryMappedFile for inter-process communication used to hold className of DS4Form window + private static MemoryMappedViewAccessor ipcClassNameMMA = null; + /// /// The main entry point for the application. /// @@ -86,15 +94,14 @@ namespace DS4Windows IntPtr hWndDS4WindowsForm = IntPtr.Zero; - Global.FindConfigLocation(); - if (args[i].Length > 0 && args[i].Length <= 256 && !string.IsNullOrEmpty(Global.appdatapath) && System.IO.File.Exists(Global.appdatapath + "\\IPCClassName.dat")) + if (args[i].Length > 0 && args[i].Length <= 256) { // Find the main DS4Form window handle and post WM_COPYDATA inter-process message. IPCClasName.dat file was created by the main DS4Windows process // and it contains the name of the DS4Form .NET window class name string (the hash key part of the string is dynamically generated by .NET engine and it may change, // so that's why the main process re-creates the file if the window class name is changed by .NET framework). Finding the WND handle usig both class name and window name // limits chances that WM_COPYDATA message is sent to a wrong window. - hWndDS4WindowsForm = FindWindow(System.IO.File.ReadAllText(Global.appdatapath + "\\IPCClassName.dat"), "DS4Windows"); + hWndDS4WindowsForm = FindWindow(ReadIPCClassNameMMF(), "DS4Windows"); if (hWndDS4WindowsForm != IntPtr.Zero) { COPYDATASTRUCT cds; @@ -178,6 +185,9 @@ namespace DS4Windows while (testThread.IsAlive) Thread.SpinWait(500); threadComEvent.Close(); + + if (ipcClassNameMMA != null) ipcClassNameMMA.Dispose(); + if (ipcClassNameMMF != null) ipcClassNameMMF.Dispose(); } private static void createControlService() @@ -233,5 +243,54 @@ namespace DS4Windows temp.WindowState = FormWindowState.Normal; } } + + public static void CreateIPCClassNameMMF(IntPtr hWnd) + { + if (ipcClassNameMMA != null) return; // Already holding a handle to MMF file. No need to re-write the data + + try + { + StringBuilder wndClassNameStr = new StringBuilder(128); + if (GetClassName(hWnd, wndClassNameStr, wndClassNameStr.Capacity) != 0 && wndClassNameStr.Length > 0) + { + byte[] buffer = ASCIIEncoding.ASCII.GetBytes(wndClassNameStr.ToString()); + + ipcClassNameMMF = MemoryMappedFile.CreateNew("DS4Windows_IPCClassName.dat", 128); + ipcClassNameMMA = ipcClassNameMMF.CreateViewAccessor(0, buffer.Length); + ipcClassNameMMA.WriteArray(0, buffer, 0, buffer.Length); + // The MMF file is alive as long this process holds the file handle open + } + } + catch (Exception) + { + /* Eat all exceptions because errors here are not fatal for DS4Win */ + } + } + + private static string ReadIPCClassNameMMF() + { + MemoryMappedFile mmf = null; + MemoryMappedViewAccessor mma = null; + + try + { + byte[] buffer = new byte[128]; + mmf = MemoryMappedFile.OpenExisting("DS4Windows_IPCClassName.dat"); + mma = mmf.CreateViewAccessor(0, 128); + mma.ReadArray(0, buffer, 0, buffer.Length); + return ASCIIEncoding.ASCII.GetString(buffer); + } + catch (Exception) + { + // Eat all exceptions + } + finally + { + if (mma != null) mma.Dispose(); + if (mmf != null) mmf.Dispose(); + } + + return null; + } } } \ No newline at end of file From e9d9e9f428e4be7b6a2d8887ea433d9062a39cc3 Mon Sep 17 00:00:00 2001 From: mika-n Date: Thu, 11 Jul 2019 23:58:03 +0300 Subject: [PATCH 4/5] Support auto-profile matching using both program path and application window title values. Support wildchar string match logic in auto-profile path and window title values. Re-wrote auto-profile checking thread to be less RAM hungry and better performance (caching to remember the previous application and window handle values) --- DS4Windows/DS4Forms/DS4Form.cs | 322 +++++++++--- DS4Windows/DS4Forms/DS4Form.fi.resx | 56 +- DS4Windows/DS4Forms/DupBox.fi.resx | 5 +- DS4Windows/DS4Forms/Options.fi.resx | 17 +- DS4Windows/DS4Forms/SpecActions.fi.resx | 5 +- DS4Windows/DS4Forms/WinProgs.Designer.cs | 58 ++- DS4Windows/DS4Forms/WinProgs.cs | 549 ++++++++++++++------ DS4Windows/DS4Forms/WinProgs.fi.resx | 14 +- DS4Windows/DS4Forms/WinProgs.resx | 181 +++++-- DS4Windows/Properties/Resources.Designer.cs | 18 + DS4Windows/Properties/Resources.fi.resx | 12 + DS4Windows/Properties/Resources.resx | 6 + 12 files changed, 947 insertions(+), 296 deletions(-) diff --git a/DS4Windows/DS4Forms/DS4Form.cs b/DS4Windows/DS4Forms/DS4Form.cs index 430016d..c5127f0 100644 --- a/DS4Windows/DS4Forms/DS4Form.cs +++ b/DS4Windows/DS4Forms/DS4Form.cs @@ -39,13 +39,21 @@ namespace DS4Windows.Forms protected CheckBox[] linkedProfileCB; NonFormTimer hotkeysTimer = null;// new NonFormTimer(); NonFormTimer autoProfilesTimer = null;// new NonFormTimer(); - string tempProfileProgram = string.Empty; double dpix, dpiy; + + List programpaths = new List(); List profilenames = new List(); - List programpaths = new List(); List[] proprofiles; List turnOffTempProfiles; - + ProgramPathItem tempProfileProgram = null; + + public static int autoProfileDebugLogLevel = 0; // 0=Dont log debug messages about active process and window titles to GUI Log screen. 1=Show debug log messages + private static IntPtr prevForegroundWnd = IntPtr.Zero; + private static uint prevForegroundProcessID = 0; + private static string prevForegroundWndTitleName = string.Empty; + private static string prevForegroundProcessName = string.Empty; + private static StringBuilder autoProfileCheckTextBuilder = null; + private bool systemShutdown = false; private bool wasrunning = false; Options opt; @@ -87,6 +95,9 @@ namespace DS4Windows.Forms [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); + public DS4Form(string[] args) { Global.FindConfigLocation(); @@ -546,20 +557,87 @@ namespace DS4Windows.Forms } } - public static string GetTopWindowName() + public static bool GetTopWindowName(out string topProcessName, out string topWndTitleName, bool autoProfileTimerCheck = false) { IntPtr hWnd = GetForegroundWindow(); - uint lpdwProcessId; + 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(autoProfileTimerCheck) + { + 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); - IntPtr hProcess = OpenProcess(0x0410, false, lpdwProcessId); + if (autoProfileTimerCheck) + { + if (autoProfileCheckTextBuilder == null) autoProfileCheckTextBuilder = new StringBuilder(1000); - StringBuilder text = new StringBuilder(1000); - GetModuleFileNameEx(hProcess, IntPtr.Zero, text, text.Capacity); + if (lpdwProcessId == prevForegroundProcessID) + { + topProcessName = prevForegroundProcessName; + } + else + { + prevForegroundProcessID = lpdwProcessId; - CloseHandle(hProcess); + hProcess = OpenProcess(0x0410, false, lpdwProcessId); + if (hProcess != IntPtr.Zero) GetModuleFileNameEx(hProcess, IntPtr.Zero, autoProfileCheckTextBuilder, autoProfileCheckTextBuilder.Capacity); + else autoProfileCheckTextBuilder.Clear(); - return text.ToString(); + prevForegroundProcessName = topProcessName = autoProfileCheckTextBuilder.Replace('/', '\\').ToString().ToLower(); + } + + GetWindowText(hWnd, autoProfileCheckTextBuilder, autoProfileCheckTextBuilder.Capacity); + prevForegroundWndTitleName = topWndTitleName = autoProfileCheckTextBuilder.ToString().ToLower(); + } + else + { + // Caller function was not the autoprofile timer check thread, so create a new buffer to make this call thread safe and always query process and window title names. + // Note! At the moment DS4Win app doesn't call this method with autoProfileTimerCheck=false option, but this is here just for potential future usage. + StringBuilder text = new StringBuilder(1000); + + hProcess = OpenProcess(0x0410, false, lpdwProcessId); + if (hProcess != IntPtr.Zero) GetModuleFileNameEx(hProcess, IntPtr.Zero, text, text.Capacity); + else text.Clear(); + topProcessName = text.ToString(); + + GetWindowText(hWnd, text, text.Capacity); + topWndTitleName = text.ToString(); + } + + if (hProcess != IntPtr.Zero) CloseHandle(hProcess); + + if(DS4Form.autoProfileDebugLogLevel > 0 ) + AppLogger.LogToGui($"DEBUG: Auto-Profile. PID={lpdwProcessId} Path={topProcessName} | WND={hWnd} Title={topWndTitleName}", false); + + return true; } private void PowerEventArrive(object sender, EventArrivedEventArgs e) @@ -651,69 +729,110 @@ namespace DS4Windows.Forms private void CheckAutoProfiles(object sender, EventArgs e) { + string topProcessName, topWindowTitle; + string[] newProfileName = new string[4] { String.Empty, String.Empty, String.Empty, String.Empty }; + bool turnOffDS4WinApp = false; + ProgramPathItem matchingProgramPathItem = null; + autoProfilesTimer.Stop(); - //Check for process for auto profiles - if (string.IsNullOrEmpty(tempProfileProgram)) + if (GetTopWindowName(out topProcessName, out topWindowTitle, true)) { - string windowName = GetTopWindowName().ToLower().Replace('/', '\\'); + // 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 = programpaths.Count; i < pathsLen; i++) { - string name = programpaths[i].ToLower().Replace('/', '\\'); - if (name == windowName) + if (programpaths[i].IsMatch(topProcessName, topWindowTitle)) { + if (DS4Form.autoProfileDebugLogLevel > 0) + AppLogger.LogToGui($"DEBUG: Auto-Profile. Rule#{i+1} Path={programpaths[i].path} Title={programpaths[i].title}", false); + for (int j = 0; j < 4; j++) { if (proprofiles[j][i] != "(none)" && proprofiles[j][i] != Properties.Resources.noneProfile) { - LoadTempProfile(j, proprofiles[j][i], true, Program.rootHub); // j is controller index, i is filename - //if (LaunchProgram[j] != string.Empty) Process.Start(LaunchProgram[j]); + newProfileName[j] = proprofiles[j][i]; // j is controller index, i is filename } } - if (turnOffTempProfiles[i]) - { - turnOffTemp = true; - if (btnStartStop.Text == Properties.Resources.StopText) - { - //autoProfilesTimer.Stop(); - //hotkeysTimer.Stop(); - ChangeAutoProfilesStatus(false); - ChangeHotkeysStatus(false); - - this.Invoke((System.Action)(() => { - this.changingService = true; - BtnStartStop_Clicked(); - })); - - while (this.changingService) - { - Thread.SpinWait(500); - } - - this.Invoke((System.Action)(() => - { - //hotkeysTimer.Start(); - ChangeHotkeysStatus(true); - ChangeAutoProfilesStatus(true); - //autoProfilesTimer.Start(); - })); - } - } - - tempProfileProgram = name; + // Matching autoprofile rule found + turnOffDS4WinApp = turnOffTempProfiles[i]; + matchingProgramPathItem = programpaths[i]; break; } } - } - else - { - string windowName = GetTopWindowName().ToLower().Replace('/', '\\'); - if (tempProfileProgram != windowName) + + if (matchingProgramPathItem != null) { - tempProfileProgram = string.Empty; + // 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++) - LoadProfile(j, false, Program.rootHub); + { + if (newProfileName[j] != String.Empty) + { + if ((Global.useTempProfile[j] && newProfileName[j] != Global.tempprofilename[j]) || (!Global.useTempProfile[j] && newProfileName[j] != Global.ProfilePath[j])) + { + if (DS4Form.autoProfileDebugLogLevel > 0) + AppLogger.LogToGui($"DEBUG: Auto-Profile. LoadProfile Controller {j+1}={newProfileName[j]}", false); + + LoadTempProfile(j, newProfileName[j], true, Program.rootHub); // j is controller index, i is filename + //if (LaunchProgram[j] != string.Empty) Process.Start(LaunchProgram[j]); + } + else + { + if (DS4Form.autoProfileDebugLogLevel > 0) + AppLogger.LogToGui($"DEBUG: Auto-Profile. LoadProfile Controller {j + 1}={newProfileName[j]} (already loaded)", false); + } + } + } + + if (turnOffDS4WinApp) + { + turnOffTemp = true; + if (btnStartStop.Text == Properties.Resources.StopText) + { + //autoProfilesTimer.Stop(); + //hotkeysTimer.Stop(); + ChangeAutoProfilesStatus(false); + ChangeHotkeysStatus(false); + + this.Invoke((System.Action)(() => + { + this.changingService = true; + BtnStartStop_Clicked(); + })); + + while (this.changingService) + { + Thread.SpinWait(500); + } + + this.Invoke((System.Action)(() => + { + //hotkeysTimer.Start(); + ChangeHotkeysStatus(true); + ChangeAutoProfilesStatus(true); + //autoProfilesTimer.Start(); + })); + } + } + + tempProfileProgram = matchingProgramPathItem; + } + else if (tempProfileProgram != null) + { + // The current active program doesn't match any of the programs in autoprofile path list. + // Unload temp profile if controller is not using the default profile already. + tempProfileProgram = null; + for (int j = 0; j < 4; j++) + { + if (Global.useTempProfile[j]) + { + if (DS4Form.autoProfileDebugLogLevel > 0) + AppLogger.LogToGui($"DEBUG: Auto-Profile. RestoreProfile Controller {j + 1}={Global.ProfilePath[j]} (default)", false); + + LoadProfile(j, false, Program.rootHub); + } + } if (turnOffTemp) { @@ -746,18 +865,25 @@ namespace DS4Windows.Forms doc.Load(appdatapath + "\\Auto Profiles.xml"); XmlNodeList programslist = doc.SelectNodes("Programs/Program"); foreach (XmlNode x in programslist) - programpaths.Add(x.Attributes["path"].Value); + programpaths.Add(new ProgramPathItem(x.Attributes["path"]?.Value, x.Attributes["title"]?.Value)); - foreach (string s in programpaths) + int nodeIdx=0; + foreach (ProgramPathItem pathItem in programpaths) { + XmlNode item; + + nodeIdx++; for (int i = 0; i < 4; i++) { - proprofiles[i].Add(doc.SelectSingleNode("/Programs/Program[@path=\"" + s + "\"]" - + "/Controller" + (i + 1)).InnerText); + item = doc.SelectSingleNode($"/Programs/Program[{nodeIdx}]/Controller{i+1}"); + if (item != null) + proprofiles[i].Add(item.InnerText); + else + proprofiles[i].Add("(none)"); } - XmlNode item = doc.SelectSingleNode("/Programs/Program[@path=\"" + s + "\"]" - + "/TurnOff"); + item = doc.SelectSingleNode($"/Programs/Program[{nodeIdx}]/TurnOff"); + bool turnOff; if (item != null && bool.TryParse(item.InnerText, out turnOff)) turnOffTempProfiles.Add(turnOff); @@ -2701,4 +2827,78 @@ Properties.Resources.DS4Update, MessageBoxButtons.YesNo, MessageBoxIcon.Question FlashWhenLateAt = (int)Math.Round(nUDLatency.Value); } } + + + // + // Class to store autoprofile path and title data. Path and Title are pre-stored as lowercase versions (case insensitive search) to speed up IsMatch method in autoprofile timer calls. + // AutoProfile thread monitors active processes and windows. Autoprofile search rule can define just a process path or both path and window title search keywords. + // Keyword syntax: xxxx = exact matach, ^xxxx = match to beginning of path or title string. xxxx$ = match to end of string. *xxxx = contains in a string search + // + public class ProgramPathItem + { + public string path; + public string title; + private string path_lowercase; + private string title_lowercase; + + public ProgramPathItem(string pathStr, string titleStr) + { + // Initialize autoprofile search keywords (xxx_tolower). To improve performance the search keyword is pre-calculated in xxx_tolower variables, + // so autoprofile timer thread doesn't have to create substrings/replace/tolower string instances every second over and over again. + if (!string.IsNullOrEmpty(pathStr)) + { + path = pathStr; + path_lowercase = path.ToLower().Replace('/', '\\'); + + if (path.Length >= 2) + { + if (path[0] == '^') path_lowercase = path_lowercase.Substring(1); + else if (path[path.Length - 1] == '$') path_lowercase = path_lowercase.Substring(0, path_lowercase.Length - 1); + else if (path[0] == '*') path_lowercase = path_lowercase.Substring(1); + } + } + else path = path_lowercase = String.Empty; + + if (!string.IsNullOrEmpty(titleStr)) + { + title = titleStr; + title_lowercase = title.ToLower(); + + if (title.Length >= 2) + { + if (title[0] == '^') title_lowercase = title_lowercase.Substring(1); + else if (title[title.Length - 1] == '$') title_lowercase = title_lowercase.Substring(0, title_lowercase.Length - 1); + else if (title[0] == '*') title_lowercase = title_lowercase.Substring(1); + } + } + else title = title_lowercase = String.Empty; + } + + public bool IsMatch(string searchPath, string searchTitle) + { + bool bPathMatched = true; + bool bTitleMwatched = true; + + if (!String.IsNullOrEmpty(path_lowercase)) + { + bPathMatched = (path_lowercase == searchPath + || (path[0] == '^' && searchPath.StartsWith(path_lowercase)) + || (path[path.Length - 1] == '$' && searchPath.EndsWith(path_lowercase)) + || (path[0] == '*' && searchPath.Contains(path_lowercase)) + ); + } + + if (bPathMatched && !String.IsNullOrEmpty(title_lowercase)) + { + bTitleMwatched = (title_lowercase == searchTitle + || (title[0] == '^' && searchTitle.StartsWith(title_lowercase)) + || (title[title.Length - 1] == '$' && searchTitle.EndsWith(title_lowercase)) + || (title[0] == '*' && searchTitle.Contains(title_lowercase)) + ); + } + + // If both path and title defined in autoprofile entry then do AND condition (ie. both path and title should match) + return bPathMatched && bTitleMwatched; + } + } } diff --git a/DS4Windows/DS4Forms/DS4Form.fi.resx b/DS4Windows/DS4Forms/DS4Form.fi.resx index 6f013b9..8ef1cc2 100644 --- a/DS4Windows/DS4Forms/DS4Form.fi.resx +++ b/DS4Windows/DS4Forms/DS4Form.fi.resx @@ -1,4 +1,4 @@ - +