0

I'm using the following code sample to scan the tray icons in the taskbar tn extract their tooltips and return them as a list of strings.

class TrayTooltip
{
    public static List<String> ScanToolbarButtons()
    {
        List<string> tooltips = new List<string>();

        var handle = GetSystemTrayHandle();
        if (handle == IntPtr.Zero)
            return null;

        var count = SendMessage(handle, TB_BUTTONCOUNT, IntPtr.Zero, IntPtr.Zero).ToInt32();
        if (count == 0)
            return null;

        int pid;
        GetWindowThreadProcessId(handle, out pid);
        var hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
        if (hProcess == IntPtr.Zero)
            throw new Win32Exception(Marshal.GetLastWin32Error());

        var size = (IntPtr)Marshal.SizeOf<TBBUTTONINFOW>();
        var buffer = VirtualAllocEx(hProcess, IntPtr.Zero, size, MEM_COMMIT, PAGE_READWRITE);
        if (buffer == IntPtr.Zero)
        {
            CloseHandle(hProcess);
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        for (int i = 0; i < count; i++)
        {
            var btn = new TBBUTTONINFOW();
            btn.cbSize = size.ToInt32();
            btn.dwMask = TBIF_BYINDEX | TBIF_COMMAND;

            IntPtr written;
            if (WriteProcessMemory(hProcess, buffer, ref btn, size, out written))
            {
                // we want the identifier
                var res = SendMessage(handle, TB_GETBUTTONINFOW, (IntPtr)i, buffer);
                if (res.ToInt32() >= 0)
                {
                    IntPtr read;
                    if (ReadProcessMemory(hProcess, buffer, ref btn, size, out read))
                    {
                        // now get display text using the identifier
                        // first pass we ask for size
                        var textSize = SendMessage(handle, TB_GETBUTTONTEXTW, (IntPtr)btn.idCommand, IntPtr.Zero);
                        if (textSize.ToInt32() != -1)
                        {
                            // we need to allocate for the terminating zero and unicode
                            var utextSize = (IntPtr)((1 + textSize.ToInt32()) * 2);
                            var textBuffer = VirtualAllocEx(hProcess, IntPtr.Zero, utextSize, MEM_COMMIT, PAGE_READWRITE);
                            if (textBuffer != IntPtr.Zero)
                            {
                                res = SendMessage(handle, TB_GETBUTTONTEXTW, (IntPtr)btn.idCommand, textBuffer);
                                if (res == textSize)
                                {
                                    var localBuffer = Marshal.AllocHGlobal(utextSize.ToInt32());
                                    if (ReadProcessMemory(hProcess, textBuffer, localBuffer, utextSize, out read))
                                    {
                                        var text = Marshal.PtrToStringUni(localBuffer);
                                        tooltips.Add(text);
                                        Marshal.FreeHGlobal(localBuffer);
                                    }
                                }
                                VirtualFreeEx(hProcess, textBuffer, size, MEM_RELEASE);
                            }
                        }
                    }
                }
            }
        }

        VirtualFreeEx(hProcess, buffer, size, MEM_RELEASE);
        CloseHandle(hProcess);

        return tooltips;
    }

    private static IntPtr GetSystemTrayHandle()
    {
        var hwnd = FindWindowEx(IntPtr.Zero, IntPtr.Zero, "Shell_TrayWnd", null);
        hwnd = FindWindowEx(hwnd, IntPtr.Zero, "TrayNotifyWnd", null);
        hwnd = FindWindowEx(hwnd, IntPtr.Zero, "SysPager", null);
        return FindWindowEx(hwnd, IntPtr.Zero, "ToolbarWindow32", null);
    }

    [DllImport("kernel32", SetLastError = true)]
    private static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hObject);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, ref TBBUTTONINFOW lpBuffer, IntPtr nSize, out IntPtr lpNumberOfBytesWritten);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, ref TBBUTTONINFOW lpBuffer, IntPtr nSize, out IntPtr lpNumberOfBytesRead);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, IntPtr nSize, out IntPtr lpNumberOfBytesRead);

    [DllImport("user32", SetLastError = true)]
    private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

    [DllImport("kernel32", SetLastError = true)]
    private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, int flAllocationType, int flProtect);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, int dwFreeType);

    [DllImport("user32")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32", SetLastError = true)]
    private static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName);

    private const int TBIF_BYINDEX = unchecked((int)0x80000000); // this specifies that the wparam in Get/SetButtonInfo is an index, not id
    private const int TBIF_COMMAND = 0x20;
    private const int MEM_COMMIT = 0x1000;
    private const int MEM_RELEASE = 0x8000;
    private const int PAGE_READWRITE = 0x4;
    private const int TB_GETBUTTONINFOW = 1087;
    private const int TB_GETBUTTONTEXTW = 1099;
    private const int TB_BUTTONCOUNT = 1048;

    private static bool IsWindowsVistaOrAbove() => Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6;
    private static int PROCESS_ALL_ACCESS => IsWindowsVistaOrAbove() ? 0x001FFFFF : 0x001F0FFF;

    [StructLayout(LayoutKind.Sequential)]
    private struct TBBUTTONINFOW
    {
        public int cbSize;
        public int dwMask;
        public int idCommand;
        public int iImage;
        public byte fsState;
        public byte fsStyle;
        public short cx;
        public IntPtr lParam;
        public IntPtr pszText;
        public int cchText;
    }
}

In particular, I'm using this code to monitor a program that is displaying its status (converting text files to MP3s) in it's tooltip text. I then call the ScanToolbarButtons() function from the code below to retrieve the updated conversion progress percentage:

/// <summary>
/// Returns the conversion status of the MP3 conversion using the tooltip text from the Balabolka command line utility
/// </summary>
/// <returns>Number from 0-100 representing status, or -1 if the text wasn't found.</returns>
private int GetMP3ConversionStatus()
{
    try
    {
        // Return a list of tooltips for all active tray icons
        List<string> tooltips = TrayTooltip.ScanToolbarButtons();

        // Iterate through the list to find the one for the Balabolka commandline app
        foreach (string text in tooltips)
        {
            if (text.Contains("Balabolka ["))
            {
                // Split the string into sections to extract the numbers
                string[] splitText = text.Split(new char[] { '[', '%' });

                // Extract the number from the correct element and return it
                return Convert.ToInt32(splitText[1]);
            }
        }
    }
    catch (Exception)
    {
        return -1;
    }

    return -1;
}

The code seems to work great, but the problem is that as I call the GetMP3ConversionStatus function continuously in a loop to query the percentage, I notice that explorer.exe in the task manager is using more and more RAM until it fills up all the memory in my computer, and my program crashes.

It's apparent that memory is not being released somewhere but I'm not sure where from looking at the code. it seems like there are some functions in there to close open handles and release memory, but I'm not sure what's going on, or how best to troubleshoot this.

Jason O
  • 753
  • 9
  • 28
  • `localBuffer` is not FreeHGlobal'd when `ReadProcessMemory` fails with false. – Cee McSharpface Aug 20 '18 at 12:29
  • @dlatikay - yep, and I have corrected my code yesterday https://stackoverflow.com/questions/51887744/trouble-implementing-code-example-using-pinvoke-declarations/51892012#51892012 for that. However, there shouldn't be errors in the normal course of operations. I think it comes from the `VirtualFreeEx(hProcess, textBuffer, size, MEM_RELEASE)` which should be `VirtualFreeEx(hProcess, textBuffer, utextSize, MEM_RELEASE)` – Simon Mourier Aug 20 '18 at 12:49
  • @SimonMourier Thanks for taking a second look at this. I tried the updated code sample you posted in the other thread with the modifications. It still runs fine and doesn't throw any errors, but explorer.exe is still gobbling up more and more memory (though it seems to be happening slower). – Jason O Aug 20 '18 at 21:58
  • To try to troubleshoot it, I put some breakpoints in the four spots in the function where the code either returns early or throws a new exception. But the execution never enters into any of these spots (good news). However, I'm still stumped as to why it is still not deallocating RAM. – Jason O Aug 20 '18 at 22:00
  • Ok, after doing some further investigating, I've isolated the function that *seems* to be the problem. The line that says `VirtualFreeEx(hProcess, buffer, size, MEM_RELEASE)` is returning false after being executed. I added an if statement and had it throw a new Win32Exception to see the contents of the error, which was "The Parameter is incorrect." This still happens even if I comment out the entire for loop. No solution yet but thought I would share what I'm seeing so far. – Jason O Aug 21 '18 at 03:17
  • OK, I fixed it! The issue is that for the VirtualFreeEx function, the third parameter, size, needs to be set to IntPtr.Zero when setting the fourth parameter to MEM_RELEASE. Making this change to both instances of the function call fixed the memory leak problem. – Jason O Aug 21 '18 at 03:39

1 Answers1

2

Ok, I finally figured out the problem, the issue is that for the VirtualFreeEx function, the third parameter, size, needs to be set to IntPtr.Zero when setting the fourth parameter to MEM_RELEASE. Making this change to both instances of the function call fixed the memory leak problem.

For those interested, here's the final version of the code I'm using now. All credit goes to Simon Mourier who was a great help to supply the original working code sample.

class TrayTooltip
{
    public static List<string> ScanToolbarButtons()
    {
        List<string> tooltips = new List<string>();

        var handle = GetSystemTrayHandle();
        if (handle == IntPtr.Zero)
        {
            return null;
        }

        var count = SendMessage(handle, TB_BUTTONCOUNT, IntPtr.Zero, IntPtr.Zero).ToInt32();
        if (count == 0)
        {
            return null;
        }

        GetWindowThreadProcessId(handle, out var pid);
        var hProcess = OpenProcess(PROCESS_ALL_ACCESS, false, pid);
        if (hProcess == IntPtr.Zero)
        {
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        var size = (IntPtr)Marshal.SizeOf<TBBUTTONINFOW>();
        var buffer = VirtualAllocEx(hProcess, IntPtr.Zero, size, MEM_COMMIT, PAGE_READWRITE);
        if (buffer == IntPtr.Zero)
        {
            CloseHandle(hProcess);
            throw new Win32Exception(Marshal.GetLastWin32Error());
        }

        for (int i = 0; i < count; i++)
        {
            var btn = new TBBUTTONINFOW();
            btn.cbSize = size.ToInt32();
            btn.dwMask = TBIF_BYINDEX | TBIF_COMMAND;
            if (WriteProcessMemory(hProcess, buffer, ref btn, size, out var written))
            {
                // we want the identifier
                var res = SendMessage(handle, TB_GETBUTTONINFOW, (IntPtr)i, buffer);
                if (res.ToInt32() >= 0)
                {
                    if (ReadProcessMemory(hProcess, buffer, ref btn, size, out var read))
                    {
                        // now get display text using the identifier
                        // first pass we ask for size
                        var textSize = SendMessage(handle, TB_GETBUTTONTEXTW, (IntPtr)btn.idCommand, IntPtr.Zero);
                        if (textSize.ToInt32() != -1)
                        {
                            // we need to allocate for the terminating zero and unicode
                            var utextSize = (IntPtr)((1 + textSize.ToInt32()) * 2);
                            var textBuffer = VirtualAllocEx(hProcess, IntPtr.Zero, utextSize, MEM_COMMIT, PAGE_READWRITE);
                            if (textBuffer != IntPtr.Zero)
                            {
                                res = SendMessage(handle, TB_GETBUTTONTEXTW, (IntPtr)btn.idCommand, textBuffer);
                                if (res == textSize)
                                {
                                    var localBuffer = Marshal.AllocHGlobal(utextSize.ToInt32());
                                    if (ReadProcessMemory(hProcess, textBuffer, localBuffer, utextSize, out read))
                                    {
                                        var text = Marshal.PtrToStringUni(localBuffer);
                                        tooltips.Add(text);
                                    }
                                    Marshal.FreeHGlobal(localBuffer);
                                }
                                VirtualFreeEx(hProcess, textBuffer, IntPtr.Zero, MEM_RELEASE);
                            }
                        }
                    }
                }
            }
        }

        VirtualFreeEx(hProcess, buffer, IntPtr.Zero, MEM_RELEASE);
        CloseHandle(hProcess);

        return tooltips;
    }

    private static IntPtr GetSystemTrayHandle()
    {
        var hwnd = FindWindowEx(IntPtr.Zero, IntPtr.Zero, "Shell_TrayWnd", null);
        hwnd = FindWindowEx(hwnd, IntPtr.Zero, "TrayNotifyWnd", null);
        hwnd = FindWindowEx(hwnd, IntPtr.Zero, "SysPager", null);
        return FindWindowEx(hwnd, IntPtr.Zero, "ToolbarWindow32", null);
    }

    [DllImport("kernel32", SetLastError = true)]
    private static extern IntPtr OpenProcess(int dwDesiredAccess, bool bInheritHandle, int dwProcessId);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool CloseHandle(IntPtr hObject);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, ref TBBUTTONINFOW lpBuffer, IntPtr nSize, out IntPtr lpNumberOfBytesWritten);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, ref TBBUTTONINFOW lpBuffer, IntPtr nSize, out IntPtr lpNumberOfBytesRead);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, IntPtr nSize, out IntPtr lpNumberOfBytesRead);

    [DllImport("user32", SetLastError = true)]
    private static extern int GetWindowThreadProcessId(IntPtr hWnd, out int lpdwProcessId);

    [DllImport("kernel32", SetLastError = true)]
    private static extern IntPtr VirtualAllocEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, int flAllocationType, int flProtect);

    [DllImport("kernel32", SetLastError = true)]
    private static extern bool VirtualFreeEx(IntPtr hProcess, IntPtr lpAddress, IntPtr dwSize, int dwFreeType);

    [DllImport("user32")]
    private static extern IntPtr SendMessage(IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);

    [DllImport("user32", SetLastError = true)]
    private static extern IntPtr FindWindowEx(IntPtr hWndParent, IntPtr hWndChildAfter, string lpClassName, string lpWindowName);

    private const int TBIF_BYINDEX = unchecked((int)0x80000000); // this specifies that the wparam in Get/SetButtonInfo is an index, not id
    private const int TBIF_COMMAND = 0x20;
    private const int MEM_COMMIT = 0x1000;
    private const int MEM_RELEASE = 0x8000;
    private const int PAGE_READWRITE = 0x4;
    private const int TB_GETBUTTONINFOW = 1087;
    private const int TB_GETBUTTONTEXTW = 1099;
    private const int TB_BUTTONCOUNT = 1048;

    private static bool IsWindowsVistaOrAbove() => Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major >= 6;
    private static int PROCESS_ALL_ACCESS => IsWindowsVistaOrAbove() ? 0x001FFFFF : 0x001F0FFF;

    [StructLayout(LayoutKind.Sequential)]
    private struct TBBUTTONINFOW
    {
        public int cbSize;
        public int dwMask;
        public int idCommand;
        public int iImage;
        public byte fsState;
        public byte fsStyle;
        public short cx;
        public IntPtr lParam;
        public IntPtr pszText;
        public int cchText;
    }
}
Jason O
  • 753
  • 9
  • 28